// docs/guides/tui.md docs online

Using the TUI

vincent with no arguments opens the terminal UI, starting a daemon if none is running. It is a pure API client: it holds no state the daemon does not have, and quitting it never affects work.

vincent          # opens the TUI

The first run

The very first launch shows a one-time notice: agents run full-auto by default and can execute arbitrary commands as you. Acknowledge it once and it does not return. The acknowledgment is stored in {data_dir}/tui.json.

It is worth reading rather than dismissing — Security model is the longer version.

Layout

Two of the six views — the board and task detail, the daily loop — share one persistent screen of three panels:

The board filtered to one running task, with the timeline of its attempts on
the left and the live output of its current step on the right

The task table is always full width, and it drives everything below it: moving the selection moves the timeline and the output pane with it. tab moves focus between panels; shift+tab goes back.

The other four views — new task, projects, workflows, daemon — are full-screen takeovers. esc closes one layer at a time (popup → screen → selection → filter) and never quits.

At 128×24 and above, New task, Projects, and Workflows use that room as a guided two-pane surface: progress or resources stay in a narrow rail, and the current decision gets the rest of the screen. Below that size they fall back to the compact form, table, or registry. Resizing does not move the cursor or close the picker, editor, project form, workflow expansion, or graph you were using.

The board

One row per task: id, project, title, state, current step k/n with its name, elapsed, and cost so far. The header shows daemon status, agent availability, running-versus-cap counts, and how many tasks need a human.

Three behaviors matter:

  • Tasks waiting on a human are pinned to the top with a distinct badge — awaiting_input, awaiting_gate and blocked. ! jumps to the next one from anywhere.
  • The terminal bell rings when a task enters awaiting_input, so most terminals flash or badge the window even when it is not focused.
  • A task waiting on a clock says when it resumes. A task whose agent hit a usage limit — or whose failed step is pacing its next attempt with retry_backoff — is queued like any other, but its state cell reads queued → 14:20, the time vincent will try it again, on its own. It holds no slot and needs nothing from you; the detail header names the reason in full (queued · usage limit → 14:20, queued · retry backoff → 14:20). See Troubleshooting and retry_backoff.
  • The header badges the agent, not just the task. An adapter vincent has watched run out reads claude ⏳14:20 in place of claude ✓, and stays that way until a step on that adapter succeeds — so a board full of queued rows says which window they are all waiting on. The badge is a statement, not a brake: admission is unchanged and nothing is withheld.

/ filters by id, title, project or state; tab commits the filter, esc clears it.

A wide terminal also gets a STATUS column: what the task’s newest step run said about itself, if it said anything — compiling internal/store, 3 tests red. It is set by the step, not by vincent, through vincent status, so it is empty until a workflow asks for it; see Reporting status from a step. It is the first column dropped when the terminal narrows, and it needs a comfortably wide title to be admitted at all — so a board that has never seen it is a board that has the width for everything else instead.

Elapsed on the board is wall clock from the task’s start. That is deliberate: a task idle on a human for 35 of its 40 minutes must not read as “5m” on the board whose job is to flag it. The per-attempt figures in the timeline are the other measure — active time, with the excluded wait shown beside it rather than silently subtracted.

Grouping

The rows are grouped by project, and by workflow within a project, out of the box:

The board grouped by project and then by workflow, each header carrying its
task count and its needs-attention badge

  • The header shows the group’s task count, and the needs-attention badge when it holds any — a group can never be the reason you missed something waiting.
  • Grouping does not reorder anything. The sort is what it always was, and a group sits where its first task does, so the group holding the oldest thing waiting on a human is the top group.
  • A grouped level loses its column — the header already names it — and the width goes to the title.
  • Headers are labels: the cursor steps over them, and nothing collapses.

g cycles project›workflow → project → workflow → flat for the session. The panel title names the grouping whenever it is not the configured one.

