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
lastOutputtext 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.tsxsrc/renderer/store/slices/conversationSlice.tssrc/renderer/components/chat/ChatHistory.tsxsrc/renderer/components/chat/searchHighlightUtils.tssrc/shared/utils/markdownTextSearch.tssrc/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 | aimatchIndexInItem: 0-based index inside one searchable itemglobalIndex: 0-based index across all matchesdisplayItemId: optional (lastOutputfor AI output)
Important distinction:
matchIndexInItemis local to one item.currentSearchIndexis 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)insrc/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(...)insrc/renderer/components/chat/searchHighlightUtils.tshighlightSearchInChildren(...)insrc/renderer/components/chat/searchHighlightUtils.ts
Each rendered highlight mark includes:
data-search-item-iddata-search-match-indexdata-search-result(currentormatch)
3) Canonicalization to rendered DOM (critical)
ChatHistory collects rendered <mark> elements in DOM order and calls:
syncSearchMatchesWithRendered(renderedMatches)insrc/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:
ChatHistorydelays 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
currentSearchIndexwith 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:
findMarkdownSearchMatchescountMarkdownSearchMatchesextractMarkdownPlainText
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:
groupIditemTypematchIndexInItemmatchStartOffset
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:
- Parser and renderer must agree on searchable text boundaries.
matchIndexInItemsemantics must stay stable per item.currentSearchIndexmust represent the global nth visible match.searchResultCountmust reflect actual rendered match count after canonicalization.- 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:
- Ensure it uses search context +
highlightSearchInChildren. - Ensure emitted marks include
data-search-item-idanddata-search-match-index. - Ensure the content is included in
setSearchQuerysource scanning. - Ensure parser collection logic in
src/shared/utils/markdownTextSearch.tsstill mirrors render behavior. - Add/adjust alignment tests.
Debug Playbook
Enable debug logs:
localStorage.setItem('search-debug', '1')
Useful logs:
[search] query/[search] samplefromsetSearchQuery[search] sync-renderedfrom DOM canonicalization[search] next/[search] prevnavigation logs
Quick checks when behavior is off:
- Compare
searchResultCountvs number of rendered marks. - Verify
currentSearchIndexincrements exactly once per click. - Check whether exact mark selector exists for current match.
- Confirm the active tab conversation is the same one used for
setSearchQuery. - Confirm virtualization is disabled during active search.
Tests
Main tests relevant to this logic:
test/shared/utils/markdownTextSearch.test.tstest/shared/utils/markdownSearchRendererAlignment.test.ts
The alignment test ensures parser match indexes and rendered mark indexes stay identical across representative markdown cases.