Skip to content

TUI Reference

./bin/craze
./bin/craze --provider grok

The TUI is the default command. It refuses to start on a non-tty.

Without --provider, a centred provider dialog lists cursor, grok, gx and native — gx shown when a binary for it resolves, or when it is the resolved default (the default is always listed, so Esc never starts a row the picker did not show), since gx is a third-party fork nobody can assume is installed (see Configuration) — before the session is constructed. The preselected row is the resolved default (see Configuration). ↑/↓/Tab move, Enter starts that row, Esc starts the default. After Start the provider cannot change.

A row the command line rules out is not started: native with --agent-bin or CRAZE_AGENT_BIN set, since it runs inside craze and has no binary to spawn. Enter on it shows the refusal --provider native would have exited 2 with, as an error row under the list, and the dialog stays open for another choice; nothing is saved as the default. Moving the cursor clears the row.

The provider dialog's keys: ↑/Shift+Tab and ↓/Tab move the cursor, wrapping, Enter starts the row under it, and Esc or a click outside the box starts the default.

Provider states

As the dialog opens, craze checks each provider the way craze providers does — whether its binary resolves, whether native has a model provider with a key, and on a Mac whether this craze runs in the login session cursor needs — and marks every row that cannot start:

╭────────────────────────────────────────────────────╮
│provider                                            │
│> cursor                       unavailable · default│
│  grok                                              │
│  native                                 needs setup│
│cursor-agent not found on PATH                      │
│install cursor-agent, or set [agents].cursor in     │
│/home/you/.craze/config.toml                        │
│↑↓ · tab · enter starts · esc uses default          │
╰────────────────────────────────────────────────────╯
  • The fix names the config file by its absolute path, whole: it wraps onto a row of its own, and a path wider than the box is broken after a /, never in the middle of a name.
  • A row that is not ready is dimmed, and its tag says its state — unavailable or needs setup — before default. When the box is too narrow for both, default is dropped first.
  • The lines under the list say why the selected row cannot start and, under that, what to do about it, as craze providers would. Each wraps over up to two rows, and one that runs longer is cut short with …, so there are at most four. The box keeps as many as the longest reason, fix or refusal of any row needs — blank on a ready row — whenever any row is not ready, so it keeps its height as the cursor moves and when a refusal shows. In a short terminal the fix rows go first, all of them, before the key hint and any row; then the reason's second row (the first then ends in …). With every row ready the dialog looks exactly as it always has.
  • Enter on a row that cannot start — or Esc, or a click outside the box, when the default cannot start — does not start it: the reason becomes can't start cursor: <reason> in red, with that provider's fix under it, and the dialog stays open for another choice. Moving the cursor clears it. A refusal of the command line's (above) is checked first, and takes the reason's rows alone, with no fix under it. It is known only once it is asked, so when it needs more rows than the box keeps, the box grows by them while it shows; so does what /connect came to (below).
  • native with no model provider that has a key (needs setup) is not refused: picking it — Enter on its row, or Esc or a click outside the box when it is the default — opens /connect's dialog over the picker, where a key is stored or the ChatGPT plan signed in to without leaving craze (see Before any session). Its fix line says so: pick it to connect one, or run "craze auth login". Every way out of the dialog comes back to the picker, with what came of it on the lines under the list, wrapped over up to four rows — connected <provider>: it can be picked now, or why not — and the states checked again; when native can start now the cursor moves onto it, and Enter starts it. Nothing starts on its own.
  • With no row that can start, every choice is refused; Ctrl+C still quits.
  • The check reads only files, never the network, and runs as the dialog opens. Until it answers every row is ready and a choice is taken as it always was. Each answer also decides the rows, as /provider's does: a gx installed since the dialog opened is listed, and one removed is dropped — unless it is the default, which always stays, marked unavailable.

The states are advice for the dialogs alone: --provider <id> and --resume start exactly as they always have, whatever the check says, so a provider it misjudges — cursor's keychain unlocked some other way — can still be started by name. On a Mac, cursor needs the GUI login session, so over ssh run craze from a terminal on the Mac, or through shed-host-agent. A start by name that this craze launches itself from outside that session does not hang silently: it adds a note, cursor may not start here: this craze runs outside the macOS login session (over ssh), where cursor may not reach the login keychain; run craze from a terminal on the Mac, and the start goes on. CRAZE_PROVIDER (like config.toml's provider) only chooses the dialog's default, which the check gates like any other row: --provider <id> is the way past it.

--resume shows a resume picker instead of that dialog, and --continue skips both and loads a session directly — see Resuming a session and CLI reference.

Layout

Top to bottom:

  1. The session band: one row naming the session (title · provider · dir) and the way back to the list (← sessions). It appears only once the session list has been opened in this terminal
  2. Transcript
  3. The overlay, while it is open: the slash menu or the @ file popup
  4. Pinned tasks panel
  5. The queue band: one row per queued message
  6. Spinner line
  7. Composer
  8. The card band, while a card is waiting for an answer
  9. Two status rows (below)
  10. One row per in-flight sub-agent (all providers)

Status rows

Row 1 reads workspace · branch · provider · model · usage · elapsed. The model carries what the agent lets craze set beside it, Name (high · fast): the effort when the model offers one, and fast only when it is on. The usage part is the native provider's alone (see Usage and cost), and the elapsed time is the session's age. When the terminal is too narrow the parts go in this order: elapsed, branch, usage, provider, then the model; only the workspace always stays.

Row 2 reads, left to right:

  • the mode chip ◆ <mode> (see Modes) and a hint beside it, or on a session with no modes nothing;
  • the permission chip: ▸▸ bypass permissions on under --force (the default), ▸ prompting for permissions under --no-force. It states what the session's agent is running with, as its host says it;
  • the tools in flight, by kind (2 reads, 1 command);
  • ⧗ n queued while messages are queued;
  • ← n agents (↓ n agents where ← opens the session list) counting the running rows below: sub-agents and background commands alike;
  • N attached while another client has the session open.

Narrow, row 2 gives up N attached first, then the hint beside the mode, then the queued count and the agent count, then the tool counts. Neither chip is dropped; if the two do not fit together the permission chip is cut short. A copy note (copied 2 lines) takes a place on the row for two seconds and is never dropped.

The composer never scrolls: it grows to show every line up to a cap (6 rows, 3 on a short terminal), then windows to keep the cursor visible. A rule above it names the session — the title the agent set, truncated to half the screen width, or craze before one arrives.

The sub-agent band under the status rows lists every running sub-agent and, for ten seconds after the TUI saw it finish, the finished ones — the one being viewed stays as long as it is viewed. A row is <gutter> <glyph> <label> <description> <suffix>: the gutter is ❯ on the selected row (its description is accent-coloured too) and blank on the others; ○ while it runs, ✓ when it completed, ✗ when it failed, – when it was cancelled, and ● for the one being viewed; the label is the sub-agent type (explore, general-purpose; cursor's rows read task). While it runs the suffix counts up — 0s · 4.7k tok — and once it finishes it reads the duration and model, 2.9s · grok-4.6. Status row 2 counts the running ones (← 2 agents; ↓ 2 agents when sessions run in detached hosts, where ← opens the session list and ↓ is the key that reaches the rows); a lingering finished row is not counted. The keyboard starts in the composer; ↓ moves it to the rows, where the selected row carries the ❯ mark, and ↑ past the first row, Esc or any typed key move it back. Returning from the sub-agent view leaves it on the rows, marking the row the view came from. Rows sit in spawn order and never reshuffle while a sub-agent streams, so ↑/↓ aim at a fixed target; a finished row keeps its slot until it leaves the band, and the rows below close up. Past the cap the band ends in … +n more; the viewed sub-agent always keeps a visible row, taking the last one when it would otherwise fall behind the cap.

When another client has the same session open, status row 2 ends in an accent N attached chip: N counts every client attached to the session, this one included — a craze attach, another TUI showing it from its session list, and the TUI that hosts it under detach = false — so a second client makes it 2 attached, and with you alone it is not there at all. It follows the host's count within about a second (at most two updates a second), hides while the connection to the host is being re-established until the host says again, and is the first thing on the row to go when the terminal is too narrow. A host from before craze counted its clients sends no count, and the chip never shows.

On a provider that can stop one child (native today), Delete or Backspace on the selected running row stops just that child (or a background command); the turn goes on and the parent reads that the user stopped it. On any other row — grok, cursor, or a finished row — the key goes to the composer as before. This works the same on a background sub-agent's row (below).

Background sub-agents

On native, a sub-agent the model started with run_in_background shows in the same band, marked bg first in its suffix — a running row reads bg · 0s · 15 tok, a finished one bg · <duration> · <model> (bg · 8.0s · grok-4.6); the agent call's own card is what goes final at the acknowledgement, before the child ever started. Delete/Backspace stops a running background row exactly like any other.

A background child that finishes while its own turn is still running is steered into the agent's next step, as if it had answered in place. One that finishes after the agent has gone idle wakes it: craze starts a turn of its own that heads the transcript with "sub-agent finished — the agent continues" and delivers the result — with no working spinner, since the status row stays idle for it (the terminal tab title still marks it, as it does grok's own foreign-turn fallback). A prompt typed while the wake runs is queued, and sent once the wake ends. Esc stops a running wake — any turn the agent runs on its own — when nothing of craze's own is working, leaving a cancelled note. Headless craze prompt never sees any of this: every run_in_background call there runs in the foreground, as if the flag had not been set.

Background commands (bash jobs)

On native, the bash tool can also run a command as a job: the model sets run_in_background for a server, a watch command or a tail -f, and the command keeps running while the turn goes on. An interactive foreground command that outlives its timeout (120 s unless the model asked for more, at most 10 minutes) is promoted to a job instead of being killed. The model reads a job's output with bash_output {id, wait_ms?} (a snapshot of what is new, or a wait of up to 10 minutes for the end) and stops one with bash_stop {id}; in a step that also stops a job, the reads do not wait, so a stop never queues behind them. A job runs until it exits, until it is stopped, or for its limit — 30 minutes by default, 2 hours at most, counted from the promotion for a promoted command — and a session holds at most 8 running jobs. When the cap is full, or the session is closing, a command that would have been promoted is killed at its timeout as before, and the result says why.

A job shows in the same band as a sub-agent, with the type bash job, the first line of its command as the description, and bg first in its suffix; Enter on it opens its output in the read-only view. Delete/Backspace on a running job's row, or inside its view, stops it, like a sub-agent. A job never keeps the working spinner on and never blocks /connect, so a dev server running for an hour leaves the session idle; nor is its command counted in status row 2's running tools, or named by the working line of a turn that runs beside it. (The ← n agents count on status row 2 still includes job rows.) A job's row counts its running time from its command's own start — for a promoted command, from before the promotion — and the bash call's own row shows no exit code for a command that was promoted.

A finished job's result is delivered to the model like a background sub-agent's: at the next step of a running turn, through bash_output, or, once the agent is idle, by a wake of its own that heads the transcript with "background command finished — the agent continues". At most 3 wakes in a row carry job results while no turn of yours has started; after that, results wait and arrive with your next prompt. Quitting or closing the session stops every running job without delivering anything. --resume does not reattach jobs: the model is told, at the first turn after the resume, that the session closed before each such command's result was delivered and craze no longer manages it — stopped if the session closed normally, possibly still running if craze crashed, and possibly finished first — and where its output was saved, so it can read that file and check whether the command still runs rather than start it again. Headless craze prompt and sub-agent children run no jobs, and their model is not offered any: its bash has no run_in_background parameter and its description says nothing of jobs or promotion, and it has no bash_output or bash_stop. A command there still runs in the foreground until its timeout kills it.

Resuming a session

--resume opens a resume picker in place of the provider dialog: rows of <title> · <provider> · <age> (age as 3m, 2h, or 5d since the session was last touched), newest first, up to 10 rows. A session that craze new --wait started for a coding agent is a sub-agent's: its row is dim and carries a sub-agent tag after the title, and it comes after your own sessions (it fills the picker only when fewer than 10 of those exist). ↑/↓/Tab/Shift+Tab move and wrap, Enter loads the selected row, and a click loads the row clicked; Esc quits craze outright (exit 0 — no session was ever started, so there is no "default" the way the provider dialog has one) and a click outside the box does nothing, unlike every other dialog. A row that is refused does not close the picker: the reason shows in red under the list, and moving the cursor clears it. --continue skips the picker and loads the newest row for the workspace directly, skipping the sub-agent sessions (when only those exist it refuses, and says how many: the wording). See CLI reference for the flags, the filtering rules, and the exit codes when nothing matches.

