Skip to content

Move the fold marker left, and make the ? pane's keys the bright thing - #17

Merged
navbytes merged 3 commits into
mainfrom
claude/message-summarization-feedback-4lpb90
Sep 9, 2026
Merged

navbytes merged 3 commits into
mainfrom
claude/message-summarization-feedback-4lpb90

Conversation

@navbytes

@navbytes navbytes commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Two changes from reading a real screen of beta.2, plus a TUI-UX review of the ? pane.

1. The fold marker moved to the row's left edge

before   │ ● user: yes build it   ▸ 38 steps · ~23.5k                      ~23.5k
after    │ ● ▸38 user: yes build it                                        ~23.5k

▸38 now sits between the and the text: the row's own disclosure control, where an outline puts one. Two things were wrong with the trailing digest:

  • The caret was nowhere near the it belonged to, so on a tree of folded turns nothing marked a collapsed row until you had read to the end of a preview that is usually clipped.
  • ~23.5k was the row's own token column again. applyFolds rolls the hidden steps' tokens into the turn, so the two figures are equal by construction — two numbers of the same magnitude a few columns apart that always agree read as one number that has gone wrong.

foldDigest becomes foldMark (the caret and the count, on the left) and foldFlags ( 1✂ 2⚠ 1✗, at the end of the text, in the same place and glyphs a step row already uses for its own). The tokens are printed once. No unit word — a bare count beside a caret is unambiguous, and "steps" was six columns the preview wanted.

2. The ? pane

Its colour was exactly inverted

route.tsx coloured whole lines, keyed on whether the line was indented:

<text fg={l.startsWith(" ") ? t.textMuted : t.accent}>{l}</text>

Every line that names a key is indented — so the headings, which name no keys at all, were the one accent-coloured thing on the pane, and every key was muted. Someone opening ? to find one key had no token-shaped thing to scan for and had to read the pane at reading speed.

The pane is now a list of typed segments (key, name, label, heading, glyph, strong, text) that the route colours individually. Keys are the brightest and the only bold thing; headings accent; prose muted; the gutter always dim instead of taking its line's colour. helpLines() remains as the joined-string API the drift tests assert against.

Act and Views became the table they already were

Every row in them was already <key> <name> — <purpose> in a consistent order. The " — " is now two columns aligned at render time, so you can enter from either side — scan the keys if you think gm, the names if you think "merge". Widths are measured per section, so one f12 rebound in Act cannot shift Legend, and a key past 8 columns takes its own row ragged rather than pushing its whole section right.

Move deliberately keeps its packed clauses: tabulating twenty motion clauses would cost twenty rows to make vim's own keys — the ones this audience already has — the most prominent thing on the pane. It gets a dim static label column instead (rows / jumps / folds / search), as does Legend (rows / tree / cost / status / / lanes). Those labels are static strings, so no rebind can break their alignment.

Nine lines were clipping at 100 columns with the width test green

A help row is inside a padding={1} box, so its budget is cols - 4 — which is what every other line on the screen uses — but the test asserted ≤ 112 against the raw string. One line clipped even at 112. The pane is now laid out for the terminal it is on: clauses drop from the right, prose clips with , nothing wraps (§7.6), and the same 34 rows survive from 100 columns down to 44.

Smaller

  • The footer names the section you are in (12–29 of 34 · Act · PgUp/PgDn scroll · q/esc back) — the pane is a three-page scroll on a short terminal and its headings scroll off. Zero extra rows.
  • The legend draws its colours instead of spelling them: █ open · █ squashed · … in the branch colours, lane swatches in the lane colours. Naming a colour in muted grey cost six columns and asked the reader to take a monochrome word's word for it; showing it costs one — which is what let the pane's two longest lines fit.
  • Glyphs take the tree's own colours ( warning, error, ◆ ≣ ✂ accent); nothing here rewrites your transcript is bold as the safety claim it is; the *model* markdown asterisks became a bold run.

Net: 34 rows before, 34 after.

Two bugs found on the way — both caught by the pane's e2e while the unit suite stayed green

  • A runtime import of @opentui/core broke the whole route. Using TextAttributes.BOLD added a runtime import the plugin had never had (peer dep only), and the host then failed to load the route at all — /tree simply never opened. The two bits are written out as constants, pinned against the real enum by a test that also asserts the built bundle imports only @opentui/solid.
  • A createMemo in the temporal dead zone. Moving HELP to read size() left it above its own dependency's declaration; a createMemo body runs the moment it is created, so it threw on every render.

Tests

  • test/help.test.ts — rewritten against the segment model. The drift guard is tightened in the direction that matters: every bound verb must appear inside a key segment, not merely somewhere in the line's text, since a key that leaks into a prose run is invisible again. Mutation-checked: folding one key back into a prose run fails 6 tests. Width is asserted against the row budget at four widths (96 / 76 / 56 / 40), and narrowing must never drop a row.
  • test/fold.test.tsfoldMark / foldFlags, including that the marker never repeats the token column.
  • test/e2e/tui.test.ts — the fold test asserts the marker is between the and the preview and is not a digest; the ? pane test asserts the new table shape and that the footer names a section.

bun run typecheck, bun test (462 pass) and bun run build are clean. The full pty e2e suite was still running when I opened this; I'll report on the PR if anything comes back red.

Known gap

docs/screenshots/help.png, linked from the README, now shows the old pane. It needs a fresh capture on a real terminal — I can't produce a PNG from here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VkCWU9Ab6wYHmTke8vuVMv


