Skip to content

chore(tui): give the interface one palette, one grid and one glyph set - #54

Merged
mangit955 merged 2 commits into
mainfrom
chore/tui-visual-consistency
Aug 5, 2026
Merged

chore(tui): give the interface one palette, one grid and one glyph set#54
mangit955 merged 2 commits into
mainfrom
chore/tui-visual-consistency

Conversation

@mangit955

Copy link
Copy Markdown
Owner

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.ts on a third set, an orange #fb923c selection row hardcoded into all four pickers, and bare color="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 the bg* tokens, so dimHex faded toward #0a0a0a, a colour nothing ever drew.

Nothing shared a left edge. In one transcript: user rows at column 1, the Woopcode label 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. toolGlyph returned five marks mixing ASCII punctuation, an arrow and a maths operator, alongside ⊘ ✗ ▪ ⊙ │ ▐ ❯ # elsewhere.

For the record, theme.ts opened "inspired by OpenCode's dark design system" and timeline.tsx carried "Left accent bar — OpenCode style". That is borrowed chrome by its own comments.

What

Area Change
Colour theme.ts is the only file with a colour literal. Surface tokens (bgCanvas/bgElevated/bgInset/bgCode) name what is actually painted; selection* tokens replace the orange; markdownColors and all 30 entries of syntaxTheme derive from tokens; dimHex retargets to bgCanvas. One primaryRamp replaces the accent ramp that was written twice.
Grid Every timeline row goes through one TimelineRow with TRANSCRIPT_GUTTER, so the transcript has a single spine.
Glyphs A settled tool call takes one ·; 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.
Surfaces Dialogs get a border, because bgElevated alone cannot say where a panel stops.
Fixes The composer rendered the literal " Google" while ignoring the providerName prop 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. borderBase measured 1.78:1 against the panel, below the 3:1 that WCAG 1.4.11 asks of a UI boundary. Since bgElevated sits only 1.14:1 above bgCanvas, the border is the separation, so a border nobody can see defeats the reason for adding it. Hence borderElevated (3.11:1 panel, 3.55:1 canvas) and styles/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 outside styles/. It walks the directory rather than using git 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 --staged over the 29 files, and the pre-commit hook independently:

verify: 29 changed file(s)
  ok    type check  1.2s
  ok    tests  21.3s
2 gate(s) passed.

Because the working tree also held unrelated in-flight work, the commit was re-checked in a worktree containing origin/main plus this commit and nothing else:

uncommitted files: 0
non-tui files in the commit: 0
$ bun run verify --all
  ok docs lint · ok docs surface · ok type check · ok tests
4 gate(s) passed.
$ bun test        → 1527 pass, 0 fail

Also run there: the reverse-order sweep (1527 pass, 0 fail) and four concurrent full suites (1527/0 each), which is the load pattern packages/tests/README.md calls 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 reinstated color="green", the " Google" literal, the unbudgeted border rows (2 tests red), and borderElevated darkened 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 #ff00ff is invisible to git ls-files yet still named by the guard at components/ScratchProbe.tsx:3.

Run, not just tested. bun cli.ts was driven in a pseudo-terminal with pyte reconstructing the screen — real PTY, real raw-mode input, real AgentController and config loader, pointed at a copy of the config, with no prompt ever submitted. Confirmed against the painted cell attributes: selection is fg=0a0a0a bg=aca3ec and #fb923c appears nowhere; the border is fg=646464 with all four corners; the panel is bg=141414; the background behind a dialog paints 45415e and 5c5c5c, exactly dimHex("#aca3ec") and dimHex("#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 — pyte reports the characters emitted, not how a terminal draws , or . The headless --prompt path was not exercised, because it costs a real provider call. And ink-text-input paints its placeholder in brightblack, a named ANSI colour inside node_modules that the token guard cannot reach.

One pre-existing issue was found and deliberately not fixed here: ApprovalPicker overflows a 6-row terminal. Measured identically at origin/main and on this branch (7 rows into 6 for both), so it is not a regression from this change.

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.
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
woop-code Ready Ready Preview Aug 5, 2026 5:25pm

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.
@mangit955
mangit955 marked this pull request as ready for review August 5, 2026 17:28
@mangit955
mangit955 merged commit 2646d20 into main Aug 5, 2026
6 checks passed
@mangit955
mangit955 deleted the chore/tui-visual-consistency branch August 5, 2026 17:28
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