feat(panel): the diff bundle draws a whole file, and notes under one patch's lines

Merged
DA
7d ago
+590-5

Summary

This is PR 1 of the approved plan Pierre for File tabs and code snippets (plan page). It adds the two capabilities the rest of the stack needs to the lazy diff bundle (packages/panel/src/diff-view/). Nothing in the app calls them yet, so nothing visible changes.

  • mountFile(host, props) draws a whole file with no diff, as the File tab, a page's source and the text-attachment viewer will (PR 2 and PR 5).
    • How it draws: Pierre's CodeView holds one type: 'file' item. Only the lines near the view are drawn, whatever the file's length. Colours come from the same kept worker pool as Files changed (#3752), and the file shows as plain text until they arrive.
    • Cache key: the file's name, revision and text pick its key in the pool's cache, so reopening the same file at the same revision gets its colours straight away. A count names the key, never the identifiers, per the TypeScript rules in AGENTS.md.
    • Lit lines: lit uses Pierre's own selected lines, the same call Files changed uses to light a thread's lines. The bundle sets them with notify: false, so Pierre doesn't report them as the reader's pick.
    • Notes: each annotation goes under its line, in the element the view keeps for it (renderAnnotation(key)), as the list does.
    • pickedLines(): returns the lines picked by pressing their numbers (GitHub-style, with ⇧ to extend). Otherwise it returns the lines a text selection in the shadow root covers. ⌥⌘K will use this.
    • revealLines(range): centres a range at once.
    • Find and screen readers: the shadow host carries FIND_SHADOW_ATTRIBUTE, so the tab's find reads it and skips the gutter. The gutter is marked aria-hidden, so a screen reader doesn't read every number and then every line.
    • Trailing newline: a text that ends in a newline draws no extra empty line.
    • onDrawn: called whenever Pierre draws rows, so a find source can repaint its matches (PR 2).
  • mount takes annotations and renderAnnotation again. #3304 removed them. The Code tab and inbox cards (PR 4) use them to put Luke's note under a snippet's last line.

No service change.

Measured

This is the same throwaway harness as the plan, now running the built diff-view.js's mountFile, with the CSSOM inline-style path and the build's worker. Setup: a 5,000-line TSX file in headless Chromium at 1280×800, a 720px pane, median of 3. Today's numbers come from today's CodeLinesView given every window up front with host colours. Both columns were measured in the same session.

today (windows, every line read stays drawn)mountFile
Fling (400 px/frame, 4 s): long tasks · longest frame · frames705 ms · 117 ms · 2070 ms · 16.8 ms · 242
Find open (all 5,000 drawn): long tasks on load1,121 msn/a (PR 2 searches the text)
Long tasks on load183 ms0 ms
First lines painted, bundle import included64 ms149–169 ms (plain)
Lines in the DOM500 → 5,00054–67
With 20 notes, wrapped, fling—0 ms · 16.8 ms · 242

First colour took about 3.3 s on this session's slower sandbox. That matches the plan's prototype measured in the same session (3.4–3.5 s), so the real bundle adds nothing on top of Pierre. The developer accepted late colours on big files.

renderer/diff-view.js: 243,477 → 245,160 gzipped bytes (+1.7 KB), inside the budget's 5% slack. panel.js is unchanged.

Tests

New file diff-view/whole-file.test.ts (8 tests). Pierre draws in jsdom on the main thread:

  • A whole file is numbered from line 1, with no empty line after its final newline.
  • Its numbers are left out of the find and of what a screen reader reads.
  • Lit lines are lit, other lit lines replace them, and none remain once cleared.
  • Each note is drawn in the view's element under its line, and moves as the notes change.
  • A file whose text changes draws its new lines.
  • No lines are picked until the reader picks some.
  • Lines picked by pressing their numbers (with ⇧) are the reader's pick, until the view lights others.
  • A patch drawn alone draws each note in the view's element under its line.

The four stand-in bundles in the tab tests gain mountFile.

Evidence

  • tsc --noEmit (panel): clean.
  • ./scripts/oxlint.sh: 0 warnings, 0 errors.
  • biome check: clean.
  • knip: no new findings.
  • ./scripts/repository-checks.sh: passed.
  • vitest: src/diff-view plus the changes, chapters, pull request and themed-lines tab suites, 14 files, 235 tests passed.
  • apps/desktop build: passed.

Invariants

  • Every behavior change has the test that fails without it; no change-detector test.
  • No new as type assertion, raw async primitive, or Effect run outside the edges.
  • PRIVACY.md unchanged: the file view's root stays under the File tab's ph-no-capture (PR 2).

Made with Luke