Skip to content

perf: optimize search and reduce unnecessary re-renders#99

Merged
matt1398 merged 2 commits intomainfrom
feat/optimize-app
Mar 9, 2026
Merged

perf: optimize search and reduce unnecessary re-renders#99
matt1398 merged 2 commits intomainfrom
feat/optimize-app

Conversation

@matt1398
Copy link
Copy Markdown
Owner

@matt1398 matt1398 commented Mar 9, 2026

Summary

  • Replace remark markdown parsing with plain indexOf for search — the biggest bottleneck was running full AST parsing (remark → HAST → tree walk) for every message on every keystroke. Now uses the same approach as VSCode.
  • Item-scoped store selectors — only chat items with matches re-render instead of all items, turning re-renders from O(all) to O(matching).
  • Eliminate eager file I/O and add session caching — memoize chat components, cache project list for global search, increase search concurrency and cache sizes.

Test plan

  • Open a large session (200+ messages), Cmd+F, type a common word like "the" — should be instant, no freezing
  • Global search (Cmd+K → Global) across all projects — results should appear within 1-2s
  • Verify search result count and navigation (Enter/Shift+Enter) still work correctly
  • Verify search highlights appear on matching items
  • Run pnpm test — all 652 tests pass

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Search results now capped at 500 matches for improved performance
  • Performance

    • Debounced search input to reduce latency while typing
    • Optimized search matching for faster processing
    • Added caching for project and session data to minimize redundant operations
    • Enhanced component rendering efficiency through memoization

matt1398 and others added 2 commits March 9, 2026 10:55
…onents

- Default to light metadata for session listing (skip full JSONL parsing)
- Skip noise filtering when using light metadata (stat-only session listing)
- Merge hasDisplayableContent into analyzeSessionFileMetadata single pass
- Add stale-while-revalidate session cache for instant project switching
- Wrap 6 key components in React.memo to prevent cascade re-renders
  (LinkedToolItem, ThinkingItem, TextItem, DisplayItemList, ExecutionTrace, SessionItem)

Co-Authored-By: Claude Opus 4.6 <[email protected]>
… results

Search was using remark markdown parsing (AST parse → HAST → tree walk)
for every message on every query, causing multi-second freezes. Replaced
with plain string.indexOf — the same approach VSCode uses.

Key changes:
- Replace findMarkdownSearchMatches with plain indexOf in both renderer
  store and main process SessionSearcher
- Add item-scoped store selectors so only items WITH matches re-render
- Add 300ms debounce on in-session search, 400ms on global search
- Cache project list for 30s to avoid re-scanning disk on every query
- Cap in-session matches at 500 to limit DOM elements
- Increase search caches from 200 to 1000 entries
- Increase search concurrency (16 sessions, 8 projects in parallel)

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the performance and responsiveness of the application's search functionality. By moving away from computationally intensive markdown parsing to a simpler string search, and by intelligently limiting UI re-renders to only affected components, the user experience for both in-session and global search is dramatically improved. Additionally, optimizations in data loading and caching reduce disk I/O and boost search concurrency, leading to near-instantaneous results even in large sessions.

Highlights

  • Optimized Search Algorithm: Replaced expensive remark markdown parsing with a plain indexOf string search for improved performance, especially on large datasets. This aligns with VSCode's approach for search.
  • Reduced Re-renders: Implemented item-scoped store selectors and React.memo for chat components, ensuring only chat items with search matches re-render, changing re-render complexity from O(all) to O(matching).
  • Enhanced Data Caching and Concurrency: Eliminated eager file I/O by introducing session caching for project lists, memoized chat components, and increased search concurrency and cache sizes for faster global search results.
