Token-efficient TUI for vp run

TL;DR

  • While tasks run, vp run shows 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

  1. 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
  2. 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  
  3. 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-details if 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.

Tokens Codex read for vp run verify Measured with Codex 0.160.0. The tasks that pass print about 300 lines in total.
  1. Plain output 7,599 no frames · 19,732 bytes
  2. TUI in agent mode 1,848 6 frames · 3,908 bytes
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.

After Ctrl-C, the rows show what was cancelled, and nothing else prints.
~/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)
Each run gets a name, and --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

…
An unknown name lists the recent runs.
~/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.

In a terminal with two or more tasks, you get the UI.
~/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
With one task, the output is plain, as it is today.
~/acme/packages/ui ❯ vp run build
$ tsdown ○ cache miss: 'src/button.tsx' modified, executing
ℹ tsdown v0.23.3
✔ Build complete in 874ms
When output is piped, or 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), which vp dev turns on under the UI.
  • Prompts you answer with arrow keys don't work. Run those tasks on their own, or pass --log=interleaved.