docs(tickets): tui-assistant-markdown完了
This commit is contained in:
parent
f6600feab5
commit
e451b07783
1
TODO.md
1
TODO.md
|
|
@ -21,7 +21,6 @@
|
|||
- spawn 失敗時に Pod の stderr が TUI に表示されない → [tickets/tui-spawn-error-surface.md](tickets/tui-spawn-error-surface.md)
|
||||
- 巻き戻されたターンの入力テキストを編集領域に復元 → [tickets/tui-empty-turn-restore.md](tickets/tui-empty-turn-restore.md)
|
||||
- セッションコンテキスト長 / ウィンドウ占有率の常時表示 → [tickets/tui-context-usage-indicator.md](tickets/tui-context-usage-indicator.md)
|
||||
- Assistant 応答の Markdown スタイル表示 → [tickets/tui-assistant-markdown.md](tickets/tui-assistant-markdown.md)
|
||||
- Manifest: Tool Output / File Upload 上限の分離とデフォルト緩和 → [tickets/manifest-output-upload-limits.md](tickets/manifest-output-upload-limits.md)
|
||||
- FileRef / file tools の symlink 診断と外部参照導線 → [tickets/file-ref-symlink-diagnostics.md](tickets/file-ref-symlink-diagnostics.md)
|
||||
- Prune: 保護境界を turn 数から末尾 token budget に置き換え → [tickets/prune-token-budget.md](tickets/prune-token-budget.md)
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
# TUI: Assistant 応答の Markdown スタイル表示
|
||||
|
||||
## 背景
|
||||
|
||||
LLM の出力は実質 Markdown だが、TUI は `Block::AssistantText { text }` を
|
||||
`push_padded_lines` で 1 行ずつ素のテキストとして
|
||||
`Style(MessageKind::Assistant)` に流しているだけで、`**強調**` /
|
||||
`` `code` `` / `# 見出し` / `- list` 等の記号がそのまま見える状態になっている
|
||||
(`crates/tui/src/ui.rs:592-595, 640-648`)。スタイルが付かないため、
|
||||
構造のあるアシスタント応答は読みにくい。
|
||||
|
||||
ratatui 0.30 の `Vec<Line<'static>>` で表現できる範囲のスタイル付けで
|
||||
十分目的を満たせる。既存の `wrap_line_into`(`crates/tui/src/ui.rs:473-`)が
|
||||
span 単位のラップを既に実装しているため、Markdown レンダラは
|
||||
スタイル付きの `Vec<Line>` を返すだけでよく、ラップ/スクロール/overview
|
||||
畳み込みの仕組みを変える必要はない。
|
||||
|
||||
## 方針
|
||||
|
||||
- `pulldown-cmark` を `tui` クレートの依存に追加し、Event ストリームを
|
||||
既存の `MessageKind` / `ratatui::style::Style` 体系へ畳み込む小さな
|
||||
自前レンダラを `crates/tui/src/markdown.rs` に置く。
|
||||
- レンダラの公開面は `render(text: &str, base: Style) -> Vec<Line<'static>>`
|
||||
程度の 1 関数。`Block::AssistantText` の `Mode::Detail` / `Mode::Normal`
|
||||
描画から呼ぶ。`Mode::Overview` は現行通り 1 行畳み込み(Markdown 記号
|
||||
含めて表示しても情報量はほぼ同じなので素のテキストでよい)。
|
||||
- ストリーミング中の不完全要素(未閉鎖の `**` や開きっぱなしのフェンス)
|
||||
は CommonMark の流儀(テキスト扱い/EOF で閉じる)に任せる。挙動が
|
||||
破綻する場合だけ末尾要素を素のテキストにフォールバックする小さな
|
||||
後処理を入れる余地を残す。
|
||||
- `tui-markdown` クレートは採用しない。syntect 依存でビルドが肥大する
|
||||
割にカスタマイズが効かず、本クレートの色味(`MessageKind`
|
||||
パレット)との整合を握りにくいため。
|
||||
|
||||
## 対応する Markdown 要素
|
||||
|
||||
最小限の "対応できる範囲" を以下に限定する。CommonMark + GFM の一部。
|
||||
|
||||
- 強調: `**bold**` / `*italic*` / `~~strike~~`(GFM)
|
||||
- インラインコード: `` `code` ``
|
||||
- フェンスコードブロック: ` ```lang ` / ` ``` `(言語タグは無視、
|
||||
ブロック全体を等幅・低彩度の背景/前景で塗る)
|
||||
- 見出し: H1〜H4(H5/H6 は H4 と同等)
|
||||
- 箇条書きリスト: `-` / `*` / `+`、ネスト可(深さ分インデント)
|
||||
- 順序リスト: `1.` / `1)`、ネスト可(番号は元の値で表示)
|
||||
- 引用: `> ...`(ネスト可)
|
||||
- 水平線: `---` / `***`
|
||||
- リンク: `[text](url)` の `text` をリンク色で着色(URL は表示しない)
|
||||
|
||||
## 範囲外
|
||||
|
||||
- 表(GFM table)
|
||||
- 画像 ``(テキストとしても表示しない)
|
||||
- HTML パススルー(タグはそのまま生テキストで出る)
|
||||
- 数式(`$...$` / `$$...$$`)
|
||||
- コードブロックの syntax highlighting
|
||||
- リンクのターミナルクリック(OSC 8)/URL の自動表示
|
||||
- `Thinking` 本文 / `SystemMessage` への適用
|
||||
(同じ `markdown::render` を後で差せばよい。本チケットは
|
||||
`Block::AssistantText` のみ)
|
||||
- ライブストリーム最中の "途中要素のフォールバック" の作り込み
|
||||
(CommonMark のデフォルト挙動で破綻が見えたら別チケット)
|
||||
|
||||
## 完了条件
|
||||
|
||||
- アシスタント応答に含まれる上記要素が、それぞれ視認可能な
|
||||
スタイルで描画される。
|
||||
- ストリーミング中、テキストが追記されるたびに描画が更新され、
|
||||
フェンスコードブロックの開きが先に着いて中身が後から流れる
|
||||
ようなケースでも、テキスト全体の見た目が大きく崩れない。
|
||||
- `Mode::Detail` / `Mode::Normal` で Markdown スタイルが、
|
||||
`Mode::Overview` では従来通りの 1 行畳み込みが出る。
|
||||
- 既存の `wrap_line_into` によるラップ・右パディング・スクロール
|
||||
が引き続き機能する(行幅計算が乱れない)。
|
||||
|
||||
## 影響範囲
|
||||
|
||||
- `crates/tui/Cargo.toml`: `pulldown-cmark` を追加(`cargo add` 経由)。
|
||||
- `crates/tui/src/markdown.rs`: 新設。`render(&str, Style) -> Vec<Line<'static>>`。
|
||||
- `crates/tui/src/ui.rs`: `Block::AssistantText` 分岐で Markdown
|
||||
レンダラを呼ぶ。`Mode::Overview` は現行のまま。
|
||||
- `crates/tui/src/main.rs` または `lib.rs`: 新モジュールの宣言。
|
||||
|
||||
## Review
|
||||
|
||||
- 状態: Approve
|
||||
- レビュー詳細: [./tui-assistant-markdown.review.md](./tui-assistant-markdown.review.md)
|
||||
- 日付: 2026-05-05
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
# Review: TUI Assistant 応答の Markdown スタイル表示
|
||||
|
||||
## 前提・要件の確認
|
||||
|
||||
### 対応する Markdown 要素 (チケット「対応する Markdown 要素」セクション)
|
||||
- 強調 `**bold**` / `*italic*` / `~~strike~~`: `Renderer::start` の `Tag::Strong/Emphasis/Strikethrough` で深さカウンタを増やし、`span_style` で `Modifier::BOLD/ITALIC/CROSSED_OUT` を付与 (`crates/tui/src/markdown.rs:219-221, 81-89`)。`Options::ENABLE_STRIKETHROUGH` も付いている (`crates/tui/src/markdown.rs:18`)。✓
|
||||
- インラインコード: `Event::Code` で `in_inline_code` を立ててから `push_text` し、`span_style` で yellow on `Rgb(40,40,40)` を返す (`crates/tui/src/markdown.rs:145-149, 70-73`)。✓
|
||||
- フェンスコードブロック: `Tag::CodeBlock` で `in_code_block=true`、`Text` イベント側で `\n` を実際に行分割しつつ等幅 (cyan) で塗る (`crates/tui/src/markdown.rs:131-140, 74-76`)。言語タグは `Tag::CodeBlock(_)` で破棄。✓
|
||||
- 見出し H1〜H6: `Tag::Heading { level, .. }` で `self.heading` を立て、`span_style` で `heading_style` を返す。H5/H6 は H4 と同色 (`crates/tui/src/markdown.rs:175-178, 277-284`)。✓
|
||||
- 箇条書きリスト (`-`/`*`/`+`、ネスト可): `Tag::List(None)` 経由で `list_stack` に積み、`LIST_INDENT` を `line_prefix` に push、`Tag::Item` で `• ` マーカー (`crates/tui/src/markdown.rs:183-211`)。テスト `nested_list_indents` で深さ 2 を確認。✓
|
||||
- 順序リスト (`1.`/`1)`、ネスト可、開始番号尊重): `Tag::List(Some(n))` で `Some(n)` を積み、`Tag::Item` で `n.` マーカーを出して `n += 1`。`pulldown-cmark` 側でも `Start(List(Some(3)))` のように開始番号が来るのを probe で確認したので、`3. a / 4. b` のような表示は意図通りになる。✓
|
||||
- 引用 (`> ...`、ネスト可): `Tag::BlockQuote(_)` で `│ ` を `line_prefix` に push、ネストすると `│ │ ` になる (`crates/tui/src/markdown.rs:212-218, 256-259`)。✓
|
||||
- 水平線 (`---`/`***`): `Event::Rule` で `─` × 40 を DarkGray で出し、前後に blank を試みる (`crates/tui/src/markdown.rs:152-161`)。✓
|
||||
- リンク `[text](url)`: `Tag::Link { .. }` で `in_link` を立て、`span_style` で cyan + underline。URL は表示しない。✓
|
||||
|
||||
### 範囲外項目の取り扱い
|
||||
- 表 (GFM): `Options::ENABLE_TABLES` は付けていないので素通り。テーブル記号がそのまま見える形になるが、ストリーム自体は破綻しない。✓
|
||||
- 画像 ``: `image_depth` カウンタで alt を含めて捨てる (`crates/tui/src/markdown.rs:97-102, 223, 264`)。テスト `image_alt_is_dropped` あり。✓
|
||||
- HTML パススルー: チケットの「範囲外」では「タグはそのまま生テキストで出る」と書かれているが、実装では `Event::Html` / `InlineHtml` をハンドラの `_ => {}` で**完全に捨てている** (`crates/tui/src/markdown.rs:166`)。probe で `<div>hi</div>` 入りの入力に対し `Start(HtmlBlock) / Html / End(HtmlBlock)` 列が出ることを確認したが、これら 3 イベントはすべて未処理 = 表示されない。挙動としては「タグ含めて消える」になっている。チケットの記述とはわずかにズレるが、UX 上は無音で消える方が望ましいケースが多く、blocking にはしない。
|
||||
- 数式 / syntax highlight / OSC 8 / Thinking 適用 / ライブストリーム途中要素フォールバック: 着手なし、チケット通り。✓
|
||||
|
||||
### 完了条件
|
||||
- 「上記要素が視認可能なスタイルで描画される」: 上記の通り全要素にスタイルが付くことをコードと 14 ケースのユニットテストで確認。✓
|
||||
- 「ストリーミング中、フェンスコードブロックの開きが先に着いて中身が後から流れるケースで全体の見た目が大きく崩れない」: probe で `before\n\n```rust\nlet x = 1;` (閉じ忘れ) を流すと `Start(Paragraph)/Text("before")/End(Paragraph)/Start(CodeBlock(Fenced))/Text("let x = 1;")/End(CodeBlock)` が出ることを確認。途中状態でも `End(CodeBlock)` が EOF で必ず付くため `in_code_block` は確実に閉じ、現状コードブロックを描画したまま自然に途切れる。fence-only (`` ```rust ``) は中身ゼロで blank 1 行分の領域だけ取る程度で破綻しない。`unfinished_emphasis_is_treated_as_text` のテストでも `**` 単体を素テキスト扱いできることが pulldown-cmark の出力から保証される。✓
|
||||
- 「`Mode::Detail` / `Mode::Normal` で Markdown スタイル、`Mode::Overview` は従来通り」: `crates/tui/src/ui.rs:592-595` の `match mode` で `Overview` だけ従来の `push_overview_line` を保ち、それ以外を `markdown::render` に流している。✓
|
||||
- 「`wrap_line_into` のラップ・右パディング・スクロールが乱れない」: `markdown::render` は `Line::from(spans)` を返すだけで line-level の `style.bg` を一切セットしない。よって `wrap_line_into` の `fill_to_width = line_style.bg.is_some()` は false のまま、右パディングは発生せず diff-style 行の挙動と干渉しない。char 幅は通常の Span をそのまま並べるだけなので `UnicodeWidthChar` 計算も従来同等。✓
|
||||
|
||||
## アーキテクチャ・スコープ
|
||||
|
||||
- 影響範囲はチケット通り `crates/tui/Cargo.toml` / `crates/tui/src/markdown.rs` (新設) / `crates/tui/src/ui.rs` の 1 行 / `crates/tui/src/main.rs` の `mod markdown;` 1 行のみ。`ui.rs` は 1 行差し替えに収まり (`crates/tui/src/ui.rs:594`)、レンダリングパイプライン (`compute_history` → `wrap_line_into` → スクロール) には触っていない。最小スコープが守られている。
|
||||
- 公開面はチケット指定通り `pub fn render(text: &str, base: Style) -> Vec<Line<'static>>` の 1 関数のみ。`Renderer` 構造体は `pub` でない。過剰抽象化なし。
|
||||
- 依存追加は `pulldown-cmark = { version = "0.13.3", default-features = false }` で、CommonMark コアのみを取り込む形。`tui-markdown` を避け、syntect 等の重量依存を持ち込んでいない (チケット方針通り)。
|
||||
- 新規クレートは作っていないので命名ポリシー (insomnia- プレフィックス禁止) は対象外。
|
||||
- `markdown` モジュールは `crates/tui/src/markdown.rs` の単一ファイルにまとまっており、`#[cfg(test)]` で 14 ケース同居。低レベル基盤クレート (`llm-worker` 等) を汚染していない、TUI レイヤ内に閉じる正しい配置。
|
||||
|
||||
## 指摘事項
|
||||
|
||||
### Non-blocking / Follow-up
|
||||
- HTML 取り扱いがチケット記載 (「タグはそのまま生テキストで出る」) と実装 (完全に破棄) で食い違う。実装側の方が UX 的に望ましいので、チケット側の文面を「HTML はそのまま無視する」に直すか、レビュー記録のままにしておくかは判断に委ねる。`crates/tui/src/markdown.rs:162-166`。
|
||||
- `span_style` 内で inline code / code block / heading が `self.base` を完全に無視している。Assistant の `kind_style` (`fg(White)`) しか base に来ない現状では実害ゼロだが、将来同じ `markdown::render` を `Thinking` (magenta + ITALIC) や `SystemMessage` (cyan) で使い回す際にコードブロックだけ palette から外れる。本チケットは Assistant のみが対象なので非ブロッキング。差すタイミングで「base を起点に code/heading の色相だけ寄せる」関数化を検討すると良い。`crates/tui/src/markdown.rs:70-94`。
|
||||
- 空のリスト項目 (`- a\n-\n- c` のような) は `pending_marker` が `flush_line` で消費される結果、`• ` だけの行が出る。`TagEnd::Item` のコメントは「marker was never consumed」と書いてあるが、現実には `flush_line` (current 空 + pending_marker Some) のガード条件をすり抜けて消費される (`crates/tui/src/markdown.rs:104-116`)。挙動として「空項目は空のバレットを 1 行出す」になっているのは妥当だが、コメントの意図と挙動がやや不一致。pending_marker を消費するか落とすかは別チケットでも構わない範囲。
|
||||
|
||||
### Nits
|
||||
- `RULE_WIDTH` が 40 固定。ターミナル幅に応じた可変化は本チケットの完了条件外なので OK だが、`wrap_line_into` 経由で右側に折り返されない (40 < width 前提) ことだけ将来確認が要る。狭幅環境でも安全側 (はみ出さない) なので問題なし。
|
||||
- `pulldown_cmark::Options::ENABLE_STRIKETHROUGH` のみ有効。GFM のうち autolink / task list は今回対象外なので妥当。
|
||||
|
||||
## 判断
|
||||
|
||||
**Approve** — チケットの「対応する Markdown 要素」「範囲外」「完了条件」「影響範囲」のすべてに、コードとテストの両面で対応している。ストリーミング途中状態の堅牢性は CommonMark + pulldown-cmark 0.13 のセマンティクスに任せる方針が妥当に効いており、`wrap_line_into` との互換性も line-level style を空に保つことで担保できている。HTML 表示の文面ズレは非ブロッキング。
|
||||
Loading…
Reference in New Issue
Block a user