yoi/tickets/tui-thinking-display.md

5.4 KiB
Raw Blame History

Thinking ブロックの TUI 表示

背景

Reasoningextended thinking系の対応は llm-worker レイヤまで降りている:

  • Anthropic の thinking、OpenAI Responses の reasoning_text / reasoning_summary_text、Gemini の thinking はいずれも DeltaContent::Thinking(String) に正規化され、Timeline 層には ThinkingBlockKind の Start / Delta / Stop が流れている
  • history 上は Item::Reasoning { text, summary, encrypted_content, ... } として保持され、session-store にも persist されている

ところが上位層への通り道が無い:

  • Worker の closure 公開 API には on_thinking_block が無い(on_text_block / on_tool_use_block のみ)
  • protocol::Event に Thinking 系イベントが無い
  • pod controller でブリッジしていない
  • TUI に Thinking 用ブロックが無い

結果として、provider が thinking 平文を返していても TUI からは「無音で時間が過ぎる」状態になる。実行中であることが見えず、終わった後に「どれくらい考えていたか」も残らない。

provider ごとに本文を流せるかは異なる:

  • Anthropic: extended thinking は平文で流れる
  • OpenAI Responses: モデル / 設定によって本文(reasoning_text)が流れない場合がある。reasoning_summary は流れることがある
  • Gemini: thinking は流れるが provider 設定依存

「平文があれば流す。無くても thinking 中であることは見せる」というのが基本方針。

方針

  • llm-worker → protocol → pod controller → TUI に Thinking の通り道を作る
  • worker は DeltaContent::Thinking 由来の本文をそのまま渡す。本文を出さない provider のときは Delta が来ないだけで Start / Done は届く
  • TUI は実行中とその後の両方を残す:
    • 実行中: Thinking... (Xs) ヘッダ + 本文があれば直近 1 行のライブ表示
    • 終了後: Thought for Xs として履歴に残す。detail モードでは累積本文を展開
  • token 数表記は当面入れない(UsageEvent に reasoning 分離が無く、別チケットで Usage を拡張するまで保留)

要件

Worker API

  • Worker::on_thinking_block(setup)on_text_block と対称に追加。setup は per-block で 1 回呼ばれ、block.on_delta(|text|) / block.on_stop(|full_text|) を登録できる

Protocol

  • Event::ThinkingStartEvent::ThinkingDelta { text }Event::ThinkingDone { text } を追加(text には完成形を載せる、TextDone と同じ流儀)
  • 本文を返さない provider では Delta が 0 件のまま Start → Done が届く(破綻しない)
  • 1 turn に複数の thinking block が来る可能性があるprovider 都合)。各ブロックは独立して扱う

Pod Controller

  • worker.on_thinking_block で上記 3 イベントに変換して event_tx に流す

TUI

  • 新ブロック種別 Block::Thinking を持つ
  • 実行中は以下のように表示:
    Thinking... (10s)
      <累積本文の末尾を 1 行に切り詰めたもの>
    
  • 終了後は Thought for 12s を残す。detail モードでは本文をそのまま展開して読める
  • overview モードは 1 行(例: Thought for 12s
  • 経過時間表示のため、Thinking ブロックが実行中の間は再描画が定期的に走る必要がある(粒度は 1Hz 程度で十分)
  • ライブ 1 行の選び方は「累積テキストの最後の改行以降を取り、表示幅で切り詰める」を MVP とする
  • 同一 turn 内の複数 thinking block は別ブロックとして表示される

イベント欠落耐性

  • ThinkingDone が来ないまま TurnEnd が来た場合、対応する Block::Thinking は経過時間を凍結した状態で履歴に残す。ToolCall 側の Incomplete と同じ思想

History 再生

  • Event::HistoryItem::Reasoning { text, ... }Block::Thinking { text, finished: true } として復元する(経過時間は持たないので Thought のみで時間表示は省く)

範囲外

  • UsageEvent の reasoning_tokens 分離。Anthropic は output_tokens に thinking を含み、OpenAI Responses は output_tokens_details.reasoning_tokens を別途返すが、現状の UsageEvent ではそれを分離していない。本チケットでは token 数表示そのものを行わない
  • Anthropic Redacted Thinking暗号化 blobの表示。現状 plaintext 経路のみ流れる
  • Thinking 本文の Markdown レンダリング / シンタックスハイライト
  • thinking を context から prune する話(別軸)

完了条件

  • Anthropic で extended thinking を有効にした session で「Thinking... (Xs) + 本文 1 行」が live で見え、終了後に Thought for Xs として履歴に残る
  • 本文を流さない provider 設定でも Thinking... ヘッダが表示され、終了後に Thought for ... が残る
  • detail モードで thinking 本文が全行展開できる
  • 同一 turn に複数 thinking block が来てもそれぞれ独立に表示される
  • Event::History 再生で過去の thinking が Block::Thinking { finished: true } として復元される
  • ThinkingDone 欠落でも panic せず、Incomplete 相当の表示で残る
  • 既存のテキスト / ツール / notification / compact 表示が壊れない