Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The composer

The input surface at the foot of a session page: one card whose top row says what this seat is doing and whose body holds exactly one thing - the field, an ask, a take’s card, or the reason it cannot take input. The queued prompts that pile above it are the queue’s page. The drawing it is held against is web-composer.html, beside this page.

The draft is the reader’s own state and this client holds it, per seat: a draft follows the seat, comes back when the reader does, and never walks into the seat being moved to. Nothing the card shows is a bundled default; every list it opens is a live read.

What it draws

PartShowsRead from
Live rowthe running turn’s figures (mark, clock, thinking, tokens, cached, written, cost) and the seat’s segment chips, one line, never wrapping; the ask that waits, and the reader’s draft chipthe seat’s record, the board’s project row, and the segment stores
Bodyexactly one of: the field (with its notice above), an ask’s dock, a take’s card, a blocked linethe host’s composer store, pending_asks, the dictate subject
Menuone popover above the card: the five lists belowthe CLI’s advertised commands, this seat’s file index, the subagent catalogue, the bundle’s emoji set, the sessions this seat can resume
Dialogthe dictation panel: the axes, the models each role runs, the input devicethe greeting’s dictate axes; dictate subject

The live row is the strip, moved onto the card. It mounts the same segment components the strip drew - browser, terminal, subagents, processes, connectors, schedules, MCPs, files, git, review, tasks, monitors - in that fixed order, so a segment gaining a state gains it here for free. A turn’s figure the record does not carry is dropped, never drawn as a zero or a dash. Chips that do not fit collapse behind +N, which lists them in a panel; the row clips rather than wrapping or scrolling sideways. A segment with something waiting - the review answer, a failed process - takes its tone: the accent for attention, red for a failure.

The row carries the ask too. While an ask holds the body, a chip at the row’s front names it (the question’s position, an approval, a held post, a document, a browser hand-off, a terminal hand-off, a held vault call or a held configuration write) in the attention tone. When the reader has words in their draft, a draft N lines · show chip sits on the row: one click swaps the field into the body, the ask stays mounted and hidden behind it, and the ask’s chip brings it back. Nothing the reader had chosen in the ask - a toggled set, the marked row - is lost to the swap.

Every send is stamped. The composer mints a uuid for the prompt and sends the words under it, which is what makes the CLI emit the lifecycle frames the queue page reads; without a stamp the CLI emits none.

The ask’s dock

While a prompt holds the body it replaces the field with its own card. A held Slack draft shows where it would go and the words that would go out, with every mention drawn as the name the core resolved (its id when the lookup fails), as Slack draws it, and its verbs follow what it would do: one wording for a post, an edit, a delete, a reaction, a reaction’s removal and an upload would ask the reader to approve the wrong act. A post draws Post and Do not post; an edit draws Apply edit and Do not apply; a delete draws Delete and Keep it; adding a reaction draws React and Do not react, and removing one draws Unreact and Do not unreact; an upload draws Upload and Do not upload. The destination line follows the same rule: the pill carries the workspace and the conversation, and the segment beside it names the thread a post or an upload replies into, or the message every other operation acts on, by its author and its opening words, with a mention in the quote drawn as its name (its id when the lookup fails), a link as its label or address, and bold without its stars. The people the draft tags follow as their names. A quoted message the core could not read reads as a thread - a message for an operation whose stamp names a message - and a tag it could not resolve draws its id, so a failed lookup is drawn as itself rather than as a missing segment. A message that itself sits inside a thread is not named: the read answers its thread’s parent, so that card reads as a message rather than naming the wrong one. A root message draws no thread segment, and a draft that tags nobody draws no tags segment.

A third answer, send back with feedback, opens a field on the card. Enter sends, Shift+Enter is a newline, and Escape closes the field without sending. The words reach the agent as the blocked call’s refusal, so it revises and calls again, and the revision parks as its own card; nothing posts until the reader approves that one. The press stands the card down, a line where it stood says the feedback is on its way, and a refusal replaces that line with its reason.