Loading a row is a replay, not a fresh start. Status row 1 reads restoring… in place of starting…, and sending a message or running /rename is refused until the whole transcript is back — even after the agent has otherwise answered the initial handshake. The replay renders as ordinary transcript entries (the user's prompt, thoughts, tool rows, replies) and ends with a restored note marking the seam between the old session's history and the live one. A long resume draws in steps of 256 events: while the replay runs, the transcript is redrawn on every 256th replayed event rather than on each one — and at once on a resize, a theme change, Ctrl+O, a switch of view, a scroll, a click, a drag or a copy — and in full when the replay ends. Scrolled up from the bottom, you see every event drawn as it lands. Once restored, the rule above the composer shows the row's stored title (see /rename) instead of craze.

A restored session does not reconstruct everything:

  • Timestamps on replayed entries are when craze loaded the session, not when the agent originally sent them — the agents themselves do not replay their own timestamps.
  • Sub-agent transcripts are not reconstructed. A finished sub-agent's row comes back from the replayed spawn/finish lifecycle, but there is no transcript behind it to open.
  • Grok and gx show no tabs in the model dialog right after a resume — their session/load result carries no catalog, unlike a fresh session/new. A later live turn may supply one again.
  • A rename is craze's own and is never sent to the agent: agent ls and grok --resume still show whatever title the agent itself gave the session.

Native sessions

The native provider (--provider native) resumes the same way as above, over its own transcript in place of an agent's session/load:

  • Tool cards have no exit code, diff or truncation metadata to restore — only the stored output text — so a replayed card's body ends with a (replayed) line: a collapsed row still previews its output's first line, an expanded one ends with the label, and a card with no body of its own (an edit, or an agent call) carries the label unseen.
  • The model is the transcript's last one, matched by identity (provider and wire model) rather than restored by name — a re-pointed alias never silently switches models; one no longer available falls back to your default_model, else to the start model of the first provider in the provider order that can run (see which model a session starts on), never to another model by name, and warns on stderr, naming the alias it fell back to and why; with none of them, the resume refuses and says why each failed. The mode (agent/plan/ask) is the transcript's last mode → <id> change, unless --plan/--ask set it explicitly; an unrecognised last mode restores agent mode instead, warning the same way.
  • The todo list is restored from the last tool entry that recorded one.
  • As with an ACP load, replay shows text output only: a foreground agent call folds to a task card with the child's final text, a background one to its launch receipt, and no sub-agent rows come back — there is no live view behind them to reopen.

The title is kept, and — native sessions being indexed now — /rename persists it back to the index like any other provider's. A session that compacted at some point replays those notes in place too; see Compaction.

Which model a session starts on

A new native session starts where you left off: a model or effort picked in any native session's /model is remembered, and the next session with no --model starts on the newest remembered model whose provider has a key, at the effort last used on it. With no remembered model it can use, it starts on your default_model when you set one and its provider has a key; else on the start model of the first provider, in craze's provider order, that you have connected — glm-5.3 for the Z.AI Coding Plan, the ChatGPT plan's preferred model its account lists, fireworks/ember-1, openrouter/gemini-flash-latest, muse-spark-1.3-contributor, the newest Claude Opus the Anthropic account lists (at medium), in that order (the provider order, which provider_order in models.toml changes); and otherwise on the first model that has a key, with a note. --model picks the model for one start — at the effort remembered for that model — and is never remembered itself. A resume is never moved by the memory: it keeps its transcript's model and effort, as above.

The memory is read when the session starts — which models its list puts first, and where it starts — and again by each switch, for the effort it lands on (below). A switch also moves the model up the running session's own list at once, as well as the next session's. It lives in recent.json beside the model files; see Model memory for the file, who writes it, and what happens when two sessions switch at once.

Effort on a switch

A native switch to a model — /model X, a pick in the model dialog, or an attached client's switch — lands on the effort last used on X, else X's default_effort; it never carries the effort you were on over to another model. So Muse Wonder at max, then /model anthropic/claude-opus-5-5, runs Opus at its own medium (or at the level you last used on Opus), and switching back to Wonder finds max again. "Last used" is, in order: the last effort this session set on X; else the one the model memory remembers for X, read at the switch, so a choice made in another craze since counts; else X's default. A remembered level X no longer offers is its default. Asking for the model you are already on keeps its effort. /model X <effort> still sets a level directly, as two steps: the switch, then the effort. A model with no effort control has none either way. ACP providers (cursor, grok, gx) are unchanged: their effort is whatever the agent's option says after the switch.

What the model is told

Native's system prompt is craze's own text: no sentence is reproduced whole from another agent's, though a few sentences are adapted from wording in the credited reference prompts (grok-build, opencode, and codex; see the opencode tool profile's NOTICE for the full list). After it come the resolved project and user instruction files, and the skills-and-commands catalog — the same one the slash menu is built from. The prompt's last section is a session-start snapshot: today's local date, and — only when the workspace sits inside a git work tree — the branch, the default branch, the status, and the last few commits, taken once when the session opens or resumes and never updated afterwards; outside a git work tree nothing runs, and the git part of the snapshot is left out. No part of the prompt's own text is reproduced here.

Tab title

craze sets the terminal tab title (Ghostty, roost, and anything else that honours OSC 0/2) to <mark> <text>: <text> is craze until the session has a title, then the stored title (agent, first-prompt fallback, or a /rename) truncated to 40 cells. The mark is one glyph, highest priority first:

State Mark When
needs you ⚠ a permission, question, or plan card is open
error ✕ a start failure or a turn error (cleared by the next send)
working ❖ a turn is running, a session is replaying, or a foreign turn is in progress
idle ✦ everything else, including the pre-start picker

The strong-send confirm line does not raise the needs-you mark: it is the user's own keystroke, not the agent asking for something. The title updates the instant the state changes and is cleared on every exit, so the tab falls back to the terminal's own derived name rather than a stale ✦ craze.

Inside a herdr pane or a roost tab the same states also go to the host itself, so its sidebar and its wait command follow craze like any other agent. herdr sees blocked for an open card or an error, working for a turn and idle otherwise; roost shows needs input for an open card, failed for an error, running for a turn, and a "Turn complete" notification when a turn ends. A replay is not reported. See Host status for what is sent, what it changes in the agent's environment, and how to turn it off.

terminal_title = false in ~/.craze/config.toml turns every write off (default: on) — see Configuration. craze prompt and craze frame never set a tab title.

Keys