Set the grouping you start with in config.yaml (tui.board.group_by); [] gives you one flat list.

Acting on several tasks at once

Archiving yesterday’s finished work one row at a time is the same keypress ten times with a confirmation between each. Select the tasks instead:

Key Does
space Select the task under the cursor (again deselects)
V Select every task the filter is showing — or clear that selection
L Drill into the selected fan-out’s lanes, or back out. Lanes are hidden from the board otherwise
esc Clear the selection

While anything is selected, a appears beside those rows, the panel title counts them (Tasks — 5 selected), and the action keys act on the whole selection:

Every row selected, the panel title reading "Tasks — 13 selected", and the
action bar offering each action with the number of selected tasks it can move

The count beside each key is how many of the selected tasks that action can actually move — an action shows up when some selected task accepts it, and the ones that do not are left alone. So a selection holding four finished tasks and one still running offers A archive (4), and the running one stays where it is.

The rest of the behavior follows from what a selection is:

  • It is a set of tasks, not of rows. Filtering, regrouping and refreshing do not change it — that is why the count is in the title, so a selected task the filter is hiding still says it is coming along.
  • One confirmation for the batch. A asks once, about all of them. Behind the scenes vincent sends one ordinary action per task, so the daemon sees nothing special; you get one line back: how many moved, and the first refusal named if any refused.
  • Uncommitted changes still re-prompt. A bulk archive archives the clean worktrees and asks again about only the dirty ones — 2 of 5 selected tasks have uncommitted changes.
  • What succeeded leaves the selection; what failed stays in it, so a retry needs no re-selecting.
  • The keys work from any panel. Whatever has focus, the footer is counting the selection, so that is what A acts on.

Task detail

enter opens the selected task. The timeline on the left lists every attempt of every step with its duration, tokens and cost; the output pane on the right shows the live tail of the running step.

Selecting an attempt in the timeline is how you read scrollback — that is why the two panels are side by side and both always visible.

A structure step gets a tier of its own. A parallel group’s sub-steps share the group’s index, so the group is one header and each sub-step sits beneath it. A loop (§7.8) goes one further: its body’s rows are grouped by iteration, folded shut with the latest one open, and a for_each iteration’s header names the item it ran on. Ten passes of a four-step body is forty rows, and the one you arrived to read is almost always the pass it stopped on.

The board’s and the header’s step column say the same thing more briefly: a task inside a loop reads 3/7 green · loop 4/10.

Two things on an attempt line are worth telling apart. A red word like check_failed is vincent’s failure reason — a fixed set of constants, and vincent’s own verdict. A cyan » 3 tests red in internal/store is the step’s own status message, free text it set while it was running and the last thing it said before it ended. It is never a cause: a step killed on a timeout may be carrying a line it wrote half an hour earlier.

An attempt that did not succeed also gets a dim line beneath it with its result summary — the agent’s final message, or the tail of a command’s output. It is the sentence that decides whether to open the transcript.

Key Does
] Switch the output pane tab: Output ⇄ Diff ([, ] and d all work)
f or G Follow the live output again
v More or less detail: compact → normal → verbose (reasoning, then unrecognized lines)
e Open this attempt’s whole transcript in $EDITOR
/ Scroll the output; scrolling up pauses follow (on the Diff tab they move between files — see The Diff tab)

Reading the whole transcript

The output pane holds the end of a transcript — the last 256 KB, capped at 5000 records — because a single attempt is allowed to produce gigabytes. When a step fails, the part you want is often the beginning, which is exactly the part not on screen. When it has dropped something, the first line in the pane says so: … earlier output truncated — press e for the whole transcript.

e hands the complete file to your $EDITOR, the same way e opens a workflow file in the workflows view. What opens is the raw JSONL on disk — the lossless record, including lines vincent’s parsers do not recognize — rather than the pane’s rendering. It is the same file vincent task show <id> prints the path of.