The send-back field takes dictation. It wears the same DictateControl as every other text box, so a reader can speak the correction rather than type it. While its take runs the take’s card replaces the field in the send-back row, and the card’s own way out abandons the take. A take begun there lands its words in the send-back field, not the composer’s draft behind the card; so does a take begun with the push-to-talk key while the field is open and holds the caret, because the key names no box of its own. A take whose field closes before its words land keeps them in the composer’s draft instead, so nothing spoken is lost. A take that never starts says so in the dock’s refusal line at its foot, because the box’s own notice row is not on screen while a prompt holds the slot.

A held configuration write draws like a held post, with both values. A settings__set or settings__unset call parks every key it would change, what that key holds now and what the write would leave; one card carries a whole batch, because a section’s mutually required keys cannot be approved apart. The two verbs are Write it and Don't write, and the dock stands down on the answer the way a post’s does. Every write is held, whether the key belongs to this project or another.

A held vault call draws the act, never a secret. The card leads with the tool’s own name and the core’s one-line summary, then names the item and its id, the field the call reveals or changes, the diff with every secret value masked, and the attachment’s name and size when the call moves one. The draft carries no secret value at all, so nothing the card draws can reveal one; the value reaches the tool’s result only after an approval. A card parked while the session is locked carries only what the call’s arguments already say - the item’s id, the field or the masked diff, an attachment’s name and size - because naming the item needs the keys; the name appears on a card parked after an unlock.

Where the device holds a biometric enrolment, the locked card leads with the gesture - Unlock with this device, or Unlock with the device password where the gate’s fallback is the password - with use the Vaultwarden password one link beneath it. Enter runs the gesture, the way Enter submits the other cards. The gesture is the approve: the unlock material it releases rides only an approval, and a send-back or a decline runs nothing. A refused gesture never settles the call: the card stands, says the platform’s own reason under the button, and the password it offers is still one click away. A lock frame turns an open card back into the unlock card: where the device holds an enrolment it offers the gesture again with the Vaultwarden password one link beneath, and otherwise it asks for the password. The frame is the only announcement the idle lock makes: a quiet session with no read in between has no frame yet, so its first approve can still answer Locked, and the call returns on its next attempt as the unlock card. Without an enrolment, or where the platform has no gate, the card is the password one: a masked master-password field and an optional 2FA code, with Approve unavailable until the password is entered, because the approval is what performs the unlock. Its verbs are Approve, Send back with a note and Decline - the gesture taking the Approve’s place where it leads - and the send-back opens the same words field a held post’s does.

The five lists

Five triggers, one popover shape, each row carrying the real icon for its kind rather than a character-cell prefix and the matched span highlighted:

TriggerListSource
/ at the start of a draftcommands, forge’s own leading, the CLI’s advertised following under their own headingforge’s command table in the bundle; the CLI’s slash_commands read for this seat
@this seat’s files, by path from the session’s rootthe file index walk
&the subagent types the CLI namesthe subagents catalogue
:nameemoji, by shortcodethe bundle’s own set
/resume and /resume <partial>the sessions this seat can jump back to, newest first: the id, and the transcript’s own title beside itthe record’s resume_sessions read

The popover is over the conversation, not a lane of the bar. It was position: static in the box, and an open menu took the band from 121px to 557px; it now hangs above the card and the bar’s height does not move with it.

The /resume list is the set that command can actually resume. The argument has to be a whole word, so a second word opens nothing rather than composing a line the core answers as a misuse. A lead is offered its own project’s sessions and never its workers’; a worker is offered only the runs carrying its own label, and only from the directory its own resume runs in. The session the seat is already on is not offered: every row is a real jump. A pick writes the id alone, so the send is the ordinary /resume <id>.

A query drops the headings and leaves the order; a list with no rows opens nothing, because a popover holding only a header reads as a list that lost its rows rather than as a query that matched nothing. The header states the number of matches and a list is cut at a bound on ranking work, so a truncated list never reads as the whole list. Forge’s table shadows the CLI’s row of the same name - one name, one row.

Voice

A second control sits beside the mic when [tts] enabled is set: the switch for the agent’s voice on the seat this client shows. It is off by default. A click turns it on and the seat’s listener count comes back; a second click turns it off. The control draws its state as a shape, not a colour: a struck-through speaker when off, a plain speaker when on, and a mark beside it while a reply is sounding. While more than one client is listening, the switch draws the room’s count beside the icon and its title names it; the count is the seat’s own answer, seeded from the seat read on a late attach.