Key Action
Enter with the @ file popup open on a candidate, pick it; with the slash menu open on a token that is not already typed out in full, accept the highlighted row; on a draft that starts with !, run it in your own shell (see Shell mode); otherwise send the draft, or queue it while a turn is running (see Queued messages)
Ctrl+L the strong send: on Grok, gx and native, add the draft to the running turn without cancelling it; on Cursor, cancel the running turn and send (it asks first). On an idle session it is a plain send. On a /btw draft, where /btw is offered, it opens the side view as Enter does and is never sent into the turn
Alt+Enter, Ctrl+J, Shift+Enter newline; Shift+Enter only in a terminal that sends it as one of the two sequences below, as Ghostty and Roost do (see below)
Esc answer the card on top; close a dialog (/help included); close the side view; leave the sub-agent view; kill a running ! command; clear a ! draft; hide the slash menu or the @ file popup for the token under the cursor — a second Esc then cancels the running turn; otherwise cancel the running turn (the transcript says cancelled), or stop a turn the agent runs on its own (a native wake), or, with nothing running, decline the plan offer. An Esc pressed immediately after Enter cancels that turn; craze never writes the cancel ahead of the prompt
Ctrl+C kill a running ! command, and nothing else — it is the only way to stop one while a card has the keyboard. With none running: cancel the running turn and everything queued behind it — the queue, a confirm on screen, a send-now waiting to fire; a second press within one second ends the session; it ends it outright when idle or after an error. Inside the sub-agent view it still cancels the main turn, and the view stays open. Inside the side view, with no card up, it closes the view and does nothing else
Ctrl+D ends the session, always
Shift+Tab cycle the ACP mode (agent / plan / ask); inside the sub-agent view, switch to the previous sub-agent instead
Ctrl+T, /tasks tasks panel: compact → expanded → hidden
Ctrl+G, /theme theme picker
Ctrl+O expand / collapse transcript detail (diff hunks, command output, thoughts) — works inside the sub-agent view too
Ctrl+Y copy the mouse selection, or the last reply when there is none (works with --no-mouse); inside the sub-agent view, copy from it; in /connect's ChatGPT sign-in, copy its address
↑ ↓ move the keyboard out of the composer (draft or not) and then between rows: the selected row carries a ❯ gutter mark and the composer loses its cursor. ↑ reaches the queue band first and the sub-agent rows when nothing is queued; ↓ reaches the sub-agent rows first and the queue band when there are none. ↑ past the first row, Esc, or typing anything returns to the composer; inside the sub-agent view, scroll it; with the slash menu or the @ file popup open, move its highlighted row instead, wrapping at either end; in a dialog, move its selection (in /help, scroll the box)
Enter while the sub-agent rows have the keyboard open it in the main area, read-only (see Sub-agent view)
Backspace / Delete on a focused running sub-agent row, or inside a running sub-agent's view stop that one sub-agent (native only); on any other row or provider the key falls through as if unhandled
Enter while the queue band has the keyboard edit that message in place — its text loads into the composer, Enter saves, Esc restores the draft
Backspace / Delete on a queued row cancel it
Ctrl+L on a queued row send it now instead of the running turn (it asks first)
Ctrl+L while editing a queued message save the edit and send that message now, so its text cannot go out twice (once as a draft and again from the queue)
← on an empty composer, /sessions open the session list — only when sessions run in detached hosts (the default); under CRAZE_DETACH=0 the key reaches the composer as it always has
← with a card up open the session list with the card left unanswered: the ask stays open, and the card is there again when you come back. A card lets this key through only where there is a list; Alt+← stays the card's
Esc or ← inside the sub-agent view return to the main transcript; entering or leaving cancels nothing
Enter on /btw <question> where the session answers side questions (native, Grok, gx): open the side view and ask it, even mid-turn; inside the view, Enter asks a follow-up
Esc or Ctrl+C inside the side view close it: the question in flight is cancelled and the side thread discarded; the main turn runs on, and Ctrl+C arms no quit (with a card up, Ctrl+C is what it always is)
Tab inside the sub-agent view switch to the next sub-agent
PgUp / PgDn scroll the transcript, or page the /help box; page the slash menu instead when it is open — the menu takes priority over both
wheel scroll the transcript three lines a notch; over an open slash menu, move its selection one row a notch instead — it selects, it does not page (see Mouse)
Tab with the slash menu open, accept the highlighted row (see Slash commands); with the @ file popup open, open the highlighted folder or pick the highlighted file; elsewhere a no-op
Ctrl+V paste the clipboard into the composer: an image becomes an [Image #N] chip, otherwise its text (see Images)

/help lists the same keys plus every slash command — see Help dialog. /exit ends the session; there are no bare q or ? bindings, so a message that starts with either is just a message.

Shift+Enter

Ctrl+J and Alt+Enter are the dependable newline keys (Alt+Enter needs a terminal that sends Alt, Option on macOS, as Meta). Shift+Enter is a newline when the terminal sends it as one of two sequences: ESC [ 27 ; 2 ; 13 ~, xterm's modifyOtherKeys form, which Ghostty and Roost (built on libghostty) send for Shift+Enter without being asked, or ESC [ 13 ; 2 u, the kitty keyboard protocol's. craze reads them when its standard input is the terminal, as it is when you start craze from a shell, and inside a bracketed paste they stay pasted text. Most other terminals send Shift+Enter as a bare Enter unless the program turns a keyboard protocol on, which craze does not (bubbletea v1 cannot read the rest of the kitty protocol); there craze cannot tell the two apart, and Shift+Enter sends the message. A terminal that lets you remap keys can be set to send either sequence for Shift+Enter.

Why Ctrl+L and not Ctrl+Enter

The same limitation: bubbletea v1 has no Ctrl+Enter and does not parse the kitty keyboard protocol, so in most terminals Ctrl+Enter arrives as a bare \r — indistinguishable from Enter, which is the key that queues. Grok's own TUI falls back to Ctrl+L for the same reason, so craze does too.

Queued messages

While a turn runs, Enter queues the draft instead of refusing it. The queued messages appear in a band above the spinner, newest last:

  #1 Reply with PINEAPPLE
  #2 Reply with MANGO          [send now] [edit] [cancel]
✳ Working · 12s · esc to interrupt

One is sent per settled turn, in order, and it only becomes a transcript entry when it is actually sent. Status row 2 carries ⧗ n queued so the number is visible even when the band has been degraded away on a short terminal.

The three actions are drawn on the row under the pointer and on the row the keyboard is on. Each is also a key: Ctrl+L sends that message now, Enter edits it in place (the row keeps its number and its place in the queue), and Backspace cancels it.

The three verbs

Verb Cursor Grok, gx, native Cancels the turn? Asks first?
queue (Enter on a draft during a turn) craze's queue craze's queue no no
send now (Ctrl+L on a queued row; on a draft under Cursor) cancels the turn, then sends same yes yes
interject (Ctrl+L on a draft under Grok, gx or native) not offered — Ctrl+L is send now merged into the running turn no no

Grok (and gx) and native have a mid-turn path: grok's x.ai/interject hands it text that it folds into the turn at its next safe point (after a tool result), and native steers it into the turn's next step. The transcript shows either as a ↳ entry. Cursor has none — a second prompt there silently cancels the first — so craze never sends one and offers send now instead, always with

cancel the running turn and send? enter · esc

in place of the composer's input rows. Esc, or any other key, declines and loses nothing: nothing leaves the composer or the queue until the message actually goes.

What clears the queue and what does not

Queue
Esc (cancel the turn) kept — the next queued message starts once the cancelled turn settles
Ctrl+C (first press) cleared, along with a confirm and a pending send-now
/clear cleared
A turn that ends in an error cleared, with a note — nothing drains from an error state
Quitting gone with the session; there is no persistence across restarts

Slash builtins never queue. /exit, /help, /theme, /tasks, /clear and /rename run immediately even mid-turn, and so does /btw where it is offered — it opens the side view beside the running turn rather than behind it — and /sessions where there is a session list. The ones that need the agent (/model, /effort, /plan, /ask, /agent, …) are refused while a turn runs — yours or one the agent started on its own — with one error row in the transcript, Finish or stop the running work first, then /model. (naming the command you typed); the draft stays in the composer, so Enter runs it once the work is done. /connect is refused with the same line. Clicking the model in the status row still opens the model box mid-turn. A slash line the agent advertises is ordinary text and queues like any other message.

The queue holds 32 messages of up to 32 KiB each. Past either bound the message is refused, never truncated, and the draft stays in the composer so you can shorten it: status row 2 says queue full or message too long.

When the agent talks on its own

Grok turns an interjection it could not merge — one that arrives when no turn is running, or too late in the one that is — into a turn of its own, called an interject fallback. craze shows its reply under

agent continued on its own (interjection fallback)

and holds the queue until it ends: a message sent into that turn would be queued behind it on the agent's side, where craze can no longer tell its ending from the fallback's.

Note

Ctrl+G is BEL. Some terminals flash or beep when it is pressed. /theme opens the same picker without the BEL.

File mentions

@ at the start of a word in the composer — the start of the draft, or after a space or a newline — opens a popup of the session's workspace, its files and folders, in the band above the composer where the slash menu opens:

───────────────────────────────────────────────────────── files in craze ─
❯ internal/config/
  internal/tui/colors.go
  internal/tui/complete.go
  internal/tui/composer.go
  internal/config/config.go
  internal/tui/composer_at.go
  internal/tui/complete_test.go
  internal/tui/composer_test.go
  ↓ 3 more
───────────────────────────────────────────────────────────────── craze ─
❯ explain @co

Typing narrows it. What you type after the @ matches a path when its letters appear in it in order, anywhere, ignoring case; a file or folder whose name starts with it comes first, then matches at the start of a path segment and in unbroken runs, and on a tie the shorter path. A / in it narrows to that folder: @internal/tui/ lists what is inside internal/tui/, and anything typed after the last / matches inside it. The popup lists the workspace alone — @/, @~/ and @../ offer nothing. Folders end in /.

Key Action
↑ ↓, Ctrl+P Ctrl+N move the highlight, wrapping at either end
Tab on a folder, open it — @internal/ is written with no space after it, and the popup stays open on what is inside; on a file, pick it. It never sends
Enter pick the highlighted entry. With nothing to pick — nothing matches, or the list is still loading — Enter is the composer's and sends the draft as typed
Esc hide the popup for this word; the draft is left alone, and a second Esc cancels a running turn. Moving the cursor, within the word or away and back, keeps it hidden; typing into the word, or starting another, brings it back. Editing a queued message, Esc still cancels the edit

While it is open the popup takes those keys from the composer — ↑/↓ do not move the keyboard to the queue or the sub-agent rows, and Ctrl+P/Ctrl+N do not move between the draft's lines. Everything else is the composer's: Alt+Enter starts a new line (which ends the word, and closes the popup), Ctrl+L is the strong send, PgUp/PgDn scroll the transcript. It has no mouse: a click on it does nothing. Only the @ word the cursor is in is completed; a draft can mention any number of files. An @ inside a word (user@example.com) opens nothing, and neither does a draft in shell mode, a card, a dialog or the sub-agent view. It works the same however the session runs — a detached host, CRAZE_DETACH=0, craze attach — and the list is always read on this machine, in the session's workspace. /help does not list it.

What is inserted

A pick replaces the word with @, the path relative to the workspace, and one space: @internal/tui/app.go. A path with a space in it is quoted, with \" and \\ escaped inside the quotes — @"docs/guide/getting started.md" — and a folder ends in /: @internal/tui/. That text is exactly what the agent receives: craze expands nothing and attaches nothing — the file's contents are not sent with the message. In the popup's row, a path's Unicode default-ignorable and format characters — a right-to-left override, a zero-width space, a variation selector, a Hangul filler: characters a terminal draws as nothing, as a blank, or by reordering what follows — are shown as their code points (<U+202E>), and a run of spaces as one; the rest of the name, an accent included, is drawn as it is. The pick writes the name exactly as it is.

What happens next is the provider's. grok reads the @path in the message itself and attaches the whole file to what it sends its model — the message unchanged, the file's contents beside it — so no tool row appears. cursor and native receive only the text and read the file with their read tool: a ✓ read README.md row before the reply (native sometimes searches for the file first).

Where the list comes from

The first @ of a popup lists the workspace once, in the background, and every keystroke after it matches against that list while the popup stays open:

  1. rg --files --hidden -g '!.git' when rg is on your PATH — the files ripgrep would search: .gitignore, .ignore and git's excludes honoured, dotfiles shown, .git never, and your RIPGREP_CONFIG_PATH not read (--no-config);
  2. otherwise, in a git work tree, git ls-files -co --exclude-standard — tracked files, and untracked ones git would not ignore;
  3. otherwise a walk of the folder, which skips .git and node_modules and neither follows nor lists a symbolic link.

Folders are derived from the files' paths, so an empty folder is never listed — nor, under rg or git, a folder whose every file is ignored. A path with a control character or invalid UTF-8 in it is left out: it could not be written into a mention.

Bound Value When it is reached
Paths read from rg or git 50,000 the rest are not listed, and the title says only the first 50,000 files
Entries the walk reads 20,000 the title says only the first 20,000 entries
Time 3 s for the whole listing what was read is offered, the title saying listing stopped after 3s; with nothing read, the popup says listing files took over 3s
Matches handed to the popup the best 100 the rest are counted in ↓ N more: type more to narrow

The popup says searching… while the list is read, nothing matches when nothing does, no files here in an empty workspace, and could not list files: <why> when the listing failed.

Images

An image in the composer is a chip: the text [Image #1], [Image #2] and so on, sitting in the draft where you put it. The picture itself is kept in craze's attachments directory and travels with the message when you send it. The transcript shows the chip text and nothing else: there are no inline previews.

Making a chip

Gesture What happens
Paste, or drag-and-drop a file into the terminal a bracketed paste whose every word is the absolute path of an image (png, jpg, jpeg, gif, webp, bmp) becomes one chip per path. Quotes, backslash escapes, file:// URLs and a leading ~/ are understood, and several paths may be separated by spaces or newlines
Ctrl+V with an image on the clipboard the image becomes a chip (on macOS only PNG clipboard data is read, which is what a screenshot is; a JPEG, GIF or WebP on the clipboard is not). With no image on the clipboard it pastes the clipboard's text, as it always has
Alt+V the same as Ctrl+V (the keys table and /help name Ctrl+V only)
An empty paste what a terminal sends when you paste an image it cannot paste as text: craze looks for an image on the clipboard. It does not look over SSH (SSH_CONNECTION, SSH_CLIENT or SSH_TTY set), where the clipboard craze can read is the remote machine's, not yours

Reading the clipboard's image needs wl-paste (Wayland), xclip (X11) or, on macOS, osascript. Without the tool, or without an image, Ctrl+V pastes text. It pastes text only when the clipboard holds text: an image that does not become a chip (in shell mode, say, or one too small) pastes nothing, and clipboard contents that are not text (binary data, a NUL byte) are never pasted anywhere, the session list and the /connect key field included; the status line says the clipboard's text was not pasted: it is not text. Pasted text loses its control characters other than newlines and tabs. Text an X11 application offers only in the old STRING form is read as Windows-1252, so its curly quotes, dashes and euro signs paste as themselves.

A path is only turned into a chip when the file exists, is a regular file, is at most 20 MiB, decodes as an image and is at least 8×8 pixels. These stay plain text, never a chip:

  • a path you type: only a paste is ever turned into a chip;
  • a paste that mixes paths with other words, or has any word that is not an image path craze can open;
  • anything pasted in shell mode;
  • a paste into the session list or the /connect key field (see /connect): they take text only;
  • a paste in a session whose host cannot read craze's attachments (see below).

Editing and sending

  • Backspace at the end of a chip, or Delete at its start, removes the whole chip. A chip you break by hand (or in an external editor) stops being an image: only an intact [Image #N] is sent.
  • The number is for the message being written: it restarts at 1 after each send and after /clear. Within one draft a number is never reused until it runs out past 99.
  • A message holds at most 10 images and 15 MiB of them together (each image counted at its processed size, or at most 3.75 MiB), and the attachment block that names its images (the tags and JSON around the paths included) is at most 4 KiB, each path at most 1024 bytes (which binds only when craze's attachments directory has a very long path). A paste over any limit stays text, with a status note.
  • Processing takes a moment, and images are processed one at a time (in no promised order). While an image is still being attached, Enter (and Ctrl+L, the first prompt of a new session, and saving a queue edit) does nothing and keeps the draft; the status line says image #N is still being attached; press enter again in a moment. A file that cannot be attached is put back as the text you pasted, with a note.
  • Drafts keep their chips when you move between sessions, and a queued message keeps its images: editing it from the queue band loads the chips back.

What craze does to the image

Anything wider or taller than 2000 pixels is scaled down to 2000 on its long edge, an image over 3.75 MiB is re-encoded smaller, and images lose their metadata, per format:

  • a jpeg loses its EXIF and XMP (APP1), IPTC (APP13), comments, every APP2 segment but its colour profile (a phone's Multi-Picture preview images among them), the other vendor application segments, and anything stored after the image itself;
  • a png keeps the chunks that say how to read the pixels (palette, transparency, gamma, colour profile and the like) and loses its text, EXIF and time chunks (a macOS screenshot's XMP among them), the other ancillary chunks and anything after the image; its pixel data is untouched;
  • a webp that carries EXIF or XMP, or anything beyond plain image data, is sent as a png; a plain one passes through;
  • a gif sends its first frame as a png.

When an image is scaled, the status line says so, for example image #1 downscaled 3024×1964 → 2000×1299, and an ACP agent is told too (a native model gets the processed image only). Sources over 20 MiB or 50 megapixels are refused.

Two other notes can appear when you paste:

  • <model> can't see images; it will get a placeholder: the session is native and its model is not known to take images.
  • this session's host can't read craze attachments; pasted as a path: a chip needs the process that hosts the session to read the file from the attachments directory of this craze. A session hosted by a process under another CRAZE_HOME (a detached host started with a different home, or a socket attached from one) cannot, so the paste stays a path and no chip is made. A session in this CRAZE_HOME, in-process or detached, is fine.

What the agent receives

Agent Receives
Cursor, Grok, gx the visible message first, then one image block per chip in order, and, for a scaled image, a line [Image #1 was downscaled from 3024×1964 to 2000×1299]. An agent that says it does not take images gets [Image #N: <path>] text in place of each image block instead (the chip stays in the message text) (Grok and gx are sent images regardless, since they take them without saying so)
An agent that rejects the images when it answers the prompt with an invalid-params error without having done anything with it, craze sends the message once more with each image as [Image #N: <path>] text. Updates about the session itself (its command list, mode, settings or title) do not count as doing something. After the agent has done anything for the turn, before its error or since, or you cancelled, the error is shown as it is
Native the same chips; how the image reaches the model depends on the model: see below
An interjection always path text, [Image #N: <path>], never image data
craze prompt it has no way to attach an image

An image craze cannot read at send time (the file was swept, is not an image any more, or is outside the attachments directory) goes as [Image #N: <path> (not attached: <reason>)] rather than failing the message. A chip you deleted from the text is never sent.

On a native session what the model receives depends on the model's vision setting (see Native vision):

  • A model that takes images (the shipped catalog marks most of its models) receives the picture itself.
  • A model that does not, such as glm-5.3, receives [Image omitted: <model> does not accept images. File: <path>] in the image's place, so it can still name the file, and the composer warns when you paste: <model> can't see images; it will get a placeholder.
  • Switching /model to a model without vision turns the images already in the conversation into those placeholders for the requests that follow. The images stay stored, so switching back to a model that takes them sends them again.
  • The native Read tool returns an image file (png, jpeg, gif or webp) to a model that takes images, under the same size limits as a paste, and refuses it with Cannot read image file: <model> does not accept images on one that does not. A PDF is still refused as a binary file.

craze prompt has no way to attach an image.

Shell mode

A draft whose first character is ! is a command for your own shell, not a message for the agent. There is no toggle: the mode is the draft. The composer's two rules take the shell colour and the top one reads

shell · enter run · esc clear

in place of the session title, and the slash menu stays shut — !ls /usr must not offer /usr. Delete the ! and everything is as it was.

Enter runs the whole draft after the !, so a multi-line paste is one script, and the draft clears as it does on a send. The command runs through $SHELL -c (/bin/sh -c when $SHELL is not an absolute path craze can execute), in the session's workspace, with craze's own environment, its stdin on /dev/null, and in a process group of its own so nothing it starts outlives it.

Warning

A command runs locally, immediately, with your own rights — the same thing would have happened had you typed it into your own shell. craze asks nothing first and the agent has no say in it: shell mode runs what you typed, and nothing an agent said can put a command in the composer.

To send a message that begins with !, type a single space before it. A draft whose first character is a space is not shell mode, and that space is trimmed on the way out, so a draft of space followed by !important reaches the agent as !important — the space is how you say which of the two you meant, and it never travels.

Keys

Key Action
Enter run the draft
Esc kill the running command; with none running, clear the draft (it never cancels the agent's turn)
Ctrl+C kill the running command — one press, one kill: it does not quit, does not cancel the turn, and does not arm the quit window. It is the way to kill one while a card has the keyboard
Ctrl+L refused in shell mode: Enter is how a command runs

A command is killed with SIGTERM to its whole process group, SIGKILL three seconds later, and it is killed on every way out of craze — Ctrl+D, /exit, a double Ctrl+C, SIGTERM, and a session change.

What can outlive craze

A command that deliberately detaches itself — setsid, disown, or a daemon that puts itself in a session of its own — leaves the process group craze kills, and craze cannot reach it afterwards: it can outlive the command and craze with it, and is yours to stop. Everything else the command started is killed when the command ends and again when craze exits, whether it is still writing output (server &) or not (server >/dev/null 2>&1 &).

Enter is refused, and the draft kept, while a card is open, while a session is still starting or restoring, and while another command is running — one at a time. A bare ! does nothing. It is allowed while the agent is working: it is your shell, and it needs nothing of the session but its workspace.

Shell mode is the composer's alone. A queued message, an edited queued row, the plan offer's Enter, a replayed prompt and a send-now all go to the agent as text however they start, and craze prompt '!x' sends !x as a prompt.

The transcript row

A command writes one row, ! <cmd>, with the spinner while it runs. When it finishes the row carries its output, dimmed and collapsed past 20 lines under the usual Ctrl+O, and the right-hand end says how it ended when it was not a clean exit: exit 2, killed, timed out after 120s.

Limit Value
Output kept 16 KiB, from the tail — an earlier head is replaced by [craze: output truncated]
Time 120 s, then the group is killed (timed out after 120s)
Carried to the agent the last 3 commands, 16 KiB of output together

Shell mode is not a terminal: no pty, no interactive programs, no job control and no history. A command that wants to be answered reads EOF; one that wants a terminal should be run in one.

What the agent is told

Each finished command is kept, and the command and its output travel with the next message you send — a sent message, a message you queue, or an interjection. They go once, in front of your text, and are gone: craze sends nothing when a command finishes, and a session you quit without writing another message never mentions any of it.

!git status --short          ← runs locally, output on screen
what changed?                ← this message carries both to the agent

The Enter that accepts the plan offer sends craze's own sentence and carries nothing. The context is cleared only once the message was accepted: a message refused for length keeps the draft and the output for the next attempt — and the attached output counts against the queue's 32 KiB per-message limit. /clear and a session change drop it.

On native, the block is redacted before it reaches the model, the transcript or the journal: a value craze knows as a key (an API key variable's value, a stored key) in a command's output becomes [craze:redacted-credential]. The ACP agents (cursor, grok, gx) get the block verbatim.

What is attached is never drawn: your transcript row, the queue band, the queue editor and the session title show the message you typed, not the block that went with it. A /name inside a command's output is output and nothing else — !cat plan.md on a file full of slash commands expands none of them.

Sub-agent view

Enter while the rows have the keyboard (↓ from the composer, then ↑/↓ to the row) — or a click on the row — opens that sub-agent in the main area, read-only. Grok and native each stream a sub-agent's own session, so its transcript is the child's own prompt, thoughts, tool calls and replies, rendered by the same machinery as the main one. Cursor streams no sub-agent transcript, so the view is what its cursor/task receipt carried: a note saying so, the prompt, a model · duration · agent id line once the receipt landed, and the output when there is one. There is no composer here — you cannot prompt or message a sub-agent from craze. On native, Delete/Backspace in a running child's view stops it (see the banner below); a grok or cursor sub-agent cannot be stopped from craze.

The composer's top rule carries the chip (model) description — (grok-4.6) List directory files — and the input line is replaced by the banner:

  • running: ○ @explore · read-only · esc to return, with · tab next agent when more than one row is visible, and, on native, a further · del to stop at the tail's end — the full banner then reads exactly ○ @general-purpose · read-only · esc to return · tab next agent · del to stop
  • finished: ✓ @explore · completed · esc to return, in the warning colour so the end is unmistakable (✗ failed / – cancelled, with the error when there is one; the esc to return half never truncates away) — a child the user stopped on native reads exactly – @general-purpose · cancelled · stopped by the user · esc to return
  • cursor: ○ @task · receipt only · esc to return while it runs, then the same warning-coloured finish line with · receipt only kept: ✓ @task · completed · receipt only · esc to return

Entering or leaving cancels nothing. The main turn keeps running underneath — while the viewed sub-agent runs, the spinner line is its own <spinner> <activity> · <elapsed> · <tokens>, not the turn's — and the draft and the main transcript's scroll position come back on return. A sub-agent that finishes while viewed stays in the view until Esc, even past the ten-second linger an unviewed row gets.

Keys inside the view: Esc and ← return; ↑/↓ scroll it a line, PgUp/PgDn page, the wheel scrolls three lines as everywhere else; Tab/Shift+Tab switch to the next/previous sub-agent (the mode cycle is unreachable here); Ctrl+O toggles detail and Ctrl+Y copies from it, both as outside; Ctrl+C still cancels the main turn and the view stays open. Typing, pasting and the slash menu are inert inside the view, and a card still lands on top and owns the keyboard until it is answered — ← aside, which opens the session list and comes back to the view and its card (see Cards).

Side questions (/btw)

/btw <question> asks the session a quick question about its work without interrupting it and without adding to it. It is offered where the session can answer one — native, Grok and gx; on Cursor /btw is not in the menu and a typed /btw … goes to the agent as the text it is. Where it is offered, a plugin or skill named btw is shadowed, as /connect shadows one on native.

It runs whatever the session is doing — mid-turn, and during a turn the agent runs on its own — and never queues. The draft is read raw: /btw (any case) then the question, which may span several lines and is sent verbatim. An empty /btw says usage: /btw <question>; a draft carrying an image chip is refused (side questions are text only) and kept; output of a ! command waiting for your next message stays waiting for it and is never attached. On a new session not started yet /btw is refused with /btw needs a running session: send a first message. Ctrl+L on a /btw draft opens the view too; it is never interjected into the turn. A queued message is never turned into a side question: /btw typed over a queued row's text while editing it is refused on Enter and Ctrl+L alike (/btw is a side question, not a queued message: esc the edit, then ask it), the edit left open.

The answer opens in a side view that takes the transcript region: a dim Side question — not saved. Esc returns to the session., your question, the answer streaming in (native streams it; Grok and gx answer in one piece), and a dim footer with the model that answered and, on native, its tokens — Echo · 1.2k in (800 cached) · 40 out. While it is answering the working line reads Answering · <elapsed> · esc closes; status row 2 reads side question · esc to return where the mode chip was, and the composer's top rule carries a btw chip. Everything else on screen — the status rows, the sub-agent rows, the queue strip and the tasks panel — is the main session's, still live underneath, and inert while the view is open (clicks on it do nothing).

Keys inside the view:

  • Enter asks a follow-up with the composer's text, carrying the earlier questions and answers of this view (it is ignored while one is answering). A view takes 21 questions; after that it says this side thread is full: Esc and start a new /btw. An earlier answer over 64 KiB is resent cut, with a […truncated] mark.
  • The composer is plain text: every editing key works, the arrows move through a draft of several lines, and Ctrl+V pastes text. There is no shell mode, slash menu, @ popup or image paste, and the keys that would leave the view or reach the main session — ↑ into the rows, ← into the session list, Ctrl+L, Shift+Tab — do nothing.
  • PgUp/PgDn and the wheel scroll it, a drag selects in it, Ctrl+Y copies its last answer; Ctrl+O, Ctrl+T and Ctrl+G are what they are everywhere.
  • Esc or Ctrl+C closes it: the question in flight is cancelled, the thread and a half-typed follow-up are discarded, and the main transcript comes back where you left it with an empty composer. Ctrl+C here does not touch the main turn and arms no quit. A card that arrives takes the keyboard as always — Esc answers the card, not the view, and the view is there again once it is answered; with a card up Ctrl+C keeps its usual meaning.

A follow-up too long for the host to take says this side question is too long (the question on its own does not fit); when it is the earlier questions and answers that make the request too long, the oldest pairs are dropped first until it fits. A failure is an error row in the view, which stays open; an answer cut off part way keeps what streamed, marked (incomplete). A question a reconnect cut says connection lost — ask again — it is never sent twice, since the host may have answered it already. Opening another session, the session list, or quitting closes the view first.

What is saved: nothing of the side thread — no transcript row, no journal entry, nothing in the session's history or event log, and nothing another client attached to the session sees. On native the tokens a side question spends count in the session's total for this process (not after a resume).

Session list

← on an empty composer (or with a card up), or /sessions, replaces the screen with every session of yours running on this machine, whatever its directory, and the saved ones that are not running. It exists only when sessions run in detached hosts (the default); under CRAZE_DETACH=0, detach = false or with the control socket off there is no list, no /sessions and no ← binding (and /help has no line for it). The session you came from stays attached behind the list — its turn keeps running, a card it raises (or the one you left it with) waits for you — and the cursor starts on its row. If it ends while the list is up (another client's /exit, a close from the list, the idle timeout), craze stays on the list and its row stays, · ended, for as long as the list is up, even once its host has gone; a later run of the same session is a row of its own.

 sessions  9 running                             ! 2 need you   ✳ 3 working   ✗ 1 failed   ○ 2 idle

needs you 2 ─────────────────────────────────────────────────────────────────────
  ! fix the roost tab rename      permission: cargo test -p roost-ipc    grok    roost            2m

working 3 ───────────────────────────────────────────────────────────────────────
❯ ✳ write the v0.1.0 release no…  Responding                             native  craze · here     1m

The header counts what needs you, what is working, what failed and what is idle, dropping counts from the right as the terminal narrows. Rows are grouped by state — needs you (an open permission, question or plan, with what it is about), working (what it is doing: the running tool, Responding, Thinking, or Starting…/Loading…, and Closing… for a session an older craze serves, below), failed (the error's first line), idle (the first line of its last reply, or waiting for a prompt), unreachable (?: its host is registered and not answering) — then ▸ saved · N not running, collapsed. Within a group the row that changed state most recently is first. Each row reads: state glyph, title, what it wants, provider, model, directory (· here for the session you came from), and how long it has been in its state. The model column is the running session's current model id, 20 cells wide and cut with … where it is longer; a saved row records no model and leaves it blank. It is drawn only when the terminal is 100 columns or wider, and below that it is dropped entirely, so the title, what it wants and the directory keep the room. A sub-agent's session, running or saved, carries the same dim sub-agent tag after its title. A session run by an older craze is listed with what that craze reports, and its version on the row (· craze 0.0.9). A host that has not answered yet is drawn with the working rows, Starting… or Connecting…. A session that is stopped — closed with Ctrl+X, craze stop, another client's stop, a signal, its host's idle exit, or its own TUI's quit — leaves the list at the roster's next round, within about a second, without showing Closing… first: its host marks itself stopping before the stop goes ahead, and the row going is the feedback. Its session is a saved row from then on. A session served by a craze from before this marking still shows Closing… until its host has exited. With no other session the list says No other sessions. Type a prompt below to start one. It needs 40×10; smaller, it says so.

The running rows come from this machine's hub, started when the list opens if none runs: the list draws at once and the hub's rows follow, and while the list reads from the hub this terminal holds no connection to any other session — only the one to the hub. When no hub answers within 2 seconds, when the hub keeps dropping the list (three times in 10 seconds), or when what it sends cannot show every session in full (more than 512 of them, or a session whose row is too large for the hub to pass on), the list asks each session's host itself for the rest of that opening, keeping a connection to each and asking it once a second, as the list did before the hub; it looks the same either way. The saved rows are read from this craze directory's session index whichever it is.

Under the rows is an input, always focused, where a new session is started (Starting a session from the list): a prompt typed there and Enter starts one, @ picks the directory it runs in, /provider and /model choose what it runs, and /effort and /fast what it starts at. The rule above the input names where and as what the next one would start:

─────────────────────────── new session → ~/projects/lumen · cursor · Composer 2.5 ─
❯ type a prompt to start a session · @ picks a directory · / provider and model
Key Action
typing goes to the input under the list: a prompt, an @ directory, a / command
↑ ↓ move the selection; it stays on its session however the rows reorder (with a popup up over the input, they move its selection instead)
Enter with something typed start it: see Starting a session from the list
Enter, → with nothing typed on another session, open it here; on a saved one, resume it here; on the session you came from, go back to it; on ▸ saved, expand or collapse the saved sessions. A host that is not answering says that session is not answering
Esc with something typed hide the input's popup, then clear the input
← → with something typed move the input's cursor (Alt+←/Alt+→ by word)
Esc, ← with nothing typed back to the session you came from — unless it ended while the list was up, when the list stays and says that session ended, or this terminal lost its connection to it, when the list stays and says lost the connection to that session
Ctrl+X on a working or asking session, stop its turn and clear its queue; on an idle or failed one, the first press arms a close (ctrl+x again closes it) and a second within two seconds ends the session on its host — any other key disarms it. Nothing on a saved row or a host still connecting; on a host that is not answering it says that session is not answering
Ctrl+S group by directory instead of by state, and back; the grouping is kept for the rest of the run
Ctrl+V paste the clipboard's text into the input under the list
Tab nothing in the input (it never submits), except to complete a value in an open popup
Ctrl+D, Ctrl+C twice, /exit in the input quit craze; every session keeps running

The hint line under the list names what the selected row takes (enter open, enter resume, enter back to it, ctrl+x stop or close), and says what a key came to: stopped: <title>, closed: <title>, could not stop <title>: …. A note longer than the line — a failed start's names the session host's log, which is what to read next — wraps over up to three rows, a path broken after a /, never in the middle of a name. The list gives up those rows while the note shows, keeping at least two of its own (and the @ or / popup keeps its rows); a note with no more room is cut short with …. An armed close's hint, and a first Ctrl+C's, are said over a note, on one row. The mouse does nothing in the list.

Opening a session in place

Enter on another session's row opens it in this terminal: the list stays up with opening <title>… on its hint line while its host is dialled, and then the screen is that session's — its transcript, its cards, its queue, the status row naming its directory and provider — as if you had started it here. The session you left is not stopped: this terminal detaches from it and it goes on on its host, where the list finds it again. A dial that fails leaves the list up with could not open <title>: <why>; Enter on another row before the first answers opens that one instead, and leaving the list abandons the open.

Once the list has been opened, every session's screen starts with a band — the session's title, provider and directory, and ← sessions on the right:

─ fix the roost tab rename · grok · roost ─────────────────────── ← sessions ─

A terminal too short for it drops it. ← on an empty composer goes back to the list, where the cursor starts on the session you now have open (· here).

The composer's text belongs to the session it was typed in: a session that ends with a draft in the composer keeps it — it is in the composer again when you open that session again — and it never follows you to another session.

When the session you are in ends — its host stopped by another terminal's /exit, a ctrl+x close from a list, or the idle timeout — craze goes back to the list, its row marked · ended, and the hint line says that session ended. Your own quit (/exit, Ctrl+D) still ends the session and quits craze.

When this terminal loses its connection to the session instead — its host stopped answering for longer than craze keeps reconnecting — craze goes back to the list too, and the hint line says lost the connection to that session and why. The session may still be running: its row is listed as its host reports it — or ? while the host does not answer — and Enter on it opens it again, Ctrl+X stops or closes it, as on any other row.

Saved sessions

Enter on ▸ saved · N not running shows the saved sessions: every session in the session index that is not running, newest first, at most 50, each once — ·, its title, provider and directory. They are chosen as the hub's sessions.saved and craze ps --saved choose theirs, with sub-agents' sessions shown here, tagged. A session is shown by its newest index row, the one a resume loads, and only when that row can be resumed: one whose newest row is of a provider this craze does not know (a newer craze's), or one the protocol could not carry — an id over 512 bytes, a directory over 4096 bytes or not absolute — is not shown at all, never by an older row. Enter on one resumes it in this terminal: a session host is started in the directory the session ran in (not this terminal's) and loads it, the hint line saying resuming <title>… meanwhile, and the screen then shows it as opening one in place does, restoring… until its transcript is back. A saved session that another terminal has resumed meanwhile is opened where it runs. The command line's own session flags (--provider, --model, --effort, --fast/--no-fast, --ask, --plan) were for the session it started and are not applied to a resumed one; its permission mode and --plugin-dir are, and so is --agent-bin (with CRAZE_AGENT_BIN) when the session's provider is the launch's own — any other runs the binary [agents] or PATH gives it. A session whose directory is gone, or whose provider this craze cannot resume, is refused on the hint line (could not resume <title>: that session ran in …, which is no longer a directory) and nothing is started. Ctrl+X does nothing on a saved row: nothing runs to stop.

Starting a session from the list

What is typed in the input under the list is the first prompt of a new session. Enter starts it in the background: the list stays up, the input says starting… and takes nothing more until the session has answered (a second Enter does nothing), and a session host is spawned in the target directory, started, and given the prompt. The hint line then says what came of it:

  • started in ~/projects/lumen — the session took the prompt; the input is cleared, and its row appears with the list's next refresh. It runs on its host like any other session, and Enter on its row opens it here.
  • could not start a session in ~/projects/lumen: <why> — no session came up (the host could not start, the agent refused the model, …) or the session refused the prompt; the input keeps what was typed, and a host that was spawned is stopped.
  • may have started — check the list — the connection went after the prompt was sent, so the session may have taken it; the host is left running and the input keeps the prompt.

Esc or ← while it starts leaves the list; the start carries on, and its row is listed as any other.

The new session runs:

  • where the rule says: the directory a leading @ token names (below); else the selected row's directory — a running session's or a saved one's; else the directory of the session you came from. With none of them the rule says no directory to start in: pick one with @.
  • as the rule says: the provider and model chosen with /provider and /model, else those of the session you came from — its current model; when its host has not said which model it is on, the launch's own --model; else the provider's default.
  • at the effort and fast mode chosen with /effort and /fast, else the model's own.
  • with the permission mode of the session you came from (--force or --no-force, as its host reports it), the launch's --plugin-dir, and — when its provider is the launch's own — its --agent-bin (and CRAZE_AGENT_BIN); a session of any other provider runs the binary [agents] or PATH gives it. The command line's own --ask, --plan, --effort, --fast/--no-fast and --provider were for the session it started and are not applied (the list's /effort and /fast set the last two for its own), and its --model only in the one case above.

All of it is decided when Enter is pressed: moving the selection while the session starts changes nothing.

@dir alone opens an unstarted session. Enter on a leading @ token with nothing after it opens a new session in that directory in place, and spawns nothing yet: the band says new session · <provider> · ~/projects/lumen, the status row names its directory, provider and model, and the composer says type the first prompt to start this session. Its first prompt spawns its host there and it comes up in this terminal like any session opened in place, the prompt sent as your first turn. If it cannot start, the session is unstarted again with the error on screen (could not start the session: …) and the prompt still in the composer. Slash commands and ! do nothing until the session exists. ← or /sessions before its first prompt discards it: nothing was spawned and nothing is left.

/exit, alone, in the list's input quits craze and leaves every session running; /exit with anything after it is a prompt like any other.

Choosing the directory: @

Only an @ token that is the first thing in the input picks the directory, and it is taken out of the prompt the agent receives (@lumen tidy the changelog sends tidy the changelog). An @… later in the input is ordinary prompt text, and text typed before a token makes it ordinary text too — the rule then falls back to the selected row, live.

While the cursor is in the leading token a popup above the input offers:

  • by name — the directory of the session you came from (here · 2 running), then every directory a session is running in, the most recently active first (1 running), then the ten most recent directories in the session index that still exist (used 3h ago). Typing narrows them: a name that starts with what is typed first, then a path that contains it. Nothing is hard-coded: there is no projects folder and no setting.
  • by path — a token that starts with ~, / or . browses: @~/ lists the subdirectories of your home directory (folders in ~), those whose names start with what follows the last /, sorted, at most 200. Dot-directories are listed only once what is typed starts with .. Tab descends into the highlighted one and stays open; Enter picks it. A directory that is not there says no directory ~/x.

↑ ↓ (or Ctrl+P Ctrl+N) choose, Tab completes (it never starts anything), Enter picks, Esc hides the popup until the token changes. Picking writes the directory's name (@lumen) — its path when two candidates share a name, and @"~/my dir" quoted when it has a space — and binds the token to that directory, whatever the list does meanwhile. Editing or deleting the token drops the binding; Enter then reads the token afresh — a name that is exactly one candidate's, or a path that is a directory — and one that names none, or more than one, is an error on the rule and the hint line (no directory named @x, @x names 2 directories; pick one with @), and nothing starts.

Provider and model

/ as the first thing in the list's input opens its commands:

───────────────────────────────────────────────────────── commands ─
❯ /provider  provider for new sessions             cursor
  /model     model for new sessions                Composer 2.5
  /exit      quit; sessions keep running

/provider and /model set what every session started from the list runs, until you change them or craze quits — across every opening of the list and whichever session you came from. The rule over the input names them, and the hint line says new sessions use cursor · Composer 2.5 when one is chosen. Choosing a command lists its values; ↑ ↓ choose, typing narrows, Tab or Enter uses the highlighted one, Esc hides the popup. A / word no command starts with is a prompt (an agent's own slash command), and opens nothing.

  • /provider lists the providers the startup provider dialog offers — gx only where its binary resolves — the one in use marked current. Each opening checks them again, as the startup dialog does (provider states): a gx installed since craze started is listed, one removed is not, and a provider that cannot start is dimmed with its reason beside it and its state (unavailable, needs setup) for its note. Tab or Enter on one does not choose it: the input and the provider new sessions run stay as they were, and the popup says can't start <provider>: <reason> — <fix> in red under the list until the next key, wrapped over up to three rows so the fix shows whole (a longer one is cut short with …; it keeps its rows while one candidate row is left beside it). native needing setup is the exception: Tab, Enter or a typed /provider native opens /connect's dialog in place of the list (Before any session), and every way out of it brings the list back as it was — what you had typed, the selected row, the provider new sessions run — with what came of it on the hint line and the states checked again; choose native again once it is ready. Choosing one that can start resets the model to that provider's default: for cursor, grok and gx, the agent's own (default: no model is passed); for native, the model a native session started with no --model would use (which model a session starts on) — the newest remembered model whose provider has a key, else your default_model when its provider has one, else the start model of the first connected provider in the provider order; else the first model (alphabetically) whose key resolves. With no provider funded it says so (no model provider has an API key — run craze auth login (an API key, or "craze auth login chatgpt" for a ChatGPT plan), or set its API key variable) and leaves the model to the agent's own.
  • /model lists the provider's models. Native's are its model table's, only those whose provider has a key, the recently used ones first in the order they were last picked and the rest by name, with no label — the same order a native session's own /model lists. The list reads the memory and never writes it: only a switch made inside a native session does. cursor, grok and gx name their models only once a session has started, so craze lists the catalog the last session of that provider installed — every detached host records it in the model catalog cache — titled with its age (cursor models for new sessions · last seen 3h ago), the agent's own default first — always offered, and no model is passed with it; a model of the catalog whose id happens to be default is a row of its own, and passes --model default. A catalog can be stale: it is a list of choices, nothing more. With no catalog recorded yet the popup says no cursor catalog seen yet · enter uses the id as typed: /model <id> then takes the id as typed, as craze --model <id> would, and so does Enter on an id no listed model matches. A bad id fails that session's start the way --model does, on the hint line. A model is chosen for the provider it was listed for: a list opened later over a session of another provider still starts that provider on it.

Typed in full, /provider grok, /model gpt-6-sol and /model default (the provider's own — a catalog model whose id is default is picked from the popup) do the same without the popup; a provider the picker does not offer is refused on the hint line, and so is one the last check found cannot start, with the line left as typed. Before the list's first check has answered, a typed provider is taken as it always was.

A session started from the list is saved as the last provider started, as any launch is (see Configuration), so the next craze without --provider preselects its provider in the provider dialog. The list's own choice still lasts only until craze quits.

Effort and fast mode

/effort and /fast set the effort and fast mode every session started from the list starts at, as craze --effort and --fast/--no-fast do for the session a launch starts. They are not in the popup a bare / opens: each is listed once what you type is the start of its name and of no other command — /ef lists /effort (/e is still /exit's alone), /f lists /fast:

───────────────────────────────────────────── commands ─
❯ /effort  effort for new sessions  default
  • /effort offers default and the levels of the model new sessions start on. On native that is that model's levels — the chosen model's, read from its model table (a typed /model SPARK resolved as --model resolves it); the start model's for /provider native and /model default; and, when you have not picked one, those of the session you came from's model, as that session states them or, where it states none (an older host), read from the table — default alone for a model with no effort control. The table is read again at each opening of the list and each choice of model, since the start model follows the model memory. On an ACP provider (cursor, grok, gx), whose levels are known only once a session runs, it offers every level: default, minimal, low, medium, high, xhigh and max (and so does native for the moment between a choice of model and its table being read).
  • /fast offers default, on and off.

default clears the setting: the model's own. Choosing works as it does for /provider — ↑ ↓ choose, typing narrows, Tab or Enter uses the highlighted value, the one in use is marked current — and /effort high or /fast off typed in full does the same without the popup; a value not offered is refused on the hint line. A choice lasts as the provider and model do, until you change it or craze quits; choosing a provider or a model does not reset it. The rule over the input appends it — new session → ~/projects/lumen · cursor · Grok · high · fast, or · no fast with fast mode off — and says nothing while both are default. An effort kept from before you chose a model that does not offer it is still passed to the new session, but the rule and the hint line say so — · max (not offered) — and the popup marks nothing current.

Beyond what is offered, the values are not checked against the provider or model here: the new session's host applies them as it starts, before its first prompt, exactly as it applies --effort and --fast — the effort matched against the levels its model offers (an exact id, then an id or a name, case folded). One that matches nothing — xhigh where a model stops at high, or a model with no effort or no fast mode at all — is skipped, noted in the session's journal, and the session starts at the model's own. So is one the agent refuses. An agent that does not answer the setting at all within 15 seconds fails the session's start instead: the hint line says could not start a session in <dir>: … and names the setting, and the host that was spawned is stopped.

Modes

Shift+Tab, /plan, /ask and /agent cycle or set the ACP session mode. The mode chip in status row 2 reads ◆ <mode id>, coloured by what kind of mode the agent advertised it as: accent for an implement-like mode, purple for plan, teal for a read-only mode like ask. Clicking the chip cycles the mode the same way Shift+Tab does — see Mouse. Every change writes a mode → <id> note, followed by the agent's own description of the mode when it gave one.

When a turn in a plan-kind mode ends with an assistant reply, the composer's placeholder offers to act on it once craze is idle:

enter implements this plan  ·  type to refine  ·  shift+tab leaves plan mode

Enter on the still-empty composer switches to an implement-kind mode and sends Implement the plan above.; typing instead keeps the session in plan mode and the offer disappears once the composer is non-empty. Esc clears the offer without losing focus. Changing mode, /clear, the next turn clears it too. A card arriving hides it until the card is answered, and then it is offered again.

On the native provider (--provider native) the same /plan, /ask, /agent, Shift+Tab and the offer above work. Plan mode lets the model edit only its plan file, which lives under the harness home beside the session transcript — by default ~/.craze/native/sessions/<workspace-slug>/<stamp>_<id>.plan.md, or under $CRAZE_HOME/native/ when the home is relocated — and is never deleted by craze. Ask mode denies every edit, write and shell command. Accepting the plan card ends the turn and arms the offer above, even when the model said nothing else in that turn.

Sub-agent models

On native, ~/.craze/native/models.toml (or $CRAZE_HOME/native/ when the home is relocated) may carry an optional [subagents] section: a default model and effort for children, and a [subagents.tiers] map from Claude's tier names (fable, opus, sonnet, haiku) to native aliases. The parent's agent tool call resolves a child's model and effort in this order — the call's own model/effort, then the persona's, then [subagents]'s default, then the parent's own running model and effort — and model (on the call, a persona, or [subagents]) may also be inherit, which always means the parent's model. A tier name with no entry in [subagents.tiers] means the parent's model too, so a persona that says model: opus still works with no tier map configured at all — except under a parent running a Claude model on the Anthropic provider, where the tier names are Claude's own families: there an unmapped fable, opus, sonnet or haiku runs on the newest model of that family the session's model table has (model: haiku under Claude Opus 5.5 runs on Claude Haiku 5.5), and on the parent's model when it has none. [subagents.tiers] still wins on every parent, and the agent tool's description lists only the tiers it maps. Every alias named in [subagents] or [subagents.tiers] should be a model — one craze ships or one in models.toml; one that is not falls back to the default, never stopping the session: silently for a model a release retired, with a warning for any other unknown name (see Sub-agent settings). inherit itself is refused as a tier key, since a tier mapping to inherit could never be reached. For example:

[subagents]
model  = ""   # optional default for children; "" = the parent's
effort = ""   # optional default effort
[subagents.tiers]   # optional: what a Claude tier name means here
opus   = "fireworks/kimi-k3"
sonnet = "glm-5.3"
haiku  = "fireworks/deepseek-v4p1-flash"

The agent tool runs at most four children at once, background ones included. A fifth foreground call waits for a slot; a background call, or a foreground one when every slot holds a background child, is refused at once (All 4 sub-agent slots are in use…).

Compaction

On the native provider (--provider native), a long session summarizes its own context rather than growing it forever. Once the context reaches 85% of the model's context window — capped at the window less its output ceiling — craze summarizes the conversation so far and carries on from the summary in place of it. The check runs before a turn's first request and again after every step that called a tool, so a turn that crosses the line partway through splits into two requests without ending: the same turn, one final done. Separately, a request the provider refuses outright as too large for the model's context window compacts once and retries, on the same turn — a second such refusal, or the compaction itself failing, fails the turn instead, its message saying so ("even after compacting" when a compaction did run first). Switching to a model with a smaller context window compacts on the previous model first, so its own context and prefix cache are what gets summarized, falling back to the new model when the previous one no longer resolves. A model with no known context window (context_window unset in models.toml) never compacts on its own — /compact below and the provider-refused case above still work. See [compaction] in the configuration reference for auto, threshold_percent and tail_tokens. Compacting on its own switches itself off after a compaction that failed or still left the context over the threshold, and back on after one that succeeded under it, or a model change.

While a compaction runs, the spinner line reads compacting context… in place of what the turn is doing (✳ compacting context… · 12s · esc to interrupt). Once it ends it leaves one note in the transcript:

how it ended note
on its own context compacted · 890k → 21k tokens
/compact context compacted on request · 890k → 21k tokens
the provider refused the request context was too large — compacted · 890k → 21k tokens
failed compaction failed: <why>

Token counts are plain under 1,000, else k or M to three significant figures (890k, 1.21M).

/compact

Native advertises one slash command, /compact [focus] — "Summarize the conversation so far to free context; /compact <what to keep>" — so a plugin's own compact is offered qualified, the same rule any name collision follows. Typed as a prompt, /compact is never sent to the model: it is a turn of its own, ending as soon as the compaction does, with no model turn after it. /compact with nothing in the context yet says so ("nothing to compact yet") instead of compacting an empty conversation. Sent while a turn is already running, /compact is never sent into it as a steer — like any interjection it is refused, which keeps the draft rather than sending it (Ctrl+L on it shows "nothing to interject into"). Nothing queues it automatically: pressing Enter on the kept draft is what queues it, to run as its own turn once the current one ends. A /compact that succeeds turns compacting-on-its-own back on only when it leaves the context under the threshold; if the context is still at or over the threshold afterward, automatic compaction stays off (or turns off, if it was not already).

Resuming a compacted session

Resuming a session that compacted at some point replays its notes in place, exactly where they happened in the original session, and the next turn's request to the model carries the summary in place of everything it stood for, with the kept tail — the most recent whole steps (a step is never split from its own tool results), up to tail_tokens and never more than a quarter of the threshold, and possibly empty — following it unchanged.

Segment files

Before it writes the summary, craze saves what the summary stands for to a Markdown file beside the transcript — <stem>.compaction/segment_001.md, segment_002.md, and so on, one file per compaction — which the model can read or grep like any other file under the harness home, exactly as it can the plan file. The segment keeps each tool result's first 8 KiB (a note says how much was left out), and the file itself is capped at 512 KiB: over that, the oldest steps are dropped first — which can drop only part of a turn, not the whole thing — with a line pointing at the transcript instead — which, unlike the segment, keeps everything, so an exact detail can still be found there when the segment no longer has it. The summary message names the directory, so the model knows the files are there when an exact detail — a command, an error string, a path — matters more than the summary's own account of it.

Usage and cost

On the native provider, status row 1 gains a usage part after the model name once the session has taken a step — how full the context is, and what it has spent, the turn's amount then the session's:

state text
priced 34% ctx · $0.04 / $1.20
some usage had no price 34% ctx · $0.04 / $1.20+
nothing priced at all 34% ctx · 12.3k / 1.21M tok
a window craze does not know $0.04 / $1.20 (the NN% ctx · prefix is left out)
no usage yet (ACP, or before native's first step) no part at all

Money is to the cent, rounded half up, and reads <$0.01 for an amount above nothing that would otherwise round to nothing. A + on an amount means at least some of the usage behind it had no price in models.toml — it is a floor, not the true cost. With nothing priced at all, the row falls back to billed tokens instead of a dollar figure: input, cache, and output, added together. The turn's amount is what the turn the last report followed spent; the session's is everything the session has spent, including its compactions' usage and its sub-agents', each priced by its own model. Row 1 drops its parts in a pinned order as the terminal narrows — elapsed first, then the branch, then this usage part, then the provider, and the model last of all.

Cards

A blocking request from the agent is a card, and the card owns the keyboard and the mouse while it is up: Ctrl+T, Ctrl+G, Ctrl+O, the arrows and the wheel all do nothing until it is answered. Ctrl+C and Ctrl+D still work. Cards queue, and only the one on top is drawn.

← is the one exception among those keys (Alt+← is not), and only where there is a session list: it opens the list and leaves the card unanswered. The ask stays open on the session's host, and the list shows the session under needs you. Esc or ← from the list comes back to the same card — the question you were on and what you had picked — with your draft still under it, and inside the sub-agent view if that is where you were. Opening the session again later, from another one, shows the card again (a question card starts again from its first question).

The tab title and a herdr pane or roost tab report the session on screen. While the list is up over the session they still report its card (the tab's ⚠, herdr's blocked, roost's needs input); once you open another session they report that one instead, and the ask you left open is reported again only when you come back to it.

Card Keys
permission line — permission <tool> [a]llow once [A]lways [n] reject a, y or 1 allow once, A allow always, n, r or 2 reject. Only the options the request actually offered are drawn and bound. Esc cancels the turn.
question card 1–9 pick, ↑ ↓ move, Enter selects (single-choice) or confirms (multiple-choice), Space toggles a multiple-choice option. Esc skips the whole request.
plan card — plan <name> [a]ccept [r]eject esc cancel a, y or 1 accept, r, n or 2 reject. Esc cancels the turn.

A question card's option may carry a description, drawn dimmed on its own line under the label.

Esc means two different things on purpose. On a question it is skipped: the agent is told you declined to answer and the turn carries on. On a plan or a permission it cancels the turn, because cancelling is the only way those two reach the agent as anything other than an accept or a reject.

An answered question or plan leaves a row in the transcript saying how it was answered:

? Pick one → B
? Pick any → X, Z
? Question → skipped
plan Ship the widget → accepted

A question draws one row per question it asked, naming the options picked (nothing when none was); a skipped question one → skipped row under its title; a plan its name and accepted or rejected. The rows are part of the session's own transcript, not this terminal's: every client attached to the session draws them, whichever one answered — this terminal, another, or a craze attach — and they are still there when the session is opened again from the list or with craze attach. A permission's answer, a cancel, a turn ending under a card, and a request answered without a card (one the provider never shows, or one craze answered headlessly) leave no row.

Slash commands

The menu opens on the / token under the cursor, not only on a whole-line /name: a / at offset 0 of the draft or right after any whitespace starts a token, so typing see /gau mid-sentence opens the menu on /gau the way Cursor's and Grok's own TUIs already let you invoke a skill there. foo/bar and https://x never trigger — the / has to sit at the start of the draft or follow whitespace, not mid-word. A bare /, anywhere in the draft, opens on an empty token and lists the whole catalog; that is how you browse it.

craze's own commands are only ever offered where Enter would actually run them: the draft's first non-whitespace run, with no newline anywhere in the draft. Typed after the first word, on a second line, or with anything else ahead of it, they drop out of the menu — offering /help where Enter could only send the text as a prompt would be a lie. Agent-advertised commands and skills carry no such restriction; they complete anywhere in the draft.

Command Action
/help Keybindings and commands
/model Switch model
/connect Store a model provider's API key, or sign in to the ChatGPT plan — native sessions only
/clear Clear transcript — see below
/tasks Tasks panel: compact, expanded, hidden — only where the agent reports a task list
/theme Theme picker, or /theme <name> — see below
/rename <title> Rename this session — craze's own; never sent to the agent — see below
/plan Set plan mode — only where the agent advertises modes
/ask Set ask mode — only where the agent advertises modes
/agent Set agent mode — only where the agent advertises modes
/btw <question> Ask a side question, not saved — native, Grok and gx only; runs mid-turn
/sessions The session list — listed only when sessions run in detached hosts
/exit End this session
/effort Change the current model's effort: a picker, or /effort <level> — only where the current model has an effort control

/plan, /ask and /agent are in the menu only when the session advertises modes, and on a session that does not offer the one typed, Enter says mode <name> is not advertised. /tasks is in the menu only when the agent reports a task list. /effort is in the menu only while the current model has an effort control (its effort option), so it comes and goes as you switch models; anywhere else it is not craze's, and an agent's or a plugin's own /effort shows and runs in its place. It is listed after /exit, so /e still selects /exit and /ef selects /effort.

/theme <name> applies and saves a preset by name, in any case, with _ or - between words. It also takes the spellings tokyo, tokyonight, craze, crazedark and crazelight. A name that is none of the presets is an error row listing them, and changes nothing. The choice is saved to config.toml like the picker's.

/rename <title> names this session: the title replaces craze in the rule above the composer and is written to the session index, so --resume and the session list show it. A title is cut to 120 characters. With none, it says usage: /rename <title>; while the session is still starting or restoring it says why it cannot yet. It is craze's own and is never sent to the agent.

/clear empties this terminal's transcript, the draft with its image chips (the numbering starts again at [Image #1]), the queued messages with any confirm or send-now waiting on them, a plan offer, and the output of ! commands waiting to go with the next message. The agent's context is not touched: the session goes on as it was.

Matches are prefix hits first, then substring hits, each group kept in the catalog's own order, case-insensitive — no fuzzy matching. The band shows up to 8 rows and scrolls to keep the selection in view rather than capping the list, so the whole catalog stays reachable by scrolling. Tab or Enter accepts the highlighted row, replacing just the token with /name (absorbing one space that already followed it) and closing the menu; Enter on a token that already spells the highlighted name falls through and runs or sends instead, and Enter on a bare / accepts the first row rather than sending / itself as a prompt. ↑/↓ move the selection with wrap; PgUp/PgDn page it, ahead of paging the transcript, while the menu is open. A click on a row accepts it and the wheel over the band moves the selection — see Mouse.

When the list is longer than the band, the first row carries k/n — k is the selection's position, n the match count — followed by ▲ once the window has scrolled past the top; the last row carries ▼ when there is more below. A band cropped to a single row shows k/n alone: it is both the first and the last row, and the count already says what the arrows would.

Skills come from the agent's own ACP catalog first: Grok's ACP server loads a plugins service, so its catalog already carries every plugin skill it has enabled, named and expanded by Grok itself — craze changes nothing there. Cursor's ACP server never constructs a plugins service, so its catalog never carries anything a plugin ships, command or skill, and never expands one either: sent as prompt text, /watch-pr reaches the model as the two words /watch-pr, and the model is left to guess what they meant. Everything Cursor's catalog does carry — project and user skills, .cursor/commands, .claude/commands, and workspace and global commands — is still expanded by cursor itself exactly as before. The on-disk scan is a supplement, for project skills the session's own catalog does not carry (Grok in particular does not advertise its own project skills over ACP, so the walk is their only path to the menu): Cursor walks .cursor/skills, .agents/skills, .codex/skills, .claude/skills under the workspace and $HOME, still skipping .cursor/plugins; Grok walks .grok/skills and .agents/skills. Cursor and Grok disagree on how a skill on disk gets its name — Cursor names it after the skill's directory, whatever its frontmatter says; Grok names it after the frontmatter name, falling back to the directory only when there is none — and on both, user-invocable: false in the frontmatter hides the skill from the menu the same way it hides it from the agent's own catalog.

The menu is drawn from the session's slash catalog, which the session's host builds and publishes, so every client attached to the session starts from the same rows — and the TUI's menu is the one it built itself before there was a catalog. What each client shows differs by design: a client hides the rows it cannot run and adds rows of its own — the TUI adds /sessions where it has a session list, and shows /btw only on a session that answers side questions. The host walks the disk for skills when the session starts, when each turn ends (done, cancelled or failed), at every command list the agent sends (an identical one included: it is the walk that is asked for, and the menu changes only if the walk found something), and when a client attaches to the session. A skill saved while the session sits idle reaches the menu at the next of those, not when you type /: let a turn finish, or attach to the session from another terminal with craze attach, and the menu updates in every client. Some agents help: Grok sends its command list again a few seconds after a skill file appears under the directories it watches, so on Grok an idle skill usually arrives within seconds by itself. Picking the session you are already in from the session list does not attach again, so it does not pick the skill up. A session with no catalog from its host — one served by a craze from before the catalog, the moment before the session's catalog arrives, or a reconnect whose snapshot could not carry the catalog — gets the menu the TUI builds itself, as before: the TUI walks the disk when the session comes up, when it goes over to its own menu, and each time a bare / is typed.

For Cursor, craze makes up the difference itself: it finds a plugin's commands and skills on disk and expands them into the prompt the way cursor's own TUI does, so /watch-pr and the rest of a plugin's commands reach the menu and work when sent even though Cursor's ACP server never sees them. It looks in three places, first source wins on a plugin:name collision: --plugin-dir directories, in the order given; then the cursor plugin cache (~/.cursor/plugins/cache/<marketplace>/<plugin>/<version>/, the version with the newest modification time among those carrying that plugin's .cache-complete sentinel — a plugin with no finished install contributes nothing); then Claude Code's enabled plugins (~/.claude/plugins/installed_plugins.json, filtered by the workspace's and the user's .claude/settings.json the way cursor's own loader filters them). An entry is offered under its own bare name when nothing else — Cursor, a builtin, another plugin — claims that name, and under plugin:name on a collision or when Cursor or a builtin already owns the bare spelling. The menu's order stays builtins, then whatever Cursor advertised, then these plugin rows, then disk skills, the same case-insensitive first-name-wins dedupe as everywhere else.

Typing : is how you ask for a plugin's qualified spelling: /git-c finds watch-pr and merge-pr through the plugin's own name even when neither collides with anything else, Tab on /git-commands:w inserts /git-commands:watch-pr, Tab on /wat inserts the bare /watch-pr, and Enter sends on either fully-typed spelling. A name several plugins share (four installs on this machine each ship a /rescue) is never offered bare, so a bare /rescue typed by hand is not a plugin reference at all — it goes to the agent as ordinary text, the same as any other name nothing advertises.

Under the user's transcript entry, a plugin reference leaves a dim ⤷ plugin:name (kind) line — the qualified spelling and whether it was a command or a skill, never the body itself. A command's body substitutes $ARGUMENTS and $1..$99 from the typed arguments, cursor's own rule for plugin commands; a skill's body is sent whole and unsubstituted, because that is what cursor itself does with a skill's content — the arguments show up only in the block's lead sentence and its args attribute. Both substitute ${CLAUDE_PLUGIN_ROOT} and ${CURSOR_PLUGIN_ROOT} with the plugin's own install path. Up to 8 references expand per prompt, first occurrence of each name only; a headless craze prompt --json run can read the whole block in the command event's text field (see CLI reference). Grok changes on none of this: it already advertises and expands every plugin skill itself, so craze's menu and wire are exactly what they were before, and /plugin:name typed by hand for a skill Grok advertises bare is honoured by Grok's own resolver.

Cursor's ACP catalog lands a few seconds after the session starts (session/new itself carries none of it), so a / typed right away shows only craze's builtins, whatever plugin rows resolved from disk — qualified, see below — and whatever the disk scan already found; the rest appears once the agent advertises it. Plugin rows stay qualified for exactly that window: a row shown as plugin:name before Cursor's first catalog update goes bare the moment that update confirms nothing else owns the name, so the menu renames itself once per session; a draft already typed with the qualified spelling keeps resolving no matter which way the row is drawn, so nothing sent during that window is ever wrong. If Cursor ever stopped sending the catalog altogether, plugin rows would simply stay qualified for the whole session — safe, just less tidy.

craze prompt's one-shot turn does not get to wait out that window by watching the menu, so when the very first prompt of a session carries a slash token that does not yet resolve, craze holds it for up to 5 seconds so a bare name is not sent before the catalog can say whether it is really unclaimed. Esc cancels the wait like any other turn — the prompt never reaches the wire. A draft with no slash token, an already-qualified name, or any prompt after the first never waits, and falling through the 5 seconds unresolved is exactly the old behaviour: the draft goes out verbatim.

Two things worth knowing about where plugin rows come from. The cursor plugin cache is read as installed, not as currently enabled, so a plugin the account has since turned off keeps its cache until cursor prunes it and craze can still offer it — because craze expands the command itself rather than counting on the agent to recognise it, that is optimistic about what the account still has on, not a lie about what will happen. And because the token scan reads the whole prompt line the way cursor's own regex does, a stray /watch-pr in the middle of a sentence ("see /watch-pr for context") expands exactly as if it had been typed at the start.

/connect

/connect gives one of the native provider's model providers an API key without leaving the TUI — the dialog twin of craze auth login, storing the key the same way, as that provider's api_key in providers.toml (see Keys). Like craze auth login it never checks the key with the provider: a wrong one shows as an error on first use. The ChatGPT plan, which takes no key, is signed in to instead, in the same box (below). It exists in native sessions only: in a cursor, grok or gx session it is not in the menu, and a typed /connect goes to the agent as ordinary text. It is also what the last row of native's /model opens, while some provider has no key, and what picking native opens before any session while no provider has one (below).

  • Step one, Connect a provider, lists every provider craze knows — the shipped ones and any in your own providers.toml — by name, a ✓ beside each one that has a usable key, from its variable or stored; connecting one that already has a key replaces its stored key. A provider whose stored key cannot be used (shorter than 8 bytes, or overlapping the redaction marker) is marked stored key unusable. The ChatGPT plan is marked ✓ while signed in with plan usage, and plan usage off when its account did not allow craze to use the plan. The box opens on the first provider with no key. ↑/↓ move, Enter or a click picks — the ChatGPT plan's sign-in, or any other provider's key field — and Esc closes.
  • Step two, <Name> API key, is a masked field: what you type or paste is drawn as •, never as text. Under it, where the key is stored — Stored in ~/.craze/native/providers.toml., or wherever this TUI's CRAZE_HOME puts it, a path wider than the box broken after a / — and, when one of the provider's variables is set in this TUI's environment, <VAR> is set in this environment; craze uses it before the stored key. A terminal paste and Ctrl+V both land in the field and nowhere else: a clipboard paste that answers after you have left the field, or opened another one, is dropped — never put in the composer. Esc goes back to step one and empties the field.
  • Enter stores the key. An empty key, one shorter than 8 bytes, one that overlaps the redaction marker, or one over 8 KiB is refused in the field — Not saved: …, naming the rule, never the key — the field is emptied, and nothing is written. Otherwise the box closes, the key is stored, and the running session is asked to take up what it funds (below). The transcript says Connected <Name>. Its models are in /model now. — or Connected <Name>. Its models will be in /model after this turn. when a turn is running (another client's, or the agent's own: /connect itself is refused while one runs). When the session could not take them up — see below — it says Connected <Name>. New sessions offer its models; to use them in this conversation, /exit and run craze -c., and when its host reads another craze directory, Connected <Name> in this craze directory, but this session's host reads another one, so it does not see its models. Another provider's stored key that cannot be used is kept as it was, and named in a note after it. A store that refuses — providers.toml a symlink, a file that no longer parses, a lock that cannot be taken — is an error row naming the problem, never the key.
  • The key is never shown: not in the transcript, a note or an error row, the composer, the session's journal, or anything sent to the model. The field is emptied on every way out of the dialog — a save, Esc, a refusal, a card arriving, another dialog opening, switching to another session, quitting craze, or the session ending or this terminal losing its connection to it (the box closes before craze goes back to the session list).

Signing in to the ChatGPT plan. The ChatGPT plan has no key field: its row opens Sign in with ChatGPT, which runs the same sign-in as craze auth login chatgpt inside the box.

  • The address. The box — as wide as the terminal leaves it, up to 104 columns — shows the address to open in a browser, where you sign in to ChatGPT and approve craze. It carries no token. Its rows are cut at the width, never at a word, and the box cuts the last one with … when the window is too short to show it all. There are three ways to get it into a browser, each of them the whole address however the box wraps or cuts it:
    • Click it, on any of its rows, or press Ctrl+Y: craze copies it through the terminal (OSC 52, so it reaches the clipboard of the machine you are typing on even over SSH) and the system clipboard. The box says Copied the address. Paste it into a browser. Nothing on the clipboard? Cmd/Ctrl+click it (some terminals need Shift while craze holds the mouse). until the next key, or for 10 seconds — craze cannot tell whether a clipboard took the copy. An address too long to copy whole (over 64 KiB, which only a damaged registration file could make) is not copied at all, rather than cut short: the box says The address is too long to copy (over 64 KiB), so nothing was copied. Under --no-mouse a click is the terminal's own, Ctrl+Y still copies, and the footer says ctrl+y copies the address.
    • Cmd/Ctrl+click it: each row of the address is a terminal hyperlink (OSC 8) to the whole address, all of them one link, so a terminal with hyperlinks opens the whole address from any row. Which modifier yours wants while craze holds the mouse is under Terminals. With TERM=dumb, or on the Linux console (TERM=linux), the rows are plain text, and so is an address craze would not link: one that is not ChatGPT's own sign-in endpoint (https://auth.openai.com/…) in printable ASCII, at most 2048 bytes.
    • Copy it from the shell instead: craze auth login chatgpt prints it plain, on one line — the way that works in any terminal.
  • The way back. craze waits for the browser to come back to it on 127.0.0.1 (Waiting for the browser.). When the browser is on another machine, the page it lands on after you approve does not load: copy that page's whole address from the address bar, paste it into the field under the address — a terminal paste or Ctrl+V — and press Enter.
    • Over SSH (SSH_CONNECTION, SSH_CLIENT or SSH_TTY set) the hint leads with that: craze runs on another machine (SSH). Open the address on your computer and approve. When the browser can't connect to 127.0.0.1, copy that page's address and paste it here — or forward the port first: ssh -L 1455:127.0.0.1:1455. The port is the one craze listens on — a re-login whose 1455 is taken listens on another, and the line names that one. With the port forwarded — the SSH session started with ssh -L 1455:127.0.0.1:1455 <host>, or the forward added to a running one with OpenSSH's ~C escape — the browser's return reaches craze through the tunnel, and there is nothing to paste.
    • If craze could not listen — another program has 127.0.0.1:1455, say — the box says why, craze is not listening for the browser: another program is using 127.0.0.1:1455., and the hint says the browser's page will not load and names the address it lands on. It offers no forward: nothing of craze's listens on the port.
    • The browser's return from a different sign-in attempt — an earlier attempt's approval, or another craze's — is refused by craze's listener, and the box says so once: The browser came back from a different sign-in attempt. Use the address shown now.
    • The field masks everything in it with •, as the key field does — the address too — so an API key pasted there out of habit is never drawn, not even before Enter, and not when it is pasted onto the end of the address either. The line under the field says what it holds instead, never repeating any of it: That's the redirect address: press enter to sign in. once it is this sign-in's address as the browser was sent to it — http://127.0.0.1:<port>/auth/callback with a one-time code and the sign-in's state — and Paste the whole address the browser was sent to. for anything else. A line that is not an address, or the address of another sign-in, is refused in the field, which is emptied, and is never repeated; the redirect of an earlier sign-in is told so, That is the redirect of an earlier sign-in attempt. Use the address shown now. Whichever comes first, the browser's return or the pasted address, finishes the sign-in (Signing in…).
  • Esc goes back to step one and stops the sign-in, closing craze's listener; so does every other way out of the box, and every way craze exits — a quit, the terminal closing, SIGTERM, or craze failing.
  • Signed in, the box closes and the transcript says Signed in to ChatGPT as <email>. The first time this registration signs in with plan usage the one-time notice follows — You're using your ChatGPT plan. and Eligible usage in this app uses your ChatGPT plan. Manage usage in your ChatGPT settings: https://chatgpt.com/settings/usage — and is not shown again. Then craze fetches the plan's models (ChatGPT plan models: chatgpt/gpt-6.1-sol, …), asks the running session to take the list up, and says The plan's models are in /model now. (or The plan's models will be in /model after this turn.; when the session could not take them up, the older New sessions offer the ChatGPT plan's models; to use them in this conversation, /exit and run craze -c.; when its host reads another craze directory, The sign-in is saved in this craze directory, but this session's host reads another one, so it does not see the plan's models.). An account that did not allow plan usage gets Signed in to ChatGPT as <email>, but ChatGPT plan usage is off: … and To turn it on, sign in again with /connect and allow ChatGPT plan usage when ChatGPT asks. A sign-in that fails — declined in the browser, refused by ChatGPT, or for another account than the one this craze directory is registered with — closes the box with an error row, /connect: …, naming the step and ChatGPT's error code, never a token; a step that ran out of time is named too, /connect: exchange: timed out after 30s.

Troubleshooting a sign-in: the sign-in log. Every /connect sign-in is recorded, as craze auth login chatgpt's is, in the TUI's craze directory's native/logs/signin.log (0600 in a 0700 directory, 1 MiB with one rotation to signin.log.1): one JSON line per thing that happened — the attempt's start, whether craze could listen for the browser and why not, a refused paste and which part of it was wrong, the listener refusing the browser's return from a different attempt (counted, not repeated), the address copied (a click or Ctrl+Y), the redirect, the attempt's one outcome, and the model list fetched after it. Its records say "surface":"tui", and a sign-in the box ends says why: "reason":"esc", "dialog" (the box closed another way, or the session was switched) or "shutdown" (craze quit, or the session ended). The log is value-free — it never holds an address, a pasted line, a code, a state, a token, an email or any text a server sent, and craze writes invalid for a value it does not know — so it is safe to share. Each event and field is described in the CLI reference's sign-in log. A log craze cannot keep — its directory a symbolic link, writable by other users or another user's, or the file not a plain file of its own — is one note in the transcript, the sign-in log is off: …, written when craze finds out, as the sign-in starts or when a later record fails, and the sign-in goes on without it. A sign-in never waits on the log. When craze quits it waits at most a second in all for a sign-in still finishing to record its outcome and for a model list still being fetched after one, which it cancels (models_failed, "class":"cancelled"), then a second more for the log to be written. A log that fails then — or is not written within that second — is the same mention, once, as a line on the terminal after craze's screen is gone: craze: the sign-in log is off: ….

Refused while work runs. While a turn is running — yours, another client's, or the agent's own — or a sub-agent is still running in the background, /connect writes Finish or stop the running work first, then /connect. and opens nothing; work that starts while the box is open refuses the save in the field instead, keeping the key for another Enter. The ChatGPT plan's row is refused the same way when it is picked, and so is a pasted address's Enter, which keeps the address in the field; a browser that comes back on its own while work runs still finishes the sign-in. A running session learns a newly stored key, to redact it, only when its next turn starts (see Keys stored while a session runs), so a shell command or a sub-agent already running would not know it. The TUI's view of the session can lag its host by a moment, and another client attached to the same session can start work while the box is open, so the refusal narrows that window rather than closing it.

Models picked up while the session runs. A native session takes up the models a key or a sign-in funds without a restart. The TUI asks it to (session.models.refresh, see the protocol) in three places: right after /connect stores a key, right after a sign-in's model list is written (not when the browser's redirect arrives, which is earlier), and each time /model opens, which also picks up what another terminal's craze auth login stored. The same ask has the session fetch, in the background, the Anthropic account's model list when the key that now funds Anthropic has none yet, or one over a day old: an Anthropic key /connect stores is the key the list is fetched with, and /model offers craze's own Claude models at once and the account's own once the list is written, with no reopening. The ask is made with this TUI's craze directory, and the session answers whether it is the one it reads. What the notice says depends on the answer:

The session says /connect after a key /connect after a sign-in
it took them up Connected <Name>. Its models are in /model now. The plan's models are in /model now.
a turn is running; it takes them up when the turn ends Connected <Name>. Its models will be in /model after this turn. The plan's models will be in /model after this turn.
it reads another craze directory (whatever else it says) Connected <Name> in this craze directory, but this session's host reads another one, so it does not see its models. The sign-in is saved in this craze directory, but this session's host reads another one, so it does not see the plan's models.
nothing had changed, or the files could not be read the old note the old note
it cannot take up models (an ACP session, or a host from before the method), or did not answer the old note, and no call is made on a session that cannot the old note

The old note is New sessions offer its models; to use them in this conversation, /exit and run craze -c. (…the ChatGPT plan's models… for a sign-in): the models are in the next session, or in this conversation after /exit and craze -c. The another craze directory case is a session attached with craze attach or a TUI under another CRAZE_HOME: this TUI wrote its own files, which that session never reads. Its note leaves the old advice out, since neither step would help there: /exit ends the session for every client attached to it, and craze -c from this craze directory does not find a session that another one holds.

The model dialog reads the list the session publishes, so a refresh that lands while it is open adds the models to the open box with no reopening, the selection staying on the model it was on. The swap changes only what /model offers and can switch to: a conversation's compaction settings and a sub-agent's model menu stay as the session opened them, and a model already running stays listed, as its client was built. Once you switch away from it, /model offers what the files say of it now: after a sign-in to another ChatGPT account, that account's model of the same name; after an edit to models.toml, its new efforts; and nothing, if the files no longer have it. /connect is still refused while a turn runs.

Where it writes. /connect writes to the providers.toml of this TUI's craze directory (CRAZE_HOME, or ~/.craze), and the ChatGPT plan's sign-in to that directory's native/auth/ and native/chatgpt-models.json (see its files). A session attached with craze attach from a shell with another CRAZE_HOME reads its host's directory, not this one — which is why step two names the file.

Before any session. A native session with nothing funded does not start, so /connect cannot be reached from inside one on a machine with no key at all. Picking native while it needs setup — in the startup provider dialog, or with the session list's /provider — opens the same dialog before any session, with the same steps, masked fields, paste rules and sign-in. It differs where a session would be involved:

  • It belongs to no session. It makes no call to one, asks none to take up models, writes nothing to any transcript, and is never refused because work is running — even when the list it was opened from was opened from inside a session.
  • It stays up until the outcome is in: Saving the key… until the key is stored (or the store has not answered in 15 seconds), and after a sign-in Fetching the plan's models… until the plan's model list is written.
  • The ChatGPT plan's one-time notice is a step of its own, You're using your ChatGPT plan.: Enter goes on, and only that Enter records it as shown; leaving any other way shows it again next time.
  • Every way out goes back to where it was opened — Esc on any step, a click outside the box (which never starts the picker's default), the key stored, the store refusing or not answering, the sign-in finished, declined or failed — and says what came of it there: connected <provider>: it can be picked now, signed in to ChatGPT as <email>: it can be picked now, or why not. Native's state is then checked again from the files, never assumed. An answer that arrives after the dialog has closed is dropped.
  • Ctrl+C or Ctrl+D quits craze, as it does from the picker or the list (every session keeps running); a sign-in in progress is cancelled first and its sign-in log closed after its outcome is written.

Model dialog

/model, or a click on the model name in status row 1, opens a centred box over the transcript: a filter (❯, type to narrow by name or id), the model list (▲/▼ when it scrolls; which models, in what order, is below), and one tab below it per select option the current model's catalog advertises, other than mode and model (semantic category first, then id, in the agent's own order). effort (low medium high xhigh, the picked one bracketed) and fast (on/off) are drawn exactly as they always were, whichever id or name the agent files them under; any other option the model offers — context or thinking on cursor's Claude models — gets a tab too, labelled with the agent's own name, lowercased, showing its raw values. A tab is shown whenever the catalog advertises the option; there is no capability-bit gate on a tab (the provider's effort/fast bits gate only the status-row chips, never a control the model itself advertises). Composer-2.5 shows fast alone; claude-opus-5 shows effort, fast, context and thinking; grok shows effort alone, never fast.

Which models, in what order. The current model is always the first row, and the row the dialog opens on. A row is the model's name and nothing else — no current or recent tag: the order says it. After the current model come, on native, the models you picked most recently in any native session's /model, newest first (the model memory); then any model whose id or name contains grok; then the rest, each group by name — except that the ChatGPT plan's models stay together in the account's own order, and Anthropic's together newest first, each block placed by its provider's name (so Anthropic's come first among the rest). An ACP provider (cursor, grok, gx) has no model memory, so its list starts at the grok group.

A native session lists only the models of providers that have a key — an exported variable or a stored one (Keys) — plus the model it is running on, and says nothing about the providers that have none, except one row: while some provider has no key, the list ends with Connect a provider…, which ↑/↓ and a click reach like any row and which opens /connect — applying nothing else, as Esc would not. It is judged when the dialog opens, against the providers this TUI's craze directory and environment know (the file /connect writes), so it goes once every provider has a key. The running session's own list follows a key or a sign-in too, at once: opening /model asks the session to take up whatever was funded since it opened, and the list the box shows is the session's, re-drawn when it changes. An ACP provider's dialog never has the row, and never asks. A model you pick in this session moves up in this session's list at once — the switch republishes the list, ranked again — and in the next session's. A typed /model <alias> naming a model the list leaves out fails as an unknown model does; --model <alias> at start still resolves against every model craze knows, and refuses one whose provider has no key, saying how to give it one.

The effort row follows the highlight on native. A native session states every model's effort levels in its model list (the session capability effortFacts), so its effort row is the highlighted model's: its levels, in the catalog's order, and bracketed — on the current model, the effort the session is at; on any other, the effort a switch to it would land on (Effort on a switch: your last choice on it, else its default), else its default, else nothing. A model with no effort control has no effort row while it is highlighted (and the footer names no tab); if the row had the focus, the focus goes back to the list. A level you choose is kept while you move the cursor: a model that offers it shows it again, and one that does not shows its own value meanwhile. A model list the session publishes while the box is open (another switch, a reload) moves every row you have not touched to its new value. While a /model switch you made has not landed yet, the box opens on the model you asked for and its row is that model's levels, as its facts state them; Enter then sends that row's effort for it, after the switch lands, so the switch ends on the effort the box showed.

On an ACP provider (cursor, grok, gx), and against a host from before effortFacts, the dialog does not rebuild itself for the model under the cursor: the tabs on screen are the current model's, and highlighting another model in the list never previews its options. A choice made on a tab still travels with a model switch in the same Enter when the destination model offers that option and value; after the switch, the new model's own tabs are edited by reopening /model, which then shows exactly them.

The list and each tab are focus targets, and the one the keys are on carries a > gutter and the selection background — so exactly one row looks active, in a screenshot and with the colour stripped alike. A model that is selected but no longer focused keeps a dimmer · mark: [value] says which value is picked, the gutter says which row is focused. The footer says which keys are live: type to filter · ↑↓ · tab <label>/<label> · enter · esc on the list, naming the tabs in order, and ←→ change · tab cycles · type filters · enter · esc on a tab. Effort-and-fast-only footers (tab effort/fast, tab effort, tab fast) are drawn at every width exactly as before, clamped the same way when the box is narrower than the text; a catalog with any other tab has no footer of old to keep and falls back to tab options once the tabs' names do not fit. Typing filters the list whatever has focus.

Tab/Shift+Tab move focus between the list and the tabs; ←/→ change the focused tab's value (on an effort row with nothing bracketed, → picks its first level and ← its last); clicking a model row picks it and applies the dialog, as Enter does, and clicking a tab focuses it. Enter closes the dialog and applies model, then each changed tab, in catalog order, each as its own step — a tab the user never moved is left alone: it always shows its option's live value (tracking a delta that lands while the box is open), and nothing is sent for it. On native, a switch also sends the effort row as it showed it for the model switched to, chosen or not — so the session lands on the effort the box showed, even if another craze process has remembered another one for that model since — and sends no effort when the row bracketed nothing. A note is written for every step that lands, and for one that could not be — the model changed under it, the destination model has no such option, or does not offer that value — never an error row for those; an actual failure is still named in an error row instead of silently reverting the rest. Esc, or a click outside the box, closes it and applies nothing. Status row 1 then reads Name (effort · fast), with fast shown only when it is on.

/model <id> and /model <id> <effort> still work without opening the dialog; a bare effort word is not a shorthand (/model high looks for a model named high). On native, /model <id> alone lands on the effort last used on that model, else its default (Effort on a switch). /model <id> <effort> resolves the model first — matched against the model list without reading any catalog — and then checks the effort word against the destination model's catalog — as /effort's word is: the level's id, in any case, else its name — so /model grok-4.6 high typed while on composer-2.5 (which has no effort) still lands on grok-4.6 instead of being read as an unknown model. There is no full-width picker band and no Ctrl+M binding; the only band left under the transcript is the slash menu.

Ctrl+G//theme opens the same kind of box (title theme, no filter); ↑/↓ (or j/k) preview live and wrap at either end, Enter keeps it, a digit 1–9 applies the theme at that position and keeps it at once, and Esc or a click outside reverts to the theme that was active when it opened. A card arriving from the agent closes any dialog — the model dialog applies nothing, the theme dialog reverts.

Effort

/effort changes the current model's effort; it is in the menu wherever the current model has an effort control (an effort option), on every provider.

A bare /effort opens a small box over the transcript, titled effort · <model>, listing that model's levels in its own order, the cursor on the one in use:

╭────────────────────────────────────────────────────╮
│effort · Alpha One                                  │
│> low                                        current│
│  high                                       default│
│↑↓ · enter · esc                                    │
╰────────────────────────────────────────────────────╯

The level in use is marked current; on native, which states each model's default, the model's default is marked default (an ACP agent names none). ↑/↓ move, Enter applies the level under the cursor (a click on a row applies it too), and Esc or a click outside closes the box with no change. The box draws from the session as it is: if the current model changes while it is open, it lists the new model's levels — keeping the cursor on its level where the new model offers it, else on the one in use — and applies to that model.

/effort <level> applies a level directly, matched as --effort is at a session's start: the level's id exactly, else its id in any case (/effort HIGH), else its name (/effort high takes a level the agent calls High whatever its id) — and sent as the agent's own id. default is reserved: it is the model's default, on native only, decided before any level is matched (a level an agent spells default is reached through the picker). Elsewhere, and for a word that names no level, the line is refused with an error row naming the levels — no effort nope: one of default, low, high — and stays in the composer for you to correct.

Either way the change goes through the same setting as /model <id> <effort>'s effort step, bound to the model it was chosen for: if the session has moved to another model by the time it is sent, nothing is sent and a note says effort not applied: the model changed. Status row 1 shows the new effort once it lands. On native the choice is remembered for that model, as a choice in /model is (Effort on a switch). Like /model, it needs the agent, so it is refused while a turn runs. It is also refused while a /model switch you made has not landed yet — The model is still switching to <model>; /effort once it has. — since until it lands the levels the session holds are still the old model's.

Help dialog

/help opens a third box in the same frame (title help, 72 cells wide because it is a two-column table). The keys are one per row, the key in a fixed left gutter and what it does beside it, grouped under headings — sending and editing, queued messages, mode (only where the session advertises modes), moving and scrolling, panels and views, selection and clipboard — then commands for craze's own slash commands and this session's commands for whatever the agent advertises and the skills found on disk, which vary by session.

The content is taller than any terminal, so the box clips to the transcript region and scrolls: ↑/↓ a row, PgUp/PgDn a page, with ▲/▼ marking what is off screen. It shrinks the same way the other two do — content rows first, then the footer — down to a title-only box. Esc, a click outside, or a card arriving closes it; nothing else is bound while it is up.

On a provider that shows the band, panels and views gains the sub-agent view keys — enter on a row to open one, esc, ← to return, and tab, shift+tab to switch while inside — and the ↑ ↓ row reads queued messages, sub-agent rows, or a list inside a dialog. On native, panels and views also gains del, backspace — stop the selected running sub-agent, or the one in view. Where /btw is offered (native, Grok, gx) the box gains a side questions group with the side view's keys.

Mouse

Mouse reporting is on by default; --no-mouse turns it off.

The wheel scrolls the transcript three lines per notch — except over the slash menu, where it moves the selection one row per notch instead, clamped rather than wrapped: the menu is a selection, not a viewport, so three rows a notch would jump past rows never seen highlighted. A left click hit-tests the same layout the frame was drawn from, so what is on screen and what is clickable cannot drift apart:

Target Effect
A row in the slash menu Accepts it (same as Tab); a click after scrolling lands on the row actually drawn there
Tasks panel header Cycles the panel (same as Ctrl+T)
Sub-agent row under the status rows Selects that sub-agent and opens it in the main area (same as Enter on it)
A queued message's text Selects that row
[send now] on a queued row Sends it instead of the running turn (asks first)
[edit] on a queued row Loads it into the composer for an in-place edit
[cancel] on a queued row Removes it from the queue
The banner line inside the sub-agent view Returns to the main transcript (same as Esc)
Model name in status row 1 Opens the model dialog (same as /model)
◆ agent mode chip in status row 2 Cycles the mode (same as Shift+Tab)
A row inside an open dialog Picks that row; a /help row is inert; in /connect's sign-in, a row of the address copies the whole address (same as Ctrl+Y) and the rest are inert
Anywhere outside an open dialog Closes it, applying nothing — except the provider picker, where it starts the default provider, and the resume picker, where it does nothing
The transcript Starts a selection

Dragging and double-clicking inside the sub-agent view select and copy from its transcript the same way. The mode chip is inert inside the view. Task rows, the … +n more row, the separators between status segments and a segment the row truncated away are all inert.

The queue's actions appear on hover, which needs motion reports the default mouse mode does not send. craze switches the terminal to all-motion reporting while at least one message is queued and back to cell motion when the last one leaves — and never at all under --no-mouse. A button is only clickable on the row it was drawn on, which is the row under the pointer and the row the keyboard has selected; a click anywhere else in the band selects that row rather than acting on it.

Selection and copy

Dragging over the transcript highlights the cells between the press and the pointer and copies them when the button comes up. The range is inclusive of both endpoints and normalises, so dragging backwards selects the same text. A double-click — two presses on the same cell inside 400 ms — selects the word under the pointer and copies that.

A drag that reaches the top or bottom row of the transcript scrolls one line and keeps extending. Cell-motion reporting only sends an event when the pointer changes cell, so a pointer held still on the edge stops scrolling; nudge it to carry on.

Status row 2 says copied 2 lines, or copied "the first thirty characters…" for a single row, for two seconds. The highlight outlives the note: it stays until the next key, the next click, the next thing the agent says (any change to the transcript, a resize or /clear included), or a blocking card.

Ctrl+Y copies the current selection. With no selection it copies the last reply — the agent's own text, not the rows it was wrapped onto. It is a keyboard feature, so it works under --no-mouse as well.

Every copy is written twice: once as a bare OSC 52 escape sequence, which is the one that survives ssh, and once to the system clipboard. Copies are capped at 64 KiB, cut on a rune boundary, and the note then ends … (truncated).

For OSC 52 to land in the system clipboard:

  • tmux needs set -g set-clipboard on. craze sends the sequence bare, so allow-passthrough is not required.
  • Some terminals need it enabled: xterm wants XTerm*disallowedWindowOps: 20,21,SetXprop. Alacritty, kitty, WezTerm, foot and iTerm2 allow it out of the box. GNOME Terminal does not implement OSC 52, which is what the second, native write is for.

Turning mouse reporting on takes native drag-select away from the terminal, and craze's own selection replaces it. To reach the terminal's instead — to select across the whole scrollback, say — hold Shift while dragging (Option in macOS terminals), or start craze with --no-mouse.

Terminals

Two of craze's mouse-era features lean on the terminal: a copy reaches the clipboard through OSC 52 (above), and /connect's ChatGPT sign-in draws its address as an OSC 8 hyperlink (see Signing in to the ChatGPT plan). What each terminal needs:

  • Ghostty opens a link with Cmd+click on macOS (Ctrl+click elsewhere). A copy reaches the clipboard as its clipboard-write setting allows; set clipboard-write = allow if one never arrives.
  • iTerm2 opens a link with Cmd+click.
  • kitty hands a plain click to the application while it holds the mouse: open the link with Ctrl+Shift+click.
  • WezTerm hands a click to the application while it holds the mouse unless Shift is held: Shift+click opens the link.
  • GNOME Terminal opens a link with Ctrl+click, not Shift+click. Should a Ctrl+click reach craze instead, add Shift (Ctrl+Shift+click), which keeps the click from an application holding the mouse in the VTE terminals. Other VTE terminals, such as Tilix, typically use Ctrl+click too.
  • tmux passes links through from 3.4 on, once told the outer terminal has them: set -as terminal-features ',*:hyperlinks'. Copies need set -g set-clipboard on, as above.
  • The Linux console (TERM=linux) and TERM=dumb get the address as plain text: craze draws no link there.

Where neither a copy nor a link gets through, craze auth login chatgpt prints the address on one line, plain, for the terminal's own selection (see the CLI reference).