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

179 lines
5.8 KiB
Markdown

---
name: ccc:markdown-search
description: 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.