Object model
What belongs to the Pi TUI alone: the view state it draws, the actions a user takes, the screen regions that show them, the components that draw them, and the visual rules. This page owns the app’s design vocabulary. The shared nouns, such as session, input, queue, run, turn, tool call, user turn, async task, and the transcript and its view, are defined in the runtime’s object model and named in the glossary; this page uses them without redefining them. Pi core uses this page’s names in its view-model types (apps/pi-core/src/view-model.ts); the HTML design reference and the app’s pi-tui components use the same component names. Pi core is the frontend’s backend; the pi --mode rpc process it runs is the Pi process.
The MVP draws every assistant message. Drawing only each user turn’s final answer waits on the user-turns package (decision). It exists, but it still runs under tui-display’s gates; running it in every mode, and drawing from it here, are later changes.
View state
Section titled “View state”Each view-model object lists what the TUI holds, its states, and where its data comes from. An object named after a shared noun holds the TUI’s copy of it. RPC names refer to the commands and events in Pi’s docs/rpc-commands.md, docs/json.md, and docs/rpc-extension-ui.md (Pi 1.3.0).
| Object | What it is | States | Source |
|---|---|---|---|
| Pi process | The pi --mode rpc process Pi core runs for the TUI (PiProcessView). | starting, ready, exited (code, signal, stderr tail) | Pi core’s transport |
| Session | The session the Pi process holds: id, file, name, cwd, repository, branch. | One at a time; a replacement swaps it | get_state; the session file’s header for the cwd; session_info_changed |
| Model | The selected provider and model, with its reasoning flag and context window. | Selected, or none | get_state, set_model, get_available_models |
| Thinking level | The model’s effort: off to max. | One value; off until get_state reports it, because initialViewState starts there | get_state, thinking_level_changed |
| Session state | What the session is doing: idle, or busy and what it is busy with (SessionStateView). It describes the session, not one run. | idle, streaming, compacting, retrying; a retry’s message is constant text with at most an HTTP status, never the provider’s | agent_start, agent_settled, compaction_*, auto_retry_* |
| Block | One drawn transcript entry: a user prompt, an assistant message, an extension message, a compaction, a notice, or a session break. | An assistant block is streaming until message_end, then done, failed, or aborted. A failed or aborted one ends with a constant tail (errorText) that never carries the provider’s text | Events live; get_entries for history |
| Tool call | One tool call inside an assistant block. Pi runs a message’s calls only after the message ends, then streams the next message: a call runs inside a settled block, and the answer is a block of its own. | pending (arguments streaming, or waiting to run), running, done, failed (also when the session settles before the call finishes) | toolcall_* deltas, tool_execution_* |
| Queue | The session’s queue: steers and follow-ups waiting for their boundary. | Empty or holding text | queue_update, clear_queue |
| Draft | The editor’s text before it is sent. | Empty or holding text | Local; set_editor_text replaces it |
| Dialog | An extension’s question: select, confirm, input, or editor. | open, then answered, cancelled, or expired | extension_ui_request; Pi core’s own timer for timeout |
| Notice | A short message: an extension notify, an extension_error, a protocol error, or the TUI’s own hint. | Live for a few seconds; errors also stay in the transcript view as notice blocks | extension_ui_request, events, Pi core, the TUI |
| Status entry | A keyed text an extension sets for the footer. | Set or cleared | setStatus |
| Widget | Keyed lines an extension places above or below the editor. | Set or cleared | setWidget (string arrays only); keys starting pi-frontend:v1: are the frontend bridge’s data, never widgets |
| Work status | Work-status rows by owner (workStatus): shell tasks, subagents, and persistent jobs; not drawn yet. | A row is unfinished (pending, active, waiting, serving) or finished; a finished row the owner drops stays until the session settles | The frontend bridge’s widgets |
| Command | A slash command: an extension command, a prompt template, a skill, or a TUI built-in. | Listed | get_commands, plus the built-ins below |
| Usage | Session cost and context use. | Known or unknown | get_session_stats, refreshed after each settle and compaction |
| Viewport | The transcript view’s scroll position. | following the end, or scrolled away with a count of rows below the view | Local |
| Selection | Transcript-view text the mouse selected. | None, or a range copied with OSC 52 | Local |
The session cost excludes subagent children, unlike the native footer. A child’s cost is not on the RPC stream. Async tasks, both shell tasks and subagents, are not MVP objects; they arrive Later. Pi core already keeps their work-status rows; drawing them waits for accepted design frames.
Actions
Section titled “Actions”Keys follow Pi’s keybinding ids and defaults (Pi docs/keybindings.md). “Later” means Milestone B; “not planned” means no current intent.
| Action | Acts on | Key | Pi id | RPC | Milestone |
|---|---|---|---|---|---|
| Send | Draft | Enter | tui.input.submit | prompt; while the session is busy, prompt with streamingBehavior: "steer" | MVP |
| New line | Draft | Shift+Enter, Ctrl+J | tui.input.newLine | — | MVP |
| Queue a follow-up | Draft | Alt+Enter | app.message.followUp | prompt with streamingBehavior: "followUp"; sends normally when idle | MVP |
| Restore the queue | Queue | Alt+Up | app.message.dequeue | clear_queue, text returns to the draft | MVP |
| Interrupt | Run | Esc, after the dialog, the command list, and the selection (see Esc precedence) | app.interrupt | clear_queue, then abort; queued text returns to the draft at once, without waiting for Pi to answer the abort | MVP |
| Clear, then exit | Draft | Ctrl+C outside a dialog: clears the draft; a second press within 500 ms exits, as in Pi | app.clear | — | MVP |
| Exit | Pi process | Ctrl+D on an empty draft | app.exit | stdin closed | MVP |
| Complete a command | Command | Tab, or Enter in the list | tui.input.tab | — | MVP |
| Scroll a page | Viewport | PgUp, PgDn | tui.altScreen.pageUp, pageDown | — | MVP |
| Scroll | Viewport | Mouse wheel | — | — | MVP |
| Jump to the top | Viewport | Ctrl+Home | tui.altScreen.top | — | MVP |
| Jump to the end and follow | Viewport | Ctrl+End | tui.altScreen.bottom | — | MVP |
| Select and copy | Selection | Mouse drag (copy on release) | — | — | MVP |
| Answer a dialog | Dialog | Enter; Up/Down to choose; Esc or Ctrl+C cancels | tui.select.* (tui.select.cancel is Esc and Ctrl+C) | extension_ui_response | MVP |
| Pick a model | Model | Ctrl+L, or /model | app.model.select | get_available_models, set_model | MVP |
| Cycle the model | Model | Ctrl+P | app.model.cycleForward | cycle_model | MVP |
| Cycle the model backward | Model | Shift+Ctrl+P | app.model.cycleBackward | none in RPC: cycle_model only goes forward | Later |
| Cycle the thinking level | Thinking level | Shift+Tab | app.thinking.cycle | cycle_thinking_level | MVP |
| Set the thinking level | Thinking level | /thinking | — | set_thinking_level | MVP |
| New session | Session | /new | app.session.new | new_session | MVP |
| Compact | Session | /compact [instructions] | — | compact | MVP |
| Name the session | Session | /name <name> | — | set_session_name | MVP |
| Quit | Pi process | /quit | — | stdin closed | MVP |
| Paste | Draft | Terminal paste; more than ten lines become a [paste #1 +12 lines] marker, sent in full | — | — | MVP |
| Editing | Draft | Pi’s tui.editor.* defaults: Ctrl+A/E, Ctrl+B/F, Alt+B/F, Ctrl+W, Alt+D, Ctrl+U, Ctrl+K, Ctrl+- undo | tui.editor.* | — | MVP |
| Yank and yank-pop | Draft | Ctrl+Y, Alt+Y | tui.editor.yank* | — | MVP |
| Prompt history | Draft | Up/Down at the draft’s edge | tui.editor.cursorUp/Down | — | MVP |
| External editor | Draft | Ctrl+G | app.editor.external | — | Later |
| Paste an image | Draft | Ctrl+V | app.clipboard.pasteImage | prompt with images | Later |
| Copy the last answer | Block | Ctrl+X | app.message.copy | get_last_assistant_text | Later |
| Tool and thinking peek | Block | Ctrl+O, Ctrl+T | app.tools.expand, app.thinking.toggle | — | Later |
| Suspend | TUI | Ctrl+Z | app.suspend | — | Later |
| Jump to the previous or next prompt | Viewport | Ctrl+Shift+Up, Ctrl+Shift+Down | tui.altScreen.previousPrompt, nextPrompt | — | Later |
| Search the transcript view | Viewport | Ctrl+Shift+F | tui.altScreen.search | — | MVP: pi-tui’s search, over the mounted window only |
| Resume, tree, fork, clone | Session | /resume, /tree, /fork, /clone | app.session.* | switch_session, get_tree, fork, clone | Later |
| Reload | Pi process | /reload | — | none in RPC | Later |
Esc precedence
Section titled “Esc precedence”Esc does the first of these that applies, and only that:
- Cancel the open dialog: it owns the keyboard.
- Close the command list.
- Clear the selection.
- Interrupt the run, while the session is busy.
Ctrl+C cancels an open dialog too, as Pi’s tui.select.cancel does. Outside a dialog it clears the draft, and a second Ctrl+C within 500 ms exits.
Conflicts resolved against the pilot
Section titled “Conflicts resolved against the pilot”- Tab completes a command; it does not move focus. Focus stays in the draft, and the transcript view scrolls by wheel and keys without focus.
- Ctrl+Z is not undo; undo is Ctrl+- as in Pi. Ctrl+Z is reserved for suspend.
- Ctrl+Y is not redo; it yanks, as in Pi.
- Ctrl+C never aborts; Esc interrupts.
- Alt+Enter is never a new line.
Regions
Section titled “Regions”Top to bottom, full terminal width:
| Region | Shows | Default | Degrades at 70 columns |
|---|---|---|---|
| Transcript view | Blocks, with tool calls as one line each | Fills the space above the bottom regions; follows the end | Text wraps; tool lines truncate their argument summary first |
| Dialog | The head of the dialog slot: a bordered box, one at a time; the rest wait in order | Hidden when no dialog is open; the transcript view gives up its rows to it | The box narrows to the width |
| Notice row | The newest live notice | Hidden when no notice is live | Truncates with an ellipsis |
| Status row | Busy indicator and elapsed time, the last tool, retry state; right side: rows below the view while scrolled away | Shown while the session is busy or the viewport is scrolled away; hidden while idle and following | Drops the last tool before the indicator |
| Queue | One dim line per queued message: steer or follow-up, then its first line | Hidden when the queue is empty | Truncates each line |
| Widgets above | Extension widget lines with aboveEditor placement | Hidden when none | Truncates each line |
| Editor | Pi’s editor: the draft between two rules, scrolling past 30 % of the terminal’s rows (at least five); the command list below the bottom rule | Always shown; holds focus | Wraps |
| Widgets below | Extension widget lines with belowEditor placement | Hidden when none | Truncates each line |
| Footer | Repository and branch, model and effort, context, cost; status entries on their own line below | Always shown | Breaks the fields into two lines at a field boundary; only when two lines cannot hold them, drops effort, then the model. Repository, branch, context, and a shown cost stay; status entries that do not fit drop |
Two parts show only at times:
- Command list: Pi’s autocomplete list, below the editor’s bottom rule, while the draft is
/and a name with no space. Up to five rows (Pi’sautocompleteMaxVisibledefault): name and description, built-ins first. - Pi exit panel (
PiExit): replaces the transcript view when the Pi process exits unexpectedly: exit code or signal, and the last stderr lines.
Focus is never in doubt: a dialog owns the keyboard while open; otherwise the draft does. The command list borrows Up, Down, Tab, Enter, and Esc while visible.
Components
Section titled “Components”One component per region and per block kind. The same names appear in the design reference (apps/my-tui/design/) and in apps/my-tui/src/. Each takes a slice of the view model.
| Component | Input | pi-tui part |
|---|---|---|
App | ViewState, the FrontendClient actions | TuiAltScreen with a VStack of the regions; owns the keys and focus |
TranscriptView | blocks, viewport | windowed ScrollView that follows the end |
UserBlock | UserBlock | lines on the user background band |
AssistantBlock | AssistantBlock | Markdown with Pi’s markdown tones and highlight.js, then ToolLine rows |
ToolLine | ToolCallView | one line: state glyph, tool name, argument summary, duration |
CustomBlock | CustomBlock | lines with the custom-message tones |
CompactionBlock | CompactionBlock | one dim line with the token count before compaction (tokensBefore), when known |
SessionBreak | SessionBreakBlock | one rule line with the new cwd |
NoticeBlock | NoticeBlock | lines in the level’s tone |
NoticeRow | newest live Notice | one line |
StatusRow | sessionState, viewport | one line with a spinner |
QueueList | queue | one line per message |
WidgetArea | widgets for one placement | one line per widget line |
Editor | draft | Pi’s Editor with Pi’s keybindings |
Footer | session, model, thinkingLevel, usage, statuses | one or two lines, then the status entries |
CommandList | filtered commands | the Editor’s autocomplete SelectList |
Dialog | head of dialogs | a drawn border around option rows, an Input, or an Editor per kind |
PiExit | piProcess | lines |
Colors are the tokens of the theme Pi’s theme setting names (Catppuccin Mocha from pi/agent/themes in this repository’s settings), named as Pi names them: text, muted, dim, accent, success, warning, error, toolOutput, thinkingText, userMessageBg, userMessageText, customMessageBg, customMessageText, border, borderMuted, the md* and syntax* tokens, and the thinking* tokens for the editor rules. The theme file is read at startup, because getTheme() is undefined in RPC mode.
Tool line
Section titled “Tool line”<glyph> <name> <summary> <duration>:
- The glyph carries the state:
○pending inmuted, a spinner inaccentwhile running,✓insuccess,✕inerror. - The name is in
toolOutput; the summary isdimand is the first argument that names a target:command,path,pattern,query, orurl, else the first string argument. - The duration shows once the call ends, as
1.2sor3m04s. - A call keeps its own state in a settled block: Pi runs calls after their message ends, so a running call spins in a block that has stopped streaming. Pi core marks a call that can no longer finish as
failed.
Visual rules
Section titled “Visual rules”The design reference draws every MVP scene as a full terminal frame from the shared scene fixtures. The frames are the visual spec; these rules state what they follow. Tones are theme token names. Widths count terminal cells.
Grid and rhythm
Section titled “Grid and rhythm”- Every gap is whole rows and cells. Each row has a one-cell inset at both edges, like Pi’s
outputPad1. - The lane is
min(96, width − 2)columns, left-aligned at the inset. Message text, tool lines, and transcript-view notices wrap inside it. User and custom bands, rules, the draft (inside the insets, as Pi’s editor wraps it), the notice row, status row, queue, widgets, and footer span the full width. - Text wraps at spaces and breaks a word only when it has to. One-row elements never wrap. Tool lines, the notice row, status row, queue lines, widget lines, and footer lines end in
…when cut; command rows are clipped, as pi-tui’s list clips them. A tool line cuts its summary first. - The transcript view holds one blank row above its first block and one below its last. It stays at the top while it fits and follows the end once it overflows.
- One blank row separates blocks. Inside an assistant block, one blank row separates the markdown blocks, the tool lines, and the error tail. Tool lines stack with no blank row between them. A block with nothing to draw, such as a thinking-only message, takes no rows and adds no blank row.
- The bottom regions stack with no blank rows between them.
Transcript view
Section titled “Transcript view”- User: a
userMessageBgband from edge to edge, with one padded row above and below the text. The text isuserMessageText, plain, with no markdown. Attached images add amuted2 imagesrow. - Assistant: markdown as Pi draws it (pi-tui’s
Markdownwith Pi’s markdown tones), intoolOutputwhile it streams and intextonce settled.- A soft line break stays a line break.
- Headings are
mdHeadingbold; levels 1 and 2 draw without#markers and a level-1 heading is also underlined; from level 3 the markers stay. - Bold and italic keep the tone around them: the block’s tone in paragraphs and lists,
mdHeadingin headings,mdQuotein quotes. Inline code ismdCodewithout backticks. - Links are
mdLink, underlined, and open their URL through an OSC 8 hyperlink. - List markers are
-or the item’s number inmdListBullet, with a hanging indent. Quotes aremdQuoteitalic behind anmdQuoteBorderbar; rules aremdHr; tables are boxed. - Fenced code keeps its fence rows,
```langand```, inmdCodeBlockBorder. Its lines are indented two cells, highlighted withsyntax*tones as Pi highlights them, ormdCodeBlockfor a language it does not know; a wrapped code line continues at the lane’s edge.
- Tool line: the format in Tool line, with single spaces between the parts. A duration under a second reads
48ms. Each call draws the state Pi core gives it, so a running call spins in a settled block. - Error tail: a failed or aborted message’s constant text on its own row,
mutedwhen the message was aborted anderrorwhen it failed. - Custom: a
customMessageBgband like the user’s. AcustomTyperow incustomMessageLabelbold comes first, then the text incustomMessageText. - Compaction: one
dimrow,[compaction] Compacted from 12,345 tokensfromtokensBefore, or[compaction] Compactedwhen the count is unknown. - Session break: one row,
── ⟳ new session <cwd> ──…, filled to the right inset. The rule isdim,⟳ new sessionisaccentbold, and the cwd istoolOutput; the cwd truncates first. - Empty session: one
dimrow of key hints in the transcript view’s first row, below its blank top row:enter send · shift+enter new line · / commands · ctrl+l model · ctrl+d quit. It never wraps and ends in…when cut. - Notice:
<glyph> <text>with a hanging indent. Info is•inaccentwithmutedtext. Warning is!with text, both inwarning. Error is✕with text, both inerror.
Bottom regions
Section titled “Bottom regions”- Notice row: the newest live notice, one row, with the notice block’s glyph and tones.
- Status row:
<spinner> <elapsed> · <last tool> · <activity>, shown while the session is busy or the viewport is scrolled away. While idle only its right side draws. The separators aredim.- The spinner is Pi’s Braille loader (80 ms frames) in
accent. The elapsed time isdim:14s,2m05s,1h02m. - The last tool reads
› bash 2swhile it runs, with›inaccent. After it ends, it reads✓ bash 3s agoor✕ bash 3s ago. The name istoolOutputand the timedim. - The activity is
thinkinginthinkingText,compactinginmuted, orretry 2/3inwarningfollowed by the retry message indim. - On the right,
↓ 12 more rowsinmutedshows while the viewport is scrolled away. It reads↓ 12+ more rowswhen blocks below the view are not mounted (see the windowed transcript view in the TUI README), so the count is a lower bound. - When the row does not fit, the last tool drops first. Then the left side truncates.
- The spinner is Pi’s Braille loader (80 ms frames) in
- Queue: one row per message, steering first. The label,
steerorfollow-up, isdimand padded to nine cells. The message’s first line follows inmuted, with…when more lines follow. - Widgets: one
mutedrow per line. - Editor: Pi’s editor. The draft sits between two full-width
─rules in the current level’sthinking*token, with a reverse-video block cursor. It scrolls past 30 % of the terminal’s rows, at least five. No cursor draws while a dialog is open or after the Pi process exits. - Command list: Pi’s autocomplete list below the bottom rule. Up to five rows, built-ins first; with more matches, a
muted(1/10)row follows.- The selected row starts with
→and isaccentthroughout. The other rows start with two spaces; their names take the terminal’s default color and their descriptionsmuted. - Names are padded to a column of the widest match plus two cells, at least 12 and at most 32.
- The selected row starts with
- Footer: the fields
repo/branch · provider/model (effort) · context · cost, with·indim, laid out as tui-footer lays them out.- The repository (
session.repo) ismutedand the branchaccent, joined by adim/. Without a repository the field is the branch alone; without either, it is absent. The provider ismutedand the modeltext, also joined by adim/. - The effort sits in
dimparentheses and keeps tui-footer’s quiet-to-loud ramp:dimforoff,mutedforminimal,thinkingTextforlow,successformedium,warningforhigh, anderrorforxhighandmax. Thethinking*tokens color only the editor rules. The effort is omitted when the model cannot reason. - The context reads
9.2%/200kindim,warningabove 70 %, anderrorabove 90 %. When usage is unknown it reads?/200k. - The cost reads
$0.04inmutedand is omitted until it is positive. - The fields take one line while they fit inside the insets. Otherwise they break at a field boundary into two lines that keep every field. A break that keeps the model with its effort wins, then the one whose wider line is narrower. At 70 columns the frames break after the effort:
my-pi/tui-mvp · personal-sub/claude-haiku-5-5 (low)over9.2%/200k · $0.04. - Only when two lines cannot hold the fields does the footer drop the effort, then the model. The repository, branch, context, and a shown cost never drop for width. When the rest still does not fit two lines, each field takes its own line and is cut with
…when it overflows. - Extension status entries take their own line below the fields, in
muted, joined by·. Whole entries from the left stay while they fit; the rest drop.
- The repository (
Dialog and exit panel
Section titled “Dialog and exit panel”- Dialog: a region between the transcript view and the notice row, beginning with one blank row; the transcript view gives up its rows to it and keeps following its end above it. It never moves the status row, queue, or editor. The box is a rounded
borderAccentbox at the inset,min(width − 2, 76)cells wide, border included. Its border runs through the box’s edge cells, as box-drawing glyphs do.- The top border holds the title,
textbold, from the box’s fourth cell. The countdown,dim, sits at the border’s right end. - The body has one cell of padding. Options start with
›inaccenton the selected option, which is alsoaccent; the other options aretext. Up to eight options show; the list scrolls with the selection. - A confirm dialog shows its message, a blank row, then
YesandNo. An input dialog shows a›row with pi-tui’sInput, its cursor, and adimplaceholder. An editor dialog shows its prefill in Pi’s editor, without its rules. - The bottom border holds the key hints in
dim, such as↑↓ choose · enter select · esc cancel.
- The top border holds the title,
- Pi exit panel (
PiExit): replaces the transcript view, starting one row down.- The first line is
✕ Pi exited with code 1inerrorbold, or✕ Pi was stopped by SIGTERM. - After a blank row, the stderr tail follows in
toolOutputbehind aborderMutedbar, indented two cells. - After another blank row,
ctrl+d quitfollows indim.
- The first line is