agent-ecosystem/.claude/commands/ccc/markdown-search-logic.md

5.8 KiB

name description
ccc:markdown-search Markdown search logic — how in-session and cross-session search works. Use when working on SearchBar, search highlighting, searchHighlightUtils, markdownTextSearch, or SessionSearcher.

Markdown Search Logic

How in-session and cross-session markdown search works end-to-end.

Scope

Current in-session search intentionally covers:

  • User message markdown text
  • AI lastOutput text markdown

Current in-session search intentionally excludes:

  • System items
  • Tool result text blocks
  • Thinking/subagent/internal display items

Primary source files:

  • src/renderer/components/search/SearchBar.tsx
  • src/renderer/store/slices/conversationSlice.ts
  • src/renderer/components/chat/ChatHistory.tsx
  • src/renderer/components/chat/searchHighlightUtils.ts
  • src/shared/utils/markdownTextSearch.ts
  • src/main/services/discovery/SessionSearcher.ts

Core Data Model

SearchMatch (renderer store) in src/renderer/store/types.ts:

  • itemId: chat group id (user-*, ai-*)
  • itemType: user | ai
  • matchIndexInItem: 0-based index inside one searchable item
  • globalIndex: 0-based index across all matches
  • displayItemId: optional (lastOutput for AI output)

Important distinction:

  • matchIndexInItem is local to one item.
  • currentSearchIndex is global position in the search result list.

Pipeline Overview

1) Query input and initial match generation

SearchBar updates the query with tab-scoped conversation data:

  • setSearchQuery(query, conversation) in src/renderer/components/search/SearchBar.tsx

setSearchQuery in src/renderer/store/slices/conversationSlice.ts:

  • Scans conversation items
  • Uses findMarkdownSearchMatches (shared parser logic) per searchable item
  • Builds initial searchMatches, searchResultCount, currentSearchIndex

2) Rendering highlights

Search highlighting is rendered in markdown component trees through:

  • createSearchContext(...) in src/renderer/components/chat/searchHighlightUtils.ts
  • highlightSearchInChildren(...) in src/renderer/components/chat/searchHighlightUtils.ts

Each rendered highlight mark includes:

  • data-search-item-id
  • data-search-match-index
  • data-search-result (current or match)

3) Canonicalization to rendered DOM (critical)

ChatHistory collects rendered <mark> elements in DOM order and calls:

  • syncSearchMatchesWithRendered(renderedMatches) in src/renderer/store/slices/conversationSlice.ts

Why this exists:

  • Real UI navigation must match visible marks exactly.
  • Parser results can temporarily differ during render timing.
  • DOM order is the final source of truth for nth navigation.

Safety guard:

  • ChatHistory delays syncing when a transient empty mark snapshot appears, to avoid wiping results mid-render.

4) Next/prev navigation and scrolling

nextSearchResult / previousSearchResult in src/renderer/store/slices/conversationSlice.ts:

  • Move currentSearchIndex with wrap-around

ChatHistory scroll effect:

  • First tries exact selector:
    • mark[data-search-item-id="..."][data-search-match-index="..."]
  • If missing, falls back to the global nth rendered mark (same currentSearchIndex)
  • Final fallback walks text nodes under [data-search-content] roots

Shared Markdown Search Engine

src/shared/utils/markdownTextSearch.ts is used by both renderer and main process:

  • findMarkdownSearchMatches
  • countMarkdownSearchMatches
  • extractMarkdownPlainText

Design principle:

  • Search parser mirrors markdown render behavior (remark + gfm + HAST traversal)
  • Matching is segment-based (no cross-node match)

Cross-Session Search (Command Palette / IPC)

Main process search path:

  • IPC handler: src/main/ipc/search.ts
  • Engine: src/main/services/discovery/SessionSearcher.ts

SessionSearcher also uses shared markdown search utils, and returns:

  • groupId
  • itemType
  • matchIndexInItem
  • matchStartOffset

These are passed into tab navigation context so opening a search result can jump to the exact in-session match.

Invariants to Keep

When changing markdown/search code, keep these invariants:

  1. Parser and renderer must agree on searchable text boundaries.
  2. matchIndexInItem semantics must stay stable per item.
  3. currentSearchIndex must represent the global nth visible match.
  4. searchResultCount must reflect actual rendered match count after canonicalization.
  5. Search source scope must be explicit (no accidental inclusion of hidden/internal text).

If You Add New Searchable Markdown Surfaces

If you make a new markdown surface searchable:

  1. Ensure it uses search context + highlightSearchInChildren.
  2. Ensure emitted marks include data-search-item-id and data-search-match-index.
  3. Ensure the content is included in setSearchQuery source scanning.
  4. Ensure parser collection logic in src/shared/utils/markdownTextSearch.ts still mirrors render behavior.
  5. Add/adjust alignment tests.

Debug Playbook

Enable debug logs:

  • localStorage.setItem('search-debug', '1')

Useful logs:

  • [search] query / [search] sample from setSearchQuery
  • [search] sync-rendered from DOM canonicalization
  • [search] next / [search] prev navigation logs

Quick checks when behavior is off:

  1. Compare searchResultCount vs number of rendered marks.
  2. Verify currentSearchIndex increments exactly once per click.
  3. Check whether exact mark selector exists for current match.
  4. Confirm the active tab conversation is the same one used for setSearchQuery.
  5. Confirm virtualization is disabled during active search.

Tests

Main tests relevant to this logic:

  • test/shared/utils/markdownTextSearch.test.ts
  • test/shared/utils/markdownSearchRendererAlignment.test.ts

The alignment test ensures parser match indexes and rendered mark indexes stay identical across representative markdown cases.