cyberine
UsageRun recipes

One dot separator, no raw colour literal, no doubled hue (k1rse)

A census (not an estimate) found the "field · field · field" dot separator (SYM.bullet, padded) rendered at THREE different widths across 31 pure separator sites in 11 files: two spaces before / one after (RunPreview, QuickReply, SteerInput, AppHeader, half of RunDetail), one space each side (RunDetail's other half, ChatStatusLine, ChatModeLine, TrajectoryReplay, WelcomeBanner, ChatRunStatusLine), and two spaces each side (KeyHints, InputBox). Counting every join embedded inside a larger help/status string too (WorkflowMonitor, HelpOverlay), the raw glyph was referenced 79 times across the file tree before this task. The same survey found three raw colour literals bypassing the palette (app.tsx's session-browser delete-armed state, hardcoded 'red'/"red") and four places where a state glyph and its adjacent label painted the identical resolved colour — the glyph's entire job, done twice.

The separator. src/tui/components/Dot.tsx exports one component, <Dot color={MUTED} />, rendering exactly one space, the bullet, one space — SPACING.TIGHT on each side, the value SPACING's own doc comment already calls out for "inter-segment gaps". theme.ts also exports the identical string as DOT_SEP, for the places a join lives inside one already-colored template literal (a .join(), a help-bar string) rather than a JSX child. Every genuine separator site across the tree now routes through one or the other. What does NOT: a placeholder glyph standing in for a missing value (FleetList / ConversationList's token count before the first usage event), an idle-vs-active row marker (app.tsx's session-browser dot), a pager's tight zero-padded "N·M" range (FleetList / ConversationList), and a row of position-marker dots or a leading list-bullet (TrajectoryReplay) — none of those join two fragments with a gap on both sides, so none of them are the separator this task constrains, and each is named with its exact count in the theme.ts k1rse block and in the smoke.

The colour literals. All three were app.tsx's "Ctrl+X again to delete" confirmation state (a folder row, its delete label, and a session row), now TONE.error. ChatApp.tsx's /mdtest markdown gallery had one FALSE positive — a sample TSX code block whose literal text happened to spell color="cyan" as content, not styling — rewritten to color={ACCENT} so the sample also demonstrates the rule it sits next to, rather than needing a per-line scanner exemption.

The doubled hue. Toasts painted its glyph and its label the same stateColor(toast.kind); ChatModeLine painted its mode glyph and "plan mode on" the same modeColor(mode); FleetList and ConversationList painted a zombie/error row's task title TONE.error — the exact colour StatusBadge's stale/error glyph on the same row already carries; WorkflowMonitor's detail-pane status line painted its agent-state icon and "Completed · claude" as one <Text> under one shared colour. In every case the fix took the SAME direction: the GLYPH keeps the state hue (it is the compact, single-cell, at-a-glance signal StatusBadge already established as canon across the app), the LABEL demotes to a neutral tone (TONE.text for primary text, MUTED_STRONG for a secondary one) — not the reverse, because a label spans many cells and reads fine at neutral weight, while a single glyph cell is the one place hue is cheap. This mirrors ACCENT_CLAIM_BUDGET's own resolution rule (demote the thing that is not the claim), applied to state hue instead of the brand accent.

Run recipe:

npm run selftest:dot-separator   # offline; source census (bullet + colour literal) + renders through Ink

Four independent sections, mirroring the accent/bold/border guards' shape: (1) SOURCE — SYM.bullet may be referenced only in theme.ts (the DOT_SEP definition), Dot.tsx (the consumer), and the four named exemption files at their exact documented count; (2) SOURCE — no color/borderColor/backgroundColor prop outside theme.ts and src/tui/brand/ may carry a bare hex string or a bare ANSI colour name, whether quoted directly or inside a {cond ? 'x' : y} expression; (3) RENDER — AppHeader and KeyHints, rendered through Ink with colour forced on, must show the join at exactly " · " and never " · " or "· "; (4) RENDER — Toasts and ChatModeLine must not resolve the same foreground colour for a glyph and its adjacent label, read back from the raw ANSI (basic named SGR codes like 37, not just 24-bit truecolor — a truecolor-only reader silently merged a 'white' label into whichever truecolor run preceded it and reported no defect, which is why both SGR shapes are tracked). FleetList / ConversationList / WorkflowMonitor's doubled-hue fixes are guarded by source regression assertions instead of a render, because their fixtures (a full Run/WorkerRun tree) are heavier than the point being verified.

Extend recipe. A new pure separator site: import Dot from src/tui/components/Dot.tsx and render <Dot /> (or <Dot color={MUTED_STRONG} />) instead of hand-writing the glyph; a join inside a plain template literal imports DOT_SEP from theme.ts instead. A genuinely non-separator use of SYM.bullet (another placeholder or position marker) needs a new named, counted entry in BOTH the EXEMPT map in scripts/k1rse-dot-separator-smoke.tsx and the k1rse block in theme.ts — an uncounted exemption is how a real separator regression hides. A new glyph+label pairing follows the same direction (glyph keeps the hue, label demotes) rather than re-deriving it from scratch.

Known Limitations and Deferred Work. The doubled-hue guard covers the sites this task actually found and fixed, not every glyph+label pair in the tree — there is no single legal shape to scan for the way there is for bold (bold has exactly one legitimate owner; a coloured glyph beside coloured text is legitimate everywhere EXCEPT when the two colours happen to match), so a future doubled-hue bug in an untouched component will not be caught until it is found and added by name. The colour-literal scanner is a targeted regex, not a JS/TSX parser: it resolves color= attribute values by quote-matching and brace-depth counting, which is exactly what this codebase's actual ternaries and ?? fallbacks need, but a sufficiently exotic expression (a literal built by string concatenation, a literal hidden behind an intermediate variable one line above) could slip past it. And the render checks measure the dark variant only — ansi's tones are terminal-defined names with no rgb sequence to diff, so a doubled-hue regression there is not measured, only the source-level structural fix (glyph and label now reference visibly different tokens) applies universally.