Skip to content

fix: prevent renderer memory leak with lazy subagent rendering#168

Open
psypeal wants to merge 1 commit intomatt1398:mainfrom
psypeal:fix/renderer-memory-leak
Open

fix: prevent renderer memory leak with lazy subagent rendering#168
psypeal wants to merge 1 commit intomatt1398:mainfrom
psypeal:fix/renderer-memory-leak

Conversation

@psypeal
Copy link
Copy Markdown
Contributor

@psypeal psypeal commented Apr 5, 2026

Summary

  • Clear expansion Maps/Sets (aiGroupExpansionLevels, expandedStepIds, expandedDisplayItemIds, expandedAIGroupIds) when switching sessions to prevent unbounded growth over long uptime
  • Add periodic pruning for sessionRefreshGeneration and projectRefreshGeneration Maps
  • Add subagentMessageCacheSlice (Zustand) with single-flight Promise dedup and LRU eviction (10 entries) for on-demand subagent message loading
  • Add useSubagentMessages hook that triggers lazy-load when a subagent is expanded
  • Refactor SubagentItem to read displayMeta for collapsed headers (toolCount, modelName, turnCount, isShutdownOnly) instead of iterating full message arrays
  • Update AIChatGroup containsToolUseId / findHighlightedItemId to prefer displayMeta.toolUseIds over message scan
  • Update modelExtractor to prefer displayMeta.modelName over message scan

Memory savings

~50MB per session (50 subagents × 1MB each) → ~50KB in cached SessionDetails. Renderer holds full bodies only for actively-expanded subagents (LRU 10). Across DataCache's 50-entry LRU: ~2.5GB → ~2.5MB.

Test plan

  • pnpm typecheck && pnpm lint:fix — clean
  • pnpm test — all tests pass (including new subagentMessageCacheSlice tests)
  • Open session with subagents — collapsed headers show correct tool count, model, turns
  • Expand a subagent — execution trace loads (loading skeleton shown briefly)
  • Collapse and re-expand — instant (cached in store)
  • Switch sessions repeatedly — expansion state clears, no memory leak over hours

🤖 Generated with Claude Code

@coderabbitai coderabbitai bot added the bug Something isn't working label Apr 5, 2026
Copy link
Copy Markdown
Contributor

@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 pull request implements a custom session refresh mechanism for Electron, adds a specialized Bash tool viewer with syntax highlighting and collapsible output, and enhances Read/Write tools with Markdown previews. It also significantly expands syntax highlighting support and introduces state management optimizations to clear UI expansion states. Feedback identifies a critical race condition in the new pruning logic that could cause in-flight requests to be ignored and suggests refactoring the syntax highlighter for improved maintainability.

