9.1 KiB
9.1 KiB
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 onRgb(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 表示の文面ズレは非ブロッキング。