feat(desktop): mark files viewed and count their threads and lines on Files changed's tree rows

Merged
DA
2d ago
+571-33

Summary

T1 of the "File tree: viewed and counts" stack, which brings stage-cli's tree rows to Files changed. Each file row in the tree beside Files changed now ends in:

  • Comment count: a speech mark with how many review threads are on the file. Like stage-cli, every thread counts, resolved or not.
  • Lines added and removed: a compact +N −M, shown only where the row has room for it.
  • Viewed toggle: a circle that writes through the same useViewedFiles door the header's Viewed box and V use, so it shows pending and refusal the same way. A press shows at once. If GitHub refuses, the mark goes back and the write note under the bar says why. Marking a file viewed folds it in the list, as the box does.
    • The toggle doesn't take room of its own. On a hovered or keyboard-focused row, it sits over the end of the marks, in their place.
    • A viewed row always shows its check in that same place, and the lane keeps room for it so the name is cut before the check rather than under it.
    • Pressing the toggle doesn't open the file or move the tree's keyboard focus.
  • Dimming: viewed rows are dimmed, except the file currently shown.

Folder rows are unchanged. No new GitHub request: the data comes from the viewed read, the review threads, and the changes answer.

Stacked on #3794.

How it's drawn

  • Contract extended, not drawn over. FileMark (file-tree/contract.ts) gains optional lines, threads and viewed, and FileTreeProps gains onViewed. The tree bundle draws them through Pierre's own renderRowDecoration, as one run of four parts in a fixed order. The new file-tree/row-marks.ts holds the decoration, the sheet that styles it, and how a press is matched to the toggle.
  • The name wins. The lane only gets the room the name leaves, as Pierre lays it out, so a mark never cuts a name short. Pierre cuts a name only when the row itself is too narrow, using its own middle ellipsis that keeps the extension. Where the leftover room runs out, the run gives up its parts from the end: −M, then +N, then the count's number, then the speech mark. The run is one line of a wrapping flex box with its second line clipped.
  • CSP fix along the way. Pierre writes unsafeCSS into an inline <style>, which the panel's style-src 'self' refuses. So the tree's existing thin-scrollbar rule never applied. The tree's rules are now a constructed sheet adopted by its shadow root, which the policy allows (as for the terminal, terminal/adopted-styles.ts). It's adopted again whenever Pierre moves its element. The thin scrollbar therefore now applies, as its comment always said it would.
  • Redraws. Pierre redraws rows only when the git status changes, so mark() asks it to redraw (setComposition) whenever a viewed mark or count changes.

Screenshots

PR #3645 drawn in the Files changed harness (Chromium, the panel's real CSP and stylesheet). The pointer rests on changes-list.tsx. Four files are marked viewed and six review threads sit on three files.

beforeafter
dark 900
light 900
dark 600
light 600

Tests

  • file-tree/index.test.ts (new) mounts the real bundle and draws real Pierre rows in jsdom:
    • each file row's run and its hint; folder rows and bare marks draw nothing;
    • a press on the toggle marks the file and opens nothing, while a press on the rest of the row opens it;
    • marks handed again redraw the rows;
    • a mark with no viewed draws no toggle.
  • agent-changes-tab.test.ts:
    • the tree is handed each file's lines, thread count (resolved threads included) and viewed mark;
    • the tree's toggle writes mark-file-viewed, ticks the header box and folds the file;
    • no toggle is drawn before the marks are read.

Evidence

  • pnpm check:affected ran out of memory in discarded-effect. Its other steps passed, so I ran the fallback list:
    • tsc --noEmit for panel and desktop: clean;
    • vitest --project panel: 197 files, 2116 tests passed;
    • ./scripts/oxlint.sh: 0 errors;
    • pnpm knip: clean;
    • ./scripts/repository-checks.sh: passed;
    • biome check: no errors.
  • cd apps/desktop && node scripts/build.mjs: passed, bundle budget included.

Invariants

  • Checks green locally, as listed above.
  • JSON Schema goldens unchanged.

Made with Luke