Two cases answer instead of opening: a step that never wrote a transcript (a manual gate), and a transcript that retention has already pruned. Neither opens an empty buffer, because an empty buffer reads as “the step produced nothing”.

Follow mode belongs to the live attempt. It is unavailable on a finished one, and a step advance moves your selection only if the cursor was already on the live attempt — so reading an old step is never interrupted by a new one starting.

The Diff tab

The Diff tab is git diff against the merge-base with the base branch, including uncommitted changes, syntax-highlighted. It is fetched when you activate the tab and on an explicit refresh, never on every output chunk.

It is grouped by file, and every file starts collapsed — so the first thing you see is what the task touched, not the first eighty lines of whichever file git wrote first:

The Diff tab listing three changed files with their line counts, two folded
and one expanded to its hunk

Key Does
/ Move between files (the pane scrolls to keep the cursor in view)
enter or space Expand or collapse the file under the cursor (/ too)
O Expand every file
C Collapse every file — which is how the tab opens
pgup/pgdn, f/b, u Scroll by lines inside what is expanded
] Back to the Output tab ([, ] and d all work)

Clicking a file’s row folds it; clicking a line of code selects its file and leaves it open. The mouse wheel scrolls whichever tab is on screen.

The counts beside each path are the added and removed lines inside that file’s hunks, and the line above the list totals them. A binary file says so instead of showing +0 -0, and a rename reads old → new.

Folds are remembered per file path, so leaving and re-entering the tab — a refresh — keeps what you had open, even if the agent has since touched other files. Moving to another task starts collapsed again, and nothing is written to disk: a fold is how you are reading one diff, not a setting.

The action bar

Below the panes, the action bar shows exactly the actions valid in the current state — the daemon computes that list, the TUI renders it. With tasks selected on the board it acts on all of them; see Acting on several tasks at once.

Key Action Valid from
a Approve the gate awaiting_gate
x Reject the gate awaiting_gate
r Retry the blocked step blocked
R Repair with an agent — a one-off run in this task’s worktree blocked
E Edit the step’s prompt or command in $EDITOR, then retry blocked
s Skip the current step blocked, awaiting_gate
p Pause / resume queued, running / paused
c Cancel the task (asks first — a running step is killed) most states
A Archive (asks first — the worktree is removed) done, aborted
F Follow up — run more work in this finished task’s worktree done, aborted

E opens the failing step’s prompt or command in your editor, and the override applies to this task’s snapshot only — the workflow file is untouched.

R and F open a form instead of acting straight away; they are the two task actions that need something written, which is also why neither is offered for a bulk selection.

What each action means in full is in Task lifecycle.

Repairing a blocked task

r, E and s all leave the worktree exactly as the failed step left it — they re-run a step, rewrite its text, or walk past it. When what is wrong is a file, press R on a blocked task and a one-off agent goes and fixes it, in this task’s worktree, on this task’s branch.

Key Does
/ Move between the prompt and the agent / model / effort rows
enter Open the row under the cursor — the prompt field, or that row’s picker
e Write the prompt in $EDITOR instead
ctrl+s Start the repair
esc Close without repairing — the draft is discarded

The prompt is the only required row, and it is prose: write what you want done, not a template. The daemon puts the context around it — the task, the blocked step’s rendered prompt or command, the failure reason and exit codes, the last 200 lines of the failed attempt’s transcript and the path to the whole file, so the agent can read further itself.

Inside the prompt field enter is a newline (a repair prompt usually wants more than one line), ctrl+s keeps the text, and esc discards it. Agent, model and effort are optional; set they apply to this run only and win over the task’s overrides and the workflow’s defaults.

When the repair agent finishes the task returns to blocked — same step, same reason — whatever it exited with. That is the point: you look at the diff and then decide whether to r. The repair appears in the timeline as its own entry under the blocked step, labelled repair (ad-hoc agent), with its own transcript, tokens and cost; it is not an attempt of that step and does not use up its retries.