Listening is a mode on the seat’s agent, not a second copy of the chat. While a client is listening, the agent speaks the substance of each update as it would say it in a live conversation, phrasing it for the ear and keeping it shorter than the page, while the written chat stays the record that carries the detail. The first client in puts a note into the session that tells the agent the person is listening and in what register.

Dictation and voice are different directions. The mic is how the user speaks to the agent: it fills the box with text and stops there. The voice switch is how the agent’s replies are heard: it turns the seat’s speech on or off for this client and does nothing else.

Voice is per client and per seat. Every connection on the seat receives its clips, and this client plays them only while its switch is on. The last client out tells the agent the person stopped when the reader turned the switch off themselves, and when the connection that held that client goes away: a closed client or a dropped socket empties the seat’s listener set too. A seat change pauses the switch and a return to a seat the reader had on reconnects it, so moving between seats does not toggle the agent. A closed client or a dropped socket turns the switch off. The clips play in the order the engine rendered them, each sentence following the one before, and a reconnect discards the queue rather than playing a conversation with holes in it, then resubscribes the switch if this client still wants voice.

Dictation

The mic sits at the body row’s end; a take runs ready (the capture is open, nothing said yet), recording from the first real sample, and transcribing after the release. The take’s card draws in the body’s own row in every phase - a dot while it records, a spinner while it transcribes, the clock, the level graph or the section bar, the counts and the way out - except a take begun in the send-back box, whose card draws in the send-back row in place of its field. There is no second take shape anywhere, no pill or badge on the live row. Push-to-talk is forge.toml’s own binding (right or left Cmd, or off), in one of three modes: a clean press-release under 300ms is a tap, and whether a tap keeps recording or finishes is the mode’s business. The panel opens from the mic: the three axes - styling, structure, context - the models each role runs, and the input device. The axes are per seat and remembered on this machine, so a reload returns what the reader chose rather than the config’s value.

The panel hangs off the input row, not the card above it. The live row changes height with every turn, so a panel anchored to the card would move and re-cap as a turn starts and ends; anchored to the row, the row grows under it and the panel stays where it is. Its models read is held for the connection rather than remade whenever the page hands the composer its props again, so a record frame never blinks the rows back to “reading”.

On the desktop the picked input stays open for the client’s whole run: the shell takes the hold as the page that takes typing appears and again whenever the pick changes, so a press records from the press rather than opening a device under it, and a take reuses the held stream instead of paying the device’s own start-up. The microphone indicator stays lit while the hold is on - the accepted cost - and macOS asks for the microphone as the page opens rather than at the first press. In a browser the capture is the webview’s own, opened per take as it always was.

A held capture the device has stopped serving is opened again. The audio layer under the client changes on its own - Android disconnects the app’s input stream when the route moves, and a disconnected stream never delivers again - so the hold and the press open the device again when the held capture has gone half a second without a block. One such event does not leave every later press recording nothing.

A capture the device is feeding nothing says so while it is running. The row’s chip stays ready and no graph draws for as long as that lasts, so past a few seconds without a sample the take line says the capture is open and nothing is coming through it, and that line goes the moment a sample lands. On the phone the shell’s own readers name the cause where the platform reports one - the phone silencing this app’s microphone, or the device’s own mute - and everywhere else the line asks the reader to check those and whether another app is on the microphone.