Changelog
  • src/main/services/discovery/ProjectScanner.ts
    • Added a constant for search project cache TTL.
    • Introduced a private searchProjectCache to store project lists, avoiding repeated disk scans.
    • Changed metadataLevel to 'light' for session scanning in certain contexts.
    • Adjusted the shouldFilterNoise logic to depend on metadataLevel.
    • Implemented caching logic for project scanning in searchSessions to reuse project lists.
    • Increased the searchBatchSize for non-SSH file systems to improve search concurrency.
  • src/main/services/discovery/SearchTextCache.ts
    • Increased the default maxSize of the SearchTextCache from 200 to 1000.
  • src/main/services/discovery/SessionSearcher.ts
    • Removed imports related to markdown text search utilities.
    • Increased the searchBatchSize for session file searching when not in fast mode.
    • Added a fast pre-filter to searchSession to skip sessions without raw text matches.
    • Refactored addSearchMatches to use plain indexOf for search instead of markdown-aware parsing, directly using raw text for context snippets.
  • src/main/utils/jsonl.ts
    • Imported SessionContentFilter.
    • Added hasDisplayableContent property to SessionFileMetadata interface.
    • Initialized hasDisplayableContent to false in metadata analysis.
    • Introduced a variable hasDisplayableContent to track if a session contains displayable content.
    • Updated metadata analysis to set hasDisplayableContent based on SessionContentFilter.isDisplayableEntry.
    • Included hasDisplayableContent in the returned SessionFileMetadata.
  • src/renderer/components/chat/DisplayItemList.tsx
    • Memoized the DisplayItemList component using React.memo to prevent unnecessary re-renders.
  • src/renderer/components/chat/LastOutputDisplay.tsx
    • Imported EMPTY_SEARCH_MATCHES constant.
    • Modified the useStore selector to fetch search state only if the current AI group has matches, using EMPTY_SEARCH_MATCHES otherwise to prevent re-renders.
  • src/renderer/components/chat/UserChatGroup.tsx
    • Imported EMPTY_SEARCH_MATCHES constant.
    • Modified the useStore selector to fetch search state only if the current user group has matches, using EMPTY_SEARCH_MATCHES otherwise to prevent re-renders.
  • src/renderer/components/chat/items/ExecutionTrace.tsx
    • Memoized the ExecutionTrace component using React.memo.
  • src/renderer/components/chat/items/LinkedToolItem.tsx
    • Memoized the LinkedToolItem component using React.memo.
  • src/renderer/components/chat/items/TextItem.tsx
    • Memoized the TextItem component using React.memo.
  • src/renderer/components/chat/items/ThinkingItem.tsx
    • Memoized the ThinkingItem component using React.memo.
  • src/renderer/components/chat/searchHighlightUtils.ts
    • Exported EMPTY_SEARCH_MATCHES as a stable empty array for item-scoped search selectors.
  • src/renderer/components/chat/viewers/MarkdownViewer.tsx
    • Imported EMPTY_SEARCH_MATCHES constant.
    • Modified the useStore selector to fetch search state only if the current item has matches, using EMPTY_SEARCH_MATCHES otherwise to prevent re-renders.
  • src/renderer/components/search/CommandPalette.tsx
    • Increased the debounce timeout for command palette search from 200ms to 400ms.
  • src/renderer/components/search/SearchBar.tsx
    • Added SEARCH_DEBOUNCE_MS constant for search input debouncing.
    • Included searchResultsCapped in the useStore selector.
    • Introduced local state localQuery and a debounce mechanism for the search input.
    • Implemented handleChange to update local query and dispatch debounced search actions.
    • Added an useEffect hook for cleanup of the debounce timeout.
    • Updated handleKeyDown to flush pending debounced search on 'Enter' key press.
    • Modified the input element to use localQuery and handleChange.
    • Updated the search result label to indicate if results are capped.
  • src/renderer/components/sidebar/SessionItem.tsx
    • Memoized the SessionItem component using React.memo.
  • src/renderer/store/slices/conversationSlice.ts
    • Removed import for findMarkdownSearchMatches.
    • Defined MAX_SEARCH_MATCHES constant to cap search results.
    • Added searchResultsCapped and searchMatchItemIds to the ConversationSlice interface.
    • Initialized searchResultsCapped and searchMatchItemIds in the initial state.
    • Refactored setSearchQuery to use plain indexOf search, cap results, and populate searchMatchItemIds.
    • Reset searchResultsCapped and searchMatchItemIds when search is hidden.
  • src/renderer/store/slices/projectSlice.ts
    • Defined SessionCacheEntry interface for caching session data.
    • Added _sessionCache map to the ProjectSlice interface.
    • Initialized _sessionCache in the initial state.
    • Modified selectProject to check and use cached session data if available, then trigger a background refresh.
  • src/renderer/store/slices/sessionSlice.ts
    • Removed connectionMode dependency from fetchSessionsInitial and fetchSessionsMore.
    • Set metadataLevel to 'light' for all getSessionsPaginated calls.
    • Implemented caching of fetched sessions in _sessionCache after fetchSessionsInitial.
    • Implemented caching of refreshed sessions in _sessionCache after refreshSessionsInPlace.
    • Removed connectionMode dependency from loadPinnedSessions and getSessionsByIds.
  • src/shared/utils/markdownTextSearch.ts
    • Increased MAX_CACHE_SIZE for the segment cache from 200 to 1000.
    • Added a fast pre-filter to findMarkdownSearchMatches to skip markdown parsing if the query is not found in raw text.
    • Added a fast pre-filter to countMarkdownSearchMatches to skip markdown parsing if the query is not found in raw text.
  • test/main/services/discovery/SessionSearcher.test.ts
    • Updated test description to reflect plain text search in code fences.
    • Adjusted assertion in test to expect a match in AI results for plain text search in code fences.
  • test/renderer/store/sessionSlice.test.ts
    • Updated test assertions to expect metadataLevel: 'light' in getSessionsPaginated calls.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai coderabbitai bot added the feature request New feature or request label Mar 9, 2026
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This is an excellent pull request that delivers significant performance improvements across the entire application. The optimizations are well-thought-out, addressing bottlenecks in search, data fetching, and UI rendering. Replacing expensive markdown parsing with indexOf for search, implementing item-scoped store selectors to prevent unnecessary re-renders, and adding caching at various levels (project list, session list) are all high-impact changes. The code is clean, and the updates to tests to reflect the new behavior are appreciated. I have a couple of minor suggestions to improve consistency in search query handling.

}

