feat(desktop,service,features,host): sync Files changed's Viewed boxes with GitHub as the member
Summary
Files changed extras, E3 of the "Files changed extras" stack, stacked on #3638.
What was asked: each file's "Viewed" checkbox reads and writes GitHub's own viewed state (viewerViewedState, markFileAsViewed / unmarkFileAsViewed), with progress shown as "N / M viewed". Viewed state belongs to each GitHub user, so both the read and the write go through the member's own GitHub sign-in, via the shared "writes as the member" seam (memberPullRequestWrite, from #3272/W1), and never fall back to the Luke GitHub App.
Service
- Read:
GET /api/agents/{id}/pull-request/viewed.- New route file
apps/web/server/routes/agents/pull-request-viewed.tsand a segment rewrite;api-rewrites.jsonandvercel.jsonare regenerated. - It goes through
memberPullRequestWrite, thenpullRequestViewedStates, a GraphQLfiles { path viewerViewedState }read in pages of 100, up to the 300 files a changes read lists. - Answers:
{ outcome: "done", files: [{ path, state }] }, where state is viewed, unviewed, or dismissed (viewed but changed since)sign-in-required, when the member has no usable sign-in, or GitHub refuses their token (401)refused, with GitHub's words- no files, when the agent has opened no pull request
- New route file
- Write: two new actions on the existing POST
pull-request/action:mark-file-viewedandunmark-file-viewed, each with a boundedpath, sent asmarkFileAsViewed/unmarkFileAsViewed. - The new read flows through the gateway (
codingAgents.pullRequestViewed), the host (compose-coding-agents,host-operator), and a panel-only act router entry (CODING_AGENTS_PULL_REQUEST_VIEWED).
Window (viewed-files.ts, changes-list.tsx)
- When the marks are read: each time the changes are read, so a refresh re-reads both.
- Viewed box: each file's header ends in a Viewed box (
@luke/uiCheckbox).- A press shows at once. If GitHub refuses it, the mark reverts and the line under the bar gives GitHub's reason, or offers sign-in, using the same
PullRequestWriteNotethe tab's other changes use. - Marks for one file go to GitHub one at a time, so GitHub applies them in the order pressed and the last press wins. Every mark GitHub accepts is recorded at once, so a refusal shows what GitHub holds. A press made after a refused one is still sent.
- A read asked before a mark settled never undoes it. A read that doesn't answer hides the boxes rather than keep marks it can no longer vouch for.
- A file viewed and then changed (GitHub's dismissed) reads as not viewed, the way GitHub unticks it.
- A press shows at once. If GitHub refuses it, the mark reverts and the line under the bar gives GitHub's reason, or offers sign-in, using the same
- Progress: the count line under the tree, or under the list while the tree is hidden, adds
· N / M viewed. - Folding:
- Viewed files start collapsed, as in stage-cli.
- Marking a file viewed folds it, and unmarking opens it (deleted files and lockfiles included), whatever fold the developer chose earlier. If GitHub refuses the press, the earlier fold comes back, unless a fold was chosen since.
- Keys:
- V (stage-cli's key) marks or unmarks the selected file and moves to the next.
- Until the marks are read, V isn't the list's key and no box shows.
- No app shortcut uses a bare V.
Goldens
LUKE_UPDATE_FIXTURES=1:
pullRequestActionRequestSchemaandcodingAgentPullRequestActionParamsSchemagain the two new actions, each{ action, path }.- New goldens:
pull-request-write-wire-pullRequestViewedAnswerSchema.jsonandcoding-agent-view-codingAgentPullRequestViewedAnswerViewSchema.json.
PRIVACY.md
Adds one sentence to the Pull request tab paragraph. It says the Viewed boxes are GitHub's own, kept per account; that our service reads and sets them only on the member's own GitHub sign-in, never as the App; and that it stores none of it.
Screenshots
Marks read: two viewed files folded, 2 / 15 viewed | V on the selected file, then on to the next |
|---|---|
![]() | ![]() |
Evidence
tsc: desktop, service, features, host, all pass.pnpm lint,pnpm knip,pnpm discarded-effect,./scripts/repository-checks.sh(rewrites and api-callers included): all pass.- Tests:
coding-agents-app.test.ts"changing an agent's pull request": 14 passed, 2 new.- A teammate reads every page of their marks, then marks and unmarks files, all on
Bearer ghu_teammate. - No sign-in, or a 401, gives
sign-in-required, and nothing is tried as the App.
- A teammate reads every page of their marks, then marks and unmarks files, all on
act-router/coding-agent-acts.test.ts: the panel can read the marks; the voice window is refused.compose-coding-agents.test.ts: the gateway method reaches the client.agent-changes-tab.test.ts, 5 new tests:- marks tick their files, count, and fold, and dismissed reads as not viewed
- a box marks at once and folds the file; a refused unmark reverts and shows GitHub's words
- V marks and moves to the next file, and isn't a key before the marks are read
- with no sign-in, the sign-in offer shows and no box does
- Refresh changes re-reads the marks
- presses on one file go out in order and the last one wins
- a stale read never undoes a settled mark, and a failed read hides the boxes
- unmarking a deleted file opens it, and a refused press restores the earlier fold
- an accepted mark stands when the press queued after it is refused
- a press made after a refused one is still sent
- a refused mark keeps a fold chosen since (Collapse all)
hosted-wire-schemas.test.ts: goldens as above.
Invariants
-
./scripts/check.shgreen. (CI) - JSON Schema goldens: changed, explained above.
- Every behavior change has a test.
- Gateway envelope goldens unchanged.
- No new
asassertion. - No
Effect.run*outside the edges. - No new raw async primitives.
- No discarded Effect.
- Test count: +16, listed above.
- No hand-rolled file replaced.
- No
CLAUDE.md/AGENTS.mdsentence names a changed part. -
PRIVACY.mdchanged, as above. - No dependency change.
- Barrel/door rule untouched.
Notes
- #3638 has merged; this PR is restacked on
main.

