168 lines
11 KiB
Markdown
168 lines
11 KiB
Markdown
# TUI: フルスクリーン化によるオーバーホール
|
||
|
||
## 背景
|
||
|
||
現在の TUI は **ratatui の inline viewport + `insert_before`** で動いている:
|
||
|
||
- `draw()` が描くのは画面下部の 3 行固定 (separator / status / input)
|
||
- 履歴は `terminal.insert_before()` でその上に押し出し、ターミナル側のスクロールバックに残す
|
||
- 一度 `insert_before` した行はアプリから書き換え不可
|
||
|
||
このモデルが以下の構造的課題を生んでいる:
|
||
|
||
1. **Input UX が貧弱**: Input は 1 行固定、複数行入力不可、ペーストすると長文が 1 行として流れる
|
||
2. **リサイズでセパレーターが増殖**: ターミナル横幅が変わるたびに separator 行が再生成されてスクロールバックに流れ、履歴が汚れる
|
||
3. **読み返し辛い**: TUI アプリ自身にスクロール機能がなく、ターミナル側スクロールバックに完全依存。折りたたみもフィルタリングもできない
|
||
4. **ツール呼び出しが断片化**: `ToolCallStart` / `ToolCallDone` / `ToolResult` がそれぞれ別行として積まれ、1 呼び出しを連続した 1 ブロックとして見られない。`ToolCallArgsDelta` は **破棄されている** (`crates/tui/src/app.rs`)
|
||
|
||
個別対症療法では直しきれないため、**レンダリングモデルを alternate screen buffer + 全描画保持に切り替えるオーバーホール**として扱う。旧 `tui-tool-call-ui.md` は inline viewport 維持を前提にした設計だったため、本チケットに要件を吸収して削除する。
|
||
|
||
## 方針
|
||
|
||
- ratatui を alternate screen buffer で初期化し、inline viewport を捨てる
|
||
- 全履歴を TUI アプリ内の state として保持、毎フレーム再描画
|
||
- ターミナル側のスクロールバックには何も流さない(リサイズ時の汚染と永続履歴依存を同時に解消)
|
||
- 履歴の見せ方をツール呼び出しやターン単位で集約可能にし、スクロール + 折りたたみ 3 段階で密度を変えられるようにする
|
||
- ツール呼び出しは 1 呼び出し = 1 ブロック。ツール名で dispatch する **ツール毎レンダラ** のフレームワークを持つ
|
||
- Input は複数行 / CJK / ペースト プレースホルダに対応
|
||
|
||
## 要件
|
||
|
||
### レンダリングモデル
|
||
|
||
- ratatui を alternate screen buffer で起動する。`insert_before` は使わない
|
||
- アプリ終了時はスクロールバックに戻る(TUI が描いたものは残らない)
|
||
- 再接続時は既存の `Event::History` で state を再構築する
|
||
- ウィンドウリサイズは state を保ったまま再レイアウトする(セパレーター増殖等は発生しない)
|
||
|
||
### レイアウト
|
||
|
||
- 画面全体を以下で構成する:
|
||
- 上部: 履歴ビュー(可変高さ、スクロール可)
|
||
- 下部: ステータス行 (1 行) + Input エリア (可変高さ)
|
||
- Input エリアが画面下部を占有しすぎないように上限を設ける(設計で決めること)
|
||
|
||
### 履歴モデル
|
||
|
||
- 最小単位は **ブロック**。種類: GreetingCard / TurnHeader / UserMessage / AssistantText / ToolCall / Notification / CompactEvent / TurnStats
|
||
- ブロックは **ターン** にグループ化される。TurnHeader / UserMessage 〜 その turn の TurnStats までが 1 ターン
|
||
- 履歴全体が state に保持され、モードに応じて各ブロックの見た目が変わる
|
||
|
||
### スクロール
|
||
|
||
- 行単位 / ページ単位のスクロール
|
||
- ターン単位のジャンプ(前のターン / 次のターン / 先頭 / 末尾)
|
||
- 末尾追従(常に最新を表示)は新規イベント到着でトリガ。ユーザーが手動で上にスクロールしている間は追従を停止
|
||
|
||
### 折りたたみモード
|
||
|
||
3 段階、全体トグルで切替。
|
||
|
||
- **detail**: 全ブロックを完全表示。ツールブロックは引数ストリーミング + 結果全体
|
||
- **normal**: 実行中のツールブロックは detail と同じ。完了後は各ブロックが概ね 5〜6 行に収まるよう圧縮
|
||
- **overview**: 各ブロックが 1 行。ツールブロックは `ToolResult.summary` をそのまま使う
|
||
|
||
### Input エリア
|
||
|
||
- 複数行、自動折り返し
|
||
- CJK 含む Unicode 表示幅に基づく正しいカーソル位置 (`unicode-width`)
|
||
- **ペースト プレースホルダ**: クリップボードから貼り付けられた文字列はプレースホルダ `[Clipboard #N | X chars, Y lines]` として入力バッファに挿入される。実テキストは裏で保持
|
||
- プレースホルダは不可分(Backspace 1 回で全体が削除される。途中カーソルでの文字単位削除は不可)
|
||
- 送信時にプレースホルダが実テキストに展開されて Pod に送られる(Pod 側には `#N` は見せない)
|
||
- 番号付けの規則は設計で決めること
|
||
- 既存キー操作(カーソル移動・削除)は複数行対応に拡張
|
||
|
||
### ツール UI フレームワーク
|
||
|
||
- ツール呼び出し 1 回 = 1 ブロック。`tool_use_id` で同一ブロックに集約
|
||
- 各ブロックは以下の状態を遷移する:
|
||
- **Pending**: `ToolCallStart` 受信、args 未確定
|
||
- **Streaming**: `ToolCallArgsDelta` を連結中。ライブ反映
|
||
- **Executing**: `ToolCallDone` 受信、args 確定、結果待ち
|
||
- **Done / Error**: `ToolResult` 受信
|
||
- **Incomplete**: `ToolResult` が来ないまま turn が終わった場合
|
||
- ブロックの見た目は **ツール毎のレンダラ** が決める。ツール名で dispatch、マッチしなければデフォルトレンダラ
|
||
- 並列ツール呼び出しは複数ブロックが同時に存在する。`tool_use_id` で振り分け
|
||
- Streaming 中の args は生の文字列として連結し、途中の JSON を整形しようとしない(破綻する)
|
||
|
||
### 組み込みツールのレンダラ
|
||
|
||
実装対象: Read / Write / Edit / Glob / Grep / default。
|
||
|
||
- **Read**: 同一 turn 内で連続する Read 呼び出しを **1 ブロックに集約**する。normal / detail とも「読んだファイル数 + ファイルパスのリスト」を表示、中身は出さない。集約中のライブ表示は実行順に最大 3 行のスクロールウィンドウで読んだファイルを下から追加
|
||
- **Write**: summary (Created / Overwrote をラベル色で区別) + 書き込まれた content の先頭 5 行。detail では content 全体
|
||
- **Edit**: TUI 側 **ファイル content キャッシュ** から該当ファイルを引き、`args.old_string` / `args.new_string` の置換箇所に対して **前後 3 行の unified diff** を赤 / 緑で表示
|
||
- **Glob**: `ToolResult.output` の先頭数行をそのまま表示
|
||
- **Grep**: 同上
|
||
- **default (未知ツール)**: normal で pretty JSON 化した args の先頭 3 行 + `ToolResult.output` の先頭 3 行。detail では全体
|
||
|
||
### ファイル content キャッシュ
|
||
|
||
Edit レンダラが diff を出すために TUI 側に持つ。責務は表示用で、ツール実装層の policy (`Tracker`) とは独立。
|
||
|
||
- Read レンダラが `ToolResult.output` からキャッシュに content を保存
|
||
- Write レンダラが `args.content` でキャッシュを更新
|
||
- Edit レンダラが `args.old_string` / `args.new_string` でローカル置換してキャッシュを更新
|
||
- `Event::History` 再生時も同じ順序でキャッシュを再構築する
|
||
|
||
### 既存機能の移植
|
||
|
||
以下は現行 TUI に既にある機能で、新アーキテクチャでも保持する:
|
||
|
||
- Greeting カード表示
|
||
- TurnHeader / UserMessage / AssistantText
|
||
- Notification (Warn / Error レベル)
|
||
- Compact 開始 / 完了 / 失敗
|
||
- ターン終了時の統計 (requests / tokens)
|
||
- Ctrl-C 2-tap でのアプリ終了 (Pod 自体は存続)
|
||
- shutdown_confirm 挙動
|
||
- Paused 状態での空 Enter → Resume
|
||
- `Event::History` によるセッション再接続時の履歴復元
|
||
|
||
### 履歴再生
|
||
|
||
- `Event::History` を受けたとき、過去のツール呼び出しは **最終状態のブロック** として履歴モデルに組み直される
|
||
- tool_call と tool_result の対応付け、および content キャッシュの再構築もこの経路で行う
|
||
|
||
### イベント欠落耐性
|
||
|
||
- プロバイダ起因でイベントが欠落したり順序が逆転しても panic しない
|
||
- `ToolResult` が来ないまま `TurnEnd` が来た場合、該当ブロックは Incomplete として残す(握りつぶさず視覚化する)
|
||
|
||
## 設計で決めること
|
||
|
||
- **キーバインド**: スクロール / ターン移動 / モード切替 のキー割り当て
|
||
- **折りたたみの粒度**: 全体トグルのみか、ターン単位の個別開閉も持たせるか
|
||
- **Input エリアの高さ上限**: 画面の N% か絶対行数か
|
||
- **ペースト番号付け**: TUI 起動中で通しか turn 毎にリセットするか
|
||
- **normal モードの圧縮基準**: 5〜6 行は目安。ブロック種別毎の実装上の判断基準
|
||
- **末尾追従の解除条件**: 上にスクロールした瞬間に解除するか、数行離れたら解除するか
|
||
- **Read 集約の切れ目**: 「連続」の定義(別ツール呼び出しや assistant text が挟まったら切る / `ToolResult` の受信順で切る 等)
|
||
|
||
## 前提
|
||
|
||
- `tickets/protocol-tool-result-shape.md`: `Event::ToolResult` に `summary: String` が追加されていること
|
||
|
||
## 完了条件
|
||
|
||
- TUI が alternate screen buffer で起動し、アプリ終了でスクロールバックに何も残らない
|
||
- ウィンドウリサイズで separator の増殖が発生しない
|
||
- 既存機能 (greeting / turn / user / assistant / tool / notification / compact / stats / ctrl-c / paused / history) がすべて保持されている
|
||
- 3 段モード (detail / normal / overview) で履歴の密度を切り替えられる
|
||
- Input が複数行 / CJK / ペースト プレースホルダに対応している
|
||
- ツール呼び出しが 1 ブロックとして表示され、start → args streaming → done → result の遷移が同じブロックの更新として見える
|
||
- 組み込み 5 ツール (Read / Write / Edit / Glob / Grep) がそれぞれ専用レンダラで表示される
|
||
- 未知ツールがデフォルトレンダラで表示される
|
||
- 並列ツール呼び出しで `tool_use_id` が正しく振り分けられる
|
||
- `ToolResult` が欠落したまま turn が終わった場合に該当ブロックが Incomplete として履歴に残り、panic しない
|
||
- `Event::History` 再生で過去のツール呼び出しがブロックとして復元され、Edit レンダラ用の content キャッシュも再構築される
|
||
|
||
## 範囲外
|
||
|
||
- ツール実行のキャンセル / 介入 UI
|
||
- 複数 Pod の spawn / 切替 UI (`tickets/tui-pod-spawn-ui.md`)
|
||
- Markdown レンダリング / シンタックスハイライト / リッチ diff レンダリング
|
||
- スクロールバック保存(仕様上出さない)
|
||
- TUI 独自の履歴永続化(再接続時は Pod 側の `Event::History` に任せる)
|
||
- 組み込みツールの `ToolOutput.content` 構造化 (protocol 拡張はスコープ外。必要になったら別チケット)
|