The console: hotl
Drive the agent from a full-screen terminal console — streaming transcript, a loop-motif activity strip, modal permission asks, and a vim-style input editor. Assumes a working agent (quickstart.md).
Launch
Section titled “Launch”hotl # new sessionhotl <id-prefix> # continue a specific earlier sessionhotl --resume # pick from recent sessions (numbered list, newest first)hotl resume [id] # same thing, spelled as a subcommandhotl --fork-from [id] # NEW session seeded with that one's history (--keep-turns N for a prefix)Resume continues a conversation; fork branches a new one from it, leaving the original free to keep running. See sessions.md for the full semantics and for phase pipelines.
Bare hotl is the console (the tui subcommand and the old line-based REPL are gone). It needs a real terminal: piped stdin/stdout exits with a pointer at hotl -p "prompt", the headless path for scripts and CI.
The console is a pure ACP client of the same engine -p headless uses — same permission gate, same session logs, same hotl undo afterwards.
Theming
Section titled “Theming”The console wears the same palette as hotl watch, from the same [settings.theme] table in ~/.config/hotl/config.toml:
[settings.theme]preset = "warm" # tokyo-night (the default) | warm | catppuccin | gruvbox | nord | draculaaccent = "#88c0d0" # optional per-slot #rrggbb overridesEight slots: active (working), blocked (waiting on you), idle (settled), ink/muted/faint (text tiers), accent, and band (the strip background). An unknown preset or invalid color falls back with a one-line warning — the console always launches.
Making it warmer
Section titled “Making it warmer”Two knobs, one in each table:
preset = "warm"— a deliberately low-blue palette (paper-white ink, amber accent, terracotta) instead of the cool blue-grey default.[settings] density = "comfortable"(the default) or"spacious"— more room between turns and a wider gutter."compact"is the old edge-to-edge look. See configuration.md.
Font size and family are your terminal’s job, not hotl’s — like vim or htop, the app draws onto whatever grid the emulator gives it and can’t set the point size or typeface. To go bigger or warmer there, change it in your terminal: Ghostty (font-size, font-family in ~/.config/ghostty/config), iTerm2 (Preferences → Profiles → Text), Kitty (font_size, font_family in kitty.conf), Alacritty (font in alacritty.toml). A warm monospace face — Berkeley Mono, Comic Code, IBM Plex Mono — pairs well with the warm preset.
The screen
Section titled “The screen”Top to bottom:
-
Transcript — every turn carries a marker in the left gutter, so you can see the shape of the conversation by scanning straight down:
❯your prompts,●the assistant (with a│bar down a long answer),✓ ✗ ⛔tool cards (✓ bash cargo test · 2s),⤷steers,·dim notices (retries, fallbacks, compaction). Inside an assistant answer, headings, bullets, and code get light styling so a long reply is scannable. With the input empty and no agent band showing, vim Normalj/kscroll it one item at a time; it snaps back to following the bottom on your next prompt. -
Activity strip — one line that tells you what the turn is doing, animated as a loop drawing itself:
You see It means · ─ ·restingidle — your move (after a turn it also shows real token usage; before the first turn, a bare N% ctx— the context the session already occupies at open, which a resumed session inherits)the loop drawing itself, then turning · “thinking” the model is reasoning the loop turning · “writing · ~N tok” the reply is streaming ( ~N tokis a chars/4 approximation; exact usage arrives at the end of the turn)a dot orbiting the loop · tool name a tool is running the loop halted with a gap · “waiting on you” a permission ask — the gap is you; nothing moves until you answer the loop coiling up · “folding history…” context compaction When the model has an active
todo_writechecklist, the strip also carries a compactdone/totalcount — and, while one item isin_progress, that item’s own label (e.g.2/5 wiring the gate) — so you can see plan progress at a glance without opening the transcript. An empty or never-started list shows nothing extra. -
Input — bordered editor, title shows
-- INSERT --/-- NORMAL --. -
Agent band — appears below the input while the turn has sub-agents running:
● mainplus one summary row per running spawn —●marks the stream you are watching. A spawn drops off the band the moment it settles (the one you are viewing or pointing at stays until you leave it), and the band disappears when nothing runs. It is navigated in place:↑/↓(any mode without vim; Normal mode with vim) orj/k(vim Normal) move a highlight over the rows,Enteropens the highlighted agent’s stream — the whole region above the strip swaps to that agent’s own tool stream — or returns tomain, andEscbacks out. See Navigating the agent band. -
Hint row — the keys that matter right now.
Tool cards that stay readable
Section titled “Tool cards that stay readable”Consecutive same-tool calls on the same target absorb into one accumulating
card — four paged reads of one file render ✓ read ~/…/app.rs · ×4 · 3s, not
four rows — and long paths middle-elide in the card (permission asks always
show the full path). A spawn card stays one calm line whether running or
settled: a · N calls count plus — while it runs — its newest child call as
a muted trailing detail (spawn survey · 3 calls · 12s · read src/app.rs).
The full call list lives in the drill-in: highlight the agent in the band
and press Enter.
Scrolling
Section titled “Scrolling”| Key | Effect |
|---|---|
PageUp / PageDown |
Scroll the transcript a page (ten items) |
Ctrl-Home / Ctrl-End |
Jump to the top / back to following the newest |
| mouse wheel | Scroll three items a notch |
j / k |
Move the agent-band highlight (vim Normal, input empty); scroll the transcript one item when no band is shown |
Home and End on their own are line motions in the input, which is why the
document-level jumps are the Ctrl pair.
Navigating the agent band
Section titled “Navigating the agent band”While the band is showing and the input is empty, ↑/↓ move a highlight
over its rows — main first, then each running spawn or
workflow — clamped at both ends. A workflow is one row
however many agents it runs; its drill-in lists them with a token count
beside each settled one.
Without vim that is any time the band shows; with vim_mode = true the
arrows move the highlight from Normal mode only (Insert keeps them for
history recall), and j/k move it too. The first press engages the
highlight on the row you are watching and moves from there, so ↓ from
main lands on the first spawn in one press.
Enter on a spawn row opens that agent’s live stream; Enter on main
returns to the transcript. The highlight stays put after Enter, so ●
(the stream you are watching) and the highlight (where you are pointing) are
two independent things that usually coincide. Typing drops the highlight and
lands in the prompt. The wheel and PageUp/PageDown scroll whichever
stream is shown and never touch the band.
A spawn you are viewing or pointing at stays listed after it settles (with
its ✓) until you move off it or back out; a spawn settling above the
highlight never shifts it onto another agent.
Esc walks back one rung per press: an open agent stream returns to main
(highlight kept), an engaged highlight disengages, and only then — with the
input empty, and under vim from Normal mode — does it interrupt the running
turn.
Selecting and copying
Section titled “Selecting and copying”Drag with the left mouse button and the region highlights; let go and it is on
your clipboard. The hint row confirms with copied N lines, and the next key
you press clears both the highlight and the notice.
What you see is what you get. Dragging a transcript line from the left edge copies the prose without the gutter pad or the role glyph, so pasting a reply elsewhere gives you the text and nothing else. Start the drag partway into a line and you get exactly the span you dragged. The input box and hint row have no spine and copy verbatim.
Selection is a region of the screen, not of the conversation, so it cannot reach above the top of the window — scroll first, then drag.
The copy travels as an OSC 52 escape, which needs no helper process and works over SSH. Two caveats worth knowing:
- Inside tmux it needs
set -g set-clipboard on. - A few terminals disable OSC 52 by default as a security measure. There is no reply to check, so a refused copy is silent.
Mouse capture is what makes the wheel and the drag work, and it costs you the
terminal’s own drag-select. Hold Shift to bypass capture on most emulators
and get the real thing back for one drag. To turn things off for good:
| Setting | Effect |
|---|---|
[behavior] copy_on_select = false |
Drag does nothing; the wheel still scrolls |
[behavior] mouse = false |
No capture at all — the terminal owns the mouse again |
HOTL_MOUSE=0 |
Same as above, per-run; overrides the config key |
Pasting
Section titled “Pasting”Multi-line paste works: the terminal hands the whole payload over at once. It
does not submit — press Enter when you are ready. (Before bracketed paste, a
ten-line paste arrived as ten Enter presses and fired ten turns.)
A paste of three or more lines compacts to a [Pasted text #1 +N lines] token
instead of flooding the input box; the full text is held aside and reaches the
model verbatim when you submit. The transcript keeps the token — what you see
is what you typed, not a wall of paste.
Drag an image file onto the terminal and the pasted path becomes an
[Image #1] token the same way. On submit, hotl reads the file and sends it
to the model as a real image (png, jpg/jpeg, gif, webp — case-insensitive;
5MB per image, 8 images per prompt, 16MB decoded total per prompt). The
console enforces those same caps itself before sending, so a path that no
longer resolves, an image over a cap, an empty file, or a prompt over the
total all degrade to an inline note on that one attachment — never an error,
and the rest of the prompt still sends. Models whose catalog entry cannot
take images get the text with a one-line omission note instead.
Past roughly 24MB of base64 (~18MB of image data) alive in the session’s live context, hotl folds older history to make room.
Tokens are ordinary text, and Backspace only swallows one whole while a
live attachment backs it — the same bracketed text typed by hand deletes one
character, like anywhere else. Editing inside a real token turns it back
into literal text (its held content is dropped at submit). Recall (↑/↓)
and prompt history both replay the expanded text — the path, not
[Image #1] — so a recalled entry is self-contained.
Model thinking
Section titled “Model thinking”When the provider returns reasoning, it renders dimmed under a faint spine,
collapsed to the first three lines with a [+N lines · ctrl-t] trailer.
Ctrl-T toggles the full text. HOTL_THINKING=0 turns extended thinking off
at the engine, which is what you want if you are not reading it — it is billed
either way.
There is no bell, ever — salience is visual only. hotl watch is the thing that pings across panes; the console itself is silent.
Prompting and steering
Section titled “Prompting and steering”Type and press Enter to prompt. Typing while a turn runs is steering: submit and it becomes a pinned ⤷ chip — dim while queued, and the engine folds it in at the next step. Shift/Alt+Enter inserts a newline.
History recall
Section titled “History recall”Your submitted prompts are remembered across sessions (shell-style), stored under [history] in config.toml (configuration.md).
↑/↓— walk previous prompts. Recall triggers only at the buffer’s edge:↑from the first line steps to an older prompt,↓from the last line steps to a newer one; anywhere else the arrows just move the cursor between lines. What’s on the line when you start walking becomes a prefix filter — typegitthen↑and you only cycle prompts that began withgit. An empty line walks everything. Your in-progress text is saved and comes back when you press↓past the newest match; editing a recalled prompt keeps it and drops you out of recall. While the agent band is showing and the input is empty,↑/↓move the band highlight instead (withvim_mode = trueonly from Normal mode — Insert always recalls).Ctrl-R— reverse-incremental search. The input line becomes(reverse-i-search)'query': match; each character narrows to the most recent prompt containing it, and pressingCtrl-Ragain steps to the next older match.Enterdrops the match into the input to edit or send;Esccancels and restores what you had.
Only prompts that start a turn are saved to disk — steers and /slash commands aren’t, though the running session still recalls everything you typed. Consecutive duplicates are collapsed, and the file is size-bounded (see [history]). Vim k/j remain pure cursor/scroll motion — recall is on the arrows.
Slash commands
Section titled “Slash commands”A line starting with / is handled locally and never becomes a prompt on its
own.
| Command | Effect |
|---|---|
/rename <name> |
Rename the session (1–64 chars); the badge and terminal title follow. |
/plan [on|off] |
Toggle plan mode: write/edit always ask, everything else follows the mode (see permissions-and-sandbox.md). Bare /plan toggles; on/off are for scripted input. |
/mode <ask|bypass|dontask> |
Switch to that permission mode. An unknown name prints usage and changes nothing; /mode plan points you at /plan. |
/effort [level] |
Set the reasoning depth: low | medium | high | xhigh | max, or default to hand it back to the provider. Bare /effort reports the current rung rather than cycling — five rungs are not a toggle. Recorded durably, so hotl resume keeps it. See configuration.md. |
/goal [condition] |
Run until a condition is met: after each turn a small fast evaluator judges the condition against the conversation — not yet and the agent keeps going with the verdict’s reason as guidance, met or impossible and the goal clears with a transcript notice. ◎ /goal active · Nm rides the status strip meanwhile. Bare /goal reports the condition, elapsed time and turns taken; /goal clear (or stop/off/reset/none/cancel) ends it early. There is no built-in turn cap — bound it in the condition itself (“…or stop after 20 turns”). An active goal survives hotl resume (counters restart); an achieved or cleared one does not. Headless: hotl -p --goal "<condition>" "<prompt>" runs the loop in one invocation. |
/reload |
Re-read config.toml without losing the session (see Reloading config). |
/help |
Open the key overlay. ? only works from an empty input; this works whatever you have typed. |
/status |
What this session is running: name, model, permission mode and plan state, reasoning effort, context window, todo count. |
/context |
What is filling the window, by source (see The context report). Safe to run mid-turn. |
/workflows |
Every run of the workflow tool this process has started, with per-phase progress, tokens and elapsed time — live, so safe mid-run. See workflows.md. |
/cost |
Session token totals and, when the provider reports one, cost. |
/clear |
Clear the transcript view. The session log and the model’s context are untouched. |
/quit |
Leave the console (the session log is already on disk). |
/<skill> [args] |
Load one of your skills by name and follow it, with the rest of the line passed as arguments — any attached images ride along too. |
/<workflow> [args] |
Run a saved workflow recipe (~/.config/hotl/workflows/<name>.toml) with the rest of the line as its arguments. Names resolve builtin → skill → workflow, so a recipe named like a skill is unreachable here (and hidden from completion); rename it. |
Typing / opens a menu of every command and skill, filtered as you keep
typing: ↑ / ↓ pick, Tab completes the highlighted name, Enter runs
it, Esc dismisses. The descriptions beside each skill come from its
roster entry and cost nothing by default: the always-sent tool description
omits them, and the model only sees one if it explicitly queries the skill
tool for it.
The context report
Section titled “The context report”/context answers “what is filling my context window, and how much room is
left?” It prints into the scrollback rather than taking over the screen, and
it is a read: it appends nothing to the session and starts no turn, so
unlike /reload it is safe to run while a turn is running.
· Context Usage — claude-opus-5 · 1.0M window·· ▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁·· reported 241.3k / 1.0M (24%) last turn· estimated 262.8k / 1.0M (26%) rows below·· ▣ system prompt 5.3k (0.5%)· ▣ tool schemas 14.4k (1.4%)· ◆ memory 1.8k (0.2%)· ▪ messages 102.4k (10.2%)· ▪ tool results 138.8k (13.9%)· ▫ free space 737.2k (73.7%)Two totals, on purpose. reported is the provider’s exact figure for the
last turn — accurate, but a single number with no breakdown. estimated is
hotl’s own per-item accounting, the same ruler that decides when to fold
history, and it deliberately overcounts so that estimation error causes an
early fold rather than an overflow. Showing both means the gap between them is
visible: that gap is the overcount margin. Free space is computed from
whichever total is larger, so the report may understate your remaining room
but never overstates it. Before the first turn there is no reported line.
The rows cover everything in the window exactly once — every item lands in one row, and the rows sum to the estimate. Zero rows are hidden.
| Group | Rows |
|---|---|
▣ stable prefix |
system prompt, tool schemas, skills roster, agents roster |
◆ session preamble |
project instructions, memory, todos |
▪ conversation |
messages, tool results, folded history, harness injections, images |
▫ free space |
turns red below 15% |
The shape carries the grouping and the color separates rows within a group, so
the table still reads on a monochrome terminal. skills roster and agents roster get their own rows because the skill and spawn tools carry their
whole roster inside the schema hotl sends every turn — they are often the
largest single line in the prefix. harness injections is where system
reminders, doom-loop nudges, retry feedback and sub-agent results land;
a large sub-agent result is the single biggest surprise a session can hit,
which is why it is not folded into messages.
The meter is the same numbers as one row, colored per row in table order. On a terminal narrower than about 24 content columns it is dropped rather than rendered as a misleading two-cell bar.
/context is TUI-only. It describes a live session’s assembled context, and
hotl -p builds one, runs it and exits — there is no session left to measure.
Under hotl acp the same thing is a session/context call: the reply is a
thin {"ok": true} ack and the report itself arrives as a context_report
session/update, so every attached surface sees it.
Reloading config
Section titled “Reloading config”/reload picks up config.toml edits without ending the session. It re-reads
the file, rebuilds the engine from it, and re-opens your session onto the new
one — the transcript stays on screen, and the model keeps its context, todos,
name and permission mode. Everything the engine owns reloads: [provider] model, [[allow]] rules, [[mcp]] servers, [[hook]], the skill roster,
system-prompt.md, [context]. So do the console’s own settings: theme,
density, vim_mode, mouse, copy_on_select.
It reports what it got — config reloaded — model … · mode … · N skill(s) —
and any warnings the reload produced. If the file does not parse, or the
provider it names cannot be selected, nothing changes: the running engine
keeps serving and the notice says the previous config is still live.
Two things it deliberately does not do:
- It refuses while a turn is running. Rebuilding replaces the session, and
the reply in flight would go with it. Let the turn finish, or press
Esctwice to take control back, then reload. Abandoning work stays your call. - It does not override a posture you chose.
/modeand/planare both logged with the session, so a session runningdontaskbecause you asked fordontaskstill runs it after a reload, and a session you put in plan mode stays there — the same inheritancehotl resumegives you. A session that never set either has none of its own, so it picks up the reloaded[permissions]values. Either way the notice and the badge name what you ended up with; the badge is always what the engine is actually enforcing.
Some settings are fixed for the life of the process and a reload will not move them. Restart hotl to change these:
| Setting | Why |
|---|---|
[sandbox] extras |
Installed once, before the first tool can run |
[network] egress |
Same — and widening egress mid-process would be a hole, not a feature |
[behavior] sandbox |
The confinement probe has already run |
[concurrency] worker_threads / blocking_threads |
Resolved before the async runtime exists |
[history] |
The recall ring is loaded at startup; re-reading it would drop prompts submitted since |
Under hotl acp the same thing is a session/reload_config call, so an editor
or orchestrator embedding hotl gets it too.
The permission-mode badge
Section titled “The permission-mode badge”The strip always shows the session’s permission mode, next to the session name. Read it before you walk away from a run.
The mode it shows is the one the engine is actually enforcing: it arrives from
the server when the session opens and again whenever it changes, so it is never
a guess. If a build coerces your request — a security-enforced build forces
bypass back to ask — the badge shows what you got, not what you asked for.
bypass and dontask wear the blocked color, because in those modes nobody is
being consulted before a tool runs.
Plan mode is the other half of the posture, so the badge carries both: with it
on the chip reads plan · bypass and takes the accent color, because plan is
something you deliberately chose rather than a default you inherited.
hotl setup writes mode = "bypass". If you took the setup default, your
sessions approve mutating tool calls without asking. /mode ask switches back,
and /plan narrows just the file edits without giving up the shell.
Switching either axis never starts a turn — it’s session bookkeeping, and both
are durable (hotl resume restores the posture you left the session in).
Built-ins are matched first, so a skill named rename cannot shadow
/rename. Any other name is looked up in your skill roster — bare
(/brainstorming) or qualified (/superpowers:brainstorming). A name that
matches nothing prints an unknown-command notice and costs you no turn.
/<skill> exists because the agent is shown a compact index rather than every
skill’s description (skills.md); when it doesn’t
reach for the skill you had in mind, this is how you hand it over directly.
Permission asks
Section titled “Permission asks”An ask freezes the loop (the gap glyph) and opens a modal with the tool summary — and a loud ⚠ line when a protected path is involved. y allows. n starts a deny: type an optional reason, Enter sends it (the reason goes to the model verbatim; Esc backs out of the deny).
Questions
Section titled “Questions”The agent can also ask a structured question (ask_user) — a header, a prompt, and 2–4 numbered options — when it hits a genuine ambiguity instead of guessing. It freezes the loop the same way a permission ask does (same gap glyph, same “waiting on you” strip), but it is not a permission ask: answering it never authorizes any tool, it only supplies text the model reads on its next turn.
Press a digit (1–4) to pick that option — it submits immediately, no confirm step. To answer with something not listed, just start typing: the modal switches to free text, Enter submits it, Esc clears it back to the picker.
In headless (-p) or JSON mode there is no one to ask, so the question resolves immediately to a documented “no human available” answer and the model proceeds on its own judgment — it never hangs a scripted run.
Interrupting
Section titled “Interrupting”Esc(with the input empty) — interrupt the running turn; press again to insist. Withvim_mode = truethe interrupt fires from Normal mode only —Escin Insert just changes mode (so it isEsc Escfrom a fresh prompt), and a shown agent stream or a band highlight backs out first, one rung per press.Ctrl-C— cancel the turn while one runs; quit from idle.
Vim keys
Section titled “Vim keys”Off by default — the input editor is a plain insert-mode field unless you ask for more. Opt in with vim_mode = true under [behavior] in config.toml (configuration.md). Note that turning it on gives Esc its Normal-mode meaning: Esc in Insert never interrupts a turn — the interrupt is Esc again from Normal, or Ctrl-C from anywhere.
(hotl watch’s own [settings] vim_mode is a separate key and stays on: there the vim letters are additive over a read-only list, and arrows/enter/q/r work either way.)
| Keys | Do |
|---|---|
Esc / i a I A o O |
Normal mode / back to Insert (with the usual cursor placement) |
h l 0 $ w b e |
Motions, with counts (3w) |
d c y + motion |
Delete / change / yank; dd cc yy for the whole line |
x p u |
Delete char · paste · undo (one level) |
j k |
Move the agent-band highlight when the input is empty (scroll the transcript when no band); move lines otherwise |
↑ ↓ |
Recall prompt history at the buffer’s edges (see History recall); Ctrl-R searches it; move the agent-band highlight from Normal with the input empty |
/ then ↑ ↓ |
Pick from the command menu; Tab completes, Enter runs it, Esc dismisses |
Enter |
Submit (either mode) |
The $EDITOR escape hatch
Section titled “The $EDITOR escape hatch”Ctrl-E (any mode) or :e (normal mode) suspends the console and opens the current input in $EDITOR (falls back to vi). Save and quit to bring the text back into the input; quit without saving to leave it unchanged.