Following up on a finished task

A done or aborted task still owns everything it made — its worktree, its branch, its commits — until you archive it. F is how you do one more thing in there without leaving vincent: rebase the branch onto a main that moved, add the commit a reviewer asked for, drop the stray file the agent left.

Key Does
/ Move between the run form, what to run, and the agent / model / effort rows
enter Open the row under the cursor — the run-form list, the text field, or that row’s picker
e Write the prompt or command in $EDITOR instead
ctrl+s Start the follow-up
esc Close without running anything — the draft is discarded

The top row picks what kind of run this is, and it decides what the row under it means:

Run form The row below it is
agent a prompt — prose, not a template
command a shell command, run under the daemon’s shell (/bin/sh, or pwsh on Windows)
workflow a name picked from the registry, run against this task’s worktree instead of a new one

Switching between the three keeps what you typed in each, so you can look at the command form and come back to your prompt.

When the run finishes, the task returns to the state it came from — done to done, aborted to aborted — whatever it exited with. A follow-up never changes a task’s verdict; if a successful one could promote an aborted task to done, any command that exits 0 could undo an abort you made on purpose.

Follow-ups are repeatable, and each one is a round. The timeline heads them ↳ follow-up 1, ↳ follow-up 2 under the workflow’s own steps, with each step of the round named beneath — they are not steps of the workflow, and the workflow’s k/n does not move.

If a follow-up step fails the task blocks at that round, and the usual keys mean the usual things there: r re-runs the follow-up where it stopped, R repairs against that failure, s abandons the follow-up and puts the task back where it came from, c aborts. E is refused — edit-and-retry rewrites a step in the task’s snapshot, and a follow-up is deliberately not in it.

For more than one task at a time, use the command line: vincent task follow-up <id> --run 'git rebase origin/main' (CLI reference).

Answering a question

When a claude step asks something mid-run, the task enters awaiting_input and gets a badge on its row plus a footer hint. Press enter on the row to open the answer form.

Key Does
space Pick an option (toggles, for a multi-select question)
e Type your own answer — options are suggestions, never a list
enter Submit; the run resumes in the same session where it stopped
esc Close without answering (what you picked is kept)

While e has a field open, enter keeps what you typed and esc discards it — the submit is the next enter, on the form itself. The field opens under the question it answers and wraps as you type, so a long answer stays readable before you commit it; the committed answer is shown back on its row, wrapped the same way.

The form is a popup, and it never steals focus: auto-opening under a keystroke is how an answer gets lost. It announces itself and waits for you.

The takeover screens

Reached from the command palette (:), except new task which keeps a direct key.

New task — n

Opens for the project you are looking at. A guided form: project → workflow (with its description and step list, flagging steps whose agent is unavailable) → (GitHub issue) → title → description → fields → base branch → priority → optional agent/model/effort override.

When the selected workflow declares fields:, the Fields row is pre-rendered in declaration order. It shows labels, descriptions, type/required badges, and regex help; boolean values toggle between true and false. Workflow-owned names are locked, but their values remain editable. You can still add and delete custom key/value rows — additional, undeclared fields remain valid and are recorded on the task. Values are kept when you switch workflows, including fields that the new workflow does not declare.

The GitHub issue row appears only when this project’s issues can be read: the github integration is on, the project’s origin remote is a github.com repository, and vincent has a credential — gh logged in, or GITHUB_TOKEN/GH_TOKEN in the environment the daemon inherited. Otherwise the row is simply not there, and no GitHub call is made. vincent doctor says which of those is missing.

Its picker lists the repository’s open issues, newest first, and narrows as you type, like every other picker here. Choosing one fills the title, fills the description with the issue body plus a trailing GitHub issue #N: <url> line, and fills any of the workflow’s declared labels, assignee or milestone fields whose declared type accepts the value. All of it lands in the ordinary editable rows — rewrite or clear anything before creating, and what you leave is what the task gets. A (none) row at the top of the picker removes the link.

The issue is read once, when you create the task, and stored on it. Editing the issue on GitHub afterwards does not change what a later step sees; the snapshot is what .Issue renders from.

On a wide terminal those fields are grouped into six stages in the left rail: Project, Workflow, Task details, Git & priority, Execution, and Review. The main pane shows only the fields in the current stage, while Review gathers the complete request beside the Create action. The rail follows the ordinary field cursor — there is no separate Next button or second set of navigation keys.

New task at its Review stage: the six stages in the left rail, and on the
right the whole request — project, workflow, title, description, base branch,
branch name, priority and agent — above the create action

Key Does
enter Open the focused field’s editor or picker
a / d In Fields, add or delete a custom row (declared rows cannot be deleted)
e Edit the description in $EDITOR
+ / - Nudge the priority (higher runs first)
R Re-probe the adapters (the list is otherwise cache-served)
ctrl+s Create the task

The agent row warns when the adapter the task would run on is out of quota — · usage limit until 14:20, from the same observation the board header badges. It warns and nothing else: the form submits, and the task waits its turn on the ordinary usage_limit hold if the window is still shut when it is admitted.

The override pickers are fed by live adapter data, tagged with where each option came from, and always accept free text. They are windowed and filterable — you type to narrow, which is what makes cursor’s ~180-model catalog usable. Each resolved field shows which level won (step, task, workflow, adapter), so the form tells you what will actually run rather than what you typed.

Projects

On a wide terminal the repository list stays in the left rail. The selected project’s path, branch convention, workflow and concurrency defaults, and current tasks fill the main pane; a or enter puts the existing add/edit form in that same pane. This keeps the project you were looking at visible while you change its configuration.

The Projects view: seven registered repositories with their running counts and
caps on the left, and the selected project's path, branch convention, execution
defaults and current workload on the right

Key Does
a Register a repository
enter or e Edit the selected project
d Remove it (asks first; its task rows go with it)
/ Filter by name or path
ctrl+s Save, in the form

Workflows

The merged registry with scope badges and validation status.

On a wide terminal the merged registry stays in the left rail. The focused pane names the selected entry’s scope and source, availability and findings, then reveals its resolved step list with enter. g uses that focused pane for the graph while keeping the registry visible, so esc returns to the same entry with its surrounding scopes still in view.

Key Does
enter Show the entry’s steps
g Draw the entry as a control-flow graph
e Open the file in $EDITOR — the view updates when you save
R Re-read the registry

The view reads the registry; it does not author it. New workflow files are written in your editor and appear on the next reload.

The graph — g

A numbered list of top-level steps can name a parallel group or a fan_out but cannot show where control goes. g draws it:

The Workflows view: the registry on the left with its scopes, shadowing and
one invalid entry, and on the right the selected workflow drawn as a graph — an
agent step into a four-lane fan_out, one lane guarded by an `if`

The graph opens over the list, not instead of it: enter’s step list, with its findings and platform notes, is still there when you press esc.

How to read it. Every one of these works with color turned off:

You see It means
A box’s second line The step’s type, and any badges
if The step is guarded — the expression is in the strip at the bottom
chk The step carries a check:
×3, for_each A loop’s driver. max N is an explicit bound
agent on a merge on_conflict: agent — an agent may resolve a conflict
A light frame A parallel group
A heavy frame A fan_out, with its lanes captioned by id and guard
A double frame A loop body, with a back-edge to its header
true / false A condition’s or break’s two ways out
END Where the workflow finishes

A fan_out has a merge node below its frame, because the join is a git merge that runs and can block. A parallel group has none: its join is only its members finishing. A guard on an ordinary step draws no second branch — false there means skip and carry on, so the flow is unchanged. A lane naming another workflow is one collapsed box; opening it is not in this version.

Key Does
or hjkl Move the selection — the view follows it
shift + arrows Pan the canvas
pgup pgdn u d f b Page it
tab / shift+tab Walk the nodes in source order
e Open the file in $EDITOR — the graph redraws when you save
R Re-fetch this workflow’s definition
esc Back to the registry

Editing is the point of e here: save the file and the graph redraws in place, with your selected node still selected. A terminal too narrow to draw a node says so rather than showing you a flattened shape that is not the workflow; a graph bigger than the terminal is panned, never reflowed.

A workflow that does not parse has no graph — g says so, and the errors are already under enter.

Daemon

Version, uptime, the config in effect, the adapters detected, and a live tail of the daemon log. The view reports, it does not act — vincent daemon stop owns stopping the daemon, and a TUI that auto-started one has no business killing it.

Each adapter row carries what vincent knows about its usage window: usage limit → 14:20 when the CLI stated that reset, usage limit ≈ 14:20 when vincent estimated it from usage_limit_recheck_interval, and a trailing quota unknown for an adapter nothing has been observed for — which is the normal state, since no CLI can report remaining quota without actually running. This is the one view that says “unknown” out loud; its job is to list every fact about an adapter, including the ones nobody has.

An orphans line appears in the identity block when the daemon has found directories under its data dir that no task claims, naming the count and vincent gc. It is a pointer, not a button: for the same reason this view does not stop the daemon, it does not delete anything either. Nothing shows when the count is zero.

A database block sits between the config and the adapters: how big the database is on disk — including the WAL, which the file size alone leaves out — what each table holds, how many bytes of workflow snapshots the tasks are carrying, and how far back the event history goes. vincent keeps database rows forever, so this is the block that tells you what that has cost so far. Like everything else here it reports and offers nothing to press; vincent doctor prints the same figures in pasteable form, and vincent doctor --fix is what compacts the file. R re-reads it along with the rest of the view.

Key Does
R Re-read the daemon info, the config, the database figures and the log
f or G Follow the end of the log again
/ Scroll the log

The log tail is read straight from {data_dir}/logs/daemon.log — the one place the TUI is not a pure API client, because an endpoint cannot serve the log when the daemon is the thing that died, which is exactly when the log is worth reading.

The command palette

: opens it. Everything reachable in the TUI is in there by name — navigation to every screen, and every task action the daemon currently offers. Type to filter, enter to run, esc to close.

The palette exists so the four takeover screens do not need memorized number keys. If you cannot remember a binding, : and ? are the two keys worth knowing.

Every key

? toggles a help overlay listing every binding for the surface you are on. The overlay, the palette and the footer all render from one registry in the source, so a key that exists is a key that is documented.

Global bindings — active whenever the focused surface is not capturing text:

Key Does
: Command palette
? Toggle help
tab / shift+tab Move focus between panels; commits a filter
! Jump to the next task needing a human
n New task
M Toggle the mouse
esc Close one layer: popup → screen → selection → filter — never quits
q Quit the TUI (the daemon keeps running)
ctrl+c Quit

Mouse, selection and paste

The mouse is on by default: click to select, scroll to scroll. That takes mouse events away from your terminal, so native text selection needs the mouse off — press M, or hold shift while dragging, which most terminals treat as “bypass the application”.

Paste normally arrives as a bracketed paste from your terminal’s own binding (Cmd+V, Ctrl+Shift+V, middle click) and lands in the focused field with no key involved. ctrl+v is the fallback for terminals that pass the key through instead.

When the daemon is unreachable

The TUI does not pretend. It shows the disconnected state in the header, keeps the last data it fetched visible rather than blanking the screen, and reconnects on its own when the daemon comes back — SSE reconnection resumes durable events from where it left off, so nothing is missed.

The daemon view stays useful throughout: its log tail is read from disk, which is the one thing still true to show when the daemon has died.


See also