TUI インタラクティブモード
InteractiveMode は pi のデフォルト実行モード——フルスクリーン TUI で、ターミナル上に会話、ツール実行、ストリーミング出力、ステータスバー、エディタ、スキルセレクタ、モデルセレクタなど数十個のコンポーネントをレンダリングする。このファイルは 5493 行で、pi-mono 全体で最も長い単一ファイルだ。すべての UI インタラクションロジック(キーバインド、コマンドディスパッチ、イベントレンダリング、拡張 UI 統合、auto-compaction、auto-retry、画像ペースト、ファイルドロップ)を一つのクラスに集約しているためだ。本稿では入口とレンダリングの主干だけを扱い、個別のコマンド処理はそれぞれの handle*Command メソッドに譲る。
責務
- UI 装配:
init()で header / chatContainer / pendingMessagesContainer / statusContainer / editorContainer / footer などのコンテナをTUIに取り付け、editorにフォーカスを置き、ui.start()を起動する。packages/coding-agent/src/modes/interactive/interactive-mode.ts:559-650参照。 - エディタ提出のディスパッチ:
defaultEditor.onSubmitが UI 主入口で、/スラッシュコマンド(/settings、/model、/export、/import、/fork、/new、/compactなど)を識別し、非コマンドテキストはsession.promptに流す。packages/coding-agent/src/modes/interactive/interactive-mode.ts:2441-2625参照。 - AgentSessionEvent レンダリング:
handleEventの switch がagent_start、queue_update、assistant_message、tool_call、tool_result、compaction、errorなど十数種類のイベント型を処理し、UI コンポーネントを同期更新する。packages/coding-agent/src/modes/interactive/interactive-mode.ts:2627-2980参照。 - キーバインド:
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参照。 - bash モード:
!プレフィックスでhandleBashCommandを発火し、実行結果はBashExecutionComponentでレンダリングする。!!プレフィックスはexcludeFromContext: trueを立てる。packages/coding-agent/src/modes/interactive/interactive-mode.ts:5364-5450参照。 - 拡張 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.onSubmit と this.editor.onSubmit の使い分け:defaultEditor は固定インスタンス、this.editor は現在アクティブなエディタ(拡張が注入したカスタムエディタの可能性がある)。setupEditorSubmitHandler は defaultEditor にだけ handler を取り付けるが、handler 内部は this.editor を読んで現在のテキストを取る。こうしてカスタムエディタに切り替えても同じコマンドディスパッチを再利用できる。
主要ファイル
packages/coding-agent/src/modes/interactive/interactive-mode.ts:142-160—isExpandableダックタイピング検査とExpandableText折りたたみテキストコンポーネント。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 InteractiveModeフィールド宣言。streamingComponent、pendingTools、autoCompactionLoader、retryLoader などを含む。packages/coding-agent/src/modes/interactive/interactive-mode.ts:559-650—initメソッド:fd/rg ロード、UI コンテナ取り付け、setupEditorSubmitHandler、ui.start()。packages/coding-agent/src/modes/interactive/interactive-mode.ts:692-730—runメソッド:init + 非同期でバージョン/パッケージ更新/tmux チェック + 起動警告表示。packages/coding-agent/src/modes/interactive/interactive-mode.ts:2441-2540—defaultEditor.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—handleEvent先頭とagent_start、queue_updateブランチ。packages/coding-agent/src/modes/interactive/interactive-mode.ts:3334-3364—handleFollowUp。ストリーミング中は Alt+Enter でstreamingBehavior: "followUp"、非ストリーミング時は通常の onSubmit にフォールバック。
defaultEditor.onSubmit はコマンドディスパッチの中心で、大量の 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;
}
// ... 後続数十個のコマンド分岐 ...Alt+Enter はストリーミング中に followUp をキューに入れ、非ストリーミング時は通常の提出にフォールバックする:
// 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 入力からイベントレンダリングまでの双方向ストリーム:
境界と失敗
- 未初期化状態でイベント受信:
handleEventは冒頭でisInitializedを検査し、未初期化ならawait this.init()を呼んで、UI 装配より先にイベントが届くのを防ぐ。packages/coding-agent/src/modes/interactive/interactive-mode.ts:2628-2630参照。 - 死んだターミナルの検出:
isDeadTerminalErrorはEIO/EPIPE/ENOTCONNエラーコードを検査し、捕获後は再描画を試みず雪崩を防ぐ。packages/coding-agent/src/modes/interactive/interactive-mode.ts:167-175参照。 - Anthropic サブスクリプション auth 警告:
sk-ant-oatプレフィックスの API key を検出したとき一度だけ「サブスクリプション auth の課金方式が異なる」警告を表示する。anthropicSubscriptionWarningShownフラグで重複を防ぐ。packages/coding-agent/src/modes/interactive/interactive-mode.ts:177-182参照。 - auto-retry の後始末:
agent_startイベントで前回 retry の escapeHandler と countdownLoader を片付け、retry 状態が次ラウンドに漏れないようにする。packages/coding-agent/src/modes/interactive/interactive-mode.ts:2641-2653参照。 - カスタムエディタ切替:
this.editorはdefaultEditorから拡張提供のエディタに切り替えられるが、onSubmit/onChangeなどのコールバックはdefaultEditorの実装を指したままになり、動作が一貫する。packages/coding-agent/src/modes/interactive/interactive-mode.ts:2182-2227参照。
まとめ
InteractiveMode は pi の TUI 中心で、5493 行にわたりすべての UI 状態とコマンドディスパッチを集約する。defaultEditor.onSubmit が入口、handleEvent がレンダリングの主干。これを装配する runtime は セッション switch/fork/import、イベントソースの AgentSession は AgentSession オーケストレーション層、非インタラクティブモードは print と rpc モード 参照。