TUI Reference¶
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 —
unavailableorneeds setup— beforedefault. When the box is too narrow for both,defaultis dropped first. - The lines under the list say why the selected row cannot start and, under
that, what to do about it, as
craze providerswould. 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. Enteron a row that cannot start — orEsc, or a click outside the box, when the default cannot start — does not start it: the reason becomescan'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/connectcame to (below).nativewith no model provider that has a key (needs setup) is not refused: picking it —Enteron its row, orEscor 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, andEnterstarts it. Nothing starts on its own.- With no row that can start, every choice is refused;
Ctrl+Cstill 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: agxinstalled since the dialog opened is listed, and one removed is dropped — unless it is the default, which always stays, markedunavailable.
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:
- 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 - Transcript
- The overlay, while it is open: the slash menu or the
@file popup - Pinned tasks panel
- The queue band: one row per queued message
- Spinner line
- Composer
- The card band, while a card is waiting for an answer
- Two status rows (below)
- 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 onunder--force(the default),▸ prompting for permissionsunder--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 queuedwhile messages are queued;← n agents(↓ n agentswhere←opens the session list) counting the running rows below: sub-agents and background commands alike;N attachedwhile 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/loadresult carries no catalog, unlike a freshsession/new. A later live turn may supply one again. - A rename is craze's own and is never sent to the agent:
agent lsandgrok --resumestill 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 anagentcall) 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 lastmode → <id>change, unless--plan/--askset 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
agentcall folds to ataskcard 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
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
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:
rg --files --hidden -g '!.git'whenrgis on yourPATH— the files ripgrep would search:.gitignore,.ignoreand git's excludes honoured, dotfiles shown,.gitnever, and yourRIPGREP_CONFIG_PATHnot read (--no-config);- otherwise, in a git work tree,
git ls-files -co --exclude-standard— tracked files, and untracked ones git would not ignore; - otherwise a walk of the folder, which skips
.gitandnode_modulesand 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
/connectkey field (see/connect): they take text only; - a paste in a session whose host cannot read craze's attachments (see below).
Editing and sending¶
Backspaceat the end of a chip, orDeleteat 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(andCtrl+L, the first prompt of a new session, and saving a queue edit) does nothing and keeps the draft; the status line saysimage #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
jpegloses 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
pngkeeps 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
webpthat carries EXIF or XMP, or anything beyond plain image data, is sent as apng; a plain one passes through; - a
gifsends its first frame as apng.
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 anotherCRAZE_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 thisCRAZE_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
/modelto a model withoutvisionturns 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
Readtool returns an image file (png,jpeg,giforwebp) to a model that takes images, under the same size limits as a paste, and refuses it withCannot read image file: <model> does not accept imageson 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
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 agentwhen more than one row is visible, and, on native, a further· del to stopat 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; theesc to returnhalf 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 returnwhile it runs, then the same warning-coloured finish line with· receipt onlykept:✓ @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:
Enterasks 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 saysthis 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+Vpastes 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/PgDnand the wheel scroll it, a drag selects in it,Ctrl+Ycopies its last answer;Ctrl+O,Ctrl+TandCtrl+Gare what they are everywhere.EscorCtrl+Ccloses 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+Chere does not touch the main turn and arms no quit. A card that arrives takes the keyboard as always —Escanswers the card, not the view, and the view is there again once it is answered; with a card upCtrl+Ckeeps 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:
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, andEnteron 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 saysno directory to start in: pick one with @. - as the rule says: the provider and model chosen with
/providerand/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
/effortand/fast, else the model's own. - with the permission mode of the session you came from (
--forceor--no-force, as its host reports it), the launch's--plugin-dir, and — when its provider is the launch's own — its--agent-bin(andCRAZE_AGENT_BIN); a session of any other provider runs the binary[agents]orPATHgives it. The command line's own--ask,--plan,--effort,--fast/--no-fastand--providerwere for the session it started and are not applied (the list's/effortand/fastset the last two for its own), and its--modelonly 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..Tabdescends into the highlighted one and stays open;Enterpicks it. A directory that is not there saysno 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.
/providerlists the providers the startup provider dialog offers —gxonly where its binary resolves — the one in use markedcurrent. Each opening checks them again, as the startup dialog does (provider states): agxinstalled 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.TaborEnteron one does not choose it: the input and the provider new sessions run stay as they were, and the popup sayscan'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).nativeneeding setup is the exception:Tab,Enteror a typed/provider nativeopens/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--modelwould use (which model a session starts on) — the newest remembered model whose provider has a key, else yourdefault_modelwhen 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./modellists 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/modellists. 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 owndefaultfirst — always offered, and no model is passed with it; a model of the catalog whose id happens to bedefaultis 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 saysno cursor catalog seen yet · enter uses the id as typed:/model <id>then takes the id as typed, ascraze --model <id>would, and so doesEnteron an id no listed model matches. A bad id fails that session's start the way--modeldoes, 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:
/effortoffersdefaultand 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 SPARKresolved as--modelresolves it); the start model's for/provider nativeand/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 —defaultalone 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,xhighandmax(and so does native for the moment between a choice of model and its table being read)./fastoffersdefault,onandoff.
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 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:
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 ownproviders.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 markedstored key unusable. The ChatGPT plan is marked✓while signed in with plan usage, andplan usage offwhen its account did not allow craze to use the plan. The box opens on the first provider with no key.↑/↓move,Enteror a click picks — the ChatGPT plan's sign-in, or any other provider's key field — andEsccloses. - 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'sCRAZE_HOMEputs 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 andCtrl+Vboth 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.Escgoes back to step one and empties the field. Enterstores 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 saysConnected <Name>. Its models are in /model now.— orConnected <Name>. Its models will be in /model after this turn.when a turn is running (another client's, or the agent's own:/connectitself is refused while one runs). When the session could not take them up — see below — it saysConnected <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.tomla 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 saysCopied 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 saysThe address is too long to copy (over 64 KiB), so nothing was copied.Under--no-mousea click is the terminal's own,Ctrl+Ystill copies, and the footer saysctrl+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 chatgptprints it plain, on one line — the way that works in any terminal.
- Click it, on any of its rows, or press
- 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 orCtrl+V— and pressEnter.- Over SSH (
SSH_CONNECTION,SSH_CLIENTorSSH_TTYset) 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 withssh -L 1455:127.0.0.1:1455 <host>, or the forward added to a running one with OpenSSH's~Cescape — 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 beforeEnter, 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/callbackwith a one-time code and the sign-in's state — andPaste 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…).
- Over SSH (
Escgoes 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.andEligible 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 saysThe plan's models are in /model now.(orThe plan's models will be in /model after this turn.; when the session could not take them up, the olderNew 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 getsSigned in to ChatGPT as <email>, but ChatGPT plan usage is off: …andTo 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-inFetching 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.:Entergoes on, and only thatEnterrecords it as shown; leaving any other way shows it again next time. - Every way out goes back to where it was opened —
Escon 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+CorCtrl+Dquits 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, soallow-passthroughis 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 itsclipboard-writesetting allows; setclipboard-write = allowif 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
Shiftis held:Shift+click opens the link. - GNOME Terminal opens a link with
Ctrl+click, notShift+click. Should aCtrl+click reach craze instead, addShift(Ctrl+Shift+click), which keeps the click from an application holding the mouse in the VTE terminals. Other VTE terminals, such as Tilix, typically useCtrl+click too. - tmux passes links through from 3.4 on, once told the outer terminal
has them:
set -as terminal-features ',*:hyperlinks'. Copies needset -g set-clipboard on, as above. - The Linux console (
TERM=linux) andTERM=dumbget 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).