Console UI — design
Status: shipped (2026-11-04). Module page:
docs/modules/ui.md.
What we’re solving
Section titled “What we’re solving”vx’s output was written line by line from many places: verbs, the run
screen, plugins. Each picked its own words, colours and layout, so the
same fact looked different in vx run, vx why and a plugin’s verb.
The rule
Section titled “The rule”One kit draws everything. src/ui holds a fixed set of components;
nothing else writes to a stream, prints with console, names a colour
or emits an escape. tests/ui-only-output.unsafe.test.ts holds core and
every plugin package to it. Plugins draw through the façade’s ui.
Looks like a TUI, is not one
Section titled “Looks like a TUI, is not one”The screen is panels stacked top to bottom, written into the scrollback.
A panel is a title rule (─ Title ───) and its lines, with no box: a box
cut a long line or wrapped it inside its sides, and a copied line carried
the │. No input, no alternate screen, no columns side by side, no
scrolling regions, no modals: everything stays visible. Only the Running panel at the bottom is redrawn;
every other panel is final once written.
The run screen: Tasks (one row per finished task) → failure output (the task’s own bytes, never boxed, so they stay copyable) → Running (live) → Summary. Warnings get their own panel on stderr.
CI and pipes
Section titled “CI and pipes”CI looks the same as the terminal: any CI (a truthy CI, or the
variables of CIs that set none: TF_BUILD, JENKINS_URL,
TEAMCITY_VERSION) gets the same coloured panels, minus only the live
redraw of Running. A plain pipe (or NO_COLOR) gets the same layout
without colour, so a test or grep reads bytes with no escapes.
Prior art
Section titled “Prior art”- Shopify
cli-kit: a fixed component set as the only output API. - Cargo
Shell: every status line goes through one writer with one verb column. - Ink
<Static>: finished rows are written once above a live region that alone is redrawn.
Rejected
Section titled “Rejected”An interactive TUI (alternate screen, keys, scrolling): output must be readable without touching it, and the same in CI.