Generated by Claude Code

A folded turn now reads `● ▸6 T5 add a retry to the flaky test  1✗ 2⚠`.
The `▸6` sits between the glyph and the text, which is where an outline
puts a disclosure control.

It used to trail the preview as `▸ 6 steps · ~12k · 1 ✗ · 2 ⚠`, and on a
real screen that failed twice over. The caret was nowhere near the ● it
belonged to, so on a tree of folded turns nothing marked a collapsed row
until you had read to the end of a preview that is usually clipped. And
the `~12k` was the row's own token column again: `applyFolds` rolls the
hidden steps' tokens into the turn, so the two figures are equal by
construction, and two numbers of the same magnitude a few columns apart
that always agree read as one number that has gone wrong.

So `foldDigest` becomes two: `foldMark` (the caret and the count, on the
left) and `foldFlags` (` 1✂ 2⚠ 1✗`, at the end of the text, in the same
place and the same glyphs a step row already uses for its own). The
tokens are printed once, in the column that already exists.

No unit word on the marker: a bare count beside a caret is unambiguous,
and "steps" was six columns the preview wanted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VkCWU9Ab6wYHmTke8vuVMv
The pane's job is to let someone find one key without reading, and its
colour was exactly inverted for that: the route coloured whole lines,
keyed on whether the line was indented, so headings — which name no keys
at all — were the one accent-coloured thing on screen and every line
that named a key was muted. A user looking for "how do I branch" had no
token-shaped thing to scan for and had to read at reading speed.

So the pane is now a list of typed segments (key, name, label, heading,
glyph, strong, text) that the route colours individually. Keys are the
brightest and the only bold thing; headings accent; prose muted; the │
gutter always dim rather than taking its line's colour. helpLines() is
still there as the joined-string API the drift tests assert against.

Act and Views become the table they already were: every row in them was
`<key> <name> — <purpose>` in a consistent order, so the em dash is now
two columns aligned at render time and you can enter from either side —
the keys if you think `gm`, the names if you think "merge". Widths are
measured per section, so one f12 rebound in Act cannot shift Legend, and
a key past 8 columns takes its own row ragged instead of pushing its
whole section right.

Move keeps its packed clauses on purpose. Tabulating twenty motion
clauses would cost twenty rows to make vim's own keys — the ones this
audience already has — the most prominent thing on the pane. It gets a
dim static label column instead (rows / jumps / folds / search), as does
Legend (rows / tree / cost / status / ⎇ / lanes); static strings, so no
rebind can break their alignment.

Nine of the pane's lines were clipping at 100 columns with the width
test green, because a help row is `│ ` inside a padding-1 box and its
budget is cols-4, while the test asserted ≤112 against the raw string.
One line clipped even at 112. The pane is now laid out for the terminal
it is on: clauses drop from the right, prose clips with …, nothing
wraps, and the same 34 rows survive down to a 44-column terminal.

Also: the footer names the section you are in, since the headings scroll
off a short terminal; the legend draws its colours instead of spelling
them in grey, which is what let the two longest lines fit; glyphs take
the tree's own colours.

Two bugs found on the way, both by the pane's e2e while the unit suite
stayed green. Importing TextAttributes from @opentui/core added a
runtime import the plugin had never had — peer dep only — and the host
then failed to load the route entirely; the two bits are written out and
pinned against the real enum by a test that also asserts the bundle
imports only @opentui/solid. And moving the HELP memo to read size()
left it above its own dependency's declaration: a createMemo body runs
the moment it is created, so it threw on every render.

The drift guard is tightened in the direction that matters: every bound
verb must appear inside a `key` segment, not merely somewhere in the
line's text, since a key that leaks into a prose run is invisible again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VkCWU9Ab6wYHmTke8vuVMv
`ctx ▓░░░░ 84.7k/1M · low · 99% cached` was one band-coloured run — the
label, the bar and the numbers all in `success` green — in both the tree
header and the prompt line, and it was reported illegible on a light
theme.

That it read at all on a dark theme was luck. A theme guarantees `text`
and `textMuted` are readable on its own background; it guarantees
nothing of the kind about `success` / `warning` / `error`, which it
picks to be distinguishable from *each other*. So the whole gauge's
legibility rested on a colour no theme promises you can read.

The rule now is: the bar carries the band, readable text never does.
`ctx` is muted, the numbers and the band word take the theme's own text
colour, and the bar splits muted-cached / band-fresh / subtle-empty. A
filled bar survives a lower contrast ratio than glyph strokes do, and
its colour is the signal rather than a decoration on text that already
says the same thing.

It lives in core/gauge.ts as a table rather than inline in the JSX,
because the rule is about tokens rather than any theme's hex values and
is therefore testable without a terminal — which is what the gauge's
colours never were.

Three neighbours had the same defect, all fixed the same way (the glyph
carries the colour, the label beside it is read): the prompt line's
`⎇ <branch> · `, which was also a category error since which branch you
are on has nothing to do with the context band; its `▲ +24% (bash)`
trend; and the sidebar card's `⎇ <branch>` and `✂ 2 crops · ~14k
hidden`.

test/gauge.test.ts holds the table to the rule, and greps both
components for a readable string inside a band-coloured element — the
shape that shipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VkCWU9Ab6wYHmTke8vuVMv
@navbytes
navbytes merged commit e5c3e20 into main Sep 9, 2026
1 check passed
@navbytes
navbytes deleted the claude/message-summarization-feedback-4lpb90 branch September 9, 2026 15:01
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.

2 participants