Skip to content

fix(search): 返答が 2 件以上でもすべての返答を強調し、原文から写す端の入力 3 件を直す (#3661) - #3667

Merged
Kewton merged 2 commits into
developfrom
feature/3661-worktree
Oct 13, 2026
Merged

Kewton merged 2 commits into
developfrom
feature/3661-worktree

Conversation

@Kewton

@Kewton Kewton commented Oct 13, 2026

Copy link
Copy Markdown
Owner

返答が 2 件以上のとき

Chat (ChatTranscript) と History (HistoryPane) は返答ごとに applyHistoryHighlights
を呼び、そのたびに CSS.highlights.set が namespace の登録を上書きし、現在の 1 件を
持たない返答の呼び出しが overlay を消していた(Issue の説明どおり、コードで確認)。

選んだ直し方: namespace ごとに範囲を足す(コンテナ → 範囲の Map を namespace ごとに
持ち、登録はその和)。「集めてから 1 回だけ登録」を選ばなかった理由:

  • 両画面とも 1 返答 1 呼び出しの形のまま直せる。既存テスト(HistoryPane.test /
    ChatTranscript-parity-2232)が applyHistoryHighlights の呼び出しを spy しており、
    バッチ API に替えるとそれらの期待値を変えることになる
  • 仮想化で後から mount された行の呼び出しも、そのまま和に入る
    各呼び出しは自分のコンテナの範囲だけを置き換え、文書から外れたコンテナは落とす。
    clearHistoryHighlights(各 effect の cleanup)で全部を忘れる。空のヒットの呼び出しは
    自分の分だけ落とす(残りが無ければ従来どおり clear)。
    overlay は currentIndex >= 0 の呼び出しだけが動かし(別コンテナにあれば作り直す)、
    -1 の呼び出しは自分の中にある overlay だけを消す。ターミナルは 1 コンテナなので従来と同じ。

端の入力

  1. : alignSubsequence は空白を前後の非空白の文字のあいだにある原文の空白に
    だけ置き、無ければ -1(置かない)にした。描画どおりでない文字参照(コード外の
    < など)は 1 文字としてその & に置き、名前の文字(NewLine の e など)を後続の
    置き場にしない。コード内の & は書いたとおり描かれるので従来どおり(陰性対照あり)。
  2. 思考の 2 節が 1 つのコードブロック: rawSpans は原文に無い行(toRaw が両端とも null)
    を隙間として飛ばす。片端だけ null なら従来どおり null。
  3. 置けなかったヒット: 契約の既定どおり「強調しない。n/m の件数と移動は変えない」。
    件数・移動は useHistorySearch 側の matchPositions で決まり、画面の範囲は数えて
    いないので食い違っても移動は壊れない(現在の 1 件が置けなければ overlay が出ない
    だけで、行のスクロールは各画面が message 要素で行う)。mermaid の経路
    (mapRawPositionsToDom)は以前から置けないものを null にしており、それにそろえた。

整合性レビュー(Codex)P2 の対応: 複数単位を描く文字参照

指摘: &fjlig; は fj の 2 文字を描くが、参照全体を f の 1 文字で消費していたため
続く j の照合に失敗し、段落全体の写像が null になって後続の通常語まで強調されなかった
(&fjlig; sentinel sentinel で件数 2 のまま強調も overlay も無し)。&Afr; のような
サロゲートペア(1 文字・2 単位)も同じ形で落ちていた。
直し方: referenceAt は参照が描く文字列を返す。数値参照は micromark と同じ規則で復号
(制御文字・サロゲート・非文字・U+10FFFF 超は U+FFFD)、名前は DOMParser の HTML 復号
(<b>&name;</b> を parse して textContent。名前は正規表現で &name; のみに限る)で
得て、結果をキャッシュする。描いた文字列が本文のその位置にあれば、その UTF-16 の単位
すべてを参照の & に対応させる。空白だけを描く参照は従来どおり飛ばす(空白は保留側で
置く)。復号できない名前(Markdown が知らない名前)は参照とみなさず、書いたとおりの
文字列として照合する。DOM が無い環境ではすべての名前を書いたとおりとみなす。
textarea.innerHTML を使う形は tests/unit/docs/design-doc-identifier-audit が
innerHTML の使用を検出して不合格にしたため、src で既に使っている DOMParser にした。
本文に無い指摘として書いていた「2 文字を描く参照・未知の名前は 1 文字として扱う」は
これで解消した。
確かめたこと: 陽性(直す前のコミットの実装で落ちることを確認)— lib: &fjlig;・&Afr;・
&#x1F600; の後の通常語が置ける、&fjlig; 全体のヒットが描いた 2 文字を覆う。
画面(Chat・History): &fjlig; sentinel sentinel と &Afr; sentinel sentinel で
2 件目が強調され、現在の 1 件の overlay が出る。陰性(直す前後とも通る)— 未知の名前
&unknownname; は書いたとおり描かれ、そのまま置ける。インラインコード内の &fjlig;
は書いたとおりに置かれ、画面でもコード内の &fjlig; が強調される。

既存テストの期待値を 1 か所変えた(要確認)

tests/unit/components/worktree/chat-search-highlight-3554-interleaved.test.tsx の
「ChatLiveTurnBubble places the answer’s hit on the answer’s word」の
expect(ranges).toHaveLength(2) と ranges[0]/[1] の 2 行。ライブの吹き出しは折り畳んだ
思考を描かないので、思考側のヒットは置けず、旧実装は生 offset へのフォールバックで
チップのラベル(i18n キーの "script.t")を光らせていた — 端の入力 3 そのもの。
契約の決定(3 は強調しない)と「3554 のテストが通ること」を両立させるため、
it/describe 名は変えずに「範囲は 1 件で、それは答えの中」に変えた。差し戻すなら
3 の扱いを変える必要がある。

確かめたこと

  • 新規: tests/unit/components/worktree/chat-search-highlight-3661.test.tsx(両画面を親から
    描画: 返答 2 件の全範囲の文字と返答の要素・overlay の位置、3/4 に進めたときの overlay
    の移動、返答 6 件、陰性対照の返答 1 件、端 1・3、Chat の端 2)、
    tests/unit/lib/terminal-highlight-3661.test.ts(複数コンテナの登録と overlay、端 1〜3、
    陰性対照: namespace 分離・ターミナル 1 コンテナ・コード内参照・通常改行・sourceText 無し)
  • 旧実装(HEAD の 2 ファイル)で新規テストの陽性 11+11 件が落ち、陰性は通ることを確認
  • npx vitest run tests/unit/lib/terminal-highlight tests/unit/components/worktree/chat-search
    tests/unit/components/worktree/chat-mermaid tests/unit/components/HistoryPane.test.tsx
    tests/unit/components/worktree/ChatTranscript tests/unit/components/worktree/HistoryPane
    tests/unit/lib/chat tests/unit/hooks/useTerminalSearch tests/unit/guards tests/unit/docs: 全件通過
  • npm run lint / npx tsc --noEmit: 通過

対になる場所を探した結果

  • applyHistoryHighlights の呼び出し元: ChatTranscript・HistoryPane の 2 か所だけ
    (どちらも lib 側の修正で直る)。applyTerminalHighlights は useTerminalSearch の
    1 コンテナのみで挙動不変。スマホ(WorktreeDetailMobile / MobileTerminalTab)・SessionTile も同じ
    コンポーネントを描くだけで、new Highlight は terminal-highlight.ts の 1 か所だけ
  • mapRawPositionsToDom(mermaid)の置けないヒット: 以前から null で、フォールバック無し
  • 型の写し(src/cli/types/): 該当なし
  • 説明: docs/architecture.md(ja/en)の makeHistoryNamespace の節は namespace 分離の
    説明のみで、今回の変更と矛盾しない。applyHistoryHighlights の JSDoc は更新した

本文に無い指摘

  • tests/unit/components/worktree/chat-search-highlight-3554-interleaved.test.tsx:273
    旧期待値が端の入力 3 の誤強調を固定していた(上記のとおり変更)

Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com

Refs #3661

## 返答が 2 件以上のとき
Chat (ChatTranscript) と History (HistoryPane) は返答ごとに applyHistoryHighlights
を呼び、そのたびに CSS.highlights.set が namespace の登録を上書きし、現在の 1 件を
持たない返答の呼び出しが overlay を消していた(Issue の説明どおり、コードで確認)。

選んだ直し方: namespace ごとに範囲を足す(コンテナ → 範囲の Map を namespace ごとに
持ち、登録はその和)。「集めてから 1 回だけ登録」を選ばなかった理由:
- 両画面とも 1 返答 1 呼び出しの形のまま直せる。既存テスト(HistoryPane.test /
  ChatTranscript-parity-2232)が applyHistoryHighlights の呼び出しを spy しており、
  バッチ API に替えるとそれらの期待値を変えることになる
- 仮想化で後から mount された行の呼び出しも、そのまま和に入る
各呼び出しは自分のコンテナの範囲だけを置き換え、文書から外れたコンテナは落とす。
clearHistoryHighlights(各 effect の cleanup)で全部を忘れる。空のヒットの呼び出しは
自分の分だけ落とす(残りが無ければ従来どおり clear)。
overlay は currentIndex >= 0 の呼び出しだけが動かし(別コンテナにあれば作り直す)、
-1 の呼び出しは自分の中にある overlay だけを消す。ターミナルは 1 コンテナなので従来と同じ。

## 端の入力
1. &NewLine;: alignSubsequence は空白を前後の非空白の文字のあいだにある原文の空白に
   だけ置き、無ければ -1(置かない)にした。描画どおりでない文字参照(コード外の
   &lt; など)は 1 文字としてその & に置き、名前の文字(NewLine の e など)を後続の
   置き場にしない。コード内の &amp; は書いたとおり描かれるので従来どおり(陰性対照あり)。
2. 思考の 2 節が 1 つのコードブロック: rawSpans は原文に無い行(toRaw が両端とも null)
   を隙間として飛ばす。片端だけ null なら従来どおり null。
3. 置けなかったヒット: 契約の既定どおり「強調しない。n/m の件数と移動は変えない」。
   件数・移動は useHistorySearch 側の matchPositions で決まり、画面の範囲は数えて
   いないので食い違っても移動は壊れない(現在の 1 件が置けなければ overlay が出ない
   だけで、行のスクロールは各画面が message 要素で行う)。mermaid の経路
   (mapRawPositionsToDom)は以前から置けないものを null にしており、それにそろえた。

## 整合性レビュー(Codex)P2 の対応: 複数単位を描く文字参照
指摘: `&fjlig;` は `fj` の 2 文字を描くが、参照全体を `f` の 1 文字で消費していたため
続く `j` の照合に失敗し、段落全体の写像が null になって後続の通常語まで強調されなかった
(`&fjlig; sentinel sentinel` で件数 2 のまま強調も overlay も無し)。`&Afr;` のような
サロゲートペア(1 文字・2 単位)も同じ形で落ちていた。
直し方: referenceAt は参照が描く文字列を返す。数値参照は micromark と同じ規則で復号
(制御文字・サロゲート・非文字・U+10FFFF 超は U+FFFD)、名前は DOMParser の HTML 復号
(`<b>&name;</b>` を parse して textContent。名前は正規表現で `&name;` のみに限る)で
得て、結果をキャッシュする。描いた文字列が本文のその位置にあれば、その UTF-16 の単位
すべてを参照の `&` に対応させる。空白だけを描く参照は従来どおり飛ばす(空白は保留側で
置く)。復号できない名前(Markdown が知らない名前)は参照とみなさず、書いたとおりの
文字列として照合する。DOM が無い環境ではすべての名前を書いたとおりとみなす。
textarea.innerHTML を使う形は tests/unit/docs/design-doc-identifier-audit が
`innerHTML` の使用を検出して不合格にしたため、src で既に使っている DOMParser にした。
本文に無い指摘として書いていた「2 文字を描く参照・未知の名前は 1 文字として扱う」は
これで解消した。
確かめたこと: 陽性(直す前のコミットの実装で落ちることを確認)— lib: `&fjlig;`・`&Afr;`・
`&#x1F600;` の後の通常語が置ける、`&fjlig;` 全体のヒットが描いた 2 文字を覆う。
画面(Chat・History): `&fjlig; sentinel sentinel` と `&Afr; sentinel sentinel` で
2 件目が強調され、現在の 1 件の overlay が出る。陰性(直す前後とも通る)— 未知の名前
`&unknownname;` は書いたとおり描かれ、そのまま置ける。インラインコード内の `&fjlig;`
は書いたとおりに置かれ、画面でもコード内の `&fjlig;` が強調される。

## 既存テストの期待値を 1 か所変えた(要確認)
tests/unit/components/worktree/chat-search-highlight-3554-interleaved.test.tsx の
「ChatLiveTurnBubble places the answer’s hit on the answer’s word」の
`expect(ranges).toHaveLength(2)` と ranges[0]/[1] の 2 行。ライブの吹き出しは折り畳んだ
思考を描かないので、思考側のヒットは置けず、旧実装は生 offset へのフォールバックで
チップのラベル(i18n キーの "script.t")を光らせていた — 端の入力 3 そのもの。
契約の決定(3 は強調しない)と「3554 のテストが通ること」を両立させるため、
it/describe 名は変えずに「範囲は 1 件で、それは答えの中」に変えた。差し戻すなら
3 の扱いを変える必要がある。

## 確かめたこと
- 新規: tests/unit/components/worktree/chat-search-highlight-3661.test.tsx(両画面を親から
  描画: 返答 2 件の全範囲の文字と返答の要素・overlay の位置、3/4 に進めたときの overlay
  の移動、返答 6 件、陰性対照の返答 1 件、端 1・3、Chat の端 2)、
  tests/unit/lib/terminal-highlight-3661.test.ts(複数コンテナの登録と overlay、端 1〜3、
  陰性対照: namespace 分離・ターミナル 1 コンテナ・コード内参照・通常改行・sourceText 無し)
- 旧実装(HEAD の 2 ファイル)で新規テストの陽性 11+11 件が落ち、陰性は通ることを確認
- npx vitest run tests/unit/lib/terminal-highlight tests/unit/components/worktree/chat-search
  tests/unit/components/worktree/chat-mermaid tests/unit/components/HistoryPane.test.tsx
  tests/unit/components/worktree/ChatTranscript tests/unit/components/worktree/HistoryPane
  tests/unit/lib/chat tests/unit/hooks/useTerminalSearch tests/unit/guards tests/unit/docs: 全件通過
- npm run lint / npx tsc --noEmit: 通過

## 対になる場所を探した結果
- applyHistoryHighlights の呼び出し元: ChatTranscript・HistoryPane の 2 か所だけ
  (どちらも lib 側の修正で直る)。applyTerminalHighlights は useTerminalSearch の
  1 コンテナのみで挙動不変。スマホ(WorktreeDetailMobile / MobileTerminalTab)・SessionTile も同じ
  コンポーネントを描くだけで、`new Highlight` は terminal-highlight.ts の 1 か所だけ
- mapRawPositionsToDom(mermaid)の置けないヒット: 以前から null で、フォールバック無し
- 型の写し(src/cli/types/): 該当なし
- 説明: docs/architecture.md(ja/en)の makeHistoryNamespace の節は namespace 分離の
  説明のみで、今回の変更と矛盾しない。applyHistoryHighlights の JSDoc は更新した

## 本文に無い指摘
- tests/unit/components/worktree/chat-search-highlight-3554-interleaved.test.tsx:273
  旧期待値が端の入力 3 の誤強調を固定していた(上記のとおり変更)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Kewton Kewton added the bug Something isn't working label Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant