TUI Interactive Mode
InteractiveMode is pi's default run mode — a full-screen TUI that renders conversations, tool execution, streaming output, status bar, editor, skill selector, model selector, and dozens of other components in the terminal. At 5493 lines, this file is the longest single file in all of pi-mono, because it concentrates all UI interaction logic (key bindings, command dispatch, event rendering, extension UI integration, auto-compaction, auto-retry, image paste, file drag-and-drop) into one class. This doc only covers the entry point and rendering backbone; per-command handling lives in the respective handle*Command methods.
Responsibilities
- UI assembly:
init()mounts containers — header / chatContainer / pendingMessagesContainer / statusContainer / editorContainer / footer — intoTUI, sets focus toeditor, and startsui.start(). Seepackages/coding-agent/src/modes/interactive/interactive-mode.ts:559-650. - Editor submit dispatch:
defaultEditor.onSubmitis the main UI entry — it recognizes/slash commands (/settings,/model,/export,/import,/fork,/new,/compact, etc.) and routes non-command text tosession.prompt. Seepackages/coding-agent/src/modes/interactive/interactive-mode.ts:2441-2625. - AgentSessionEvent rendering:
handleEventis a switch overagent_start,queue_update,assistant_message,tool_call,tool_result,compaction,error, and a dozen-plus other event types, synchronously updating UI components. Seepackages/coding-agent/src/modes/interactive/interactive-mode.ts:2627-2980. - Key bindings:
setupKeyHandlersregisters Ctrl+C, Ctrl+D, Ctrl+Z, Alt+Enter (followUp), Ctrl+P (cycle model), Ctrl+T (cycle thinking), etc. Seepackages/coding-agent/src/modes/interactive/interactive-mode.ts:2352-2417. - Bash mode: the
!prefix triggershandleBashCommand, with results rendered viaBashExecutionComponent; the!!prefix setsexcludeFromContext: true. Seepackages/coding-agent/src/modes/interactive/interactive-mode.ts:5364-5450. - Extension UI integration:
rebindSessionre-callssession.bindExtensionsto provideuiContext, so extensions can inject widgets, dialogs, and status. Seepackages/coding-agent/src/modes/interactive/interactive-mode.ts:1812-1870.
Design Motivation
Why not split 5493 lines? This class is the center of the state machine — dozens of fields (streamingComponent, pendingTools, autoCompactionLoader, retryLoader, retryCountdown, pendingBashComponents, skillCommands) interact within the same event flow. Splitting into smaller classes would scatter state and make cross-class synchronization harder. pi's choice is to keep all UI state in one class, unified through the handleEvent switch — long code, but clear state flow.
The isExpandable helper (packages/coding-agent/src/modes/interactive/interactive-mode.ts:142-144) is a duck-type check on the render layer: any component with a setExpanded method can be collapsed/expanded. This lets ExpandableText, headers, and tool output share one expand logic without inheriting from a common abstract base.
The distinction between defaultEditor.onSubmit and this.editor.onSubmit: defaultEditor is the fixed instance, while this.editor is the currently active editor (which may be a custom editor injected by an extension). setupEditorSubmitHandler only attaches the handler to defaultEditor, but the handler reads this.editor to get the current text, so custom editors swapped in still reuse the same command dispatch.
Key Files
packages/coding-agent/src/modes/interactive/interactive-mode.ts:142-160—isExpandableduck-type check and theExpandableTextcollapsible text component.packages/coding-agent/src/modes/interactive/interactive-mode.ts:213-226—InteractiveModeOptions:migratedProviders,modelFallbackMessage,initialMessage,initialImages,initialMessages,verbose.packages/coding-agent/src/modes/interactive/interactive-mode.ts:228-360—class InteractiveModefield declarations, including streamingComponent, pendingTools, autoCompactionLoader, retryLoader.packages/coding-agent/src/modes/interactive/interactive-mode.ts:559-650—initmethod: loads fd/rg, mounts UI containers, setupEditorSubmitHandler,ui.start().packages/coding-agent/src/modes/interactive/interactive-mode.ts:692-730—runmethod: init + async checks for version/package updates/tmux + show startup warnings.packages/coding-agent/src/modes/interactive/interactive-mode.ts:2441-2540— front half ofdefaultEditor.onSubmit:/settings,/model,/export,/import,/share,/copy,/name,/session,/fork,/clone,/tree,/login,/logout,/new,/compact.packages/coding-agent/src/modes/interactive/interactive-mode.ts:2627-2665— start ofhandleEvent, withagent_startandqueue_updatebranches.packages/coding-agent/src/modes/interactive/interactive-mode.ts:3334-3364—handleFollowUp: Alt+Enter during streaming goes throughstreamingBehavior: "followUp"; when not streaming, it falls back to a regular onSubmit.
defaultEditor.onSubmit is the core of command dispatch — a long chain of if (text === "/xxx"):
// packages/coding-agent/src/modes/interactive/interactive-mode.ts:2441-2470
this.defaultEditor.onSubmit = async (text: string) => {
text = text.trim();
if (!text) return;
// Handle commands
if (text === "/settings") {
this.showSettingsSelector();
this.editor.setText("");
return;
}
if (text === "/scoped-models") {
this.editor.setText("");
await this.showModelsSelector();
return;
}
if (text === "/model" || text.startsWith("/model ")) {
const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
this.editor.setText("");
await this.handleModelCommand(searchTerm);
return;
}
// ... dozens more command branches ...Alt+Enter queues a followUp during streaming and falls back to regular submit otherwise:
// packages/coding-agent/src/modes/interactive/interactive-mode.ts:3352-3363
if (this.session.isStreaming) {
this.editor.addToHistory?.(text);
this.editor.setText("");
await this.session.prompt(text, { streamingBehavior: "followUp" });
this.updatePendingMessagesDisplay();
this.ui.requestRender();
}
// If not streaming, Alt+Enter acts like regular Enter (trigger onSubmit)
else if (this.editor.onSubmit) {
this.editor.setText("");
this.editor.onSubmit(text);
}Data Flow
The bidirectional flow from UI input to event rendering:
Edges and Failures
- Events before init: the start of
handleEventchecksisInitialized; if not initialized, itawait this.init()first, to handle events arriving before UI assembly. Seepackages/coding-agent/src/modes/interactive/interactive-mode.ts:2628-2630. - Dead terminal detection:
isDeadTerminalErrorchecksEIO/EPIPE/ENOTCONNerror codes and stops trying to render after catching them, to avoid an avalanche. Seepackages/coding-agent/src/modes/interactive/interactive-mode.ts:167-175. - Anthropic subscription auth warning: when an API key with the
sk-ant-oatprefix is detected, a one-time warning "subscription auth is billed differently" is shown, guarded by theanthropicSubscriptionWarningShownflag to avoid repeats. Seepackages/coding-agent/src/modes/interactive/interactive-mode.ts:177-182. - auto-retry escape handling: the
agent_startevent clears the previous retry's escapeHandler and countdownLoader, so retry state doesn't leak into the next turn. Seepackages/coding-agent/src/modes/interactive/interactive-mode.ts:2641-2653. - Custom editor swap:
this.editorcan swap fromdefaultEditorto an extension-provided editor, but theonSubmit/onChangecallbacks still point todefaultEditor's implementations, keeping behavior consistent. Seepackages/coding-agent/src/modes/interactive/interactive-mode.ts:2182-2227.
Summary
InteractiveMode is the TUI center of pi: 5493 lines centralizing all UI state and command dispatch. defaultEditor.onSubmit is the entry; handleEvent is the rendering backbone. For the runtime that assembles it, see Session switch/fork/import; for the event source AgentSession, see AgentSession Orchestration Layer; for non-interactive modes, see print and rpc Modes.