Comment on lines +513 to +521
if (sessionRefreshGeneration.size > 100) {
const entries = Array.from(sessionRefreshGeneration.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 50);
sessionRefreshGeneration.clear();
for (const [key, value] of entries) {
sessionRefreshGeneration.set(key, value);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

The pruning logic for sessionRefreshGeneration has a correctness issue and a logic flaw:

  1. Race Condition / Correctness Bug: If a pruning event occurs (triggered by any session refresh) while a specific session refresh is in flight (awaiting the API call at line 524), the refreshKey for that in-flight request might be evicted from the Map. When the API call returns, the check at line 527 (sessionRefreshGeneration.get(refreshKey) !== generation) will fail because the key is missing (returning undefined), causing the legitimate update to be silently dropped.
  2. Recency vs. Frequency: Sorting by the generation counter (b[1] - a[1]) keeps the most frequently refreshed sessions, not the most recently refreshed ones as stated in the PR description. A session refreshed 100 times yesterday will be prioritized over a session refreshed for the first time just now.

Given that these Maps only store a number per session ID, the memory overhead is negligible (a few hundred KB for 10,000 sessions). It is safer to remove this pruning entirely. If pruning is required, ensure that keys currently in sessionRefreshInFlight are never evicted and use a proper LRU approach (e.g., by re-inserting keys to leverage JS Map's insertion order).

Comment on lines +261 to +269
if (projectRefreshGeneration.size > 100) {
const entries = Array.from(projectRefreshGeneration.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 50);
projectRefreshGeneration.clear();
for (const [key, value] of entries) {
projectRefreshGeneration.set(key, value);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

This pruning logic suffers from the same issues identified in sessionDetailSlice.ts. It can cause in-flight project refreshes to be ignored if their key is evicted during the API call, and it incorrectly uses frequency (counter value) as a proxy for recency.

Comment on lines 925 to 928
if (
(language === 'python' || language === 'bash' || language === 'r' || language === 'ruby' || language === 'php') &&
(language === 'python' || language === 'bash' || language === 'zsh' || language === 'fish' ||
language === 'r' || language === 'ruby' || language === 'php' || language === 'yaml') &&
remaining.startsWith('#')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The list of languages using # for comments is growing. To improve maintainability and readability, consider defining a Set of languages for each comment style at the top of the file (e.g., const HASH_COMMENT_LANGUAGES = new Set(['python', 'bash', ...])) and checking against that set here.

References
  1. Adhering to general clean code principles for maintainability by avoiding long conditional chains when a Set lookup is more appropriate. (link)

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

📝 Walkthrough

Walkthrough

This PR introduces lazy loading for subagent messages with client-side caching, adds a new IPC channel for message retrieval, precomputes subagent display metadata to avoid runtime message scanning, and implements memory management improvements (generational tracking pruning, LRU cache eviction) across store slices.

Changes

Cohort / File(s) Summary
IPC/API layer
src/preload/constants/ipcChannels.ts, src/preload/index.ts, src/shared/types/api.ts
Added SUBAGENT_GET_MESSAGES IPC channel constant and exposed getSubagentMessages(projectId, sessionId, subagentId) method via preload bridge with Promise<ParsedMessage[]> return type. Also extended IpcFileChangePayload with optional fileSize field.
HTTP client
src/renderer/api/httpClient.ts
Implemented getSubagentMessages method performing HTTP GET request to /api/projects/{projectId}/sessions/{sessionId}/subagents/{subagentId}/messages endpoint returning Promise<ParsedMessage[]>.
Subagent message caching slice
src/renderer/store/slices/subagentMessageCacheSlice.ts
New store slice managing lazy-loaded subagent message cache with LRU eviction (max 10 entries), in-flight deduplication, per-subagent loading and error tracking, and invalidation helpers for session/global cache clearing.
Store state management
src/renderer/store/index.ts, src/renderer/store/types.ts, src/renderer/store/slices/sessionSlice.ts, src/renderer/store/slices/sessionDetailSlice.ts
Integrated subagent message cache slice into store; reworked session refresh scheduling to use file size delta instead of group counts, added renderer heap memory gating, enforced per-session cooldowns, and implemented generational pruning (keep top 50 of 100+ entries) in both sessionSlice and sessionDetailSlice to prevent unbounded growth of tracking maps.
State reset utilities
src/renderer/store/utils/stateResetHelpers.ts, src/renderer/store/slices/tabSlice.ts
Extracted getConversationExpansionResetState() helper clearing expansion-related state (expansion levels, expanded IDs, active detail item); applied in fetchSessionDetail, setActiveTab, and updated getFullResetState to include this reset.
Subagent message lazy loading hook
src/renderer/hooks/useSubagentMessages.ts
New React hook for lazy-loading subagent message bodies on-demand; caches results, deduplicates concurrent requests, and provides loading/error states for UI components.
Component and utility refactoring
src/renderer/components/chat/AIChatGroup.tsx, src/renderer/components/chat/items/SubagentItem.tsx, src/renderer/utils/modelExtractor.ts
Updated components to use precomputed displayMeta instead of scanning messages; removed slash-info extraction from AIChatGroup; added lazy message loading to SubagentItem via useSubagentMessages; updated tool-ID and model detection to prefer displayMeta with legacy message-scan fallback.
Type definitions
src/main/types/chunks.ts
Extended Process with optional displayMeta?: SubagentDisplayMeta (tool counts/IDs, model name, turn count, phase breakdown); added _nativePipeline tracking to SessionDetail; added optional fileSize to FileChangeEvent.
Test infrastructure
test/mocks/electronAPI.ts, test/renderer/store/storeTestUtils.ts, test/renderer/store/subagentMessageCacheSlice.test.ts
Added mock getSubagentMessages to test API; integrated subagent cache slice into test store; added comprehensive test suite validating cache hit/miss, single-flight deduplication, LRU eviction, error handling, and state clearing.

Possibly related PRs


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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (5)
src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx (1)

31-57: Consider extracting shared markdown toggle component.

The toggle button UI (lines 31-57) is nearly identical to ReadToolViewer.tsx (lines 62-88). Consider extracting a shared MarkdownToggle component to reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx` around
lines 31 - 57, Extract the duplicated markdown toggle UI into a shared component
(e.g., MarkdownToggle) and use it from both WriteToolViewer and ReadToolViewer:
identify the repeated JSX block that reads/writes viewMode and setViewMode in
WriteToolViewer.tsx and the similar block in ReadToolViewer.tsx, create a new
MarkdownToggle component that accepts props {viewMode, setViewMode} (or onChange
and value) and encapsulates the two buttons and their styling/behavior, then
replace the in-file JSX in both WriteToolViewer and ReadToolViewer with
<MarkdownToggle .../> calls passing the existing viewMode and setViewMode
handlers.
src/renderer/components/chat/items/linkedTool/BashToolViewer.tsx (2)

10-17: Keep the @renderer import block above the relative imports.

This new file currently breaks the repo ordering rule by placing @renderer/types/groups after ../ and ./ imports.

As per coding guidelines, "Organize imports in order: external packages, path aliases (@main, @renderer, @shared, @preload), then relative imports".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/items/linkedTool/BashToolViewer.tsx` around
lines 10 - 17, The import ordering in BashToolViewer.tsx is incorrect: move all
`@renderer` path-alias imports (e.g., the LinkedToolItem import from
'@renderer/types/groups' and CodeBlockViewer from
'@renderer/components/chat/viewers') above the relative imports (../BaseItem,
./CollapsibleOutputSection, ./renderHelpers) so the file follows the rule
"external packages, path aliases (`@renderer`, etc.), then relative imports";
update the import block so CodeBlockViewer and LinkedToolItem appear before
ItemStatus, CollapsibleOutputSection, and renderOutput.

24-26: Implement a type guard for Bash tool input to validate string types before casting.

The upstream guard hasBashContent() only checks truthiness of input.command, not whether it's actually a string. A malformed payload with command: 123 or command: {} would pass the guard and then be unsafely cast in BashToolViewer. Following the isXxx naming convention from the coding guidelines, add an isBashToolInput() guard in toolContentChecks.ts that validates both command and description are strings or undefined, then use it in BashToolViewer instead of raw as string casts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/items/linkedTool/BashToolViewer.tsx` around
lines 24 - 26, The BashToolViewer currently casts
linkedTool.input.command/description unsafely (using as string) and relies on
hasBashContent(), which only checks truthiness; add an isBashToolInput() type
guard in toolContentChecks.ts that returns true only when input.command is a
string and input.description is either a string or undefined, then update
BashToolViewer to call isBashToolInput(linkedTool.input) before reading values
and avoid direct casts (handle the invalid case by returning null or a safe
fallback UI). Ensure names match the isXxx convention and reference
isBashToolInput, hasBashContent, BashToolViewer, and
linkedTool.input.command/description when making changes.
src/main/index.ts (1)

487-495: Consider mirroring SESSION_REFRESH in the local constants block.

This file already duplicates preload channel names locally to avoid a main→preload import. Inlining 'session:refresh' at Line 494 makes that one channel easier to drift during a future rename.

🧹 Small cleanup
 const HTTP_SERVER_GET_STATUS = 'httpServer:getStatus';
+const SESSION_REFRESH = 'session:refresh';
 ...
-      mainWindow.webContents.send('session:refresh');
+      mainWindow.webContents.send(SESSION_REFRESH);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/index.ts` around lines 487 - 495, Replace the inlined channel string
'session:refresh' with a local constant declared alongside the other preload
channel names and use that constant in mainWindow.webContents.send;
specifically, add a constant (e.g., SESSION_REFRESH) in the local constants
block where other channel names are duplicated and change the send call from
mainWindow.webContents.send('session:refresh') to
mainWindow.webContents.send(SESSION_REFRESH) so the channel name mirrors the
existing constants and avoids drift on renames.
src/renderer/store/slices/sessionDetailSlice.ts (1)

407-423: Consider using getConversationExpansionResetState() for consistency.

The inline reset of expansion Maps/Sets duplicates the logic in getConversationExpansionResetState(). If the helper is updated (e.g., new expansion structures added), this inline version could drift out of sync.

♻️ Proposed refactor to use the helper
+import { getConversationExpansionResetState } from '../utils/stateResetHelpers';
+
 // In fetchSessionDetail, around line 407:
-      // Clear expansion Maps/Sets from previous session to prevent unbounded growth over long uptime
-      set({
-        aiGroupExpansionLevels: new Map(),
-        expandedStepIds: new Set(),
-        expandedDisplayItemIds: new Map(),
-        expandedAIGroupIds: new Set(),
-        activeDetailItem: null,
+      set({
+        ...getConversationExpansionResetState(),
         sessionDetail: detail,
         sessionDetailLoading: false,
         // ... rest of the state
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/store/slices/sessionDetailSlice.ts` around lines 407 - 423, The
inline reset of expansion Maps/Sets duplicates logic in
getConversationExpansionResetState(); replace the explicit entries
(aiGroupExpansionLevels, expandedStepIds, expandedDisplayItemIds,
expandedAIGroupIds, activeDetailItem) inside the set({...}) call by calling
getConversationExpansionResetState() and spreading its return into the object so
the expansion reset remains centralized and future changes to expansion
structures only need to be made in getConversationExpansionResetState(); ensure
you still assign sessionDetail, sessionDetailLoading, conversation,
conversationLoading, visibleAIGroupId, selectedAIGroup, sessionClaudeMdStats,
sessionContextStats, and sessionPhaseInfo as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/components/chat/ChatHistory.tsx`:
- Around line 380-387: The global 'session-refresh-scroll-bottom' listener in
ChatHistory subscribes across all mounted tabs and must be scoped per-tab:
change the useEffect handler in ChatHistory to accept the CustomEvent, read
event.detail.tabId, and only call scrollToBottom('smooth') when that tabId
matches this ChatHistory's tab identifier (e.g., the prop/state holding this
tab's id); also update refresh dispatch sites to send the starting tab id via
window.dispatchEvent(new CustomEvent('session-refresh-scroll-bottom', { detail:
{ tabId: activeTabId } })). Ensure references to scrollToBottom, ChatHistory,
window.addEventListener, and CustomEvent.detail are used so the handler ignores
events for other tabs.

In `@src/renderer/components/chat/items/LinkedToolItem.tsx`:
- Around line 149-150: LinkedToolItem.tsx is passing variant={toolVariant} to
BaseItem but BaseItemProps (in BaseItem.tsx) doesn't declare a variant prop; add
a variant field to the BaseItemProps interface (e.g., variant?: StepVariant) and
update BaseItem component's props type to accept and forward that prop (use the
existing StepVariant type or import it where BaseItem is declared) so BaseItem
can use variant for styling; ensure any usages of BaseItem (including
LinkedToolItem) still type-check and adjust default behavior in BaseItem if
variant is optional.

In `@src/renderer/components/chat/viewers/syntaxHighlighter.ts`:
- Around line 924-929: The current comment detection in the if block that checks
(language === ... ) && remaining.startsWith('#') is too naive; update the check
around the variables language and remaining (the comment-start branch in
syntaxHighlighter.ts) to only treat '#' as a comment when it is at the start of
the token or is preceded by a boundary (e.g., start-of-string or whitespace or
an operator/punctuation like = : , ; ( [ { | & >) and not when embedded in
words/variable expansions or URLs; implement this by replacing
remaining.startsWith('#') with a context-aware check (e.g., a regex or explicit
lookbehind/peek at the preceding character) that excludes cases like `${`#var`}`,
`${var#prefix}` and `http://...#frag` while still matching real comments in
shell/YAML.

In `@src/renderer/store/slices/sessionSlice.ts`:
- Around line 260-269: The pruning logic using projectRefreshGeneration and
per-project generation can evict a just-started refresh because generation is
not a global recency metric; modify the pruning to preserve recency correctly by
either (A) tracking last-touch order separately (e.g., maintain a lastTouched
map or timestamp per key and sort by that when pruning) or (B) explicitly never
prune the current/in-flight key (detect the in-flight project id and always keep
it) instead of sorting by generation; apply the same change to the identical
helper in src/renderer/store/slices/sessionDetailSlice.ts so both slices use
last-touch timestamps or honor the in-flight key when pruning.

---

Nitpick comments:
In `@src/main/index.ts`:
- Around line 487-495: Replace the inlined channel string 'session:refresh' with
a local constant declared alongside the other preload channel names and use that
constant in mainWindow.webContents.send; specifically, add a constant (e.g.,
SESSION_REFRESH) in the local constants block where other channel names are
duplicated and change the send call from
mainWindow.webContents.send('session:refresh') to
mainWindow.webContents.send(SESSION_REFRESH) so the channel name mirrors the
existing constants and avoids drift on renames.

In `@src/renderer/components/chat/items/linkedTool/BashToolViewer.tsx`:
- Around line 10-17: The import ordering in BashToolViewer.tsx is incorrect:
move all `@renderer` path-alias imports (e.g., the LinkedToolItem import from
'@renderer/types/groups' and CodeBlockViewer from
'@renderer/components/chat/viewers') above the relative imports (../BaseItem,
./CollapsibleOutputSection, ./renderHelpers) so the file follows the rule
"external packages, path aliases (`@renderer`, etc.), then relative imports";
update the import block so CodeBlockViewer and LinkedToolItem appear before
ItemStatus, CollapsibleOutputSection, and renderOutput.
- Around line 24-26: The BashToolViewer currently casts
linkedTool.input.command/description unsafely (using as string) and relies on
hasBashContent(), which only checks truthiness; add an isBashToolInput() type
guard in toolContentChecks.ts that returns true only when input.command is a
string and input.description is either a string or undefined, then update
BashToolViewer to call isBashToolInput(linkedTool.input) before reading values
and avoid direct casts (handle the invalid case by returning null or a safe
fallback UI). Ensure names match the isXxx convention and reference
isBashToolInput, hasBashContent, BashToolViewer, and
linkedTool.input.command/description when making changes.

In `@src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx`:
- Around line 31-57: Extract the duplicated markdown toggle UI into a shared
component (e.g., MarkdownToggle) and use it from both WriteToolViewer and
ReadToolViewer: identify the repeated JSX block that reads/writes viewMode and
setViewMode in WriteToolViewer.tsx and the similar block in ReadToolViewer.tsx,
create a new MarkdownToggle component that accepts props {viewMode, setViewMode}
(or onChange and value) and encapsulates the two buttons and their
styling/behavior, then replace the in-file JSX in both WriteToolViewer and
ReadToolViewer with <MarkdownToggle .../> calls passing the existing viewMode
and setViewMode handlers.

In `@src/renderer/store/slices/sessionDetailSlice.ts`:
- Around line 407-423: The inline reset of expansion Maps/Sets duplicates logic
in getConversationExpansionResetState(); replace the explicit entries
(aiGroupExpansionLevels, expandedStepIds, expandedDisplayItemIds,
expandedAIGroupIds, activeDetailItem) inside the set({...}) call by calling
getConversationExpansionResetState() and spreading its return into the object so
the expansion reset remains centralized and future changes to expansion
structures only need to be made in getConversationExpansionResetState(); ensure
you still assign sessionDetail, sessionDetailLoading, conversation,
conversationLoading, visibleAIGroupId, selectedAIGroup, sessionClaudeMdStats,
sessionContextStats, and sessionPhaseInfo as before.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c00556c9-a8c2-46f4-8f34-0af83d41001e

📥 Commits

Reviewing files that changed from the base of the PR and between c0708bb and 106665c.

📒 Files selected for processing (23)
  • src/main/index.ts
  • src/preload/constants/ipcChannels.ts
  • src/preload/index.ts
  • src/renderer/api/httpClient.ts
  • src/renderer/components/chat/ChatHistory.tsx
  • src/renderer/components/chat/items/LinkedToolItem.tsx
  • src/renderer/components/chat/items/linkedTool/BashToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/CollapsibleOutputSection.tsx
  • src/renderer/components/chat/items/linkedTool/DefaultToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/ReadToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/WriteToolViewer.tsx
  • src/renderer/components/chat/items/linkedTool/index.ts
  • src/renderer/components/chat/viewers/syntaxHighlighter.ts
  • src/renderer/components/layout/TabBar.tsx
  • src/renderer/hooks/useKeyboardShortcuts.ts
  • src/renderer/store/index.ts
  • src/renderer/store/slices/sessionDetailSlice.ts
  • src/renderer/store/slices/sessionSlice.ts
  • src/renderer/store/slices/tabSlice.ts
  • src/renderer/store/utils/stateResetHelpers.ts
  • src/renderer/utils/toolRendering/index.ts
  • src/renderer/utils/toolRendering/toolContentChecks.ts
  • src/shared/types/api.ts

Comment on lines +380 to +387
// Listen for session-refresh-scroll-bottom events (from Ctrl+R / refresh button)
useEffect(() => {
const handler = (): void => {
scrollToBottom('smooth');
};
window.addEventListener('session-refresh-scroll-bottom', handler);
return () => window.removeEventListener('session-refresh-scroll-bottom', handler);
}, [scrollToBottom]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scope this scroll event to a single tab.

Inactive session tabs stay mounted, so every ChatHistory instance subscribes to this global event. Refreshing one tab will scroll background tabs to bottom too, and if the active tab changes before the async refresh finishes, the wrong tab can jump. Pass the refreshed tabId in CustomEvent.detail and ignore events for other tabs here.

🎯 Suggested fix
  useEffect(() => {
-    const handler = (): void => {
+    const handler = (event: Event): void => {
+      const { tabId: targetTabId } =
+        (event as CustomEvent<{ tabId?: string }>).detail ?? {};
+      if (targetTabId !== effectiveTabId) return;
       scrollToBottom('smooth');
     };
     window.addEventListener('session-refresh-scroll-bottom', handler);
     return () => window.removeEventListener('session-refresh-scroll-bottom', handler);
-  }, [scrollToBottom]);
+  }, [effectiveTabId, scrollToBottom]);

Update the refresh call sites to dispatch the tab id captured when the refresh starts:

window.dispatchEvent(
  new CustomEvent('session-refresh-scroll-bottom', { detail: { tabId: activeTabId } })
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/ChatHistory.tsx` around lines 380 - 387, The
global 'session-refresh-scroll-bottom' listener in ChatHistory subscribes across
all mounted tabs and must be scoped per-tab: change the useEffect handler in
ChatHistory to accept the CustomEvent, read event.detail.tabId, and only call
scrollToBottom('smooth') when that tabId matches this ChatHistory's tab
identifier (e.g., the prop/state holding this tab's id); also update refresh
dispatch sites to send the starting tab id via window.dispatchEvent(new
CustomEvent('session-refresh-scroll-bottom', { detail: { tabId: activeTabId }
})). Ensure references to scrollToBottom, ChatHistory, window.addEventListener,
and CustomEvent.detail are used so the handler ignores events for other tabs.

Comment on lines +149 to +150
// Determine step variant for colored borders/icons
const toolVariant: StepVariant = status === 'error' ? 'tool-error' : 'tool';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify BaseItemProps interface definition
echo "=== Checking BaseItemProps interface ==="
rg -n -A 30 'interface BaseItemProps' --type=ts

echo ""
echo "=== Checking if variant prop exists anywhere in BaseItem ==="
rg -n 'variant' src/renderer/components/chat/items/BaseItem.tsx

Repository: matt1398/claude-devtools

Length of output: 2809


🏁 Script executed:

#!/bin/bash
# Verify LinkedToolItem.tsx around line 175
echo "=== Checking LinkedToolItem.tsx lines 170-180 ==="
sed -n '170,180p' src/renderer/components/chat/items/LinkedToolItem.tsx

echo ""
echo "=== Full BaseItem usage in LinkedToolItem.tsx ==="
rg -n -B 5 -A 5 'BaseItem' src/renderer/components/chat/items/LinkedToolItem.tsx | head -50

Repository: matt1398/claude-devtools

Length of output: 1585


variant prop is not defined on BaseItemProps.

LinkedToolItem.tsx passes variant={toolVariant} to BaseItem (line 175), but the BaseItemProps interface in BaseItem.tsx does not accept a variant prop. This will cause a TypeScript error or the prop will be silently ignored at runtime.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/items/LinkedToolItem.tsx` around lines 149 -
150, LinkedToolItem.tsx is passing variant={toolVariant} to BaseItem but
BaseItemProps (in BaseItem.tsx) doesn't declare a variant prop; add a variant
field to the BaseItemProps interface (e.g., variant?: StepVariant) and update
BaseItem component's props type to accept and forward that prop (use the
existing StepVariant type or import it where BaseItem is declared) so BaseItem
can use variant for styling; ensure any usages of BaseItem (including
LinkedToolItem) still type-check and adjust default behavior in BaseItem if
variant is optional.

Comment on lines 924 to 929
// Check for comment (# style for Python/Shell/R/Ruby/PHP/YAML)
if (
(language === 'python' || language === 'bash' || language === 'r' || language === 'ruby' || language === 'php') &&
(language === 'python' || language === 'bash' || language === 'zsh' || language === 'fish' ||
language === 'r' || language === 'ruby' || language === 'php' || language === 'yaml') &&
remaining.startsWith('#')
) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify the current rule in the file, then show representative inputs that
# would be classified as comments by the same predicate.
sed -n '924,929p' src/renderer/components/chat/viewers/syntaxHighlighter.ts

python <<'PY'
samples = [
    ('bash', 'echo ${`#var`}'),
    ('bash', 'echo ${var#prefix}'),
    ('yaml', 'url: http://example.com/#frag'),
]
for language, line in samples:
    for i in range(len(line)):
        if line[i:].startswith('#'):
            print(f"{language}: {line!r} -> current rule would start a comment at index {i}")
            break
PY

Repository: matt1398/claude-devtools

Length of output: 616


Hash comments are too eager for shell and YAML.

The # check treats any occurrence as a comment start, which mis-highlights valid syntax like echo ${#var}, echo ${var#prefix}, and YAML URLs like http://example.com/#frag. Add context-aware boundary checks (e.g., preceded by whitespace or operator) before treating # as a comment start.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/viewers/syntaxHighlighter.ts` around lines 924 -
929, The current comment detection in the if block that checks (language === ...
) && remaining.startsWith('#') is too naive; update the check around the
variables language and remaining (the comment-start branch in
syntaxHighlighter.ts) to only treat '#' as a comment when it is at the start of
the token or is preceded by a boundary (e.g., start-of-string or whitespace or
an operator/punctuation like = : , ; ( [ { | & >) and not when embedded in
words/variable expansions or URLs; implement this by replacing
remaining.startsWith('#') with a context-aware check (e.g., a regex or explicit
lookbehind/peek at the preceding character) that excludes cases like `${`#var`}`,
`${var#prefix}` and `http://...#frag` while still matching real comments in
shell/YAML.

Comment on lines +260 to +269
// Prune to prevent unbounded growth: when exceeding 100 entries, keep only the 50 most recent
if (projectRefreshGeneration.size > 100) {
const entries = Array.from(projectRefreshGeneration.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 50);
projectRefreshGeneration.clear();
for (const [key, value] of entries) {
projectRefreshGeneration.set(key, value);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

This pruning can evict the refresh that just started.

generation is only per-project, so it does not represent global recency. After 101 projects have each refreshed once, the newly touched project still has generation 1 and can be pruned immediately here; then Line 280 reads undefined !== generation and drops its own fresh response. Please track last-touch order separately, or never prune the current/in-flight key, instead of sorting by generation count. The same helper in src/renderer/store/slices/sessionDetailSlice.ts should be updated too.

🧩 One recency-safe approach
+const projectRefreshTouchedAt = new Map<string, number>();
+let projectRefreshSequence = 0;
...
 const generation = (projectRefreshGeneration.get(projectId) ?? 0) + 1;
 projectRefreshGeneration.set(projectId, generation);
+projectRefreshTouchedAt.set(projectId, ++projectRefreshSequence);

 if (projectRefreshGeneration.size > 100) {
-  const entries = Array.from(projectRefreshGeneration.entries())
-    .sort((a, b) => b[1] - a[1])
-    .slice(0, 50);
-  projectRefreshGeneration.clear();
-  for (const [key, value] of entries) {
-    projectRefreshGeneration.set(key, value);
+  const staleKeys = Array.from(projectRefreshTouchedAt.entries())
+    .filter(([key]) => key !== projectId)
+    .sort((a, b) => a[1] - b[1])
+    .slice(0, projectRefreshGeneration.size - 50)
+    .map(([key]) => key);
+  for (const key of staleKeys) {
+    projectRefreshGeneration.delete(key);
+    projectRefreshTouchedAt.delete(key);
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/store/slices/sessionSlice.ts` around lines 260 - 269, The
pruning logic using projectRefreshGeneration and per-project generation can
evict a just-started refresh because generation is not a global recency metric;
modify the pruning to preserve recency correctly by either (A) tracking
last-touch order separately (e.g., maintain a lastTouched map or timestamp per
key and sort by that when pruning) or (B) explicitly never prune the
current/in-flight key (detect the in-flight project id and always keep it)
instead of sorting by generation; apply the same change to the identical helper
in src/renderer/store/slices/sessionDetailSlice.ts so both slices use last-touch
timestamps or honor the in-flight key when pruning.

WesleyMFrederick added a commit to WesleyMFrederick/claude-devtools that referenced this pull request Apr 12, 2026
…ion state

Integrates psypeal/claude-devtools#168 into fork with conflict resolution:
- ipcChannels.ts: kept both PR's session:refresh and upstream's search API channels
- linkedTool/index.ts: added BashToolViewer export from PR
- sessionDetailSlice.ts: merged PR's expansion state clearing with upstream's slimDetail optimization

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Clear expansion Maps/Sets (aiGroupExpansionLevels, expandedStepIds,
  expandedDisplayItemIds, expandedAIGroupIds) when switching sessions
  to prevent unbounded growth over long uptime
- Add periodic pruning for sessionRefreshGeneration and
  projectRefreshGeneration Maps
- Add SubagentDisplayMeta type to Process for memory-efficient subagent
  rendering without holding full transcripts
- Add subagentMessageCacheSlice (Zustand) with single-flight Promise
  dedup and LRU eviction for on-demand subagent message loading
- Refactor SubagentItem to read displayMeta for collapsed headers and
  lazy-load message bodies only when expanded via useSubagentMessages
- Update AIChatGroup and modelExtractor to prefer displayMeta over
  iterating raw messages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@psypeal psypeal force-pushed the fix/renderer-memory-leak branch from 106665c to 4b8c12a Compare April 16, 2026 09:29
@psypeal psypeal changed the title fix: prevent renderer memory leak from unbounded expansion state fix: prevent renderer memory leak with lazy subagent rendering Apr 16, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/renderer/store/index.ts (1)

88-93: Consider adding a fallback or documenting the limitation.

The performance.memory API is Chrome-specific and non-standard. While returning 0 when unavailable is a safe fallback, this means memory pressure gating will be disabled in non-Chromium environments. Since this is an Electron app (Chromium-based), this should work, but the code comment could clarify this assumption.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/store/index.ts` around lines 88 - 93, Update getRendererHeapMB
to either document the Chromium-only assumption or add a graceful fallback:
clarify in the function comment that performance.memory is a
Chromium/Renderer-only API (Electron) and will return 0 in non-Chromium
environments, or implement an alternative/path (e.g., use
process.getProcessMemoryInfo or IPC to main which can call
process.getProcessMemoryInfo) when performance.memory is unavailable; reference
the function name getRendererHeapMB to locate where to add the comment or
fallback logic.
src/renderer/store/slices/subagentMessageCacheSlice.ts (1)

158-176: Consider clearing inflightPromises in invalidateSubagentMessagesForSession.

The clearSubagentMessageCache method correctly clears inflightPromises, but invalidateSubagentMessagesForSession does not. If a fetch is in-flight when a session switch triggers invalidation, the fetch will complete and re-populate the cache with potentially stale data.

However, this is a minor edge case since:

  1. Session switches are infrequent
  2. The cache is small (10 entries)
  3. The data would be from the same session that was just invalidated
♻️ Optional: Clear in-flight promises on session invalidation
 invalidateSubagentMessagesForSession: (_sessionId) => {
   // We only key by subagentId in the cache (no session in the key), so
   // a coarse clear is correct: when any session refreshes we drop all
   // cached subagent bodies. The cache is small so this is cheap.
+  inflightPromises.clear();
   set({
     subagentMessageCache: new Map(),
     subagentMessageErrors: new Map(),
   });
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/store/slices/subagentMessageCacheSlice.ts` around lines 158 -
176, The invalidateSubagentMessagesForSession function currently resets
subagentMessageCache and subagentMessageErrors but does not clear
inflightPromises, so in-flight fetches can repopulate the cache with stale data;
update invalidateSubagentMessagesForSession to also call
inflightPromises.clear() (same as clearSubagentMessageCache) so any pending
promises are discarded when invalidating, keeping the cache and inflightPromises
consistent (refer to invalidateSubagentMessagesForSession,
clearSubagentMessageCache, inflightPromises, subagentMessageCache, and
subagentMessageErrors).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/components/chat/AIChatGroup.tsx`:
- Around line 27-28: ChatHistory now pre-computes preceding-slash info but
ChatHistoryItem fails to pass it down to AIChatGroup; update ChatHistory to
compute the precedingSlash map (using the existing extractPrecedingSlashInfo or
the pre-computed map already added), thread that value through ChatHistoryItem
(add a precedingSlash prop to its props) and then pass precedingSlash into the
AIChatGroup instantiation (the component expecting the precedingSlash prop),
ensuring AIChatGroup receives the pre-computed value for each aiGroup so
slash-command linking works.

In `@src/renderer/hooks/useSubagentMessages.ts`:
- Line 50: Replace the `void loadSubagentMessages(projectId, sessionId,
subagentId)` call with explicit promise handling: call
`loadSubagentMessages(projectId, sessionId, subagentId)` and append a
`.catch(...)` handler to acknowledge the returned promise from the async
function (the function already manages errors via store state so the catch can
be a no-op or a minimal logger). Update the call site in useSubagentMessages.ts
where `loadSubagentMessages` is invoked to remove the `void` operator and add
the `.catch()` to satisfy sonarjs/void-use.

---

Nitpick comments:
In `@src/renderer/store/index.ts`:
- Around line 88-93: Update getRendererHeapMB to either document the
Chromium-only assumption or add a graceful fallback: clarify in the function
comment that performance.memory is a Chromium/Renderer-only API (Electron) and
will return 0 in non-Chromium environments, or implement an alternative/path
(e.g., use process.getProcessMemoryInfo or IPC to main which can call
process.getProcessMemoryInfo) when performance.memory is unavailable; reference
the function name getRendererHeapMB to locate where to add the comment or
fallback logic.

In `@src/renderer/store/slices/subagentMessageCacheSlice.ts`:
- Around line 158-176: The invalidateSubagentMessagesForSession function
currently resets subagentMessageCache and subagentMessageErrors but does not
clear inflightPromises, so in-flight fetches can repopulate the cache with stale
data; update invalidateSubagentMessagesForSession to also call
inflightPromises.clear() (same as clearSubagentMessageCache) so any pending
promises are discarded when invalidating, keeping the cache and inflightPromises
consistent (refer to invalidateSubagentMessagesForSession,
clearSubagentMessageCache, inflightPromises, subagentMessageCache, and
subagentMessageErrors).
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e32f83b3-ee0a-4c8e-ad83-e55a2380f296

📥 Commits

Reviewing files that changed from the base of the PR and between 106665c and 4b8c12a.

📒 Files selected for processing (19)
  • src/main/types/chunks.ts
  • src/preload/constants/ipcChannels.ts
  • src/preload/index.ts
  • src/renderer/api/httpClient.ts
  • src/renderer/components/chat/AIChatGroup.tsx
  • src/renderer/components/chat/items/SubagentItem.tsx
  • src/renderer/hooks/useSubagentMessages.ts
  • src/renderer/store/index.ts
  • src/renderer/store/slices/sessionDetailSlice.ts
  • src/renderer/store/slices/sessionSlice.ts
  • src/renderer/store/slices/subagentMessageCacheSlice.ts
  • src/renderer/store/slices/tabSlice.ts
  • src/renderer/store/types.ts
  • src/renderer/store/utils/stateResetHelpers.ts
  • src/renderer/utils/modelExtractor.ts
  • src/shared/types/api.ts
  • test/mocks/electronAPI.ts
  • test/renderer/store/storeTestUtils.ts
  • test/renderer/store/subagentMessageCacheSlice.test.ts
✅ Files skipped from review due to trivial changes (1)
  • src/renderer/store/utils/stateResetHelpers.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/renderer/store/slices/sessionSlice.ts
  • src/preload/constants/ipcChannels.ts
  • src/renderer/store/slices/tabSlice.ts
  • src/renderer/store/slices/sessionDetailSlice.ts
  • src/shared/types/api.ts
  • src/preload/index.ts

Comment on lines +27 to +28
// extractPrecedingSlashInfo moved to ChatHistory — pre-computed as a map to
// avoid O(n) scan per visible group per refresh cycle.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all usages of AIChatGroup to verify precedingSlash is passed
rg -n '<AIChatGroup' --type=tsx --type=ts -A 5

Repository: matt1398/claude-devtools

Length of output: 95


🏁 Script executed:

rg '<AIChatGroup' -t typescript -A 5

Repository: matt1398/claude-devtools

Length of output: 102


🏁 Script executed:

rg -g '*.tsx' '<AIChatGroup' -A 5

Repository: matt1398/claude-devtools

Length of output: 1062


🏁 Script executed:

cat -n src/renderer/components/chat/AIChatGroup.tsx | head -200

Repository: matt1398/claude-devtools

Length of output: 9275


🏁 Script executed:

cat -n src/renderer/components/chat/ChatHistoryItem.tsx | head -150

Repository: matt1398/claude-devtools

Length of output: 5401


🏁 Script executed:

rg -g '*.tsx' 'ChatHistory' --type-add 'tsx:*.tsx' -l | head -20

Repository: matt1398/claude-devtools

Length of output: 365


🏁 Script executed:

cat -n src/renderer/components/chat/ChatHistory.tsx | head -300

Repository: matt1398/claude-devtools

Length of output: 13387


🏁 Script executed:

cat -n src/renderer/components/chat/ChatHistory.tsx | tail -n +300 | head -200

Repository: matt1398/claude-devtools

Length of output: 8757


🏁 Script executed:

cat -n src/renderer/components/chat/ChatHistory.tsx | tail -n +500 | head -200

Repository: matt1398/claude-devtools

Length of output: 8979


🏁 Script executed:

cat -n src/renderer/components/chat/ChatHistory.tsx | tail -n +700 | head -300

Repository: matt1398/claude-devtools

Length of output: 8814


🏁 Script executed:

rg -g '*.tsx' 'precedingSlash' -B 3 -A 3

Repository: matt1398/claude-devtools

Length of output: 3371


🏁 Script executed:

rg -g '*.tsx' 'extractPrecedingSlashInfo\|precedingSlash\s*=' -B 2 -A 2

Repository: matt1398/claude-devtools

Length of output: 50


🏁 Script executed:

rg -g '*.ts' -g '*.tsx' 'extractPrecedingSlashInfo' -B 2 -A 2

Repository: matt1398/claude-devtools

Length of output: 496


🏁 Script executed:

rg -g '*.ts' -g '*.tsx' 'precedingSlash.*=' -A 2

Repository: matt1398/claude-devtools

Length of output: 300


🏁 Script executed:

rg -g '*.ts' -g '*.tsx' 'enhanceAIGroup' -B 5 -A 5

Repository: matt1398/claude-devtools

Length of output: 4031


🏁 Script executed:

cat -n src/renderer/utils/aiGroupEnhancer.ts | head -100

Repository: matt1398/claude-devtools

Length of output: 3866


🏁 Script executed:

cat -n src/renderer/utils/slashCommandExtractor.ts | head -150

Repository: matt1398/claude-devtools

Length of output: 6160


Pass precedingSlash prop from ChatHistory to AIChatGroup instantiation.

The precedingSlash prop is optional but expected to be pre-computed and passed from ChatHistory. However, ChatHistoryItem instantiates AIChatGroup without the precedingSlash prop (lines 116-121):

<AIChatGroup
  aiGroup={item.group}
  highlightToolUseId={toolUseIdForGroup}
  highlightColor={highlightColor}
  registerToolRef={registerToolRef}
/>

This means precedingSlash is always undefined, preventing proper slash command linking. The comments in AIChatGroup.tsx (lines 27-28, 175-177) indicate this was intended to be an optimization—moving the O(n) precedingSlash extraction from component render to ChatHistory—but the implementation is incomplete. Compute precedingSlash for each AI group in ChatHistory and pass it down through ChatHistoryItem to AIChatGroup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/AIChatGroup.tsx` around lines 27 - 28,
ChatHistory now pre-computes preceding-slash info but ChatHistoryItem fails to
pass it down to AIChatGroup; update ChatHistory to compute the precedingSlash
map (using the existing extractPrecedingSlashInfo or the pre-computed map
already added), thread that value through ChatHistoryItem (add a precedingSlash
prop to its props) and then pass precedingSlash into the AIChatGroup
instantiation (the component expecting the precedingSlash prop), ensuring
AIChatGroup receives the pre-computed value for each aiGroup so slash-command
linking works.

if (!projectId || !sessionId) return;
if (messages !== null) return; // already cached
if (isLoading) return; // already in flight
void loadSubagentMessages(projectId, sessionId, subagentId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the current violation and whether the Sonar rule is configured in this repo.
rg -n "void\\s+loadSubagentMessages\\(" src/renderer/hooks/useSubagentMessages.ts
rg -n "sonarjs/void-use" --iglob "*eslint*" .

Repository: matt1398/claude-devtools

Length of output: 135


🏁 Script executed:

#!/bin/bash
# Find eslint configuration files
fd -type f \( -name ".eslintrc*" -o -name "eslint.config.*" \) 
# Also check package.json for eslintConfig
grep -l "eslintConfig" package.json 2>/dev/null

# Check the context around line 50 in the file
echo "=== Context around line 50 in useSubagentMessages.ts ===" 
sed -n '45,55p' src/renderer/hooks/useSubagentMessages.ts

# Find the loadSubagentMessages function definition
echo "=== Searching for loadSubagentMessages definition ===" 
rg -n "loadSubagentMessages\s*[=:]|function loadSubagentMessages" src/

Repository: matt1398/claude-devtools

Length of output: 1087


🏁 Script executed:

#!/bin/bash
# Find and display eslint configuration
echo "=== ESLint Config Files ===" 
find . -maxdepth 2 -type f \( -name ".eslintrc*" -o -name "eslint.config.*" \)

# Check package.json
echo "=== ESLint in package.json ===" 
grep -A 30 '"eslint' package.json | head -40

# Verify loadSubagentMessages is async and returns Promise
echo "=== Full loadSubagentMessages definition ===" 
sed -n '86,100p' src/renderer/store/slices/subagentMessageCacheSlice.ts

# Check if sonarjs package is installed
echo "=== ESLint plugins in package ===" 
rg "sonarjs" package.json package-lock.json 2>/dev/null | head -10

Repository: matt1398/claude-devtools

Length of output: 2493


🏁 Script executed:

#!/bin/bash
# Check the eslint.config.js file for sonarjs configuration
echo "=== ESLint Config Content ===" 
cat eslint.config.js | head -150

Repository: matt1398/claude-devtools

Length of output: 4809


🏁 Script executed:

#!/bin/bash
# Continue reading eslint.config.js to see sonarjs rule configuration
echo "=== ESLint Config (continued) ===" 
cat eslint.config.js | tail -n +150 | head -100

# Also search for any mention of "void-use" in the config
echo "=== Searching for void-use in config ===" 
rg "void-use" eslint.config.js

Repository: matt1398/claude-devtools

Length of output: 2769


🏁 Script executed:

#!/bin/bash
# Get the rest of eslint.config.js
wc -l eslint.config.js
cat eslint.config.js | tail -100

# Also check what sonarjs.configs.recommended includes
# by checking if sonarjs has any void-use rule at all
npm ls eslint-plugin-sonarjs 2>/dev/null | head -5

Repository: matt1398/claude-devtools

Length of output: 3770


🏁 Script executed:

#!/bin/bash
# Check sonarjs documentation and what rules are in the recommended config
echo "=== Checking sonarjs rule info ===" 
# Search in node_modules for sonarjs rules
find node_modules/eslint-plugin-sonarjs -name "*.js" -o -name "*.json" | head -20

# Look for void-use rule definition
rg "void-use" node_modules/eslint-plugin-sonarjs/ 2>/dev/null | head -10

# Also check what the sonarjs recommended config includes
rg "void-use|configs\.recommended" node_modules/eslint-plugin-sonarjs/ 2>/dev/null | head -20

Repository: matt1398/claude-devtools

Length of output: 1719


🏁 Script executed:

#!/bin/bash
# Check the sonarjs void-use documentation
cat node_modules/eslint-plugin-sonarjs/docs/void-use.md

# Also verify loadSubagentMessages returns a Promise
echo "=== Full context of loadSubagentMessages return type ===" 
sed -n '86,120p' src/renderer/store/slices/subagentMessageCacheSlice.ts

Repository: matt1398/claude-devtools

Length of output: 1798


🏁 Script executed:

#!/bin/bash
# Verify the full implementation to see if error state is actually handled
echo "=== Full loadSubagentMessages implementation ===" 
sed -n '86,150p' src/renderer/store/slices/subagentMessageCacheSlice.ts

Repository: matt1398/claude-devtools

Length of output: 2759


Replace void loadSubagentMessages(...) with explicit promise handling to satisfy sonarjs/void-use.

The void operator is flagged by the enabled sonarjs/void-use rule in ESLint config. Since loadSubagentMessages is async and already handles errors internally via store state, use .catch() to explicitly acknowledge the promise:

Proposed fix
-    void loadSubagentMessages(projectId, sessionId, subagentId);
+    loadSubagentMessages(projectId, sessionId, subagentId).catch(() => {
+      // Error state is already captured in `subagentMessageErrors` by the slice.
+    });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void loadSubagentMessages(projectId, sessionId, subagentId);
loadSubagentMessages(projectId, sessionId, subagentId).catch(() => {
// Error state is already captured in `subagentMessageErrors` by the slice.
});
🧰 Tools
🪛 ESLint

[error] 50-50: Remove this use of the "void" operator.

(sonarjs/void-use)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/useSubagentMessages.ts` at line 50, Replace the `void
loadSubagentMessages(projectId, sessionId, subagentId)` call with explicit
promise handling: call `loadSubagentMessages(projectId, sessionId, subagentId)`
and append a `.catch(...)` handler to acknowledge the returned promise from the
async function (the function already manages errors via store state so the catch
can be a no-op or a minimal logger). Update the call site in
useSubagentMessages.ts where `loadSubagentMessages` is invoked to remove the
`void` operator and add the `.catch()` to satisfy sonarjs/void-use.

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

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant