3. The interface
The screen
The TUI is three regions: scrollback (the conversation), the prompt, and the status bar along the bottom.
Type while a turn is running and your message is not lost: it is held in a queued-prompt region pinned directly below the status bar, where it stays visible until the turn absorbs it. Queued lines never scroll away into the conversation while they are waiting, and they survive a resize. When the turn takes them they join your next prompt; when the turn ends in an error they come back to the input line rather than vanishing.
Assistant replies render as markdown β headings, lists, tables, and
fenced code blocks with tree-sitter syntax highlighting. The model's
thinking appears dimmed above its answer (turn it off with
ui.showThinking). File edits render as git-style
diff cards: an Update(path) header, an
added/removed summary, and red/green @@ hunks. A brand-new
file streams its contents dimmed as it is written.
The status bar is two rows. The top one answers "which tree am I in"
and holds still: the working directory, the git branch, and a summary of
what you have changed in that tree (π 3 Β· +128 -41 β files
touched, then lines added in green and lines deleted in red, staged and
unstaged together, untracked files included; a clean tree shows
nothing). The bottom one carries everything that churns β where
inference is running, a brain with the reasoning level beside it (click
it to hide or show thinking for the session, without writing the
setting), a context-usage gauge, β§ N jobs while background
shell jobs are running (click it for the /jobs panel), a π
while a debug console is actually connected to the session (not merely
--debug on the command line), a πΎ while the loaded model
is streaming its experts from SSD (blinking while a pass is in flight),
βΈ paused: memory while plank has released the engine under
memory pressure and is waiting to take it back, one βοΈ per
span while the memory extraction pass takes notes, the task counter, and
the remote marker. When a tool is running its name sits in the
notification slot and blinks; otherwise a rotating tip appears there.
There is no idle word: the prompt caret's colour already
says the model is waiting. The figures that change many times a second β
prefill and generation tokens and rates, the MTP per-step numbers β are
not in the status bar at all; they float at the right end of the rule
below the prompt, the way the session name floats on the rule
above it, and the rule goes back to a plain line when the turn ends. The
memory extraction pass keeps its figures to itself unless plank was
started with --show-memory-stats: by default the rule looks
idle while notes are taken and the footer's βοΈ is the only
sign. The line pinned under the output while the model works carries the
throbber, what the model is doing, and the clock.
Where inference runs is named on the second row, and there can be more than one answer:
api.regolo.ai, (local β‘100%)
Every engine in play is listed in the order it first appeared, so a
hosted main agent beside a provider: local subagent shows
both. The local engine carries its GPU power share (/power,
--power), because the cap applies to that engine and to no
other.
The brain blinks while the local model is prefilling or generating. It is the one signal that says which engine is working right now, which is otherwise invisible when a local subagent runs under a hosted main agent. It blinks in step with the elapsed and tokens/second readouts beside it.
While plank is compacting, the throbber-and-verb line is replaced by
a flashing compacting with a progress bar and
percentage:
compacting β°β°β°β°β±β±β±β±β±β±β±β±β±β±β±β±β±β± 21%
Most of the bar tracks re-reading the conversation, which is the slow part; the tail tracks the summary being written. It goes away as soon as the pass ends. See Compaction.
Every turn closes with a line of its own before the prompt comes back:
β» Planked for 0h 02m 20s
That clock covers the whole turn β every generate-and-tools round, plus any lines you typed while it was busy and it absorbed β not just the last pass. It is the number to quote when something felt slow.
Keys
Editing the prompt
| Key | Action |
|---|---|
Enter |
submit |
Shift+Enter, Alt+Enter,
Ctrl-J |
newline instead of submitting |
Ctrl-A / Home |
start of line |
Ctrl-E / End |
end of line |
Ctrl-B / Ctrl-F |
left / right one character |
Alt+Left / Alt+Right |
left / right one word |
Ctrl-K |
delete to end of line |
Ctrl-U |
delete to start of line |
Ctrl-W / Alt+Backspace |
delete previous word |
Ctrl-D |
delete forward, or quit on an empty line |
Up / Down |
walk prompt history |
Ctrl-P / Ctrl-N |
history back / forward (REPL) |
Ctrl-L |
clear the screen (REPL) |
Ctrl-G |
open the current prompt in an editor |
Shift+Enter needs the kitty keyboard protocol to be
reported, which not every terminal does β Alt+Enter and
Ctrl-J work everywhere.
Ctrl-G is the escape hatch for a prompt
too long to edit inline. By default it opens plank's built-in editor (a
fork of Microsoft Edit, running in-process, no external dependency). Set
ui.builtinEditor to false to shell out to
$EDITOR instead. Either way, what you save comes back into
the prompt; cancelling keeps what you typed.
The same editor also edits files on disk:
/open [path] takes over the terminal with
the file loaded, Ctrl-S saves and Esc
discards. See Slash
commands.
During a turn
| Key | Action |
|---|---|
Ctrl-C |
interrupt the generation (at an idle prompt: clear the input line) |
Esc |
interrupt the generation |
β on an empty prompt |
step into the agent roster, when there is one |
| mouse wheel / trackpad | scroll the scrollback |
| click-drag | select text to copy |
With an arcade game open, the first Ctrl-C closes the
game and a second interrupts the model β you are never locked out of
stopping a turn.
Esc at an idle prompt dismisses a /btw
panel left open from an earlier turn, which is the only way it
closes.
Most slash commands wait for the turn to finish, but the read-only
reports do not: /usage, /mcp and
/help answer from a turn-start snapshot, while
/context, /jobs and /toks read
live state and redraw as the turn runs β open /context
mid-turn and you can watch the window fill, category breakdown refreshed
at each tool boundary and the total on every status tick.
/exit asks [y/N] before interrupting the turn
and leaving once it stops.
In a question panel
The ask tool's panel takes
Up/Down to move, Space to toggle
an option when the question is multi-select, Enter to
answer, Esc to decline, and Ctrl-C to
interrupt.
Below the model's own options sits a built-in Chat about this row, for when the question itself is the problem. It is a decision rather than a selection: it never ticks in a multi-select, and picking it wins over anything else you had ticked. The model is told to stop and wait for you instead of acting. plank's own confirmation panels β leaving plan mode, the write and web-fetch grants, plain yes/no β do not offer it, because talking it over is not an answer they can act on.
The agent roster
The moment anything is delegated β by you with
/subagent, or by the model with the agent tool
β a roster appears below the status bar: main first, then
one row per run with a state bullet (β working,
β finished), the agent's name, the task it was given on one
line, and, flush right, how long it has been going and what it has spent
in tokens.
β main
β reviewer check the diff on this branch 1m 12s Β· 4.1k
β researcher find every caller of load_config() 48s Β· 9.7k
A fan-out gets a row each, with its own output buffer, so concurrent
agents never overwrite one another. β on an empty prompt
steps into the roster, β/β walk the rows,
Enter expands the selected agent's output over the
transcript with its own scroll position, and Esc comes
back. A finished row leaves the roster a minute after it ends, and
stepping back in does not bring it back: the roster shows what is
running now, not what ran. Only the row under your cursor is kept until
you move off it, so nothing vanishes while you are reading it.
While a sub-agent is working, the progress line under the transcript
reads Waiting⦠(for sub-agent <name> to complete).
The live counters β the verb, the clock, the tokens and the tokens per
second β belong to the sub-agent's pass, so they are shown in that
agent's own view, reached through the roster. The main transcript no
longer looks like it is the one generating.
It is a live readout: it appears with the first agent and goes away
with the last, staying put only while you are reading it, and
β brings a finished roster back so a report you delegated
is still reachable. The last eight runs are kept. The transcript itself
gets only a one-line signpost, which is the point of delegating in the
first place β see Extending plank.
The plain REPL has no roster and prints subagent output inline
instead; --ui console stays silent so its stdout protocol
is not corrupted.
@ file completion
Type @ in the prompt and a fuzzy-completion popup offers
file paths from the working tree; pick one and the path is spliced into
your message. Tab accepts.
ui.popupRowssets how many rows it offers (default 15).ui.respectGitignoredecides whether untracked files that.gitignoreexcludes are offered (defaulttrue).ui.indexRefreshSecsis how long the file index is trusted before it is rebuilt (default 5).
Prompt suggestions
After an answer, plank works out the most likely thing you are about
to type and shows it as grey ghost text on the empty prompt.
Tab or Right places it in the line so you can
edit it before sending, Enter sends it as it stands, and
any other keystroke dismisses it. It only ever appears on an empty
prompt, so it cannot land on top of something you are writing.
The guess is a short generation against the conversation already held in the KV cache, run in the background on a quiet moment rather than while you wait, so the prompt stays live and typing interrupts it. It skips itself when the cache would have to be rebuilt from scratch, which is the one case where it would be expensive.
A suggestion is only ever text. A line beginning with /
or ! is discarded rather than offered, since
Enter over a placed suggestion sends it straight away.
suggestions.enabledturns it off (defaulttrue).suggestions.maxTokensbounds the guess (default 160).
The suggestion is always generated before a waiting memory pass, which then takes the next quiet moment.
! and
!! β run a shell command yourself
Prefix a line with ! and it runs in your shell, in
plank's working directory, with the output streaming into the screen as
it is produced:
!cargo test --lib parser
The number of !s decides whether the model ever sees
it:
| What the model gets | |
|---|---|
!command |
the command and its output are recorded in the conversation, so the model has them as history on your next message |
!!command |
nothing β the output is yours alone |
Neither form starts a turn. A ! command does not make
the model respond; it just means that when you do send your
next message, the model can already see what happened. So
!cargo test followed by "fix the failure" works without
pasting anything, while !!git log keeps a bit of poking
around out of a context you are paying for.
Recorded output is capped (200 lines, 16 KB) with a truncation
marker, so one runaway command cannot flood the conversation. If you
want the model to act on something right now rather than on
your next message, ask it in a normal turn and let it run
bash itself.
Either form is your command, not the model's, so it
is never sandboxed β you typing it is the
authorization. Both are also the right way to do anything interactive (a
login flow, an editor, a pager) that the model's bash tool
cannot drive.
Esc or Ctrl-C kills a running command, and
Up/Down on a line that starts with
! walks only your previous shell commands.
The marker is coloured as you type, by where the output goes:
red ! feeds the command and its output to
the model, green !! keeps it between you
and the shell. That is the only difference between the two forms and the
only thing you cannot see once the line is typed. When the command
finishes, plank says done. in green β a command that
printed nothing is otherwise indistinguishable from one still running.
An interrupted command says [interrupted] instead, and a
non-zero exit adds [exit code: N].
Both forms run with PLANK_GPU_YIELD=1 set, so a command
that needs the GPU the local model holds (see Commands that need the
GPU) makes plank unload the model, run the command a second time and
reload the model, exactly as for the model's own bash
calls; what you see and what ! records is the second
run.
Pasting images
Paste an image (or a path to one) into the prompt and plank attaches
it. On macOS an image on the clipboard arrives as an empty paste, which
is the signal plank uses; pasting the path to an image file
works too, including a file dragged onto the terminal. Either way the
file is deduplicated by content into ~/.plank/image-cache/
and attached to your message.
The bytes are cached exactly as you pasted them. plank does not resample or re-encode, which it used to do to satisfy an image-upload limit it never actually had. Nothing uploads your pixels anywhere, so shrinking them only threw away the pixel density and DPI metadata that anything reading the image later would want. The vision encoder does its own resizing at encode time, from the original file.
The model can see it. plank ships a vision encoder
alongside the main model, the engine's vision companion
(~/.plank/ds4vision.vision.gguf for the default engine),
and offers it to the model without any setup on your part: it is
downloaded on first launch if it is missing, and the
view_image tool is always available. Paste a screenshot of
a stack trace and ask what broke, and the model looks at the picture
rather than at a filename. The same applies to a diagram, a screenshot
of a failing UI, or a photo of a whiteboard.
What your message carries is still the path. The model turns that
path into pixels by calling view_image on it, which is why
you see a view_image line in the transcript before the
answer. The image is encoded locally, on the same Metal device as the
model, so nothing about it leaves the Mac.
There is a size budget. An image is resized to fit 384 visual tokens, so a very wide or very tall screenshot loses detail before it loses shape. If you want the model to read small text, crop to the part that matters rather than sending the whole desktop.
For a page of dense text, an OCR pass is still the better tool: ocr-mcp transcribes an image to text instead of describing it, which is what you want for a full-page stack trace or a scanned document.
Reading PDFs
read converts PDFs to Markdown transparently, so you can
just point at one:
summarize the first few pages of manual.pdf
See Tools.
Notifications and the window title
Long turns end with a native macOS notification: your prompt as the
headline, the tail of the answer as the body (interrupted
for an aborted turn). The terminal title tracks the current task:
π fix the bugβ¦ while a turn runs, with the rocket
sparkling through a few glyphs until the turn ends (a static rocket
under ui.reducedMotion), πͺ΅ Plank - READY. at
the prompt. It names the phase when plank is busy with something that is
not your turn: ποΈ compacting... while it reclaims context,
π introspecting... during /insights. The
title it displaced comes back afterwards, so a compaction mid-turn
returns the title to your prompt.
ui.notificationsβalways,unfocused(only when the terminal is not focused), ornever.ui.notifyAfterSecsβ minimum turn length before a completion notification fires (default 10). Awaiting-input notifications ignore it./notifychanges the mode for the running session.
Animation, screensaver, exit
ui.reducedMotioncollapses every animation β throbber, shimmer, pulse, flash, stall-fade β to a static fallback.ui.screensaversets how long the TUI must sit idle before a screensaver takes the screen:1m,2m,5m, ornever. Any key or mouse event dismisses it, and it never appears mid-turn or over a dialog.ui.screensaverFacepicks which one β see The arcade.ui.crtOffplays a CRT power-off animation of the final frame when you exit cleanly./exitduring a turn asks for confirmation, then interrupts the model and leaves as soon as the turn stops; the session is saved as usual.
Leaving prints the session's token totals and, per model, the fastest sustained rates it reached:
peak DeepSeek V4 Flash prefill 167.1 tok/s Β· generation 16.8 tok/s
Both figures are scoped to this session and never persisted β a peak
from last week was a different engine build on a differently loaded
machine. They are the quickest way to see what a flag like
--mtp actually did on your hardware.
Quieting the display
Three settings control how much of the machinery you see. None of them change what the model receives:
| Setting | Default | Off means |
|---|---|---|
ui.showToolCalls |
false |
tool-call banners hidden; tools still run |
ui.showToolResults |
false |
tool output not echoed; the model still gets it |
ui.showThinking |
true |
thinking hidden from the display; the model still thinks |
Next: Slash commands β