This page is the source of truth for the terminal (src/view/ and
src/terminal/). It states how the journal of a room reaches the screen,
how the dock and the keys work, and what a person sees and presses. Update
this page with each change to a file of those two folders.
The page has two parts. "Design" is for a contributor. "Use" is for a
person at the keyboard. The terminal reads the host through the Lab
interface. Host holds that side, and Sensors holds
the camera server that the viewfinder reads.
| Part | Where | What it holds |
|---|---|---|
| The projections | src/view/ |
Pure functions of the room record: blocks, steps, refs, live work |
| The state | src/terminal/state/ |
The session, the readers, the browsers of the layers, the voice rules |
| The widgets | src/terminal/widgets/ |
OpenTUI boxes that draw state, and the layout of the screen |
| The app | src/terminal/app/ |
The wiring, the painter, the key router, the dock, the voice devices |
| The keys | src/terminal/state/keymap.ts |
KEYMAP, the one table of every key. The keys sheet draws it |
| The commands | src/terminal/state/commands.ts |
COMMANDS, the parser, and the rows of the palette |
| The spacing scale | src/terminal/widgets/space.ts |
GUTTER, INSET, GAP, APART, SCROLLBAR, TRACK, edgeRows |
| The colors | src/terminal/widgets/brand.ts |
The product name and the dark palette |
| The import rules | biome.jsonc, CLAUDE.md |
One noRestrictedImports override for each layer |
| The view checks | test/timeline.test.ts and others |
steps, live, steering, refs, system, tool-phrases, text |
| The state checks | test/session.test.ts and others |
room-reader, feed, refs-session, status-row, title, layers |
| The widget checks | test/panels.test.ts and others |
transcript, row-diff, header, thumbnails, attachments-ui |
| The app checks | test/panel-keys.test.ts |
The keys, the dock, and the layers, over test/fake-host.ts |
| The voice checks | test/voice.test.ts and others |
voice-fakes, whisper, whisper-server, speech, kokoro |
The data goes one way, from the journal to the screen. The host reads
the journal and the canvas. The state reads the host and calls the
projections of src/view/. The painter draws the blocks that the state
holds. A key goes the other way: the key router calls the state, and the
state calls the host.
flowchart LR
journal["Room journal<br/>and canvas"] --> host["Lab<br/>src/host/host.ts"]
host -- "read, watch" --> reader["RoomReader<br/>state/room-reader.ts"]
reader --> session["Session<br/>state/session.ts"]
session -- "calls" --> view["Projections<br/>src/view/"]
view -- "blocks, refs,<br/>live, waiting" --> session
session --> painter["Painter<br/>app/draw.ts"]
painter --> widgets["Widgets<br/>widgets/"]
widgets --> screen["Screen"]
keys["Keys<br/>app/keys.ts"] -- "submit, open" --> session
session -- "send, control" --> host
A change of the open room reaches the screen in seven steps.
- The host calls
changedafter each entry that a running room records and after each activation step (Lab.watch).RoomReader.selectsets the watch. RoomReader.refreshreads the room withLab.read(room, since). A call during a read asks for one more read, so many changes cost at most two reads (coalescedinstate/coalesce.ts).RoomFeedmerges the new messages byseqand moves the cursor. A counter of generations drops the result of a read for a room that the person left.Session.applyViewkeeps theRoomView. It reads the steps of the activations (readSide) and starts the process tails of the running seats.Session.rebuildcallsbuildTimelineand keeps the result inSession.blocks.Sessioncallschanged, which isEngineTui.renderinapp/tui.ts. It places the dock and calls the painter.Painter.renderdraws the transcript only when its signature changed. It then draws the header, the composer, and the top layer of the dock.
Other sources wake a read, and each has its own trigger.
| Source | Trigger | What it reads |
|---|---|---|
| Room watch | Each entry and each activation step | The open room |
| Room-list watch | A room opens, starts, stops, or is archived | The room list (Lab.watchRooms, Session.listRooms) |
| Slow poll | Every 4 s (SLOW_MS in app/tui.ts) |
The room list and the root files. The open room only while it is stopped. The processes layer |
| Process count | A process starts or ends (ProcessCount) |
The process table, for the status row |
| Process tails | Every 1 s while a seat of the open exchange runs | The output of the processes of that seat |
| Clock | Every 1 s while a seat works (Ticker) |
Nothing. It repaints the time of the status row |
| Viewfinder tick | Every 1 s while the camera layer draws frames | Nothing. It repaints the age of each frame |
The view turns the record into plain data. It holds no state and draws
nothing. It imports no file of domain, host, or terminal. The host
imports it too, for stepLog, WORKSPACE, and the database preview.
| Module | What it projects |
|---|---|
timeline.ts |
buildTimeline: messages and exchanges into blocks |
steps.ts |
stepsView: a trace into passes with lines. stepLog keeps the traces |
live.ts |
liveActivations, workingOf, totalsOf, and endedLine |
steering.ts |
waitingMessages: the messages of the person that a running seat did not read |
refs.ts |
resolveRef, refItems, citedFiles, and pickIds |
system.ts |
systemRow: the folded row of a system message |
tool-phrases.ts |
callPhrase, resultPhrase, and failurePhrase: a tool call as one line |
text.ts |
ellipsize, ellipsizeMiddle, brief, firstLine, and clock |
database.ts |
readTables: a SQLite file into tables for the files layer |
buildTimeline makes six kinds of block. A message block holds one
message with its role: question (from a person), said, or system. A
run of system messages joins into one system block. A stays block holds
the folded activations of closed exchanges. A note block ends a closed
exchange that waits on a person or that the room gave up on. A steps
block holds the trace that /steps asked for. The live block ends the
list while an exchange is open.
A ref is untrusted text from a seat. resolveRef accepts four forms:
file:///<path>, a snapshot ref, a commit ref, and a message ref. It checks
each against the lists that the host gave (Known). A ref that fails the
check opens nothing, and its chip says why. No ref reaches a host file
without that check.
A tool without a phrase shows its name and its input. TOOLS in
tool-phrases.ts maps a tool name to an icon, words from the input, and
optionally a short result and a short failure. A tool with no entry still
shows.
The state holds everything that is not drawing. It imports no OpenTUI,
no widget, and no app file. A test builds it over a fake host
(test/fake-host.ts) with no renderer.
| Group | Files | What it does |
|---|---|---|
| Session | session.ts, session-text.ts, attention.ts, dismiss.ts, breakouts.ts |
The person, the rooms, the commands, the room list |
| Reading | room-reader.ts, feed.ts, coalesce.ts, ticker.ts |
The open room, read as often as it changes |
| Composer | commands.ts, audience.ts, cue.ts, status-row.ts, title.ts, attachments.ts |
What the input reaches, and the text around it |
| Keys | keymap.ts, mode.ts |
KEYMAP and the four modes |
| Layers | layers.ts, browser.ts, process-browser.ts, viewfinder-browser.ts, action-pad.ts |
The stack of the dock and the state of each layer |
| Counts | process-count.ts, process-tails.ts |
The processes for the status row and the live block |
| Pictures | pictures.ts, picture-cache.ts |
The thumbnails under a message |
| Voice | voice.ts, speech.ts |
The rules of voice mode and of spoken replies |
RoomReader reads the open room as often as it changes. It owns one
RoomFeed and one watch. select(room) drops the messages of the old room,
ends the old watch, and sets a watch on the new room. Each watch call starts
refresh. A read hands the room view to apply in the same request. An
error of the read or of apply goes to failed, which sets
Session.offline. stop() ends the watch.
Session is the one object that the app talks to. It holds the person,
the room list, the files, the open room and its view, the blocks, the
notice, the error, and the files browser. It calls the host directly. After
each change it calls the changed function that the app gave it.
start: sets the room-list watch, starts the process count, reads the rooms and the files with the homes, and enters the first running room.switchRoom: drops the state of the old room: the view, the blocks, the focus, the steps, the folds, the tails, the notice, and the staged attachments. It then selects the new room in the reader, leaves the old room, joins the new one, and reads.submit: reads a goal, a message, or a command withparse. A command handler returns anIntent(quit,files,processes,camera,voice, orcompose) for the app to apply. Thehandlerstable has one entry for each name inCOMMANDS, and the compiler checks it.send: refuses when a send is in flight, no person or room is open, the room is archived, or the room is stopped. Else it callsLab.sendwith a new key, the staged refs, and the named seat. A failed send keeps the staged files.readLive: keeps the full steps of the open exchange. For an activation of a closed exchange, it keeps only the totals (the calls and the span). The line of that activation reads the full trace again when the person expands it.poll: the slow fallback. It reads the room list and the root files. It reads the open room only while the room is stopped.leave: ends the watches and the timers, and ends the visit of the person.
The state keeps small pure helpers for the composer. status-row.ts
fits the segments of the status row by priority. title.ts builds the
window title. audience.ts tells which seats a text reaches. cue.ts
builds the lines above the input. A test drives each of them with plain
values.
A widget draws state and keeps no state of the room. It imports state, view, and OpenTUI. It imports no app file.
| Widget | File | What it draws |
|---|---|---|
Transcript |
transcript.ts |
The blocks. It keeps one node for each block, by signature |
Composer |
composer.ts |
The chip, the input, the palette, the cue, and the status row |
Palette |
palette.ts |
The picked row of the command palette |
Header |
header.ts, header-fit.ts |
The room path, the goal, the person, and the participants |
DockPanel |
dock.ts |
The box at the right: tabs line and the top layer |
SidePanel |
side-panel.ts |
The base of a layer: a scrolling body, a message line, a clipboard |
| Layer panels | files-panel.ts, process-panel.ts, viewfinder-panel.ts, keys-panel.ts |
One class for each layer |
ActionRows |
action-rows.ts |
The buttons under a camera |
| Pure helpers | file-list.ts, row-diff.ts, markdown-style.ts |
The lines of the files list, the row plan, the Markdown style |
Transcript.render keeps the nodes that did not change. Each block has
a signature that holds the block, its refs, the chosen ref, the focus, the
fitted width, and its strips. planRows finds the rows that are equal at
the top and at the bottom, and it keeps the equal rows between them. The
transcript builds the other rows. It then puts the scroll back: at the end
when it was there, at the same line when it was not, or at the node that the
caller asks to reveal.
The composer takes a pasted picture path. PasteAwareTextarea offers a
pasted line to pastedImagePath. A path of a picture into an empty
composer fills /attach <path> and consumes the paste.
The app wires the parts, draws them, and routes the keys. The session holds the state, and the app holds no state of the room.
| File | What it does |
|---|---|
tui.ts |
runEngine: opens the host and the renderer, builds EngineTui, runs it |
draw.ts |
Painter: reads the session and draws the header, the transcript, and the composer |
keys.ts |
Keys: the mode, the chosen ref, and the route of each key |
dock.ts |
Dock: the stack of layers and the placement of the box |
surface.ts |
Surface: the interface of one layer |
*-surface.ts |
The files, processes, keys, and camera layers |
keyboard.ts |
The Kitty keyboard flags, and the check for voice mode |
microphone.ts, whisper.ts, whisper-server.ts |
The devices of voice mode: the recorder and the transcriber |
kokoro.ts, kokoro-server.ts, speaker.ts |
The devices of spoken replies: the engine and the player |
runEngine is the only entry. src/main.ts calls it with the data
directory, the person, and the path of workstation.json. It calls
openLab and creates the renderer. The renderer asks for the Kitty keyboard
events and a target of 30 frames each second. EngineTui then builds the
widgets, the session, the four surfaces, the dock, the painter, the key
router, and voice mode. When the renderer ends, the app stops the keys,
voice mode, and the timers. It then stops the whisper server and the Kokoro
server, ends the visit of the person, and closes the host.
domain and view are leaves. host imports both. terminal may
import the other three. Biome holds the rule with one noRestrictedImports
override for each layer in biome.jsonc.
| Layer | May not import | Why |
|---|---|---|
src/view/ |
domain, host, terminal, main.ts |
It formats the record for display |
src/terminal/state/ |
OpenTUI, widgets/, app/, main.ts |
It draws nothing, so a test needs no renderer |
src/terminal/widgets/ |
app/, main.ts |
A widget draws state. The app wires widgets |
src/terminal/app/ |
main.ts |
The entry point composes the terminal |
Each layer has its own kind of test. A view function takes plain
records, so its test passes records. A state class takes a host, so its test
passes FakeHost (test/fake-host.ts). A widget test draws on the headless
renderer of OpenTUI (createTestRenderer). Cognitive complexity is 10 at
most in source and 15 in tests.
The dock is one box at the right of the conversation, with a stack of
layers. LAYERS in state/layers.ts names them: files, processes,
keys, and camera. LayerStack keeps them in order of recency.
- Open:
Dock.openraises the layer. A layer that was closed opens on fresh state, and the layer that was on top stops its work. A layer that was open keeps its state. - Top: only the top layer draws and runs its work.
Dock.layoutstarts the work of the top layer and stops the work of the others. - Close: closing the top layer shows the layer that was on top before.
- Tabs: the tabs line keeps the order of
LAYERS, so a tab does not move when its layer rises.Dock.cycleshows the next tab.
The width of the terminal decides where the dock draws. NARROW in
app/dock.ts is 100 columns. At or above it, the box sits beside the
conversation and takes half of the width, 40 columns at least
(DockPanel). Below it, a layer fits only when its surface has
narrow: true, which the files, processes, and keys layers have. A narrow
terminal draws the dock over the conversation, 80% wide, and only while the
dock has the keys. The camera layer has narrow: false, so a terminal under
100 columns never shows it.
| Layer | Surface | State | Panel | Takes keys | Work |
|---|---|---|---|---|---|
files |
files-surface.ts |
FileBrowser, through the session |
FilesPanel |
Yes | Loads the preview as the choice moves |
processes |
process-surface.ts |
ProcessBrowser |
ProcessesPanel |
Yes | Watches the process table. Follows the output of a running process each second |
keys |
keys-surface.ts |
None | KeysPanel |
Yes | None |
camera |
viewfinder-surface.ts |
ViewfinderBrowser and ActionPad |
ViewfinderPanel |
No | Opens a viewfinder on the host, and redraws the age of each frame each second |
A Kitty picture draws above every cell. While the dock covers the conversation, the painter draws no thumbnail, so none shows through the dock.
A new layer takes four steps (app/surface.ts). Write a class that
implements Surface. Add its id to LAYERS. Add it to the table of
surfaces in tui.ts. Add an open method to Keys and an Intent to
Session for the command that opens it.
One mode at a time holds the keys. Mode in state/mode.ts has four
values. Keys in app/keys.ts holds the current one.
| Mode | Who holds the keys | How it starts | How it ends |
|---|---|---|---|
compose |
The composer and its palette | The start of the terminal | Tab, Ctrl+L, Ctrl+O, or a layer that takes the keys |
refs |
The pick over refs, rows, and lines | Tab in the composer, with the palette closed | Esc, r, or Tab. It ends when nothing can be picked |
actions |
The ActionPad of the camera layer |
Ctrl+L, while the camera is on top and a camera has an action | Esc or q, Ctrl+C, or when no action is left |
dock |
The top layer | A layer that takes keys opens, or Ctrl+O in the composer | Esc, q, Ctrl+O, Ctrl+C, or when no layer shows |
Keys.onKey routes each key in a fixed order.
controlKeytakes Ctrl+C and Ctrl+D in every mode. Ctrl+C drops a recording first. Else it closes the top layer indockmode, leavesactionsmode, or clears the composer. Ctrl+D leaves the terminal on the second press withinQUIT_WINDOW_MS(2 s), from an empty composer incomposemode.voiceKeytakes Space incomposemode when voice mode wants it. The dock and the refs use Space themselves. It takes F13 in every mode, because no other part uses F13.- In
dockmode,dockKeytakes the keys of thedockscope (Ctrl+O, Tab, Ctrl+L). The top layer gets every other key. - Page Up and Page Down scroll the conversation in the other modes.
- The key goes to the
ActionPad, the refs, or the composer, by mode.
The camera layer leaves the keys with the composer. The files,
processes, and keys layers set takesKeys, so opening one blurs the
composer and sets dock mode. The camera layer sets it to false.
Keys.reconcile runs at each draw. It drops a pick that no longer exists.
It returns the keys to the composer when refs or dock mode has nothing
left to work on.
KEYMAP is the one table of keys. A handler reads its keys through
actOf(scope, key). The keys sheet draws the same table, so the two cannot
differ. A handler table has the type Record<Act<scope>, ...>, so a binding
without a handler does not compile. To add a key, add a binding to its
section in keymap.ts, then add the handler.
The terminal asks for the Kitty keyboard events. KEYBOARD in
app/keyboard.ts sets events: true, which reports the release of a key.
The flags leave out "all keys as escape codes", because macOS then reports a
character that Option types as an Alt key.
New layout takes its distances from widgets/space.ts. A cell is about
twice as tall as it is wide, so a gap of one row and a gap of two cells look
alike. No lint rule or test enforces the scale. A reviewer checks it. When a
layout needs a new distance, add a named constant to the file with a comment.
| Constant | Value | Use |
|---|---|---|
GUTTER |
1 | Cells between a rail, a border, or the window edge and the text |
INSET |
2 | The column where text starts inside a block: a rail, then the gutter |
GAP |
1 | Blank rows between two blocks |
APART |
2 | Cells between two texts on one row |
SCROLLBAR |
2 | Cells that keep text clear of a scrollbar |
TRACK |
1 | Cells that the track of a scrollbar takes |
edgeRows(rows) |
0 or 1 | Blank rows at the top and at the bottom: GAP from 30 rows up |
Voice in state/voice.ts holds the rules and no device. The app gives
it the parts (VoiceParts). They are the recorder, the transcriber, the
check that voice mode can run, the name of the place that a message goes
to, and the send. A test passes fakes (test/voice-fakes.ts).
The send calls Session.submit(text, { voice: true }). A transcript that
parses as a message then starts with VOICE_MARK (src/domain/voice.ts),
so the specialists know that the person spoke. A command gets no mark.
- The hold: Space on an empty composer starts a recording. F13 starts
one with any composer, and F13 types no text. A foot pedal sends F13
(
make pedalprograms it). The release of either key ends the hold. A hold underMIN_HOLD_MS(300 ms) sends nothing. A hold ends atMAX_HOLD_MS(60 s). A terminal repeats a held Space with no flag, so a Space withinHOLD_GAP_MS(2.5 s) of the last one belongs to the hold. Kitty flags the repeat of F13 (repeated), and the same gap applies. When voice mode is off, a new press of F13 shows "Voice mode is off. /voice turns it on." and changes nothing else. - The release:
Keys.onReleasecallsVoice.releasein every mode, because the hold can outlast a mode change. Only a terminal with Kitty key events reports a release, sokeyboardProblemrefuses voice mode elsewhere. - The recorder:
app/microphone.tsrecords mono audio at 16 kHz into a temporary WAV file with the audio engine of OpenTUI. - The transcriber:
app/whisper-server.tsstartswhisper-serveron a free port of127.0.0.1. It posts the WAV file to/inference. The process ends with SIGTERM and then SIGKILL.app/whisper.tsreadsWORKBENCH_WHISPER_MODELandWORKBENCH_WHISPER_LANGUAGE, builds the arguments, and checks that the program and the model exist. - The delivery: the transcript goes through
Session.submit, the path of Enter.cleanTranscriptdrops the lines that only name a sound. A transcript for another room or person is dropped, and the status line shows the words.
Speech in state/speech.ts reads the replies aloud. It holds the rules
and no device, as Voice does. The app gives it the parts (SpeechParts):
the check for the engine, the start and the end of the engine, the sound, the
open room, and the note. Voice needs no change. The serve and halt parts
of Voice also turn Speech on and off. The start part calls
Speech.stop before the microphone records, so the microphone does not
record the reply. The app also calls Speech.stop on
Ctrl+C. EngineTui.render calls Speech.update after each repaint.
- What it reads: the latest message of the person in the open room must
start with
VOICE_MARK. Each newsaidmessage after it is read when its author is a seat and itstois the person or empty. System messages, breakout reports, and says to another seat are not read. - What is new: a message is new when its seq is above the watermark. The watermark moves to the last seq when voice mode turns on and when the open room changes, so no message plays twice. A change of room also stops the sound.
- The text: only the words between
<voice>and</voice>(VOICE_OPENandVOICE_CLOSEinsrc/domain/voice.ts). A message can hold several spans, and the terminal reads them in order. A span can hold several paragraphs. An open tag with no close tag runs to the end of the message. A close tag with no open tag is removed and does not start a span. A tag inside a fenced code block or an inline code span is text. Text outside the tags is never read, so a reply with no tags is silent. Headings, list marks, emphasis, code ticks, and links lose their Markdown, and a link keeps its text. A paragraph with no words left is skipped. - The transcript:
voiceParts(src/domain/voice.ts) splits a say into its voice spans and the text between them.Transcriptdraws one node for each part, in order, with one blank row between two nodes. The text between the spans goes through the Markdown body. A voice span is a row on the raised tone: the dim labelvoice, then the words as plain text in thenotecolour. The tags do not show, and a span shows no Markdown formatting. A say that the room returns to its seat draws the same way, and its folded row shows no tags. A one-line preview of a say, such as the/dismisslist, useswithoutVoiceTags, which keeps the content and removes the tags that stand outside code. - The queue:
Speechqueues one text for each paragraph of a span, so the first sound starts soon. One text plays at a time, in seq order.stopaborts the sound and clears the queue. A failure shows one note and clears the queue. - The microphone: while the
busypart is true, the phase ofVoiceis notidle.Speech.updatethen stops the sound and moves the watermark. The replies that land are never read. - The engine:
app/kokoro-server.tsstartskokofrom Kokoros in server mode on a free port of127.0.0.1, with one instance. It posts the text to/v1/audio/speechat speed 1.2 and gets a WAV file.SPEECH_SPEEDsets the speed.app/speaker.tswrites the file to a temporary folder and plays it withafplayon macOS orffplayon other systems. An abort kills the player, and the folder goes away.app/kokoro.tsbuilds the arguments and checks thatkoko, the two model files, and the player exist. It runs~/.cache/kokoro/bin/kokoand does not search PATH for it. - The missing engine: voice mode works without it. One note says what is missing, and no reply is read.
Type / to list the commands, and /help to read them with the keys.
COMMANDS in src/terminal/state/commands.ts is the list. Start a message
with // to send a leading slash.
| Command | What it does |
|---|---|
/room <name> |
Switch to another room. Ctrl+R lists the rooms |
/new <name> [goal] |
Create a room. Without a goal, the next line is the goal |
/user <name> |
Switch to another person |
/files |
Search the files of the workspace, and read one in the dock |
/open <path> |
Open the files layer on one file |
/attach <path> |
Copy a local file into the workspace, and cite it in your next message |
/ps |
Show the background processes of the specialists |
/camera |
Show or hide the cameras of the open room in the dock |
/voice |
Switch voice mode on or off |
/try |
Fill the composer with the suggested question of the room |
/abort |
Cancel the open exchange |
/dismiss <n> |
Dismiss the say n that waits to return |
/stop, /resume |
Stop the room, and start it again |
/steps [n] |
Show the steps of the newest activation of exchange n. /steps off hides them |
/help |
Show the commands and the keys |
/quit |
Leave the terminal. The rooms stop with it |
Breakout rooms show in the room list, the header, and the status row. Breakout rooms holds that side.
- Addressing: start a message with
@nameto wake one specialist. A message with no mention wakes the Engineer. Type@to list the specialists with their attention in the room. The host seats a specialist atnamedfirst when the room has not seated it. Start with@@to send a leading at sign. A message takes one mention, and a second one stays in the text. The chip before the input names the seats that hear the message, and a note shows when the host seats the named specialist first or the seat has no login. The rail at the left of the input takes one color for a command and another for a mention. - Status row: the row under the input says what the room does. While a
message goes out to the host, it shows
Sending…. While a specialist works, it shows the seat, its step, and the time since its first step. At the right, it counts the background processes, the breakout rooms that run, and the says that wait, each only when the count is not zero. The keys hint follows the counts. A narrow terminal drops the says first, then the processes, then the breakout rooms, then the hint. - Terminal title: the title of the window names the open room. One mark
comes before the room:
◆when the room waits for your reply,●while a message goes out, and●with the seat name after the room while a specialist works. A reply that waits takes priority over the other two marks. An idle room has no mark. The product name ends the title. - Waiting messages: a message that you send into an open exchange shows
dim above the input as
↳ steering: <text>until each running specialist reads it. Up to three messages show. With more, two show and+N morecounts the rest. - Pictures:
/attach <path>copies a local file into/attachments, snapshots it, and cites the snapshot in your next message. Host holds the size limit. Paste the path of a picture into an empty composer, and it fills/attachfor you. A specialist reads the copy withreadand receives the picture. The files layer shows a picture, also from a snapshot ref. A terminal with Kitty graphics also shows a thumbnail under a message for each picture that the message cites with a snapshot ref, such as a frame thatfetchsaved. The thumbnails hide while the dock covers the conversation. - Staged pictures: a row above the composer names the files that wait
for your next message. Press Enter on an empty composer to send them
alone, with the text
Attached <names>. A bare@namesends them to that seat. A failed send keeps the files staged. Ctrl+C on an empty composer drops the attachments, and so does a switch to another room. - Keys: Ctrl+C clears the composer, and it cancels a new room that
waits for its goal. In the dock it closes the top layer. Ctrl+D twice
in 2 seconds leaves the terminal from an empty composer, and
/quitalso leaves. The rooms stop with the terminal. Enter sends a message, and Shift+Enter, Alt+Enter, or Ctrl+J adds a line. Page Up and Page Down scroll the conversation.
- Messages: every message shows in the order of the room. A header shows
the author and the time. A directed say shows
→ seat. A say that a seat scheduled for itself showsreturns <time>, ordismissed. The body shows as Markdown. - Working block: while an exchange is open, a line ends the
conversation:
Working on <name>’s question, then the hint/abort cancels it. Each running activation shows its seat, its purpose, the step it does now, and its latest five calls with their results.+N earlier callscounts the older calls. The three latest ended activations fold to one title. The background processes of a running seat show below it, up to three, each with its time and the newest line of its output. - Folded activations: when an exchange closes, each of its activations
folds to one line above the first message that it wrote. The line shows a
mark, the seat, the purpose, the calls, the time, and the cost. A failed activation shows
its reason. Choose the line with Tab, and press Enter to expand it to its
steps. Enter again folds it. One line is open at a time.
/stepsshows the steps of an exchange in the conversation, and/steps offhides them. - Notes: a closed exchange that waits on a person ends with
Waiting on <person>. A closed exchange that the room gave up on ends with the seat that failed and whether the room tried again. A say that waits to return shows as<seat> comes back at <time>: <text> (/dismiss <n>). - System messages: a message of the host shows as one folded row: a mark,
the source, the first line, and the time. A say that the room gives back to
its seat shows
↩. The report of a breakout room names that room. Choose the row with Tab, and press Enter to show the whole message. - Refs: a message lists its refs as chips.
↗marks a ref that opens, and✗marks a ref that opens nothing, with the reason. A ref names a workspace file, a snapshot of a file, a commit of a lab repository, or a message of the open room. Tab chooses a ref, and Enter or Space opens it. A file, a snapshot, and a commit open in the files layer, and a message ref moves the focus to that message. Esc,r, or Tab goes back to the composer.
- Dock: the files, the processes, the keys sheet, and the camera are layers of one dock at the right of the conversation. The dock takes half of the terminal width, has the panel background, and has one line at its left edge. A tabs line names the open layers, and the top one is bright. A new layer opens on top. A layer that is open comes to the top when you open it again, and the other layers keep their state below it. Only the top layer draws and reads. A terminal under 100 columns draws the dock over the right part of the conversation, only while the dock has the keys.
- Dock keys: the composer keeps the keys. The files, the processes, and
the keys sheet take them when they open, and the camera leaves them with
the composer. In a layer, Esc gives the keys back to the composer, and the
layer stays open. In the composer, Esc closes the top layer while the dock
shows. In the files layer, the first Esc clears the search. In the
composer, Ctrl+O gives the keys to the dock. In the dock, Ctrl+O gives
them back to the composer, and Tab shows the next layer. Ctrl+C closes the
top layer while the dock has the keys, and so does
qin every layer but the files. While the dock shows and the composer has the keys, the status row showsEsc close. - Keys sheet:
?on an empty composer opens the keys sheet as a layer.?orqcloses it. The sheet lists every key of the terminal. - Processes layer:
/psopens a layer with the background processes of the specialists, the running ones first and then the newest start first. A row shows the name, the seat, the state, and the command. The end of the output of the chosen process shows below the list and follows the output while the process runs. Pressxtwice to cancel the chosen process, and Ctrl+Y to copy the output. - Files layer:
/filesopens a search box over the files of the workspace. The head of the list isCited here. It shows the files, snapshots, and commits that the messages of the open room cite, one row for each path or repository, with the newest first. A row names the author and the time of the newest citation. The snapshots of one path share a row, and the row shows the count of versions. Below that, the files of the workspace sit under one heading for each root, such as/libraryand/shared, and then under one heading for each seat, such as~engineer. A•marks a file that the open room cites. The search keeps the rows whose path holds every word. - Files layer keys: Up and Down choose a row. Enter closes the layer and
focuses the newest message that cites the file. On a file that no message
cites, Enter says so in the status line. Tab or Enter on a ref in the
conversation opens the file in the layer, so the two keys make a round
trip.
/fileswith no argument starts on the newest cited file. The keys sheet lists the other keys, such as the table keys of a database and the copy key. - Files layer width: a narrow dock drops the columns of a row in a fixed order: the time of a citation, then the size of a file. The path takes the ellipsis in its middle, so the file name stays.
- Seat homes: the terminal lists the homes of the seats at the start and
each time the layer opens. The slow poll reads the roots alone. A home that
fails to list shows no file, and the other groups stay. A
file:///ref to a file in a home opens in the layer. Host holds how the host lists and reads a home.
- Camera viewfinder:
/camerashows a layer in the dock with the cameras that the open room shows. The Engineer shows a camera with aframewidget after its camera server answers. The layer draws one box for each shown camera, stacked. Sensors holds the count of cameras that the layer shows. A box holds the title of the widget, or its name, the latest frame, its age, and a note when a read fails. Ahideof the widget removes its box. When the Engineer shows no camera, the layer says so and asks you to have the Engineer show the camera. When the process of a camera ends, the box says so, and it draws a frame again after a newshow. - Camera reads: the layer follows the room that the terminal opens. The host reads a camera only while the layer is on top of the dock and the terminal can draw a frame. Sensors holds the protocol of a read, the interval, and the limits.
- Camera layout: each frame keeps its 16:9 shape. A second
/cameracloses the layer when it is on top. The composer stays active while the layer shows. Another layer on top covers the camera and stops its reads. The camera shows again when that layer closes. A terminal under 100 columns does not show it, and/cameracloses an open camera layer there. The layer needs a terminal with Kitty graphics, such as Ghostty. - Look now: the Engineer shows each camera with the action "Look now".
The layer draws it as a button under the box of the camera. Ctrl+L takes
the keys, Up and Down choose a button, Enter presses it, and Esc goes
back. A press is a message of you to the Engineer, which observes that
camera and answers. The button is inactive when the widget is
foranother person, or when the room is stopped. A refusal shows its reason, and a press that the host could not confirm goes again as it was.
-
Voice mode:
/voiceswitches voice mode on and off. Hold Space on an empty composer to record, and let go to send what you said as a message. F13 does the same in every mode, with any composer.make pedalprograms a USB foot pedal to send F13. A press under 300 ms sends nothing. Ctrl+C drops a recording. A transcript goes to the room that was open when you pressed the key. When you switch room before it is ready, Workbench drops it and shows the words. The terminal records mono audio at 16 kHz. It transcribes withwhisper-serverof whisper.cpp, which runs on this computer. No audio leaves it. The terminal must report key release: Kitty and Ghostty do./voicestartswhisper-serveron a free port of127.0.0.1, and the model stays loaded until you switch voice mode off or leave the terminal. The line "loading model" shows until the model is ready. A recording that you make before then waits for the model, and Workbench sends it when the model is ready. When the server ends by itself, one line shows its last message, and the next press of Space or F13 starts it again. When the system kills Workbench with SIGKILL, the server can stay. End it withpkill whisper-server.Set up once with
make voice. It installswhisper-cppwith Homebrew and downloads the model of about 3 GB to~/.cache/whisper/ggml-large-v3.bin.WORKBENCH_WHISPER_MODELnames another model file, such asggml-large-v3-turbo.binfor speed. Whisper reads the speech as English.WORKBENCH_WHISPER_LANGUAGEnames another language, such asro, andautomakes whisper detect it. The first press asks macOS for the microphone.Voice mode also reads the replies aloud. When your last message was a voice message, the terminal reads aloud the text that a specialist puts between
<voice>and</voice>in each new reply. While a specialist works on a voice message, it also speaks a short update when it starts, at each milestone, at a blocker, and when it finishes. The screen shows the reply with no tags. The terminal uses Kokoro, a speech model that runs on this computer./voicestartskokoon a free port of127.0.0.1, and switching voice mode off ends it. A press of Space or Ctrl+C stops the reply. A reply to a typed message stays silent, and so does a reply with no tags. A reply that lands while the microphone records or whisper transcribes stays silent too.make voicealso buildskokowithcargointo~/.cache/kokoro/bin/, and downloads the model files of about 340 MB to~/.cache/kokoro/. The build folder~/.cache/kokoro/buildstays, becausekokoreads its phoneme data from there. Whenkokoor the files are missing, voice mode works and one note says to runmake voice.WORKBENCH_KOKORO_VOICEnames another voice, such asbf_emma. The default isaf_heart. When the system kills Workbench with SIGKILL, the server can stay. End it withpkill koko.