feat(sessions): Batch 1 — full-prompt search, match snippets, junk fold, highlight fix#132
feat(sessions): Batch 1 — full-prompt search, match snippets, junk fold, highlight fix#132grimmerk wants to merge 5 commits into
Conversation
- Search ALL sessions x ALL user prompts main-side (fixes #131); matches beyond the loaded 100 append with lazy enrichment - Matched-prompt snippet line (prompt #N + context) when the hit isn't visible in the row's first/last lines - Unified high-contrast amber search highlight (9 sites) - Fold minor sessions (<=2 msgs, untitled, no PR, closed) into an expandable row - New pure module session-search.ts + 10 unit tests (45 total) - Plan doc: docs/session-finding-plan-zh-tw.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughSession search now scans all sessions and prompts in the main process, returns contextual match snippets, and merges deep results into the switcher. The UI adds consistent highlighting, minor-session folding, debounced search coordination, lazy enrichment, and updated documentation and release metadata. ChangesSession search experience
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant SwitcherUI
participant ElectronAPI
participant searchClaudeSessions
participant history.jsonl
SwitcherUI->>ElectronAPI: schedule debounced query
ElectronAPI->>searchClaudeSessions: request deep session search
searchClaudeSessions->>history.jsonl: read sessions and prompt text
history.jsonl-->>searchClaudeSessions: session and prompt data
searchClaudeSessions-->>ElectronAPI: matched sessions and snippets
ElectronAPI-->>SwitcherUI: deep results
SwitcherUI->>SwitcherUI: merge, enrich, fold, and highlight results
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
src/electron-api.d.ts (1)
133-139: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTyped payload is correct; consider sharing the shape instead of triple-declaring it.
The
snippetsrecord shape ({ snippet, promptIndex, isLastPrompt }) is duplicated here, asSessionSearchMatchinclaude-session-utility.ts, and inline inswitcher-ui.tsx'ssearchSnippetsstate. A type-only import (erased at compile time, so it's safe across the main/renderer boundary) would give a single source of truth and catch drift at compile time instead of silently at runtime.♻️ Share the type instead of duplicating it
+import type { SessionSearchMatch } from './claude-session-utility'; + searchClaudeSessions: (query: string) => Promise<{ sessions: any[]; - snippets: Record< - string, - { snippet: string; promptIndex: number; isLastPrompt: boolean } - >; + snippets: Record<string, SessionSearchMatch>; }>;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/electron-api.d.ts` around lines 133 - 139, Reuse the existing SessionSearchMatch type from claude-session-utility.ts for the snippets record value in searchClaudeSessions, and update switcher-ui.tsx’s searchSnippets state to use the same shared type. Remove the duplicated inline `{ snippet, promptIndex, isLastPrompt }` declarations while preserving the current payload shape.src/claude-session-utility.ts (1)
215-239: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy liftFull-corpus scan+lowercase runs synchronously on the main process for every search.
searchClaudeSessionsrebuildstarget(projectName + project + all prompts joined) and lowercases it for every session on every call — this is exactly the workload issue#131wants to unlock (searching beyond the ~100 loaded sessions across all accounts/history). Since the function is synchronous and likely invoked viaipcMain.handle, this blocks the Electron main process for the full scan duration on each debounced keystroke. With large multi-account histories (the target use case), this could introduce noticeable UI jank/freezes app-wide (window drag, other IPC, status updates), not just in the switcher.Two independent, low-cost mitigations:
- Cache pre-lowercased prompt text alongside
promptsBySession(built once per cache refresh) instead of re-lowering the same megabytes of text on every keystroke-triggered search.- If profiling on real large histories shows meaningful main-thread blocking, consider offloading the scan to a
worker_threadsworker or chunking it withsetImmediatebetween session batches.Please verify actual latency against realistic account sizes/history volumes before deciding whether (2) is necessary; (1) is cheap and worth doing regardless.
⚡ Minimal mitigation: cache lowercased prompts once per rebuild
-let promptsBySession: Map<string, string[]> = new Map(); +let promptsBySession: Map<string, string[]> = new Map(); +let promptsBySessionLower: Map<string, string[]> = new Map();Populate
promptsBySessionLoweralongsidepromptsBySessioninreadClaudeSessions, then insearchClaudeSessionsbuildtargetfrom the pre-lowered arrays instead of calling.toLowerCase()on the joined string each time.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/claude-session-utility.ts` around lines 215 - 239, Cache lowercased prompt text during the cache rebuild in readClaudeSessions by maintaining a promptsBySessionLower structure alongside promptsBySession. Update searchClaudeSessions to use the pre-lowered prompt array when constructing its search target, avoiding repeated lowercasing of joined prompts on every query; preserve the existing project-name/project matching and result behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/session-finding-plan-zh-tw.md`:
- Line 125: Update the D3 `/pin` row in the Batch 3 table to include a third
cell matching the `項 | 內容 | 備註` header. Preserve the existing content in the
first two cells and add the appropriate notes cell, even if it is empty.
In `@src/claude-session-utility.ts`:
- Line 215: Run Prettier on searchClaudeSessions and the template-literal
assignment around the reported lines so both follow the project’s print-width
and line-break rules. Ensure any multiline arrays or objects in the affected
code use trailing commas.
In `@src/session-search.test.ts`:
- Line 12: Format the affected code in the session-search test using the
project’s Prettier configuration, including wrapping long string literals and
multi-argument calls and adding required trailing commas. Apply the changes to
the test cases around the target declaration and the referenced sections,
without altering their behavior.
In `@src/session-search.ts`:
- Around line 26-57: Format the return object in findPromptMatch so it fits the
configured print width, wrapping the snippet property as needed, and add the
required trailing comma to the object literal. Keep the matching logic
unchanged.
In `@src/switcher-ui.tsx`:
- Around line 411-429: Cap the non-duplicate deep matches appended by
applySearchFilter to MAX_APPENDED_DEEP_MATCHES before merging and sorting,
preserving the existing base results and ordering. In scheduleDeepSearch,
replace the appended-result literal 30 with the same MAX_APPENDED_DEEP_MATCHES
constant so display and enrichment limits remain synchronized.
---
Nitpick comments:
In `@src/claude-session-utility.ts`:
- Around line 215-239: Cache lowercased prompt text during the cache rebuild in
readClaudeSessions by maintaining a promptsBySessionLower structure alongside
promptsBySession. Update searchClaudeSessions to use the pre-lowered prompt
array when constructing its search target, avoiding repeated lowercasing of
joined prompts on every query; preserve the existing project-name/project
matching and result behavior.
In `@src/electron-api.d.ts`:
- Around line 133-139: Reuse the existing SessionSearchMatch type from
claude-session-utility.ts for the snippets record value in searchClaudeSessions,
and update switcher-ui.tsx’s searchSnippets state to use the same shared type.
Remove the duplicated inline `{ snippet, promptIndex, isLastPrompt }`
declarations while preserving the current payload shape.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: f9209980-4fc6-4fe1-bf29-2c30c5200fe7
📒 Files selected for processing (9)
CHANGELOG.mdREADME.mddocs/session-finding-plan-zh-tw.mdpackage.jsonsrc/claude-session-utility.tssrc/electron-api.d.tssrc/session-search.test.tssrc/session-search.tssrc/switcher-ui.tsx
There was a problem hiding this comment.
All reported issues were addressed across 9 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
- Read search value via ref in fetchClaudeSessions (stale closure could clear the filtered list on window focus) [cubic P1] - Gate minor-session folding on first active-detection completion so a just-started session never folds at app start [cubic P2] - Un-cap deep-match enrichment (was 30) to match the uncapped append; both now bounded by the search result cap of 100 [CodeRabbit Major, resolved toward no-silent-caps] - Prettier: new files wholesale; changed lines only in claude-session-utility.ts / switcher-ui.tsx - Docs: plan status header reflects PR-1 in flight; D3 table row third cell [cubic P2 / CodeRabbit Minor] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add docs/session-finding-plan.md (full English translation, same status/decisions/gotchas) - Untrack the zh-TW copy (stays as a local working copy; note added pointing at the English canonical version) - CHANGELOG plan reference updated Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/switcher-ui.tsx (1)
793-797: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
onSessionStatusesUpdatedstill usesfilterSessionsLocally— deep matches vanish during status updates.This PR updated all other search-aware
setSessionscall sites (lines 509, 550, 592) to useapplySearchFilter, but missed this one. When a session status update arrives during an active search,filterSessionsLocallyfilters out deep-matched sessions (matched by prompt text in the main process, not by local enrichment fields). Deep matches disappear and don't reappear until the user types again, undermining the PR's core objective.🔧 Use `applySearchFilter` for consistency
setSessions((prev: any[]) => { const updated = updateSessions(prev); const search = sessionSearchRef2.current; - return search.trim() ? filterSessionsLocally(updated, search) : updated; + return search.trim() ? applySearchFilter(updated, search) : updated; });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/switcher-ui.tsx` around lines 793 - 797, Update the search-aware setSessions callback in onSessionStatusesUpdated to use applySearchFilter instead of filterSessionsLocally, preserving the existing updateSessions flow and current search value handling so deep-matched sessions remain visible during status updates.
🧹 Nitpick comments (1)
src/switcher-ui.tsx (1)
1604-1611: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winFold/expand rows lack keyboard interaction support.
Both the expand row (line 1604) and the collapse row (line 1371) only have
onClickhandlers. Keyboard users cannot toggle minor-session folding. Consider addingtabIndex={0},role="button", and anonKeyDownhandler for Enter/Space.⌨️ Add keyboard support to the expand row
{!isSearchingSessions && !minorsExpanded && minorSessions.length > 0 && ( <div + tabIndex={0} + role="button" + aria-label={`Expand ${minorSessions.length} minor sessions`} onClick={() => setMinorsExpanded(true)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setMinorsExpanded(true); + } + }} style={{ padding: '6px 10px 8px 24px', color: '`#777`', fontSize: '12px', cursor: 'pointer' }} > ▸ {minorSessions.length} minor sessions (≤2 msgs, untitled) </div> )}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/switcher-ui.tsx` around lines 1604 - 1611, Update the minor-session expand row near the isSearchingSessions/minorsExpanded conditional to add keyboard accessibility with tabIndex={0}, role="button", and an onKeyDown handler that triggers setMinorsExpanded(true) for Enter and Space. Apply the same interaction support to the collapse row's existing toggle near the minorsExpanded collapse control, preserving their current click behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/switcher-ui.tsx`:
- Around line 454-456: Apply Prettier formatting to the session-loading code
around the loaded and appended declarations, and the related code at the second
flagged location, so it matches the repository’s formatting rules and passes
prettier/prettier ESLint checks. Preserve the existing behavior and logic.
---
Outside diff comments:
In `@src/switcher-ui.tsx`:
- Around line 793-797: Update the search-aware setSessions callback in
onSessionStatusesUpdated to use applySearchFilter instead of
filterSessionsLocally, preserving the existing updateSessions flow and current
search value handling so deep-matched sessions remain visible during status
updates.
---
Nitpick comments:
In `@src/switcher-ui.tsx`:
- Around line 1604-1611: Update the minor-session expand row near the
isSearchingSessions/minorsExpanded conditional to add keyboard accessibility
with tabIndex={0}, role="button", and an onKeyDown handler that triggers
setMinorsExpanded(true) for Enter and Space. Apply the same interaction support
to the collapse row's existing toggle near the minorsExpanded collapse control,
preserving their current click behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0a3b1b12-3367-43a8-ab3b-1b9af4599271
📒 Files selected for processing (6)
CHANGELOG.mddocs/session-finding-plan.mdsrc/claude-session-utility.tssrc/session-search.test.tssrc/session-search.tssrc/switcher-ui.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- src/session-search.test.ts
- src/session-search.ts
- src/claude-session-utility.ts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pre-existing bug surfaced during Batch 1 testing: - Enrichment spawned ~400 concurrent exec greps (100 sessions x 4); the biggest transcripts regularly blew the silent 3s timeout (errors resolve to ''), so their title/branch vanished at random. Now batched 10 sessions at a time; timeout 3s -> 5s. Same batching applied to loadLastAssistantResponses (100 concurrent tails). - Branch was read from the last 5 transcript lines; an active session's tail is often tool output with no gitBranch field (measured on a live 26MB session: tail -5 hit 0, tail -20 hit 12). Window widened to 50 lines. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/claude-session-utility.ts">
<violation number="1" location="src/claude-session-utility.ts:1743">
P2: The batched enrichment in `loadSessionEnrichment` writes the cache timestamp using the value captured at function entry. With the new `runInBatches(…, 10)` approach, a large session list can take longer than the 5-second `CACHE_TTL_MS` to finish, so the just-populated cache is considered immediately stale and another full scan is triggered. Overlapping concurrent requests also start independent scans because there is no in-flight promise shared between callers.
Keep `titlesCacheTimestamp = Date.now()` instead of reusing the entry-time `now` so the TTL clock starts after work completes, and consider caching the in-flight promise so concurrent callers deduplicate.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| const branches = new Map<string, string>(); | ||
| const prLinks = new Map<string, PRLinkInfo>(); | ||
| const promises = sessions.map(async (session) => { | ||
| await runInBatches(sessions, 10, async (session) => { |
There was a problem hiding this comment.
P2: The batched enrichment in loadSessionEnrichment writes the cache timestamp using the value captured at function entry. With the new runInBatches(…, 10) approach, a large session list can take longer than the 5-second CACHE_TTL_MS to finish, so the just-populated cache is considered immediately stale and another full scan is triggered. Overlapping concurrent requests also start independent scans because there is no in-flight promise shared between callers.
Keep titlesCacheTimestamp = Date.now() instead of reusing the entry-time now so the TTL clock starts after work completes, and consider caching the in-flight promise so concurrent callers deduplicate.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/claude-session-utility.ts, line 1743:
<comment>The batched enrichment in `loadSessionEnrichment` writes the cache timestamp using the value captured at function entry. With the new `runInBatches(…, 10)` approach, a large session list can take longer than the 5-second `CACHE_TTL_MS` to finish, so the just-populated cache is considered immediately stale and another full scan is triggered. Overlapping concurrent requests also start independent scans because there is no in-flight promise shared between callers.
Keep `titlesCacheTimestamp = Date.now()` instead of reusing the entry-time `now` so the TTL clock starts after work completes, and consider caching the in-flight promise so concurrent callers deduplicate.</comment>
<file context>
@@ -1718,15 +1732,15 @@ export const loadSessionEnrichment = async (sessions: ClaudeSession[]): Promise<
const branches = new Map<string, string>();
const prLinks = new Map<string, PRLinkInfo>();
- const promises = sessions.map(async (session) => {
+ await runInBatches(sessions, 10, async (session) => {
const encodedProject = session.project.replace(/[^a-zA-Z0-9-]/g, '-');
const jsonlPath = path.join(
</file context>
Summary
Session-finding Batch 1 — "search & noise" (plan:
docs/session-finding-plan.md, included in this PR). Fixes #131.1. Full search: ALL sessions × ALL user prompts (fixes #131)
Before: the search box only filtered the ~100 loaded sessions' visible fields (
filterSessionsLocally), and the main-sidesearchClaudeSessions(500-pool) was dead code — ~314 of 414 sessions were unfindable.Now, dual-path union:
searchClaudeSessions): scans every session's every prompt (fromhistory.jsonl, all accounts), debounced 180ms, stale-response guarded. Prompt text stays in the main process (promptsBySessionmap rebuilt with the existing 5s session cache) — only small snippets cross IPC.2. Matched-prompt snippet line
When the hit is in a middle prompt (invisible in the row), the row shows
⌕ #N …±40 chars context…so you can see why it matched. Suppressed when it would duplicate the visible first/last lines (respects the first/last/both display-mode setting).3. High-contrast search highlight
All 9
react-highlight-wordssites now share one amber-background/dark-text style (SEARCH_HIGHLIGHT_STYLE). The old per-field translucent styles were near-invisible on colored text.4. Minor-session folding
Closed, untitled, PR-less sessions with ≤2 messages collapse into an expandable
▸ N minor sessionsrow at the bottom (recency order preserved inside). Search always shows everything. Conservative: unknownmessageCountand active sessions never fold. Manual per-session hide arrives with the pins PR (Batch 1 PR-2).Implementation notes
src/session-search.ts(no fs/electron):matchesAllWords/extractSnippet/findPromptMatch/isMinorSession— unit-testable.searchClaudeSessionsreturn shape changed ({sessions, snippets}); it was previously uncalled, so no consumers were affected. IPC handler/preload are passthrough;electron-api.d.tsupdated.history.jsonl— full transcript search is Batch 2 (FTS5, plan §5.2).Tests
npx tsc --noEmitclean.🤖 On behalf of @grimmerk — generated with Claude Code
Summary by cubic
Adds full‑prompt search across all sessions with clearer matches and less noise in the Sessions switcher. You can now find any session by any user prompt and see why it matched; small, one‑shot sessions fold out of the way. Fixes #131.
New Features
react-highlight-wordssites for better readability.Bug Fixes
Written for commit d07c385. Summary will update on new commits.
Summary by CodeRabbit
New Features
Documentation