Token-efficient TUI for vp run
TL;DR
-
While tasks run,
vp runshows them as a live tree and hides what they print. At the end it prints the output of tasks that failed or warned. - In the first demo below, Codex read 76% fewer tokens than it did with plain output.
How a run looks
-
The task tree
Each task is a row showing its state, and its status if the tool reports one.
The tree doesn't clear the terminal, and it doesn't show what tasks print.
~/acme ❯ git pull Already up to date. ~/acme ❯ vp run verify ⡿ acme#verify 2/6 ⡿ vp run -r build 2/4 ✓ @acme/utils#build: tsdown cache hit · 2 files · 6.1 kB ⡿ @acme/ui#build 1/2 ✓ tsc -b 1.7s ⡿ tsdown bundling › ⡿ @acme/web#build: vp build rendering chunks ○ vp run -r test ○ @acme/utils#test: vitest run ○ @acme/web#test: vitest run ↑↓ move ←→ fold enter output ctrl-c stop brave-otter
-
A task's output
Press Enter on a task to see its output.
Agents don't press keys, so they never see this.
vite v8.3.2 building client environment for production... transforming... ✓ 812 modules transformed. rendering chunks... ⡿ @acme/web#build: vp build rendering chunks esc tree ctrl-f search
-
When it's done
The tree stays on screen, and the output of failed tasks prints below it.
Agents can read logs of successful tasks with
vp run --last-detailsif they want.~/acme ❯ git pull Already up to date. ~/acme ❯ vp run verify ✗ acme#verify 5.4s ✓ vp run -r build 2.9s ✓ @acme/utils#build: tsdown cache hit · 2 files · 6.1 kB ✓ @acme/ui#build ▸ 2.9s ✓ @acme/web#build: vp build 812 modules · 209 kB · 2.9s ✗ vp run -r test 2.5s ✓ @acme/utils#test: vitest run cache hit · 36 passed ✗ @acme/web#test: vitest run 1 failed · 41 passed · 2.5s ✗ @acme/web#test: vitest run ─────────────────────────────────────────────────── RUN v5.0.3 /acme/apps/web ❯ src/pages/profile.test.tsx (6 tests | 1 failed) × shows the user's name FAIL src/pages/profile.test.tsx > shows the user's name AssertionError: expected 'Loading…' to be 'Ada Lovelace' ❯ src/pages/profile.test.tsx:28:41 Tests 1 failed | 41 passed (42) --- vp run: 2/7 cache hit (28%), 1 failed, 1.4s saved. (Run `vp run --last-details b rave-otter` for full details)
Try it: lint, build and test
This demo runs vp run verify. The root verify task is
vp lint && vp run -r build && vp run -r test. The linter
warns, two tasks are cache hits, one test fails, and the builds and tests that pass
print long logs. The run starts when you scroll to it.
Try it: dev servers
vp run -r dev runs until you stop it. Each dev server's row shows its URL
once the server is up. tsc --watch doesn't report a status, so its row
only says it's running.
AI agents
An agent reads every byte vp run writes. In agent mode it leaves out colors
and key hints, and redraws the tree at most every two seconds.
vp run verify
Measured with Codex 0.160.0. The tasks that pass print about 300 lines in
total.
Show the numbers
| What Codex ran | Frames | Bytes | Tokens |
|---|---|---|---|
| Plain output, without a terminal | 0 | 19,732 | 7,599 |
| TUI in agent mode | 6 | 3,908 | 1,848 |
What Codex sees
These are the six frames Codex read.
The reports at the end are shortened here.
Frame 6 of 6
✗ acme#verify 9.5s
! vp lint 412 files · 2 warnings · 1.2s
✓ vp run -r build ▸ 5.7s
✗ vp run -r test 2.6s
✓ @acme/utils#test: vitest run cache hit · 143 passed
✓ @acme/ui#test: vitest run 342 passed · 2.0s
✓ @acme/api#test: vitest run 117 passed · 1.8s
✗ @acme/web#test: vitest run 1 failed · 41 passed · 2.6s
! acme#verify › vp lint
…
✗ @acme/web#test: vitest run
…
AssertionError: expected 'Loading…' to be 'Ada Lovelace' // Object.is equality
…
---
vp run: 2/11 cache hit (18%), 1 failed, 2.3s saved. (Run `vp run --last-details
witty-otter` for full details)
Why not print only the final state?
That would cut this run to about 1,100 tokens.
But an agent checking on a run that takes a while would see nothing until it ended, and a run of dev servers doesn't end.
How it works
Task config becomes the tree
// vite.config.ts
tasks: {
verify: 'vp lint && vp run -r build',
}
// packages/ui/vite.config.ts
tasks: {
build: ['tsc -b', 'tsdown'],
}
// apps/web/vite.config.ts
tasks: {
codegen: 'graphql-codegen',
build: {
command: 'vp build',
dependsOn: ['codegen'],
},
}
~/acme ❯ vp run verify ⣾ acme#verify 2/5 ✓ vp lint 412 files · 1.2s ⣾ vp run -r build 1/4 ⣾ @acme/ui#build 0/2 ⣾ tsc -b ○ tsdown ✓ @acme/web#codegen: graphql-codegen 700ms ○ @acme/web#build: vp build waiting for @acme/ui#build
&&, arrays- Each step is a row one level under its task
vp run -r- The tasks it runs get a level of their own
dependsOn, package deps- Rows on the same level; a waiting task names the one it waits for
Tools report a status
import { setStatus, setReady } from
'@voidzero-dev/vite-task-client';
// vp build
setStatus('transforming · 590 modules');
// vp lint
setStatus('412 files · 2 warnings', {
tone: 'warning',
});
// vp dev
setReady('http://localhost:5174');
⣾ @acme/web#build: vp build transforming · 590 modules ! @acme/web#lint: vp lint 412 files · 2 warnings · 1.2s ◉ @acme/docs#dev: vp dev http://localhost:5174 ✓ @acme/utils#test: vitest run cache hit · 36 passed
- Cache hit
- The row shows the status saved with the cache entry
tone: 'warning'- The row gets the ! icon, and the task's output prints after the run
- Outside
vp run - The calls do nothing, and plain output doesn't show statuses
After the run
The tree's last frame stays, followed by the output of tasks that failed or warned and a one-line summary.
~/acme ❯ vp run -r dev ⊘ @acme/web#dev: vp dev cancelled ⊘ @acme/docs#dev: vp dev cancelled ⊘ @acme/api#dev: wrangler dev cancelled --- vp run: stopped by Ctrl-C. (Run `vp run --last-details calm-river` for full deta ils)
--last-details with that name prints every
task's output.
~/acme ❯ vp run --last-details brave-otter ✓ @acme/utils#build: tsdown ──────────────────────────────────────────────────── ✔ Build complete in 182ms ✓ @acme/ui#build › tsc -b ────────────────────────────────────────────────────── ✓ @acme/ui#build › tsdown ────────────────────────────────────────────────────── ℹ tsdown v0.23.3 ℹ dist/index.mjs 31.40 kB │ gzip: 8.95 kB ✔ Build complete in 874ms …
~/acme ❯ vp run --last-details brave-otr error: no saved run is named `brave-otr`. Recent runs: calm-river vp run -r dev brave-otter vp run verify
When the UI is used
The UI hides the output of successful tasks, which is fine only when you can open it. So
vp run uses the UI only in a terminal, and only when a run has more than
one task.
~/acme ❯ vp run -r build ✓ @acme/utils#build: tsdown cache hit · 20 files ⡿ @acme/ui#build: tsdown bundling ○ @acme/web#build: vp build waiting for @acme/ui#build ↑↓ select a task ctrl-c stop brave-otter
~/acme/packages/ui ❯ vp run build $ tsdown ○ cache miss: 'src/button.tsx' modified, executing ℹ tsdown v0.23.3 ✔ Build complete in 874ms
CI or TERM=dumb is set, the
output is plain too.
~/acme ❯ vp run -r build | tee build.log ~/packages/utils$ tsdown ◉ cache hit, replaying ✔ Build complete in 182ms ~/packages/ui$ tsdown ○ cache miss: 'src/button.tsx' modified, executing ℹ tsdown v0.23.3 ✔ Build complete in 874ms ~/apps/web$ vp build ○ cache miss: 'src/App.tsx' modified, executing …
--log=labeled, grouped or interleaved picks
that mode instead. Set VP_LOG=interleaved to never get the UI.
~/acme ❯ vp run --log=labeled -r build [@acme/utils#build] ~/packages/utils$ tsdown ◉ cache hit, replaying [@acme/ui#build] ~/packages/ui$ tsdown ○ cache miss: 'src/button.tsx' modified, executing [@acme/utils#build] ✔ Build complete in 182ms [@acme/ui#build] ℹ tsdown v0.23.3 [@acme/ui#build] ✔ Build complete in 874ms …
Details
The task tree
Like a progress bar, the tree is drawn below the cursor. It doesn't clear what's already in your terminal, and it doesn't show task output. When the run ends, its last frame stays in your scrollback.
Icons
| ○ | Waiting for a dependency or a free concurrency slot. The status says which. |
| ⣾ | Running. In agent mode the icon is ● and doesn't spin. |
| ◉ | Ready: a long-running task, such as a dev server, reported that it's up. The icon turns red or yellow if its status is an error or a warning. |
| ✓ |
Succeeded. A cache hit has the same icon; its status starts with
cache hit. |
| ! | Succeeded with a warning. Its output prints after the run. |
| ✗ | Failed. |
| ⊘ | Cancelled, by Ctrl-C or because another task failed. |
| – | Never started, because the run stopped first. |
A group shows its most urgent child: failed first, then running, then waiting, then done.
How the tree fits on screen
- Rows never move when their state changes, so the selection doesn't jump around.
- The tree is never taller than the terminal. Groups that finished cleanly fold automatically.
-
If it still doesn't fit, siblings that finished cleanly fold into one
N more done
row, and the rest scrolls, with↑ N more
and↓ N more
rows at the edges. - Nothing is drawn in the first 150 ms, so a run that's over by then, such as one with only cache hits, prints just its summary.
- No row is highlighted until you press an arrow key, so a run you only watch has no selection.
The task view
Enter on a task opens its output full-screen, like a tmux pane. Colors, wrapped lines and lines that redraw themselves look the way they would in a terminal. A bar at the bottom shows the task, its status and the keys you can press.
| Esc | Back to the tree |
| ↑ ↓ PgUp PgDn Home | Scroll. The bar shows how far back you are. |
| End | Jump to the newest output and keep following it |
| Typing, Enter | Send a line to the task, as in a terminal |
| Ctrl F | Search the output as you type, and use ↑ ↓ to move between matches. A lowercase query matches any case. |
| Ctrl C | Interrupt the task, or stop the run if the task has ended |
| Ctrl D | Close the task's input |
| Mouse wheel | Scroll |
| Drag | Select text. Dragging past the top or bottom scrolls, and letting go copies the text to the clipboard. |
When a tool clears the screen, as tsc --watch does, the view keeps the
earlier output and adds a screen cleared
divider after it.
Typing to a task
Tasks write to a pipe, so tools don't draw spinners or progress bars. The status shows their progress instead.
- Type in a task's output and press Enter to send the line. Your typing shows at the end of the output, as a terminal would echo it.
- Ctrl D closes the task's input, and Ctrl C interrupts only that task.
- Tasks that aren't cached can read what you type. Cached tasks get no input, as today.
-
Prompts that read a line work, and so do Vite's shortcuts (
h + enter
), whichvp devturns on under the UI. -
Prompts you answer with arrow keys don't work. Run those tasks on their own, or pass
--log=interleaved.