feat(desktop,service,features,host): sync Files changed's Viewed boxes with GitHub as the member

Merged
DA
3d ago
+1467-34

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.ts and a segment rewrite; api-rewrites.json and vercel.json are regenerated.
    • It goes through memberPullRequestWrite, then pullRequestViewedStates, a GraphQL files { 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
  • Write: two new actions on the existing POST pull-request/action: mark-file-viewed and unmark-file-viewed, each with a bounded path, sent as markFileAsViewed / 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/ui Checkbox).
    • 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 PullRequestWriteNote the 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.
  • 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:

  • pullRequestActionRequestSchema and codingAgentPullRequestActionParamsSchema gain the two new actions, each { action, path }.
  • New goldens: pull-request-write-wire-pullRequestViewedAnswerSchema.json and coding-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 viewedV on the selected file, then on to the next
darklight

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.
    • 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.sh green. (CI)
  • JSON Schema goldens: changed, explained above.
  • Every behavior change has a test.
  • Gateway envelope goldens unchanged.
  • No new as assertion.
  • 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.md sentence names a changed part.
  • PRIVACY.md changed, as above.
  • No dependency change.
  • Barrel/door rule untouched.

Notes

  • #3638 has merged; this PR is restacked on main.