debounceRef.current = setTimeout(() => {
setSearchQuery(value, conversation);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To ensure consistent search behavior and avoid issues with leading/trailing whitespace, it's best to trim the query before passing it to the store. This aligns with the global search behavior in SessionSearcher.

Suggested change
setSearchQuery(value, conversation);
setSearchQuery(value.trim(), conversation);

Comment on lines +102 to +103
if (localQuery !== searchQuery) {
setSearchQuery(localQuery, conversation);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similarly to the debounced search, the query should be trimmed here to ensure the search executed on 'Enter' is consistent and handles whitespace correctly. The condition should also check the trimmed query against the current search query.

Suggested change
if (localQuery !== searchQuery) {
setSearchQuery(localQuery, conversation);
if (localQuery.trim() !== searchQuery) {
setSearchQuery(localQuery.trim(), conversation);
}

@matt1398 matt1398 merged commit 929b31e into main Mar 9, 2026
6 of 7 checks passed
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5c7f921ec3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +100 to +103
// Flush any pending debounce immediately on Enter
clearTimeout(debounceRef.current);
if (localQuery !== searchQuery) {
setSearchQuery(localQuery, conversation);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Cancel pending debounce before closing search

The new debounce path only clears pending timers on Enter, so if a user types and immediately closes find (Esc or the close button), the queued setSearchQuery still fires after 300ms and re-populates hidden search state/highlights. This regression comes from introducing delayed dispatch in handleChange without canceling it on close; closing search should clear debounceRef before calling hideSearch to prevent stale queries from being restored.

Useful? React with 👍 / 👎.

includeTotalCount: false,
prefilterAll: false,
metadataLevel: connectionMode === 'ssh' ? 'light' : 'deep',
metadataLevel: 'light',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve deep metadata for local session lists

Hard-coding metadataLevel: 'light' for the main session fetch drops fields that the sidebar depends on (buildLightSessionMetadata returns messageCount: 0 and no contextConsumption), so local users lose accurate message counts and the “Sort by context consumption” mode effectively stops working. This is a behavior regression from the previous local 'deep' fetch path and should keep deep metadata (or a compatible equivalent) where those UI features are used.

Useful? React with 👍 / 👎.

Comment on lines +147 to +149
const cacheProjectId = get().selectedProjectId;
if (cacheProjectId) {
get()._sessionCache.set(cacheProjectId, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Key session cache writes by requested project

After fetchSessionsInitial(projectId) resolves, the cache is written under selectedProjectId instead of the original projectId. If the user switches projects while the request is in flight, this can store project A’s results under project B’s cache key; selectProject then immediately hydrates B from corrupted cache before refresh, showing the wrong session list. Cache writes here should use the function argument (and ideally ignore stale responses).

Useful? React with 👍 / 👎.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 9, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This PR implements search optimization and caching enhancements across discovery, state management, and UI rendering. Changes include introducing TTL-based project scan caching, replacing markdown-based search with plain-text matching, capping search results at 500 matches, adding per-item search tracking to reduce component re-renders, memoizing multiple chat components, implementing session state caching per project, and adjusting metadata levels to 'light' for SSH paths.

Changes

Cohort / File(s) Summary
Project Discovery Caching
src/main/services/discovery/ProjectScanner.ts
Adds TTL-based caching for project scans, switches from rescanning to cached results within TTL window, expands search batch sizing from 4 to 8, forces 'light' metadata for SSH paths, adjusts noise-filter logic for metadata depth consideration.
Search Implementation
src/main/services/discovery/SessionSearcher.ts, src/main/services/discovery/SearchTextCache.ts, src/shared/utils/markdownTextSearch.ts
Replaces markdown parsing with plain text indexOf search, adds fast pre-filter to skip entries without query text, increases cache sizes (1000 from 200), adjusts non-fast search batch size from 8 to 16 for SSH mode, simplifies match indexing and context extraction logic.
Search State & Results
src/renderer/store/slices/conversationSlice.ts
Introduces MAX_SEARCH_MATCHES cap (500), adds searchResultsCapped flag and searchMatchItemIds Set for per-item match tracking, migrates from markdown to plain-text search matching, tracks items containing matches to optimize re-renders.
Search UI Components
src/renderer/components/search/SearchBar.tsx, src/renderer/components/search/CommandPalette.tsx, src/renderer/components/chat/viewers/MarkdownViewer.tsx, src/renderer/components/chat/LastOutputDisplay.tsx, src/renderer/components/chat/UserChatGroup.tsx
Adds debouncing to search input (400ms for CommandPalette, new debounced local state for SearchBar), implements conditional search highlighting based on per-item match presence using EMPTY_SEARCH_MATCHES, adds result count label with capping indicator.
Search Utilities
src/renderer/components/chat/searchHighlightUtils.ts
Exports new EMPTY_SEARCH_MATCHES constant to provide stable empty array reference for re-render optimization.
Component Memoization
src/renderer/components/chat/DisplayItemList.tsx, src/renderer/components/chat/items/ExecutionTrace.tsx, src/renderer/components/chat/items/LinkedToolItem.tsx, src/renderer/components/chat/items/TextItem.tsx, src/renderer/components/chat/items/ThinkingItem.tsx, src/renderer/components/sidebar/SessionItem.tsx
Wraps multiple chat and sidebar components with React.memo to optimize re-render performance, changes from arrow function to named function within memoization wrapper.
Session & Project Caching
src/renderer/store/slices/projectSlice.ts, src/renderer/store/slices/sessionSlice.ts
Introduces SessionCacheEntry type and per-project session cache (_sessionCache Map), restores cached sessions on project selection with background refresh, consolidates metadata level to constant 'light' across pagination and refresh paths.
Metadata & Utils
src/main/utils/jsonl.ts
Adds hasDisplayableContent boolean flag to SessionFileMetadata, propagates flag through analyzeSessionFileMetadata using SessionContentFilter to detect displayable entries.
Test Updates
test/main/services/discovery/SessionSearcher.test.ts, test/renderer/store/sessionSlice.test.ts
Updates test expectations to reflect plain text search behavior (code fence identifiers now match) and light metadata level usage instead of deep.

Possibly related PRs

Suggested labels

feature request

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant