Skip to content

TUI Interactive Mode

源码版本v0.73.1

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

  1. UI assembly: init() mounts containers — header / chatContainer / pendingMessagesContainer / statusContainer / editorContainer / footer — into TUI, sets focus to editor, and starts ui.start(). See packages/coding-agent/src/modes/interactive/interactive-mode.ts:559-650.
  2. Editor submit dispatch: defaultEditor.onSubmit is the main UI entry — it recognizes / slash commands (/settings, /model, /export, /import, /fork, /new, /compact, etc.) and routes non-command text to session.prompt. See packages/coding-agent/src/modes/interactive/interactive-mode.ts:2441-2625.
  3. AgentSessionEvent rendering: handleEvent is a switch over agent_start, queue_update, assistant_message, tool_call, tool_result, compaction, error, and a dozen-plus other event types, synchronously updating UI components. See packages/coding-agent/src/modes/interactive/interactive-mode.ts:2627-2980.
  4. Key bindings: setupKeyHandlers registers Ctrl+C, Ctrl+D, Ctrl+Z, Alt+Enter (followUp), Ctrl+P (cycle model), Ctrl+T (cycle thinking), etc. See packages/coding-agent/src/modes/interactive/interactive-mode.ts:2352-2417.
  5. Bash mode: the ! prefix triggers handleBashCommand, with results rendered via BashExecutionComponent; the !! prefix sets excludeFromContext: true. See packages/coding-agent/src/modes/interactive/interactive-mode.ts:5364-5450.
  6. Extension UI integration: rebindSession re-calls session.bindExtensions to provide uiContext, so extensions can inject widgets, dialogs, and status. See packages/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

defaultEditor.onSubmit is the core of command dispatch — a long chain of if (text === "/xxx"):

typescript
// 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:

typescript
// 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

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.