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(モデル切替)、Ctrl+T(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 は現在アクティブなエディタ(拡張が注入したカスタムエディタの可能性がある)。setupEditorSubmitHandlerdefaultEditor にだけ 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 モード 参照。