Skip to content

TUI 互動模式

源码版本v0.73.1

InteractiveMode 是 pi 的預設執行模式——全螢幕 TUI,在終端機裡渲染對話、工具執行、串流輸出、狀態列、編輯器、技能選擇器、模型選擇器等幾十個元件。這個檔案 5493 行,是整個 pi-mono 裡最長的單檔,因為它把所有 UI 互動邏輯(鍵綁定、命令分派、事件渲染、擴充 UI 整合、auto-compaction、auto-retry、圖片貼上、檔案拖放)都集中在一個類別裡。本文只講入口和渲染主幹,細節命令處理在各自 handle*Command 方法裡。

職責

  1. UI 組裝:init() 裡掛 header / chatContainer / pendingMessagesContainer / statusContainer / editorContainer / footer 等容器到 TUI,設焦點到 editor,啟動 ui.start()。見 packages/coding-agent/src/modes/interactive/interactive-mode.ts:559-650
  2. 編輯器提交分派:defaultEditor.onSubmit 是 UI 主入口,識別 / 斜線命令(/settings/model/export/import/fork/new/compact 等),非命令文字走 session.prompt。見 packages/coding-agent/src/modes/interactive/interactive-mode.ts:2441-2625
  3. AgentSessionEvent 渲染:handleEvent switch 處理 agent_startqueue_updateassistant_messagetool_calltool_resultcompactionerror 等十幾種事件類型,同步更新 UI 元件。見 packages/coding-agent/src/modes/interactive/interactive-mode.ts:2627-2980
  4. 鍵綁定:setupKeyHandlers 註冊 Ctrl+C、Ctrl+D、Ctrl+Z、Alt+Enter(followUp)、Ctrl+P(cycle model)、Ctrl+T(cycle thinking)等,見 packages/coding-agent/src/modes/interactive/interactive-mode.ts:2352-2417
  5. bash 模式:! 前綴觸發 handleBashCommand,執行結果透過 BashExecutionComponent 渲染;!! 前綴設 excludeFromContext: true。見 packages/coding-agent/src/modes/interactive/interactive-mode.ts:5364-5450
  6. 擴充 UI 整合:rebindSession 重新呼叫 session.bindExtensions 提供 uiContext,擴充可以注入 widget、dialog、status,見 packages/coding-agent/src/modes/interactive/interactive-mode.ts:1812-1870

設計動機

為什麼 5493 行不拆?因為這個類別是狀態機中心——幾十個欄位(streamingComponent、pendingTools、autoCompactionLoader、retryLoader、retryCountdown、pendingBashComponents、skillCommands)在同一事件流裡互相影響,拆成多個小類後狀態會散落,跨類同步會更難。pi 的選擇是把所有 UI 狀態集中在一個類,透過 handleEvent 這個大 switch 統一處理,程式碼長但狀態流轉清晰。

isExpandable 這個小工具函式(packages/coding-agent/src/modes/interactive/interactive-mode.ts:142-144)是渲染層的鴨子類型檢查:任何有 setExpanded 方法的元件都能被摺疊/展開。這讓 ExpandableText、header、工具輸出共用一套展開邏輯,不需要繼承同一個抽象基底類別。

defaultEditor.onSubmitthis.editor.onSubmit 的區分:defaultEditor 是固定實例,this.editor 是當前啟用的編輯器(可能是擴充注入的自訂編輯器)。setupEditorSubmitHandler 只在 defaultEditor 上掛 handler,但 handler 內部讀 this.editor 拿當前文字,這樣自訂編輯器切換進來也能重用同一套命令分派。

關鍵檔案

defaultEditor.onSubmit 是命令分派的核心,大量 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;
	}
	// ... 後續幾十個命令分支 ...

Alt+Enter 在串流時排隊 followUp,非串流時退化成普通提交:

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);
}

資料流

UI 輸入到事件渲染的雙向流:

邊界與失敗

小結

InteractiveMode 是 pi 的 TUI 中心,5493 行集中管理所有 UI 狀態和命令分派。defaultEditor.onSubmit 是入口,handleEvent 是渲染主幹。組裝它的 runtime 看 會話 switch/fork/import,事件源 AgentSessionAgentSession 編排層,非互動模式看 print 與 rpc 模式