chore(tui): give the interface one palette, one grid and one glyph set - #54
Merged
Conversation
The TUI had drifted into four visual systems that each looked correct on their own and only disagreed with each other. Colour came from five places: `styles/theme.ts`, a separate markdown palette on One Dark hues, `styles/syntax.ts` on a third set, an orange `#fb923c` selection row hardcoded into all four pickers, and bare `color="green"` ANSI. One screen could show ten hues from four unrelated systems, and nothing failed — every one of them was a valid colour that rendered fine alone. The surfaces actually painted (`#000000`, `#101010`, `#1a1a1a`, `#1e1e1e`) matched none of the `bg*` tokens, so `dimHex` faded toward a colour nothing drew. The transcript ran four indent conventions at once, so no row shared a left edge with any other and the assistant's label sat two columns outside its own prose. `toolGlyph` returned five marks mixing ASCII punctuation, an arrow and a maths operator, alongside four more used elsewhere. Tokens are now the only way a colour enters the interface, and `styles/tokens.test.ts` fails on a raw hex or a named ANSI colour anywhere outside `styles/`. A directory walk rather than `git ls-files`, because the tracked-files sweep skips exactly the new untracked component where a fresh literal would appear. Two things the change turned up that the eye would not. The dialog border added here to lift a panel off the canvas measured 1.78:1 against that panel, below the 3:1 WCAG 1.4.11 asks of a UI boundary — and since `bgElevated` sits only 1.14:1 above `bgCanvas`, the border is the whole separation, so it defeated its own purpose. Hence `borderElevated` and `styles/contrast.test.ts`. And the border costs two rows, which made a 6-row terminal unable to hold a dialog at all; it now degrades like the hints and the label already do. The composer fix is unrelated to the palette but was found the same way: it rendered the literal " Google" while ignoring the `providerName` prop it was handed, so it claimed Google on every provider, with no separator.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Ink resolves interactivity as `interactive ?? (!isInCi && stdout.isTTY)`. The test's capture stream reports `isTTY`, so on a developer machine ink wrote a frame on every render and reading it before unmount worked. Under CI the `isInCi` half of that expression flips, ink batches instead, and nothing reaches the stream until unmount — so all four assertions read an empty frame and the file failed on both runners. Nothing local caught it. It passed the reverse-order sweep, four concurrent suites, and a clean-room worktree, because every one of those still had `isInCi` false. `CI=true bun test` reproduces it in one command, which is the run that was missing. `prompt.shape.test.tsx` already passed the flag explicitly and was unaffected; this brings its neighbour in line rather than inventing a new harness.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The TUI had drifted into four visual systems that each looked correct on their own and only disagreed with each other. Nothing failed, because every part of it was individually valid — which is why it survived.
Colour came from five places.
styles/theme.ts, a separate markdown palette on One Dark hues,styles/syntax.tson a third set, an orange#fb923cselection row hardcoded into all four pickers, and barecolor="green"ANSI in the status bar and approval footer. One screen could show ten hues from four unrelated systems. Separately, the surfaces actually painted —#000000,#101010,#1a1a1a,#1e1e1e— matched none of thebg*tokens, sodimHexfaded toward#0a0a0a, a colour nothing ever drew.Nothing shared a left edge. In one transcript: user rows at column 1, the
Woopcodelabel at column 1 with its body at column 3, tool rows at column 3,CommandBlock's rail back at column 1. Four conventions, no gutter constant, and the speaker floating outside its own prose.Nine families of glyph.
toolGlyphreturned five marks mixing ASCII punctuation, an arrow and a maths operator, alongside⊘ ✗ ▪ ⊙ │ ▐ ❯ #elsewhere.For the record,
theme.tsopened "inspired by OpenCode's dark design system" andtimeline.tsxcarried "Left accent bar — OpenCode style". That is borrowed chrome by its own comments.What
theme.tsis the only file with a colour literal. Surface tokens (bgCanvas/bgElevated/bgInset/bgCode) name what is actually painted;selection*tokens replace the orange;markdownColorsand all 30 entries ofsyntaxThemederive from tokens;dimHexretargets tobgCanvas. OneprimaryRampreplaces the accent ramp that was written twice.TimelineRowwithTRANSCRIPT_GUTTER, so the transcript has a single spine.·; the label already says Read/Edit/Grep. Marks are kept only for running, blocked and failed, where the state is not written anywhere else on the row.bgElevatedalone cannot say where a panel stops." Google"while ignoring theproviderNameprop it was handed — it claimed Google on every provider, with no separator. Capability chips drop their#. The header stops printing the app's name directly above the ASCII wordmark of the same name.Two things the change turned up that reading the diff would not:
The border I added was too dark to do its job.
borderBasemeasured 1.78:1 against the panel, below the 3:1 that WCAG 1.4.11 asks of a UI boundary. SincebgElevatedsits only 1.14:1 abovebgCanvas, the border is the separation, so a border nobody can see defeats the reason for adding it. HenceborderElevated(3.11:1 panel, 3.55:1 canvas) andstyles/contrast.test.ts.The border costs two rows, which made a 6-row terminal unable to fit a dialog at all. It now degrades like the hints and the label already do, rather than being paid for out of the list it sits around.
The guard is
styles/tokens.test.ts: it fails on a raw hex or a named ANSI colour anywhere outsidestyles/. It walks the directory rather than usinggit ls-files, because the tracked-files sweep skips exactly the case that matters — a new, not-yet-added component carrying a fresh literal. That is checked, not assumed.Scope deliberately excluded: no theming system, no light mode, no new UI concepts. This tightens what exists.
Verified
bun run verify --stagedover the 29 files, and the pre-commit hook independently:Because the working tree also held unrelated in-flight work, the commit was re-checked in a worktree containing
origin/mainplus this commit and nothing else:Also run there: the reverse-order sweep (
1527 pass, 0 fail) and four concurrent full suites (1527/0each), which is the load patternpackages/tests/README.mdcalls for.Every new test was proven by reverting its fix and watching it go red, with the mutation grepped to confirm it applied and was removed each time — the assistant indent (3 tests red), a reinstated
#fb923c, a reinstatedcolor="green", the" Google"literal, the unbudgeted border rows (2 tests red), andborderElevateddarkened back to#404040. The vacuity guards were proven too: pointing the token walk at a small directory fails the "finds the files" test while the others pass, and an untracked component carrying#ff00ffis invisible togit ls-filesyet still named by the guard atcomponents/ScratchProbe.tsx:3.Run, not just tested.
bun cli.tswas driven in a pseudo-terminal withpytereconstructing the screen — real PTY, real raw-mode input, realAgentControllerand config loader, pointed at a copy of the config, with no prompt ever submitted. Confirmed against the painted cell attributes: selection isfg=0a0a0a bg=aca3ecand#fb923cappears nowhere; the border isfg=646464with all four corners; the panel isbg=141414; the background behind a dialog paints45415eand5c5c5c, exactlydimHex("#aca3ec")anddimHex("#e5e5e5"), with no undimmed accent left behind it;selectionFgWarn(6d3a06) paints on the selected unsafe row; plan mode turns the composer rail amber. Ctrl+C exits 0, leaving the alternate screen buffer and restoring the cursor.Not verified. How the glyphs render in a given font —
pytereports the characters emitted, not how a terminal draws╭,⠋or⊘. The headless--promptpath was not exercised, because it costs a real provider call. Andink-text-inputpaints its placeholder inbrightblack, a named ANSI colour insidenode_modulesthat the token guard cannot reach.One pre-existing issue was found and deliberately not fixed here:
ApprovalPickeroverflows a 6-row terminal. Measured identically atorigin/mainand on this branch (7 rows into 6 for both), so it is not a regression from this change.