An open that the device never answers is reported rather than waited on. The open blocks on the hardware, and a device layer that is wedged - a permission whose prompt never surfaced, a device another process holds - answers neither way. Past twenty seconds the line above the body says the microphone did not come up, and the take the open began is left nobody’s: when it answers late it is ended, so the next press records rather than being refused as busy. A device layer that fails by panicking during the take’s open is answered the same way: the shell catches it, names it on that line (the microphone device layer panicked during the take's open: ...), and writes the operation, the device and a backtrace to the client log. The audio callback has its own guard, in the capture crate: a panic in the pipeline body forge calls from cpal’s frame is caught there before it crosses the C boundary, and the capture stops delivering. That line is a tracing record, and the shell forwards it into the client log beside its own lines. A panic that unwinds through a foreign frame still aborts; no catch at either layer can reach it.

Every landing copies its words. After a take’s words land in a box - the composer’s draft, the dock’s words row or the send-back field - the same text is written to the clipboard with navigator.clipboard.writeText, overwriting what was there. A page over a plain-http origin has no clipboard at all and a live one can still refuse the write; neither costs the reader the words that just landed, and a refusal is logged at debug.

The states the surface can be in

StateDraws
Idlethe field row alone when the seat has nothing to report, the live row above it otherwise
Typingthe draft, the send joining the row; the keys as a pointer’s detail
Runningthe turn’s figures leading the live row; stop beside the mic, and escape its key
A question parkedthe dock in the body, its chip on the live row, the reader’s draft a chip beside it
An ask foldedevery kind folds to one bar: the kind’s mark, its subject, the count behind it, a refusal line
Sign-in neededthe field stays, with the sign-in line above it: the reader is told where, not blocked
A handoffhanding this session over, the sweep, no field
Compactingcompacting this session, the sweep, no field
A command in flightRunning <name> for the slash command the turn is still working on, the sweep
WakingWaking up agent... while the lead’s seat comes up, the sweep
Not runningnot running and the seat’s own reason: a worker’s seat has no spawn coming, and says so
ReadingReading the conversation... while the seat’s first page is still out, with the sweep, no field
A take that does not come upthe body stays, with the line above it: the microphone did not come up, or the device layer’s own named failure
A take that failedthe field comes back with the notice above it, no recording band and no stop control: the take produced no words, so the reader is told to re-speak rather than discover it
A take that landed with a gapthe words land with the notice above the field, this is what fitted · part of the take is missing: the take hit its cap, a segment ran out of decode budget or stopped on a repetition loop, or a segment failed after words landed, so the reader knows part of what they said did not land rather than that they stopped speaking
Voice onthe voice switch beside the mic in the accent: a plain speaker, with a mark beside it while a reply is sounding
Voice offthe same switch, a struck-through speaker and dim, meaning this client is not listening to this seat
FailedInput disabled due to error and what to do: quit and start forge again

A blocked state replaces the body entirely and always says why. The order the states are read in is the one a reader has to be told: a failing seat first, then a seat nothing has started, then the more specific truth about a turn the reader caused.

The read wait is one of them. A seat whose first page is still out says Reading the conversation... and holds the body like every other narrative state: there is nothing to type into until the page lands, so the line takes the box rather than sitting above a live field - a row above the field grew the composer and shrank it again before the chat appeared. A take in flight or a parked ask takes the body ahead of it, because both are the reader’s own surface; while the read wait holds the box the push-to-talk key begins nothing, since the field is not on screen to receive the words. A row the column already holds - a send carried across a reload, or one the takeover bar sent - still draws above as the reader’s own, so those words are never lost. A command the core answers itself draws no row: its answer is the feedback.

Every narrative hold is one draw. Waking, reading, a command in flight, a compaction and a handoff all replace the body with one line and the same sweep moving through its words - only the text changes - so a hold reads the same wherever it comes from. The two states that are not progress carry no motion: not running states a stop, and the error keeps its own tone and its sub.

A machine turn is one the reader cannot type into. A handoff replaces the seat when its judgment turn ends, and a compaction is the CLI rewriting the context; both take the field, the send and the mic away and name themselves in the line. The push-to-talk key begins nothing there, but a take already running still ends on it: the body draws no row to end one from, and Escape would throw the words away. The draft is the reader’s and stays on the seat, so the words come back with the field when the machine turn ends.

A parked prompt outranks the machine line. Both machine turns are ordinary tool-using turns - the judgment turn writes memory files and stops processes - so they raise permission prompts, and this card is the only surface that answers them. The dock draws, the line stands down, and the turn carries on once the prompt is answered; the alternative is a seat waiting for an answer nobody can give. A compaction still running when a handoff ends is read back off the record, so the body does not come alive under it.

The two widths

The card is drawn and checked on the session page’s column, at 1600 and at 430. At 1600 the idle band measures 83px, or 46px on a seat with nothing to report; an open menu does not move it. At 430 it measures 121px: every control a finger reaches takes a 44px target under a coarse pointer, keyed on the pointer rather than the width - the phone has no push-to-talk key, so the mic is the only way into dictation there - and the key hints stand down because a finger has no use for them. The live row never wraps at either width; the figures give way first, and what still does not fit is behind +N.