feat(desktop): expand a whole file from its header, and show a file that only moved as its lines

Merged
DA
1d ago
+471-11

Summary

Part of the stage-cli features the developer chose: expand or collapse a whole file, as stage-cli's file header does (file-diff-list.tsx:522, lib/file-expansion-context.tsx, lib/full-file-preview.ts). Built on #3762's readTexts and stacked on the header actions PR.

  • Expand full file / Collapse to changes.
    • A compact header button (UnfoldVertical/FoldVertical, with a pressed state and a tooltip) draws the whole file, every unchanged line among its changes, until it's pressed again.
    • It's offered only on files #3762 lets expand: changed in place or moved with changes, a patch drawn, not a secret path.
    • It's also offered on files that only moved. It's never offered on images drawn as pictures.
  • How the file gets drawn. Pierre 1.5.2's CodeView only has a list-wide expandUnchanged. So instead of driving expandHunk across a file's hunks, the item's fileDiff is replaced with parseDiffFromFile(before, after, { context: MAX }): one hunk holding every line.
    • It's cached per texts object, so redrawing a file for its notes keeps the same diff.
    • Collapse re-parses the patch fresh, which returns the file to its changes, gaps closed.
    • It survives virtualization by construction: the item's diff is the whole file, so scrolling away and back draws it the same. I checked this in the harness: lines 1… are still drawn after scrolling 30,000 px away and back.
  • Reading the file. It goes through the same door as the gaps (FileTexts.read): one TASKS_FILE read at headSha, blobSha-checked, kept for the answer's life.
    • Nothing is read until the button is pressed, and a file whose gaps were already opened isn't read again.
    • A refused read leaves the file as it is and shows the refusal line, as a gap's refusal does.
  • A pure rename (no hunks) still says "renamed without changes" at first. Once expanded, it shows its whole text as unchanged lines (textsOf treats the text before as the text after).
    • stage-cli shows these up front, because its CLI sends both sides of every file.
    • Here that would be one GitHub read per renamed file on every open, so it's behind the same button.
    • The note itself offers it too: "File renamed without changes to its contents. · Show file", a link-toned TextButton that runs the same toggle.
  • Threads stay anchored, since annotations are keyed by side and line. Commenting stays limited to lines GitHub accepts (commentLines is unchanged).

Screenshots

PR #2908's build.mjs in the harness (Chromium).

lightdark
the button's hint
expanded, lines from 1
a pure rename's Show file (PR #2855)

Evidence

  • tsc --noEmit for panel and desktop: clean.
  • ./scripts/oxlint.sh: 0 errors.
  • pnpm knip: clean.
  • ./scripts/repository-checks.sh: passed.
  • vitest run --project panel --project desktop: 231 files, 2366 tests passed, run with this change in the tree.
  • New test in agent-changes-tab.test.ts:
    • An added file is offered nothing; a file that only moved starts as its notice.
    • Expanding hands the bundle whole texts. For the moved file, pressed through its note's Show file, before equals after and it's no longer a lead.
    • Collapse to changes drops whole.
    • Re-expanding reads nothing new: two files, two reads.

Invariants

  • Checks green locally, as listed above.
  • JSON Schema goldens unchanged.
  • Behaviour tested.
  • No new as.
  • No raw async primitives.
  • Test count +1.
  • Deps unchanged.

Made with Luke

Review fixes (Devin)

  • A pure rename shown whole drew nothing: two identical texts diff to no hunk. diff-view/whole-diff.ts now lays such a file out as one context hunk. A file with no lines keeps its notice.
  • Threads on a file drawn whole are placed against every line of its texts, not just the patch's rows. This covers a pure rename and threads outside the hunks of an expanded file.
  • A stale whole read (from before a refresh) is dropped instead of overwriting files expanded since.