Skip to content

fix(panel): every gap in Files changed opens alike: its words show all of it, its arrows twenty lines - #4111

Merged
luke-agent[bot] merged 3 commits into
mainfrom
luke/consistent-gap-expand
Oct 20, 2026
Merged

luke-agent[bot] merged 3 commits into
mainfrom
luke/consistent-gap-expand

Conversation

@dastratakos

@dastratakos dastratakos commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The developer said:

I don't like how the hover states are different for expanding lines - If there are a lot of lines then we do an underline and otherwise we highlight the arrow. Let's make the behavior consistent: clicking on the number itself will expand everything no matter how many lines there are. Otherwise the arrow is used for expanding a couple lines at a time, like how it is right now

Every gap between hunks in Files changed now opens and hovers the same way:

  • The words ("N unmodified lines") open the whole gap. This holds on every gap: short or long, at the file's top, between two hunks, or at its foot. The words carry Pierre's own data-expand-all-button mark, so Pierre's own click handler opens them with expandHunk(i, "both", ∞). The trailing "More unchanged context may be available" words read the file first, then open it to its end. Files that can't expand keep feat(desktop): expand context lines in Files changed #3762's behaviour: no arrows, and the words are unmarked and can't be clicked.
  • The arrows open 20 lines in their direction, or all of a short gap, as before. ⇧-click still opens all of it, but the tooltip no longer mentions it.
  • One hover style:
    • An arrow fills its own cell.
    • The words get a pointer cursor and lift their ink to the code's own colour (--diffs-fg, the same ink the arrow lifts to). There is no underline; Pierre's underline on the content area is overridden.
    • style(desktop): draw Files changed's unmodified-lines band as one surface #3833's pass-through hack is removed (pointer-events: none on the words, and the arrow's full-width ::after). The band itself reacts to nothing.
  • Keyboard:
    • The words and each arrow are role="button" with tabindex="0" and an aria-label. Enter or Space presses them the way a click does, ⇧ included.
    • After Pierre redraws the file, focus returns to the same control, as long as its gap still stands.
    • The focus ring is the window's own ring, drawn inside the shadow root, and only when the keyboard moved focus (:host-context(:root:not([data-keyboard="true"]))).
  • Tooltips take their text from the control's name. The view shows them on hover and on keyboard focus:
    • Words: "Show all N lines", or "Show the rest of the file" on the trailing gap.
    • A long gap's arrow: "Show 20 more lines".
    • A short gap's arrow: "Show all N lines".

How

  • diff-view/gap-controls.ts (new): Pierre rewrites a file's rows through innerHTML on every draw, so a MutationObserver on each file's shadow root puts the marks back before paint. It is scoped to stay cheap while scrolling:
    • It skips records under [data-content] (the code rows, which are the bulk of every write) and records with no added nodes.
    • It reads only the columns that were written anew.
    • It marks only the gaps that are seen ([data-gutter] [data-separator][data-expand-index]). The hidden copies among the code rows are left alone.
  • Watch lifetime: each file's watch stops on Pierre's onPostRender "unmount" phase and starts again when the element draws a file. Every watch stops when the list unmounts.
  • contract.ts: gains GAP_CONTROL_ATTRIBUTE and GAP_CONTROL, which the LOOK_CSS styles and the view's tooltip key on.
  • diff-list.tsx: the hint reads the control's aria-label instead of guessing from Pierre's DOM. It also shows on focusin, and goes away on focusout or a key press. Those are native listeners, so Biome's static-element rule holds.

Screenshots

Taken from the real built diff-view.js (apps/desktop && node scripts/build.mjs), mounted with mountList in a harness in headless Chromium. One file has a top gap, a two-arrow middle gap, and the trailing gap. The window's tooltip is React's own and isn't in this harness; the tests cover it.

light dark
Top gap, words hovered
Top gap, arrow hovered
Middle (two-arrow) gap, words hovered
Middle gap, arrow hovered
Bottom gap, words hovered
Bottom gap, arrow hovered
Keyboard focus, words
Keyboard focus, arrow

In the same harness I checked by hand, counting rows in the real bundle:

  • A middle down-arrow added 20 rows.
  • The middle gap's words then opened the remaining 43.
  • Tab and Enter on the top words opened all 26.
  • The trailing gap's words opened the remaining 37 to the file's end.

Scroll cost

I built diff-view.js from main and from this branch and mounted each with mountList on PR #2908's 122 real patches. This was headless Chromium at 1280×800, scrolling 60 px per frame for 4 s after a 4 s settle, median of 3 runs. Every MutationObserver callback on the page was timed, Pierre's own included.

#2908 (122 files), 4 s scroll main this PR
Long tasks 0 ms 0 ms
Longest frame 17 ms 17 ms
Frames drawn 241 241
All MutationObserver callback time 14.7 ms (195 calls) 17.3 ms (373 calls)

The marking costs about 2.6 ms per 4 s scroll, roughly 0.65 ms per scroll second. This is Linux headless Chromium, not Electron on a Mac, so compare the columns rather than the absolute values.

Evidence

  • pnpm check:affected ran out of memory in the sandbox, as expected, so I ran the steps one by one:
    • tsc --noEmit for panel: clean.
    • vitest run packages/panel/src/diff-view packages/panel/src/tasks: 127 files, 1303 tests passed. The full panel project also passed before the final, CSS-only edits.
    • ./scripts/oxlint.sh: 0 warnings, 0 errors.
    • pnpm knip: clean.
    • ./scripts/repository-checks.sh: passed. It caught a var(--diffs-fg) with no fallback, now var(--diffs-fg, var(--luke-color-text)).
    • biome check on the changed files: clean.
  • Bundle budget: cd apps/desktop && node scripts/build.mjs passes.
  • New tests in diff-view/gap-controls.test.ts run on real Pierre FileDiff in jsdom, with line-info-basic and 20 lines per press:
    • A gap's words open all of it: short and long gaps, at the top, between two hunks (one-arrow and two-arrow), and at the foot. Each fails on main, where the words open 20 lines each way.
    • A long gap's arrows open 20 lines their way, and a short gap's arrow opens all of it.
    • Every gap's words and arrows wear the same data-gap-control, role, and tabindex, and they are named for what they open.
    • A gap opened part way keeps its marks after Pierre draws it again.
    • Enter and Space press a control, and focus comes back to the arrow that was drawn anew.
    • The trailing gap's words read the file once, then open it to its end.
    • A file told it only moved, as the list tells a file whose gaps can't open, marks nothing.
  • Open lines are measured with drawnAmong, the reading feat(panel): review threads move onto lines a gap opens in Files changed #4008 uses to move review threads onto the lines a gap opens. So the threads-move path is checked for every label click.
  • agent-changes-tab.test.ts: the old "⇧-click for all" hint test is replaced by "a gap's control under the pointer or the keyboard's focus hangs its hint, what it opens, and a press lets it go".

Invariants

  • ./scripts/check.sh green: its pieces were run one by one, as listed above, and CI runs the whole of it.
  • 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*.
  • No new raw async primitives in source: the setTimeout is in a test's settle only.
  • No discarded Effect.
  • Test count +8 in gap-controls.test.ts (the eighth is the keyboard case from Devin's review: a long gap pressed from both ends keeps its two arrows and the focus); one hint test rewritten.
  • No hand-rolled file replaced.
  • No CLAUDE.md/AGENTS.md change needed.
  • PRIVACY.md unchanged. The new aria-labels are masked by session replay's attribute list.
  • Deps unchanged.
  • No barrel or door change.

Notes

  • Desktop and panel only, so no Preview run is needed.

Made with Luke

…l of it, its arrows twenty lines

A gap's words now open the whole gap on every gap, short or long, at a
file's top, between two hunks, or at its foot, as Pierre opens a gap from
its own hidden Expand all; the trailing "More unchanged context" reads the
file first and opens it to its end. The arrows keep opening twenty lines
their way, or all of a short gap. The words and the arrows hover alike on
every gap (the words lift their ink, no underline; an arrow fills its own
cell), and the band itself no longer passes the pointer through to an
arrow. Each control is a named button in the tab order, pressed by Enter or
Space, its hint its name ("Show all N lines", "Show 20 more lines").

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
luke-web Ready Ready Preview Oct 20, 2026 4:49pm UTC

Request Review

@luke-agent
luke-agent Bot marked this pull request as ready for review October 20, 2026 16:32
@luke-agent
luke-agent Bot enabled auto-merge October 20, 2026 16:32
devin-ai-integration[bot]

This comment was marked as resolved.

…its arrows and the focus

Co-authored-by: luke-agent[bot] <339870089+luke-agent[bot]@users.noreply.github.com>
…xpand

# Conflicts:
#	packages/panel/src/diff-view/index.ts
#	packages/panel/src/tasks/diff-list.tsx
@luke-agent
luke-agent Bot added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit f843412 Oct 20, 2026
12 checks passed
@luke-agent
luke-agent Bot deleted the luke/consistent-gap-expand branch October 20, 2026 16:59

This branch was successfully deployed

1 active deployment
Preview — 9935ea8f Deployed Oct 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant