feat(desktop): list every file in Files changed, with a filter, collapse all, and stage-cli's keys

Merged
DA
1d ago
+1626-304

Summary

Files changed extras, E1 of the "Files changed extras" stack (based on #3245). Asked for: stage-cli's file filter, collapse / expand all, and keyboard navigation between files in the Pull request tab's Files changed pane, working in the unified view below 640px. stage-cli's Files changed is a list of every file, which is what collapse all and its keys act on, so the pane changes from one file at a time to that list (the orchestrator approved this shape).

  • One list of every file, drawn by Pierre's own CodeView (diff-view/index.ts's new mountList).

    • Each file has a sticky header: a fold chevron, the A/M/D/R letter, its path (old → new for a rename), and +/−.
    • Only files near the view are drawn and highlighted.
    • Files are listed in the tree's order (folders first, as Pierre's tree sorts), so J/K follow the tree's rows.
    • A file with no hunks (binary, too large, renamed with no changes) is drawn as what stands in their place: why it has none, and any of its review threads, across its width.
      • Pierre draws and measures file-level notes only above a line. So the bundle hands such a file one empty line (DiffListFile.lead), and hides that line, the old side, the gutter and the hatching in the shadow root.
      • The header stays one row. Pierre reserves a fixed height for it and keeps it sticky, so anything taller would throw off every file's position below it.
  • Filter. The tree's "Search files" field is the filter, as stage-cli's sits on its file picker.

    • The tab holds the query, so it narrows the list and the tree together, by path, ignoring case, matching exactly what the tree's search matches.
    • Choosing a file the filter leaves out, from the bar's crumbs (which list every changed file), clears the filter and scrolls the list to that file.
    • While the tree is hidden, a line over the list names the filter and offers Clear.
  • Collapse / expand all.

    • A bar button, with the ⇧; hint, folds or unfolds every file the filter keeps.
    • Each header's chevron folds its own file (;).
    • Deleted files, lockfiles, and files over 1,000 changed lines start collapsed, as GitHub keeps such diffs folded.
  • stage-cli's keys (changes-list.tsx):

    KeyDoes
    J / Knext / previous file (a first press selects the file at the top)
    ;collapse or expand the selected file
    ⇧;collapse or expand all
    Escclear the selection
    • A key handler on the Files changed view reads them, and an answered press goes no further.
    • J/K keep their Inbox meaning everywhere else in the window (#shared/shortcuts' plain J/K), and keys typed into a field stay the field's.
    • Esc is taken only while a file is selected. Otherwise it reaches the window's layered Escape.
    • ⌘F stays the tab's find.
    • The bar's breadcrumbs, ⋯ (copy name / patch) and the tree follow the selected file, or else the one at the top of the list.
  • Narrow. Below 640px the list is drawn unified, whatever the setting, as before.

  • #3245's threads, moved onto the list.

    • Line threads are each file's annotations, with the same key → element contract; renderAnnotation now takes (path, key).
    • Inline threads keep #3308's Reply / Resolve (ThreadActions); its Files changed test is ported to the list.
    • Threads the diff draws no row for (THREAD_PLACE.ABOVE: outdated, on a line no hunk shows, or on a binary or too-large file) are Pierre file-level notes above the first hunk, on the side they count on.
    • Refresh changes also reads the threads again.
    • The single-file mount is kept for the Conversation's thread previews. It loses only its annotations, which nothing else used.

Also in this PR

The list's styles and #3245's inline thread card now read the --luke-* tokens. #3524 removed the old aliases (--hairline, --section-fill, --space-*, …), and without them the headers lost their borders, which put every file's position 1px off.

Known limit

⌘F finds only what Pierre has drawn. The list is virtualized, so files far from the view aren't searched until they come near, as with GitHub's own lazily drawn diffs. Every drawn file's shadow root is marked for the find, and new files are picked up as they scroll in.

Follow-up: search undrawn patches through the list's data.

Bundle budget

renderer/diff-view.js grows from 142,443 to 178,690 gzipped bytes (+36.2 KB), for Pierre's CodeView and virtualizer. That bundle is lazy (loaded with the first diff shown), and only its baseline is re-recorded.

Data cost

No new requests. readChanges already carries every file's patch: up to 300 files, and a patch over 100k characters arrives with no body. Pierre highlights only the files near the view, and a collapsed file draws only its header.

Screenshots

From a throwaway harness (fake window.luke, real diff bundle and stylesheet), since deleted.

Light, J pressed (selected file outlined)Dark, an inline thread and a binary file's notice
lightdark
Collapse all (⇧;), one file opened againFiltered by the tree's search
collapsedfiltered

Narrow (600px, tree hidden, split setting on, drawn unified):

narrow

Evidence

  • pnpm check:affected: typecheck passed. The lint step first failed on formatting and an unused suppression, both fixed. Re-run by hand after the fixes:
    • pnpm lint: 0 errors
    • pnpm knip: passed
    • pnpm discarded-effect: passed
    • ./scripts/repository-checks.sh: passed
    • pnpm exec vitest run src/renderer/tasks/ src/shared/ src/renderer/diff-view (apps/desktop): 83 files, 707 tests passed
  • New tests:
    • agent-changes-tab.test.ts:
      • every file is handed over in the tree's order
      • a file with no hunks says why in its header
      • deleted files and lockfiles start folded; a file's fold and Collapse all / Expand all turn them
      • the tree's search narrows the list, said over it while the tree is hidden, until cleared
      • J/K, ;, ⇧; and Esc inside the view
      • J outside the view still runs the Inbox's command, and inside it only steps files (fails without the press being stopped)
      • a key typed into the search is the field's
    • changes-model.test.ts: tree order, filter, start-collapsed rules.
  • Changed tests: the existing Files changed tests now read the list's props. The #3245 thread tests (agent-changes-tab.test.ts, agent-pull-request-tab.test.ts) read per-file annotations.
  • LUKE_UPDATE_BUNDLE_BUDGET=1: diff-view.js only, as above.

Invariants

  • ./scripts/check.sh green. (CI)
  • JSON Schema goldens unchanged.
  • Every behavior change has, in this PR, the test that fails without it; no change-detector test added.
  • Gateway envelope goldens unchanged.
  • No new as type assertion.
  • No Effect.run* outside the edges.
  • No new raw async primitives.
  • No discarded Effect.
  • Test count: +9 in agent-changes-tab.test.ts and changes-model.test.ts (listed above).
  • No hand-rolled file replaced.
  • No CLAUDE.md/AGENTS.md sentence names a changed part.
  • PRIVACY.md unchanged. The list stays under ph-no-capture, and every word in a header is the repository's.
  • No dependency change.
  • Barrel/door rule untouched.

Notes

  • #3245 has merged, and this PR is restacked on main.
  • #3245 merged with the retired tokens still in .diff-inline-thread, .diff-note and .diff-outdated-threads. This PR's token commit fixes those rules on main.