Skip to content

perf: optimize SessionListItem style recalculation#496

Open
jeffscottward wants to merge 23 commits intoRunMaestro:mainfrom
jeffscottward:symphony/issue-373-mm85yqkw
Open

perf: optimize SessionListItem style recalculation#496
jeffscottward wants to merge 23 commits intoRunMaestro:mainfrom
jeffscottward:symphony/issue-373-mm85yqkw

Conversation

@jeffscottward
Copy link
Contributor

@jeffscottward jeffscottward commented Mar 1, 2026

Summary

  • Refactored SessionListItem rendering to reduce inline style allocations and align with memoization expectations.

Notes

  • No behavioral change intended; styling logic remains equivalent.

Auto Run checklist

  • Auto Run docs were reviewed for completion status before PR creation. See details in handoff message.

Summary by CodeRabbit

  • New Features

    • Session discovery caching to speed up agent session listing.
    • Parallel branch checks for faster git default-branch resolution.
  • Improvements

    • Widespread async I/O for snappier startup and file operations.
    • Process spawning made asynchronous for more reliable task execution.
    • Unified UI styling and component memoization for smoother rendering.
    • Faster, more accurate search/highlight with precomputed regex logic.
@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2026

📝 Walkthrough

Walkthrough

This PR converts many synchronous file- and process-related APIs to async/Promise-based flows, updates call sites to await results, adds session discovery and Codex config caching, changes StatsDB/HistoryManager to async, makes process spawning asynchronous, and applies extensive renderer component refactors (memoization and centralized styling). Tests were updated accordingly.

Changes

Cohort / File(s) Summary
History & IPC
src/main/history-manager.ts, src/main/ipc/handlers/history.ts, src/main/web-server/..., src/main/web-server/managers/CallbackRegistry.ts
HistoryManager methods converted to async Promise returns; IPC handlers and web-server code updated to await history calls; CallbackRegistry.getHistory now async.
Stats DB & Migrations
src/main/stats/stats-db.ts, src/main/stats/singleton.ts, src/main/stats/aggregations.ts, src/main/stats/migrations.ts, src/shared/stats-types.ts
StatsDB methods (initialize, vacuum, backups, size) converted to async; migrations extended (v5–v7) and schema index SQL added; STATS DB version bumped to 7; initializeStatsDB now async.
Process Manager & Spawners
src/main/process-manager/ProcessManager.ts, src/main/process-manager/spawners/ChildProcessSpawner.ts, src/main/process-manager/spawners/PtySpawner.ts, src/main/process-manager/utils/imageUtils.ts
ProcessManager.spawn and spawners now return Promise; image temp file handling uses async saveImageToTempFile and Promise.all; spawn call sites updated to await.
Group Chat & Context Groomer
src/main/group-chat/*, src/main/utils/context-groomer.ts, src/main/ipc/handlers/groupChat.ts
IProcessManager/GenericProcessManager spawn signatures changed to Promise; call sites in agent, moderator, router, and groomContext now await spawn results.
FS → fs/promises Conversions & Collectors
src/main/debug-package/collectors/*.ts, src/main/parsers/codex-output-parser.ts, src/main/parsers/index.ts
Multiple collectors and parsers now use fs.promises; codex config loader added with caching, load/invalidate APIs; collectStorage/group-chats switched to async size calculations.
Agent Sessions & Discovery Cache
src/main/ipc/handlers/agentSessions.ts
Introduces TTL-based session discovery cache, batching for discovery, discoverSessionFilesWithCache, and test helper __clearSessionDiscoveryCacheForTests().
Git & Worktree Parallelization
src/main/ipc/handlers/git.ts
Parallelized branch and worktree checks with Promise.all; reordered repoRoot/toplevel derivation for concurrent git queries.
WSL & Startup
src/main/utils/wslDetector.ts, src/main/index.ts
isWsl and checkWslEnvironment now async using fs/promises; startup awaits checkWslEnvironment and initializeStatsDB.
Web Server / API Types
src/main/web-server/types.ts, src/main/web-server/routes/apiRoutes.ts, src/main/web-server/web-server-factory.ts, src/main/web-server/WebServer.ts
GetHistory callback type changed to return Promise<HistoryEntry[]>; route and factory updated to await history calls.
Renderer: styling, memoization & UI changes
src/renderer/components/... (AICommandsPanel, SessionList, SessionListItem, FilePreview, CsvTableRenderer, ExecutionQueueIndicator, AutoRun, AgentSessionsBrowser, Toast, etc.)
Large UI refactors: centralized memoized styles, many components wrapped in React.memo/useMemo, regex/constants introduced for search/highlighting, props adjustments (e.g., isSelected), and performance/layout tweaks.
Tests
src/__tests__/** (many files including history-manager.test.ts, group-chat.integration.test.ts, ChildProcessSpawner.test.ts, git.test.ts, and 20+ others)
Tests updated to mock/promisify fs.promises, use mockResolvedValue, mark tests async and await new async APIs; new test helpers for session-discovery and Codex config caching added.

Sequence Diagram(s)

sequenceDiagram
    actor App as App Startup
    participant Index as main/index.ts
    participant WSL as wslDetector.checkWslEnvironment()
    participant StatsSingleton as initializeStatsDB()
    participant StatsDB as StatsDB.initialize()
    participant FSPromises as fs/promises

    App->>Index: app.whenReady()
    Index->>WSL: await checkWslEnvironment(cwd)
    WSL->>FSPromises: await readFile('/proc/version')
    FSPromises-->>WSL: content/throw
    WSL-->>Index: boolean
    Index->>StatsSingleton: await initializeStatsDB()
    StatsSingleton->>StatsDB: await db.initialize()
    StatsDB->>FSPromises: await access/mkdir/open as needed
    FSPromises-->>StatsDB: responses
    StatsDB-->>StatsSingleton: initialized
    StatsSingleton-->>Index: done
Loading
sequenceDiagram
    actor Renderer as Renderer (IPC)
    participant HistoryHandler as ipc/history:getAll
    participant HistoryMgr as HistoryManager
    participant FSPromises as fs/promises

    Renderer->>HistoryHandler: ipcInvoke('history:getAll')
    HistoryHandler->>HistoryMgr: await getAllEntries()
    HistoryMgr->>FSPromises: await readdir/readFile per session (parallel)
    FSPromises-->>HistoryMgr: file contents
    HistoryMgr->>HistoryHandler: Promise<HistoryEntry[]>
    HistoryHandler-->>Renderer: entries
Loading
sequenceDiagram
    actor Router as GroupChat Router
    participant RouterHandler as route handler
    participant ProcMgr as ProcessManager
    participant Spawner as ChildProcessSpawner/PtySpawner
    participant OSProc as spawned child process

    Router->>RouterHandler: initiate spawn (moderator/participant)
    RouterHandler->>ProcMgr: await processManager.spawn(config)
    ProcMgr->>Spawner: spawn(config) (async)
    Spawner->>OSProc: spawn process / prepare images (await saveImageToTempFile)
    OSProc-->>Spawner: spawn result
    Spawner-->>ProcMgr: Promise<SpawnResult>
    ProcMgr-->>RouterHandler: SpawnResult
    RouterHandler->>Router: continue flow
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • pedramamini
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'perf: optimize SessionListItem style recalculation' accurately reflects the main change in the pull request, which involves adding memoization to the SessionListItem component to reduce inline style allocations and improve rendering performance.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

Copy link

@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: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
src/__tests__/main/stats/stats-db.test.ts (1)

560-582: ⚠️ Potential issue | 🟡 Minor

Tests assert against sync mock but production uses async.

The tests on lines 569 and 581 assert against mockFsMkdirSync, but after the async migration, initialize() uses fsp.mkdir which is mocked by mockFsMkdir. These assertions will pass vacuously (the sync mock is never called) and don't actually verify the intended behavior.

🐛 Fix mock assertions
 		it('should create userData directory if it does not exist', async () => {
 			// Simulate directory not existing
 			mockFsExistsSync.mockReturnValue(false);

 			const { StatsDB } = await import('../../../main/stats');
 			const db = new StatsDB();
 			await db.initialize();

-			// Verify mkdirSync was called with recursive option
-			expect(mockFsMkdirSync).toHaveBeenCalledWith(mockUserDataPath, { recursive: true });
+			// Verify mkdir (async) was called with recursive option
+			expect(mockFsMkdir).toHaveBeenCalledWith(mockUserDataPath, { recursive: true });
 		});

 		it('should not create directory if it already exists', async () => {
 			// Simulate directory already existing
 			mockFsExistsSync.mockReturnValue(true);

 			const { StatsDB } = await import('../../../main/stats');
 			const db = new StatsDB();
 			await db.initialize();

-			// Verify mkdirSync was NOT called
-			expect(mockFsMkdirSync).not.toHaveBeenCalled();
+			// Verify mkdir (async) was NOT called
+			expect(mockFsMkdir).not.toHaveBeenCalled();
 		});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/stats/stats-db.test.ts` around lines 560 - 582, The tests
are asserting against the old synchronous mkdir mock (mockFsMkdirSync) but the
production code now uses the async fsp.mkdir mocked as mockFsMkdir; update the
two expectations in the StatsDB.initialize tests to assert on mockFsMkdir (not
mockFsMkdirSync) being called with mockUserDataPath and { recursive: true } when
mockFsExistsSync returns false, and to assert mockFsMkdir was not called when
mockFsExistsSync returns true; ensure you reference the StatsDB.initialize flow
and keep mockFsExistsSync and mockUserDataPath as the inputs under test.
src/main/group-chat/group-chat-router.ts (2)

547-569: ⚠️ Potential issue | 🟠 Major

Handle spawnResult.success === false before proceeding.

All four spawn paths treat the operation as successful immediately after await, but none checks spawnResult.success. That can leave incorrect state transitions (for example, participants marked pending without a real spawned process).

💡 Suggested guard (apply to each spawn call site)
 const spawnResult = await processManager.spawn({
   // ...
 });
+if (!spawnResult.success) {
+	throw new Error(`Spawn failed for session ${sessionId}`);
+}
 
 console.log(`[GroupChat:Debug] Spawn result: ${JSON.stringify(spawnResult)}`);

Also applies to: 937-965, 1265-1289, 1465-1485

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

In `@src/main/group-chat/group-chat-router.ts` around lines 547 - 569, The code
calls processManager.spawn(...) and immediately proceeds without checking
spawnResult.success; update each spawn call site (the spawn block that assigns
spawnResult, including uses of processManager.spawn, spawnResult, sessionId, and
chat.moderatorAgentId) to verify if spawnResult.success is false, and if so log
the error (include spawnResult.error/details), revert any state changes made
before the spawn (e.g., unmark participants as pending or clear pending process
ids), emit an appropriate failure response or throw/return to stop further
processing, and ensure no success-path logs or state transitions run when
spawnResult.success is false; apply the same guard-and-rollback pattern to the
other three spawn blocks referenced in the comment.

1256-1283: ⚠️ Potential issue | 🟠 Major

Synthesis spawn path is missing SSH wrapping when sshRemoteConfig is enabled.

The synthesis flow goes straight to processManager.spawn(...) with local command/cwd, unlike the other group-chat spawn flows that call wrapSpawnWithSsh(...). This causes inconsistent remote execution behavior for synthesis rounds.

🔧 Suggested patch pattern for synthesis path
+let spawnCommand = command;
+let spawnArgs = finalArgs;
+let spawnCwd = os.homedir();
+let spawnPrompt: string | undefined = synthesisPrompt;
+let spawnEnvVars =
+	configResolution.effectiveCustomEnvVars ??
+	getCustomEnvVarsCallback?.(chat.moderatorAgentId);
+
+if (sshStore && chat.moderatorConfig?.sshRemoteConfig) {
+	const sshWrapped = await wrapSpawnWithSsh(
+		{
+			command,
+			args: finalArgs,
+			cwd: os.homedir(),
+			prompt: synthesisPrompt,
+			customEnvVars: spawnEnvVars,
+			promptArgs: agent.promptArgs,
+			noPromptSeparator: agent.noPromptSeparator,
+			agentBinaryName: agent.binaryName,
+		},
+		chat.moderatorConfig.sshRemoteConfig,
+		sshStore
+	);
+	spawnCommand = sshWrapped.command;
+	spawnArgs = sshWrapped.args;
+	spawnCwd = sshWrapped.cwd;
+	spawnPrompt = sshWrapped.prompt;
+	spawnEnvVars = sshWrapped.customEnvVars;
+}
+
 const spawnResult = await processManager.spawn({
 	sessionId,
 	toolType: chat.moderatorAgentId,
-	cwd: os.homedir(),
-	command,
-	args: finalArgs,
+	cwd: spawnCwd,
+	command: spawnCommand,
+	args: spawnArgs,
 	readOnlyMode: true,
-	prompt: synthesisPrompt,
+	prompt: spawnPrompt,
 	contextWindow: getContextWindowValue(agent, agentConfigValues),
-	customEnvVars:
-		configResolution.effectiveCustomEnvVars ??
-		getCustomEnvVarsCallback?.(chat.moderatorAgentId),
+	customEnvVars: spawnEnvVars,
 	// ...
 });

Based on learnings: "When implementing features that spawn agent processes, support SSH remote execution by checking for sshRemoteConfig, using wrapSpawnWithSsh() from ssh-spawn-wrapper.ts, and passing the SSH store adapter".

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

In `@src/main/group-chat/group-chat-router.ts` around lines 1256 - 1283, The
synthesis spawn path currently calls processManager.spawn(...) directly and
ignores chat.moderatorConfig?.sshRemoteConfig; update the logic to mirror other
group-chat spawn flows by using wrapSpawnWithSsh(...) from ssh-spawn-wrapper.ts
when sshRemoteConfig is present: build the same spawn options you pass into
processManager.spawn (including command, finalArgs, cwd,
shell/runInShell/sendPromptViaStdin flags, prompt, customEnvVars from
configResolution.effectiveCustomEnvVars or getCustomEnvVarsCallback, promptArgs,
noPromptSeparator, contextWindow from getContextWindowValue(agent,
agentConfigValues), etc.), then call wrapSpawnWithSsh(spawnOptions,
chat.moderatorConfig.sshRemoteConfig, <ssh store adapter>) so the wrapped call
delegates to processManager.spawn for local runs and to the SSH wrapper for
remote runs; ensure getWindowsSpawnConfig and its
shell/runInShell/sendPromptViaStdin flags are preserved in the spawnOptions you
wrap.
src/main/utils/context-groomer.ts (1)

206-337: ⚠️ Potential issue | 🔴 Critical

Syntax blocker: await used in non-async Promise executor.

Line 321 uses await but the Promise executor callback (line 206) is not declared as async. Wrap the spawn logic in an async IIFE with proper error handling.

Proposed fix
 	return new Promise<GroomContextResult>((resolve, reject) => {
 		let responseBuffer = '';
 		let lastDataTime = Date.now();
 		let idleCheckInterval: NodeJS.Timeout | null = null;
 		let resolved = false;
 		let chunkCount = 0;
 		let cancelled = false;
@@
 		// Listen for events BEFORE spawning
 		processManager.on('data', onData);
 		processManager.on('exit', onExit);
 		processManager.on('agent-error', onError);
 
-		// Spawn the process in batch mode
-		const spawnResult = await processManager.spawn({
-			sessionId: groomerSessionId,
-			toolType: agentType,
-			cwd: projectRoot,
-			command: agent.command,
-			args: finalArgs,
-			prompt: prompt, // Triggers batch mode (no PTY)
-			promptArgs: agent.promptArgs, // For agents using flag-based prompt (e.g., OpenCode -p)
-			noPromptSeparator: agent.noPromptSeparator,
-			// Pass SSH config for remote execution support
-			sessionSshRemoteConfig,
-			sessionCustomPath,
-			sessionCustomArgs,
-			sessionCustomEnvVars,
-		});
-
-		if (!spawnResult || spawnResult.pid <= 0) {
-			cleanup();
-			reject(new Error(`Failed to spawn grooming process for ${agentType}`));
-			return;
-		}
-
-		logger.debug('Spawned grooming batch process', LOG_CONTEXT, {
-			groomerSessionId,
-			pid: spawnResult.pid,
-		});
-
-		// Set up idle check
-		idleCheckInterval = setInterval(() => {
-			const idleTime = Date.now() - lastDataTime;
-			if (idleTime > IDLE_TIMEOUT_MS && responseBuffer.length >= MIN_RESPONSE_LENGTH) {
-				finishWithResponse('idle timeout with content');
-			}
-		}, 1000);
+		// Spawn the process in batch mode
+		void (async () => {
+			const spawnResult = await processManager.spawn({
+				sessionId: groomerSessionId,
+				toolType: agentType,
+				cwd: projectRoot,
+				command: agent.command,
+				args: finalArgs,
+				prompt: prompt, // Triggers batch mode (no PTY)
+				promptArgs: agent.promptArgs, // For agents using flag-based prompt (e.g., OpenCode -p)
+				noPromptSeparator: agent.noPromptSeparator,
+				// Pass SSH config for remote execution support
+				sessionSshRemoteConfig,
+				sessionCustomPath,
+				sessionCustomArgs,
+				sessionCustomEnvVars,
+			});
+
+			if (!spawnResult || spawnResult.pid <= 0) {
+				cleanup();
+				reject(new Error(`Failed to spawn grooming process for ${agentType}`));
+				return;
+			}
+
+			logger.debug('Spawned grooming batch process', LOG_CONTEXT, {
+				groomerSessionId,
+				pid: spawnResult.pid,
+			});
+
+			// Set up idle check
+			idleCheckInterval = setInterval(() => {
+				const idleTime = Date.now() - lastDataTime;
+				if (idleTime > IDLE_TIMEOUT_MS && responseBuffer.length >= MIN_RESPONSE_LENGTH) {
+					finishWithResponse('idle timeout with content');
+				}
+			}, 1000);
+		})().catch((error) => {
+			cleanup();
+			if (!resolved) {
+				resolved = true;
+				reject(error instanceof Error ? error : new Error(String(error)));
+			}
+		});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/utils/context-groomer.ts` around lines 206 - 337, The Promise
executor is using await (processManager.spawn) but is not async; wrap the spawn
logic in an async IIFE (e.g., (async () => { const spawnResult = await
processManager.spawn(...); /* existing spawn handling */ })().catch(err => {
cleanup(); if (!resolved) { resolved = true; logger.error('Grooming spawn
failed', LOG_CONTEXT, { groomerSessionId, error: String(err) }); reject(err); }
})); ensure the IIFE uses the same variables/callbacks (onData, onExit, onError,
cancelOperation, finishWithResponse, cleanup, activeGroomingSessions) and that
any error path performs cleanup, sets resolved/cancelled appropriately, and
rejects the outer Promise.
src/renderer/components/SessionListItem.tsx (1)

44-50: ⚠️ Potential issue | 🟠 Major

Props interface inconsistent with internal usage and call sites.

The SessionListItemProps interface declares index: number and selectedIndex: number (lines 48-50), but:

  1. Line 109 computes isSelected = index === selectedIndex internally
  2. The call site in AgentSessionsBrowser.tsx passes isSelected directly instead of index/selectedIndex

This creates a mismatch. Based on the AI summary stating the props changed to use isSelected, the interface should be updated:

🔧 Proposed fix to update props interface
 export interface SessionListItemProps {
 	/** The Claude session data */
 	session: ClaudeSession;
-	/** Zero-based index in the list */
-	index: number;
-	/** Currently selected index for keyboard navigation */
-	selectedIndex: number;
+	/** Whether this item is currently selected */
+	isSelected: boolean;
 	/** Whether this session is starred */
 	isStarred: boolean;

And remove line 109 since isSelected would come from props:

-	const isSelected = index === selectedIndex;

Also applies to: 88-109

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

In `@src/renderer/components/SessionListItem.tsx` around lines 44 - 50, The props
interface is inconsistent with usage — update SessionListItemProps to accept
isSelected: boolean (remove index and selectedIndex) so it matches how
AgentSessionsBrowser.tsx calls SessionListItem, and then remove the internal
computed isSelected = index === selectedIndex in the SessionListItem component
(the line around the current line 109) so the component uses the passed-in
isSelected prop instead.
🧹 Nitpick comments (14)
src/renderer/components/FilePreview.tsx (1)

1616-1667: Consider adding searchRegex to the dependency array for clarity.

The effect checks !searchRegex at line 1617 but doesn't include searchRegex in the dependency array. While this works correctly because editModeSearchMatches transitively depends on searchRegex, the ESLint exhaustive-deps rule would flag this. Adding it explicitly would improve maintainability and silence the lint warning.

Additionally, editContent in the dependency array is redundant since editModeSearchMatches already captures changes to it.

♻️ Suggested dependency array refinement
 }, [
   editModeSearchMatches,
   currentMatchIndex,
   isEditableText,
   markdownEditMode,
+  searchRegex,
   searchQuery,
-  editContent,
 ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/FilePreview.tsx` around lines 1616 - 1667, The effect
uses searchRegex in its guard but doesn’t include it in the dependency array and
also includes redundant editContent; update the useEffect dependencies for the
effect that updates editModeSearchMatches/navigation by adding searchRegex and
removing editContent so the array reads include editModeSearchMatches,
currentMatchIndex, isEditableText, markdownEditMode, searchQuery, searchRegex
(refer to useEffect, editModeSearchMatches, currentMatchIndex, searchQuery,
searchRegex, editContent, textareaRef in the diff).
src/renderer/components/AutoRun.tsx (2)

725-729: Collapse redundant memoization for completed task count.

completedTaskMatches is only used to read .length, so the extra memo adds indirection without benefit.

♻️ Proposed simplification
-	const completedTaskMatches = useMemo(() => localContent.match(AUTO_RUN_COMPLETED_TASK_REGEX) || [], [localContent]);
-	const completedTaskCountFromLocalContent = useMemo(
-		() => completedTaskMatches.length,
-		[completedTaskMatches]
-	);
+	const completedTaskCountFromLocalContent = useMemo(
+		() => localContent.match(AUTO_RUN_COMPLETED_TASK_REGEX)?.length ?? 0,
+		[localContent]
+	);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AutoRun.tsx` around lines 725 - 729,
completedTaskMatches is memoized but completedTaskCountFromLocalContent only
reads its .length, so remove the redundant useMemo: keep the useMemo that
computes completedTaskMatches (using
localContent.match(AUTO_RUN_COMPLETED_TASK_REGEX) || []) and change
completedTaskCountFromLocalContent to a plain derived value (e.g., const
completedTaskCountFromLocalContent = completedTaskMatches.length) referencing
completedTaskMatches directly; update any imports/usages accordingly (symbols:
completedTaskMatches, completedTaskCountFromLocalContent,
AUTO_RUN_COMPLETED_TASK_REGEX, localContent).

1082-1102: Avoid recounting matches on every next/prev navigation.

Line 1102 includes currentMatchIndex in the effect dependencies, so navigating results retriggers full match counting even when query/content are unchanged.

⚡ Proposed dependency-safe update
 	useEffect(() => {
 		// Clear any pending count
 		if (searchCountTimeoutRef.current) {
 			clearTimeout(searchCountTimeoutRef.current);
 		}

 		if (searchQueryRegex) {
 			// Debounce the match counting for large documents
 			searchCountTimeoutRef.current = setTimeout(() => {
-				const matches = localContent.match(searchQueryRegex) || [];
-				const count = matches ? matches.length : 0;
+				const count = (localContent.match(searchQueryRegex) || []).length;
 				setTotalMatches(count);
-				if (count > 0 && currentMatchIndex >= count) {
-					setCurrentMatchIndex(0);
-				}
+				setCurrentMatchIndex((prev) => (count > 0 && prev >= count ? 0 : prev));
 			}, 150); // Short delay for search responsiveness
 		} else {
 			setTotalMatches(0);
 			setCurrentMatchIndex(0);
 		}

 		return () => {
 			if (searchCountTimeoutRef.current) {
 				clearTimeout(searchCountTimeoutRef.current);
 			}
 		};
-	}, [searchQueryRegex, localContent, currentMatchIndex]);
+	}, [searchQueryRegex, localContent]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AutoRun.tsx` around lines 1082 - 1102, The effect
that counts regex matches is re-run on navigation because currentMatchIndex is
included in the dependency array; remove currentMatchIndex from the dependencies
of that counting effect and keep only searchQueryRegex and localContent so
navigating next/prev doesn't retrigger counting. To preserve the existing
behavior that resets the index when the match count becomes 0 or out of range,
move that index-reset logic into the same timeout callback (already present) or
into a separate effect that depends on totalMatches and currentMatchIndex (use
setCurrentMatchIndex with a conditional/fallback), and ensure you still clear
searchCountTimeoutRef via the existing cleanup; update references to
searchQueryRegex, localContent, searchCountTimeoutRef, setTotalMatches, and
setCurrentMatchIndex accordingly.
src/main/debug-package/collectors/group-chats.ts (1)

51-80: Consider parallelizing per-file metadata reads.

Line 51-80 currently processes chats serially. For larger chat directories, this can become noticeably slow.

⚡ Optional parallelized variant
-	for (const file of files) {
-		if (!file.endsWith('.json') || file.endsWith('.log.json')) {
-			continue;
-		}
-
-		const filePath = path.join(groupChatsPath, file);
-
-		try {
-			const content = await fs.promises.readFile(filePath, 'utf-8');
-			const chat = JSON.parse(content);
-
-			// Get corresponding log file for message count
-			const logPath = path.join(groupChatsPath, `${path.basename(file, '.json')}.log.json`);
-			const messageCount = await countMessages(logPath);
-
-			const chatInfo: GroupChatInfo = {
-				id: chat.id || path.basename(file, '.json'),
-				moderatorAgentId: chat.moderatorAgentId || chat.moderator?.agentId || 'unknown',
-				participantCount: Array.isArray(chat.participants) ? chat.participants.length : 0,
-				participants: Array.isArray(chat.participants)
-					? chat.participants.map((p: any) => ({
-						agentId: p.agentId || 'unknown',
-					}))
-					: [],
-				messageCount,
-				createdAt: chat.createdAt || 0,
-				updatedAt: chat.updatedAt || 0,
-			};
-
-			groupChats.push(chatInfo);
-		} catch {
-			// Skip files that can't be parsed
-		}
-	}
+	const chatFiles = files.filter((file) => file.endsWith('.json') && !file.endsWith('.log.json'));
+	const results = await Promise.all(
+		chatFiles.map(async (file): Promise<GroupChatInfo | null> => {
+			const filePath = path.join(groupChatsPath, file);
+			try {
+				const content = await fs.promises.readFile(filePath, 'utf-8');
+				const chat = JSON.parse(content);
+				const logPath = path.join(groupChatsPath, `${path.basename(file, '.json')}.log.json`);
+				const messageCount = await countMessages(logPath);
+				return {
+					id: chat.id || path.basename(file, '.json'),
+					moderatorAgentId: chat.moderatorAgentId || chat.moderator?.agentId || 'unknown',
+					participantCount: Array.isArray(chat.participants) ? chat.participants.length : 0,
+					participants: Array.isArray(chat.participants)
+						? chat.participants.map((p: any) => ({ agentId: p.agentId || 'unknown' }))
+						: [],
+					messageCount,
+					createdAt: chat.createdAt || 0,
+					updatedAt: chat.updatedAt || 0,
+				};
+			} catch {
+				return null;
+			}
+		})
+	);
+	for (const chatInfo of results) {
+		if (chatInfo) groupChats.push(chatInfo);
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/debug-package/collectors/group-chats.ts` around lines 51 - 80, The
loop in group-chats.ts that reads and parses each chat file (iterating over
files, building filePath, reading with fs.promises.readFile, parsing JSON,
calling countMessages, and pushing GroupChatInfo into groupChats) runs serially
and should be parallelized: replace the for-loop with a concurrency-safe
parallel approach (e.g., filter files to the .json ones, map them to async
worker promises that perform readFile, JSON.parse, call countMessages, build the
GroupChatInfo using chat.id, chat.moderatorAgentId/chat.moderator?.agentId,
participants, createdAt/updatedAt, etc.), then await Promise.all or
Promise.allSettled (or use a p-limit to bound concurrency) and push/collect
results into groupChats; also ensure individual file errors are caught so one
failure doesn't reject the whole batch (handle per-promise try/catch or use
allSettled and log/skips failures).
src/__tests__/main/history-manager.test.ts (1)

1057-1118: Add a multi-session-file case for updateSessionNameByClaudeSessionId.

Current tests only validate one session file. Add a case where two session files contain the same agentSessionId and assert both files are written.

🧪 Suggested test addition
 describe('updateSessionNameByClaudeSessionId()', () => {
+	it('updates matching agentSessionId across multiple session files', async () => {
+		mockExistsSync.mockReturnValue(true);
+		mockReaddirSync.mockReturnValue([
+			'sess_a.json' as unknown as fs.Dirent,
+			'sess_b.json' as unknown as fs.Dirent,
+		]);
+
+		mockReadFileSync.mockImplementation((p: string | fs.PathLike) => {
+			const s = p.toString();
+			if (s.includes('sess_a.json')) {
+				return createHistoryFileData('sess_a', [
+					createMockEntry({ id: 'a1', agentSessionId: 'agent-123', sessionName: 'old' }),
+				]);
+			}
+			if (s.includes('sess_b.json')) {
+				return createHistoryFileData('sess_b', [
+					createMockEntry({ id: 'b1', agentSessionId: 'agent-123', sessionName: 'old' }),
+				]);
+			}
+			return '{}';
+		});
+
+		const count = await manager.updateSessionNameByClaudeSessionId('agent-123', 'new-name');
+		expect(count).toBe(2);
+		expect(mockWriteFileSync).toHaveBeenCalledTimes(2);
+	});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/history-manager.test.ts` around lines 1057 - 1118, Add a
new test in history-manager.test.ts for
manager.updateSessionNameByClaudeSessionId that exercises multiple session
files: mock fs.existsSync to true and fs.readdirSync to return two filenames
(e.g., 'sess_a.json' and 'sess_b.json'), make mockReadFileSync return different
file contents where both files include entries with the same agentSessionId
('agent-123'), call updateSessionNameByClaudeSessionId('agent-123','new-name'),
assert the returned count equals the total updates across both files, and assert
mockWriteFileSync was called twice with each written file JSON containing the
updated sessionName for entries in both sess_a and sess_b; reference
manager.updateSessionNameByClaudeSessionId and verify written payloads by
parsing mockWriteFileSync.mock.calls.
src/main/history-manager.ts (1)

391-417: Prefer async session listing inside async methods.

getEntriesByProjectPath* and clearByProjectPath are async but still call listSessionsWithHistorySync(), which keeps blocking filesystem work on the main thread.

♻️ Suggested refactor
 async getEntriesByProjectPath(projectPath: string): Promise<HistoryEntry[]> {
-	const sessions = this.listSessionsWithHistorySync();
+	const sessions = await this.listSessionsWithHistory();
 	const entries: HistoryEntry[] = [];
 	...
 }

 async getEntriesByProjectPathPaginated(
 	projectPath: string,
 	options?: PaginationOptions
 ): Promise<PaginatedResult<HistoryEntry>> {
-	const sessions = this.listSessionsWithHistorySync();
+	const sessions = await this.listSessionsWithHistory();
 	const entries: HistoryEntry[] = [];
 	...
 }

 async clearByProjectPath(projectPath: string): Promise<void> {
-	const sessions = this.listSessionsWithHistorySync();
+	const sessions = await this.listSessionsWithHistory();
 	for (const sessionId of sessions) {
 		...
 	}
 }

Also applies to: 502-504

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

In `@src/main/history-manager.ts` around lines 391 - 417, The methods
getEntriesByProjectPath, getEntriesByProjectPathPaginated (and
clearByProjectPath) are async but call the blocking
listSessionsWithHistorySync(); replace that sync call with the non-blocking
async session lister (e.g., await listSessionsWithHistory() or the equivalent
async method) so filesystem work runs off the main thread, update each method to
await the async call, propagate errors as needed, and verify behavior of session
iteration using the existing getEntries(sessionId) flow remains unchanged.
src/main/stats/stats-db.ts (3)

449-490: Inconsistent sync filesystem usage in getAvailableBackups().

This method still uses synchronous fs.readdirSync and fs.statSync while other methods in this class have been migrated to async fsp.* equivalents. Consider converting this to async for consistency with the rest of the class, especially since callers may not expect blocking I/O from a method in an otherwise async-first class.

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

In `@src/main/stats/stats-db.ts` around lines 449 - 490, The getAvailableBackups()
method uses blocking fs.readdirSync and fs.statSync while the class uses async
fsp APIs; change getAvailableBackups to an async function (e.g., async
getAvailableBackups()) and replace readdirSync/statSync with await
fsp.readdir(dir) and await fsp.stat(fullPath) (or the existing fsp alias used in
this file), update callers if necessary to await the method, preserve the same
backup filename regex logic for daily and timestamp matches and keep the
try/catch with logger.warn(…) on error.

97-99: Simplify early return in async function.

Since initialize() is now async, the early return on line 98 can simply be return; or return undefined; rather than explicitly returning Promise.resolve(). Async functions automatically wrap return values in a resolved promise.

Suggested simplification
 	async initialize(): Promise<void> {
 		if (this.initialized) {
-			return Promise.resolve();
+			return;
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/stats/stats-db.ts` around lines 97 - 99, The initialize() async
method uses an explicit Promise.resolve() for an early return; replace the
explicit Promise.resolve() with a plain return (or return undefined) to let the
async function implicitly return a resolved promise—update the check in
initialize() that references this.initialized to simply do "if
(this.initialized) return;" so the behavior remains the same but is idiomatic
for async functions.

496-534: restoreFromBackup() uses sync filesystem calls.

This method uses fs.existsSync, fs.unlinkSync, and fs.copyFileSync while the rest of the class has migrated to async patterns. The method returns boolean synchronously, which limits its integration with async flows. Consider converting to async and returning Promise<boolean> for consistency with other backup-related methods like backupDatabase().

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

In `@src/main/stats/stats-db.ts` around lines 496 - 534, The restoreFromBackup
function uses synchronous fs calls and returns boolean; change it to async
restoreFromBackup(backupPath: string): Promise<boolean> and convert all sync
operations to promises (use fs.promises.access/stat instead of existsSync,
fs.promises.unlink instead of unlinkSync, fs.promises.copyFile instead of
copyFileSync). Ensure you await closing the DB (handle this.db.close() if it
returns a promise or wrap in Promise.resolve), set this.db = null and
this.initialized = false after close, and wrap the whole flow in try/catch to
log errors with logger.error (preserve LOG_CONTEXT) and return false on failure
or true on success; also update any callers to await restoreFromBackup.
src/main/ipc/handlers/tabNaming.ts (1)

250-259: Consider handling spawn rejection.

Using void discards the Promise, but if processManager.spawn() rejects (e.g., command not found), the rejection will be unhandled. While the outer try-catch at line 261 catches synchronous errors, it won't catch async rejections from the voided Promise.

Consider either awaiting the spawn or adding a .catch() to handle potential rejections:

Option: Add catch handler
-						void processManager.spawn({
+						processManager.spawn({
 							sessionId,
 							toolType: config.agentType,
 							cwd,
 							command,
 							args: finalArgs,
 							prompt: fullPrompt,
 							customEnvVars,
 							sendPromptViaStdin: shouldSendPromptViaStdin,
-						});
+						}).catch((err) => {
+							if (!resolved) {
+								resolved = true;
+								clearTimeout(timeoutId);
+								processManager.off('data', onData);
+								processManager.off('exit', onExit);
+								logger.error('Tab naming spawn failed', LOG_CONTEXT, { sessionId, error: String(err) });
+								resolve(null);
+							}
+						});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/tabNaming.ts` around lines 250 - 259, The voided call
to processManager.spawn({ sessionId, toolType: config.agentType, cwd, command,
args: finalArgs, prompt: fullPrompt, customEnvVars, sendPromptViaStdin:
shouldSendPromptViaStdin }) can reject and cause an unhandled promise rejection;
update the call to handle async failures by either awaiting the promise inside
the surrounding async function or chaining a .catch(...) handler that logs the
error and reports it back (e.g., via processLogger or the IPC error response) so
spawn rejections (like command-not-found) are handled instead of discarded.
src/__tests__/main/ipc/handlers/git.test.ts (1)

3176-3179: Relax the order assertion to avoid brittle concurrency tests.

The current assertion requires a strict main-then-master launch order, which is more implementation-specific than necessary for validating parallel checks.

♻️ Suggested assertion tweak
-			expect(callOrder).toEqual(['remote', 'main', 'master']);
+			expect(callOrder[0]).toBe('remote');
+			expect(callOrder).toHaveLength(3);
+			expect(new Set(callOrder.slice(1))).toEqual(new Set(['main', 'master']));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/ipc/handlers/git.test.ts` around lines 3176 - 3179, The
test is brittle because expect(callOrder).toEqual(['remote', 'main', 'master'])
enforces a specific ordering for concurrent launches; update the assertion
around callOrder so it only requires that 'remote' was called first and that
both 'main' and 'master' were invoked (in any order) thereafter—for example,
assert callOrder[0] === 'remote' and that callOrder contains both 'main' and
'master' (or compare sets) instead of exact array equality; locate the failing
assertion using the variable name callOrder and the expect(callOrder).toEqual
invocation to make the change.
src/main/ipc/handlers/agentSessions.ts (1)

442-468: Consider deduplicating concurrent cache misses in session discovery.

When cache is stale, simultaneous calls can each trigger full Claude/Codex scans before the first one repopulates cache. A single in-flight Promise guard avoids redundant heavy I/O.

♻️ Proposed refactor
+let sessionDiscoveryInFlight: Promise<{
+	claudeFiles: SessionFileInfo[];
+	codexFiles: SessionFileInfo[];
+}> | null = null;
+
 async function discoverSessionFilesWithCache(): Promise<{
 	claudeFiles: SessionFileInfo[];
 	codexFiles: SessionFileInfo[];
 }> {
 	if (isSessionDiscoveryCacheFresh(sessionDiscoveryCache)) {
 		return {
 			claudeFiles: [...sessionDiscoveryCache!.claudeFiles],
 			codexFiles: [...sessionDiscoveryCache!.codexFiles],
 		};
 	}
 
-	const [claudeFiles, codexFiles] = await Promise.all([
-		discoverClaudeSessionFiles(),
-		discoverCodexSessionFiles(),
-	]);
-
-	sessionDiscoveryCache = {
-		timestampMs: Date.now(),
-		claudeFiles,
-		codexFiles,
-	};
-
-	return {
-		claudeFiles: [...claudeFiles],
-		codexFiles: [...codexFiles],
-	};
+	if (sessionDiscoveryInFlight) {
+		return sessionDiscoveryInFlight;
+	}
+
+	sessionDiscoveryInFlight = (async () => {
+		const [claudeFiles, codexFiles] = await Promise.all([
+			discoverClaudeSessionFiles(),
+			discoverCodexSessionFiles(),
+		]);
+
+		sessionDiscoveryCache = {
+			timestampMs: Date.now(),
+			claudeFiles,
+			codexFiles,
+		};
+
+		return {
+			claudeFiles: [...claudeFiles],
+			codexFiles: [...codexFiles],
+		};
+	})();
+
+	try {
+		return await sessionDiscoveryInFlight;
+	} finally {
+		sessionDiscoveryInFlight = null;
+	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/agentSessions.ts` around lines 442 - 468,
discoverSessionFilesWithCache currently lets concurrent callers trigger
duplicate discoverClaudeSessionFiles/discoverCodexSessionFiles work when the
cache is stale; add an in-flight promise guard (e.g., a module-scoped
sessionDiscoveryPromise) so only the first caller starts
Promise.all([discoverClaudeSessionFiles(), discoverCodexSessionFiles()]) and
others await that same promise. On cache miss: if sessionDiscoveryPromise exists
await it and return the cache snapshot; otherwise set sessionDiscoveryPromise =
(the Promise that runs the discovery and populates sessionDiscoveryCache), await
it, then clear sessionDiscoveryPromise in finally and return the cloned results.
Use the existing symbols discoverSessionFilesWithCache, sessionDiscoveryCache,
discoverClaudeSessionFiles, discoverCodexSessionFiles to locate where to add the
guard.
src/renderer/components/AICommandsPanel.tsx (2)

275-277: Indentation inconsistency.

The onClick handler on line 275 appears to use spaces instead of tabs, breaking alignment with surrounding code. As per coding guidelines, use tabs for indentation throughout the codebase.

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

In `@src/renderer/components/AICommandsPanel.tsx` around lines 275 - 277, The
onClick prop for the add button in AICommandsPanel's JSX (onClick={() =>
setIsCreating(true)}) uses spaces for indentation causing misalignment; update
the indentation to use tabs to match the surrounding JSX formatting, ensuring
the line containing onClick and its attributes (including className and style)
aligns with sibling props/lines in the AICommandsPanel component and retains the
same tab-based indentation pattern as the file.

301-316: Indentation inconsistency in form inputs.

Multiple input elements and their closing tags have inconsistent indentation (appears to use spaces instead of tabs). This affects lines 301-316 in the create form and lines 396-410, 416-438 in the edit form.

As per coding guidelines for src/**/*.{ts,tsx,js,jsx,json}: Use tabs for indentation, not spaces.

Also applies to: 396-410

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

In `@src/renderer/components/AICommandsPanel.tsx` around lines 301 - 316, The JSX
inputs in AICommandsPanel (the create and edit forms around the newCommand
inputs and their closing tags) use space indentation instead of tabs; update the
indentation to tabs for the <input> and surrounding JSX elements (references:
newCommand, setNewCommand, commandStyles.fieldBase, and the edit form
counterparts) so all lines in the create form and edit form blocks consistently
use tabs per project style.
🤖 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/main/history-manager.ts`:
- Around line 459-493: The loop in updateSessionNameByClaudeSessionId stops
after updating the first matching file because of the stray "break" after the
write; remove that "break" so the outer loop over parsedSessions continues and
updates all parsed.data.entries matching agentSessionId. Keep the per-file
try/catch and logging (references: parsedSessions, parsed, parsed.filePath,
parsed.data.entries, agentSessionId, sessionName, sessionUpdatedCount,
updatedCount) so each file is written and errors are captured independently.

In `@src/renderer/components/AgentSessionsBrowser.tsx`:
- Line 1491: The call to SessionListItem currently passes isSelected={i ===
selectedIndex} but the SessionListItemProps interface expects index: number and
selectedIndex: number; fix by changing the call site to pass the required props
(e.g. index={i} selectedIndex={selectedIndex}) and remove the isSelected prop,
or alternatively update the SessionListItemProps interface and the
SessionListItem component to accept isSelected: boolean instead of
index/selectedIndex—choose one approach and make corresponding updates to the
SessionListItem component and its usage to keep props consistent (refer to
SessionListItemProps, SessionListItem, selectedIndex, index, isSelected).

In `@src/renderer/components/AutoRun.tsx`:
- Around line 1067-1075: The highlight/search mismatch stems from passing the
untrimmed searchQuery into searchHighlight (via createMarkdownComponents) while
the regex and match counting use trimmedSearchQuery/searchQueryRegex; update all
places that set searchHighlight.query (notably the createMarkdownComponents call
in the component rendering) to pass trimmedSearchQuery instead of searchQuery so
highlightSearchMatches uses the same trimmed input as searchQueryRegex (search
for usages of createMarkdownComponents, searchHighlight, highlightSearchMatches
and replace query: searchQuery with query: trimmedSearchQuery).

In `@src/renderer/components/CsvTableRenderer.tsx`:
- Around line 152-160: The current key generation in CsvTableRenderer (using
offset derived from text.split(searchRegex)) can collide when split yields empty
strings; update the key for the mapped segments to be collision-safe by
combining the running offset with the index (or another stable discriminator) so
each element's key is unique (e.g., use `${offset}-${index}` or similar) when
mapping parts in the function that splits text with searchRegex and computes
offset and isMatch.
- Around line 5-6: The NUMBER_VALUE_REGEX is too permissive (accepts multiple
signs/paren like `(123`, `--10`, `((123))`) causing isNumericValue() to
misclassify values and skew detectColumnAlignments(); tighten NUMBER_VALUE_REGEX
to allow at most one optional leading sign or a single pair of surrounding
parentheses (i.e., either an optional leading '-' or a single surrounding '('
... ')' pair), permit an optional single currency symbol and optional trailing
'%' but disallow repeated '-', '(' or ')' characters and require balanced
parens; update the constant NUMBER_VALUE_REGEX accordingly (so isNumericValue()
uses the stricter pattern) while leaving REGEX_ESCAPE_REGEX unchanged.

In `@src/renderer/components/SessionList.tsx`:
- Around line 1573-1714: The review points out that sessionListStyles.skinny.*
is undefined; update all references that read
sessionListStyles.skinny.skinnyDotFallback,
sessionListStyles.skinny.unreadBadge, and sessionListStyles.skinny.skinnyTooltip
(used in collapsed/skinny mode) to instead read
sessionListStyles.root.skinnyDotFallback, sessionListStyles.root.unreadBadge,
and sessionListStyles.root.skinnyTooltip so they reference the actual style
definitions returned by the useMemo that defines sessionListStyles.
- Around line 894-963: The memo for tooltipStyles can become stale because it
depends on session.contextUsage and the threshold props but only lists
theme.colors in its dependency array; update the useMemo dependencies for
tooltipStyles to include session.contextUsage, contextWarningYellowThreshold,
and contextWarningRedThreshold (in addition to theme.colors) so that
contextUsageColor (from getContextColor) and the width
`${session.contextUsage}%` are recalculated when those values change.
- Around line 121-173: The useMemo for styles includes undefined identifiers
(session.contextUsage, contextWarningYellowThreshold,
contextWarningRedThreshold) in its dependency array which will throw at runtime;
update the dependency array for the useMemo that computes styles (the useMemo
that returns container/textMain/textAccent/... in SessionContextMenu / styles)
to only include the actual used values (e.g., theme.colors or theme) and remove
session.contextUsage, contextWarningYellowThreshold, and
contextWarningRedThreshold from the array.

---

Outside diff comments:
In `@src/__tests__/main/stats/stats-db.test.ts`:
- Around line 560-582: The tests are asserting against the old synchronous mkdir
mock (mockFsMkdirSync) but the production code now uses the async fsp.mkdir
mocked as mockFsMkdir; update the two expectations in the StatsDB.initialize
tests to assert on mockFsMkdir (not mockFsMkdirSync) being called with
mockUserDataPath and { recursive: true } when mockFsExistsSync returns false,
and to assert mockFsMkdir was not called when mockFsExistsSync returns true;
ensure you reference the StatsDB.initialize flow and keep mockFsExistsSync and
mockUserDataPath as the inputs under test.

In `@src/main/group-chat/group-chat-router.ts`:
- Around line 547-569: The code calls processManager.spawn(...) and immediately
proceeds without checking spawnResult.success; update each spawn call site (the
spawn block that assigns spawnResult, including uses of processManager.spawn,
spawnResult, sessionId, and chat.moderatorAgentId) to verify if
spawnResult.success is false, and if so log the error (include
spawnResult.error/details), revert any state changes made before the spawn
(e.g., unmark participants as pending or clear pending process ids), emit an
appropriate failure response or throw/return to stop further processing, and
ensure no success-path logs or state transitions run when spawnResult.success is
false; apply the same guard-and-rollback pattern to the other three spawn blocks
referenced in the comment.
- Around line 1256-1283: The synthesis spawn path currently calls
processManager.spawn(...) directly and ignores
chat.moderatorConfig?.sshRemoteConfig; update the logic to mirror other
group-chat spawn flows by using wrapSpawnWithSsh(...) from ssh-spawn-wrapper.ts
when sshRemoteConfig is present: build the same spawn options you pass into
processManager.spawn (including command, finalArgs, cwd,
shell/runInShell/sendPromptViaStdin flags, prompt, customEnvVars from
configResolution.effectiveCustomEnvVars or getCustomEnvVarsCallback, promptArgs,
noPromptSeparator, contextWindow from getContextWindowValue(agent,
agentConfigValues), etc.), then call wrapSpawnWithSsh(spawnOptions,
chat.moderatorConfig.sshRemoteConfig, <ssh store adapter>) so the wrapped call
delegates to processManager.spawn for local runs and to the SSH wrapper for
remote runs; ensure getWindowsSpawnConfig and its
shell/runInShell/sendPromptViaStdin flags are preserved in the spawnOptions you
wrap.

In `@src/main/utils/context-groomer.ts`:
- Around line 206-337: The Promise executor is using await
(processManager.spawn) but is not async; wrap the spawn logic in an async IIFE
(e.g., (async () => { const spawnResult = await processManager.spawn(...); /*
existing spawn handling */ })().catch(err => { cleanup(); if (!resolved) {
resolved = true; logger.error('Grooming spawn failed', LOG_CONTEXT, {
groomerSessionId, error: String(err) }); reject(err); } })); ensure the IIFE
uses the same variables/callbacks (onData, onExit, onError, cancelOperation,
finishWithResponse, cleanup, activeGroomingSessions) and that any error path
performs cleanup, sets resolved/cancelled appropriately, and rejects the outer
Promise.

In `@src/renderer/components/SessionListItem.tsx`:
- Around line 44-50: The props interface is inconsistent with usage — update
SessionListItemProps to accept isSelected: boolean (remove index and
selectedIndex) so it matches how AgentSessionsBrowser.tsx calls SessionListItem,
and then remove the internal computed isSelected = index === selectedIndex in
the SessionListItem component (the line around the current line 109) so the
component uses the passed-in isSelected prop instead.

---

Nitpick comments:
In `@src/__tests__/main/history-manager.test.ts`:
- Around line 1057-1118: Add a new test in history-manager.test.ts for
manager.updateSessionNameByClaudeSessionId that exercises multiple session
files: mock fs.existsSync to true and fs.readdirSync to return two filenames
(e.g., 'sess_a.json' and 'sess_b.json'), make mockReadFileSync return different
file contents where both files include entries with the same agentSessionId
('agent-123'), call updateSessionNameByClaudeSessionId('agent-123','new-name'),
assert the returned count equals the total updates across both files, and assert
mockWriteFileSync was called twice with each written file JSON containing the
updated sessionName for entries in both sess_a and sess_b; reference
manager.updateSessionNameByClaudeSessionId and verify written payloads by
parsing mockWriteFileSync.mock.calls.

In `@src/__tests__/main/ipc/handlers/git.test.ts`:
- Around line 3176-3179: The test is brittle because
expect(callOrder).toEqual(['remote', 'main', 'master']) enforces a specific
ordering for concurrent launches; update the assertion around callOrder so it
only requires that 'remote' was called first and that both 'main' and 'master'
were invoked (in any order) thereafter—for example, assert callOrder[0] ===
'remote' and that callOrder contains both 'main' and 'master' (or compare sets)
instead of exact array equality; locate the failing assertion using the variable
name callOrder and the expect(callOrder).toEqual invocation to make the change.

In `@src/main/debug-package/collectors/group-chats.ts`:
- Around line 51-80: The loop in group-chats.ts that reads and parses each chat
file (iterating over files, building filePath, reading with
fs.promises.readFile, parsing JSON, calling countMessages, and pushing
GroupChatInfo into groupChats) runs serially and should be parallelized: replace
the for-loop with a concurrency-safe parallel approach (e.g., filter files to
the .json ones, map them to async worker promises that perform readFile,
JSON.parse, call countMessages, build the GroupChatInfo using chat.id,
chat.moderatorAgentId/chat.moderator?.agentId, participants,
createdAt/updatedAt, etc.), then await Promise.all or Promise.allSettled (or use
a p-limit to bound concurrency) and push/collect results into groupChats; also
ensure individual file errors are caught so one failure doesn't reject the whole
batch (handle per-promise try/catch or use allSettled and log/skips failures).

In `@src/main/history-manager.ts`:
- Around line 391-417: The methods getEntriesByProjectPath,
getEntriesByProjectPathPaginated (and clearByProjectPath) are async but call the
blocking listSessionsWithHistorySync(); replace that sync call with the
non-blocking async session lister (e.g., await listSessionsWithHistory() or the
equivalent async method) so filesystem work runs off the main thread, update
each method to await the async call, propagate errors as needed, and verify
behavior of session iteration using the existing getEntries(sessionId) flow
remains unchanged.

In `@src/main/ipc/handlers/agentSessions.ts`:
- Around line 442-468: discoverSessionFilesWithCache currently lets concurrent
callers trigger duplicate discoverClaudeSessionFiles/discoverCodexSessionFiles
work when the cache is stale; add an in-flight promise guard (e.g., a
module-scoped sessionDiscoveryPromise) so only the first caller starts
Promise.all([discoverClaudeSessionFiles(), discoverCodexSessionFiles()]) and
others await that same promise. On cache miss: if sessionDiscoveryPromise exists
await it and return the cache snapshot; otherwise set sessionDiscoveryPromise =
(the Promise that runs the discovery and populates sessionDiscoveryCache), await
it, then clear sessionDiscoveryPromise in finally and return the cloned results.
Use the existing symbols discoverSessionFilesWithCache, sessionDiscoveryCache,
discoverClaudeSessionFiles, discoverCodexSessionFiles to locate where to add the
guard.

In `@src/main/ipc/handlers/tabNaming.ts`:
- Around line 250-259: The voided call to processManager.spawn({ sessionId,
toolType: config.agentType, cwd, command, args: finalArgs, prompt: fullPrompt,
customEnvVars, sendPromptViaStdin: shouldSendPromptViaStdin }) can reject and
cause an unhandled promise rejection; update the call to handle async failures
by either awaiting the promise inside the surrounding async function or chaining
a .catch(...) handler that logs the error and reports it back (e.g., via
processLogger or the IPC error response) so spawn rejections (like
command-not-found) are handled instead of discarded.

In `@src/main/stats/stats-db.ts`:
- Around line 449-490: The getAvailableBackups() method uses blocking
fs.readdirSync and fs.statSync while the class uses async fsp APIs; change
getAvailableBackups to an async function (e.g., async getAvailableBackups()) and
replace readdirSync/statSync with await fsp.readdir(dir) and await
fsp.stat(fullPath) (or the existing fsp alias used in this file), update callers
if necessary to await the method, preserve the same backup filename regex logic
for daily and timestamp matches and keep the try/catch with logger.warn(…) on
error.
- Around line 97-99: The initialize() async method uses an explicit
Promise.resolve() for an early return; replace the explicit Promise.resolve()
with a plain return (or return undefined) to let the async function implicitly
return a resolved promise—update the check in initialize() that references
this.initialized to simply do "if (this.initialized) return;" so the behavior
remains the same but is idiomatic for async functions.
- Around line 496-534: The restoreFromBackup function uses synchronous fs calls
and returns boolean; change it to async restoreFromBackup(backupPath: string):
Promise<boolean> and convert all sync operations to promises (use
fs.promises.access/stat instead of existsSync, fs.promises.unlink instead of
unlinkSync, fs.promises.copyFile instead of copyFileSync). Ensure you await
closing the DB (handle this.db.close() if it returns a promise or wrap in
Promise.resolve), set this.db = null and this.initialized = false after close,
and wrap the whole flow in try/catch to log errors with logger.error (preserve
LOG_CONTEXT) and return false on failure or true on success; also update any
callers to await restoreFromBackup.

In `@src/renderer/components/AICommandsPanel.tsx`:
- Around line 275-277: The onClick prop for the add button in AICommandsPanel's
JSX (onClick={() => setIsCreating(true)}) uses spaces for indentation causing
misalignment; update the indentation to use tabs to match the surrounding JSX
formatting, ensuring the line containing onClick and its attributes (including
className and style) aligns with sibling props/lines in the AICommandsPanel
component and retains the same tab-based indentation pattern as the file.
- Around line 301-316: The JSX inputs in AICommandsPanel (the create and edit
forms around the newCommand inputs and their closing tags) use space indentation
instead of tabs; update the indentation to tabs for the <input> and surrounding
JSX elements (references: newCommand, setNewCommand, commandStyles.fieldBase,
and the edit form counterparts) so all lines in the create form and edit form
blocks consistently use tabs per project style.

In `@src/renderer/components/AutoRun.tsx`:
- Around line 725-729: completedTaskMatches is memoized but
completedTaskCountFromLocalContent only reads its .length, so remove the
redundant useMemo: keep the useMemo that computes completedTaskMatches (using
localContent.match(AUTO_RUN_COMPLETED_TASK_REGEX) || []) and change
completedTaskCountFromLocalContent to a plain derived value (e.g., const
completedTaskCountFromLocalContent = completedTaskMatches.length) referencing
completedTaskMatches directly; update any imports/usages accordingly (symbols:
completedTaskMatches, completedTaskCountFromLocalContent,
AUTO_RUN_COMPLETED_TASK_REGEX, localContent).
- Around line 1082-1102: The effect that counts regex matches is re-run on
navigation because currentMatchIndex is included in the dependency array; remove
currentMatchIndex from the dependencies of that counting effect and keep only
searchQueryRegex and localContent so navigating next/prev doesn't retrigger
counting. To preserve the existing behavior that resets the index when the match
count becomes 0 or out of range, move that index-reset logic into the same
timeout callback (already present) or into a separate effect that depends on
totalMatches and currentMatchIndex (use setCurrentMatchIndex with a
conditional/fallback), and ensure you still clear searchCountTimeoutRef via the
existing cleanup; update references to searchQueryRegex, localContent,
searchCountTimeoutRef, setTotalMatches, and setCurrentMatchIndex accordingly.

In `@src/renderer/components/FilePreview.tsx`:
- Around line 1616-1667: The effect uses searchRegex in its guard but doesn’t
include it in the dependency array and also includes redundant editContent;
update the useEffect dependencies for the effect that updates
editModeSearchMatches/navigation by adding searchRegex and removing editContent
so the array reads include editModeSearchMatches, currentMatchIndex,
isEditableText, markdownEditMode, searchQuery, searchRegex (refer to useEffect,
editModeSearchMatches, currentMatchIndex, searchQuery, searchRegex, editContent,
textareaRef in the diff).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba80730 and d6bd0bc.

📒 Files selected for processing (56)
  • src/__tests__/integration/group-chat.integration.test.ts
  • src/__tests__/main/debug-package/collectors.test.ts
  • src/__tests__/main/history-manager.test.ts
  • src/__tests__/main/ipc/handlers/agentSessions.test.ts
  • src/__tests__/main/ipc/handlers/director-notes.test.ts
  • src/__tests__/main/ipc/handlers/git.test.ts
  • src/__tests__/main/ipc/handlers/history.test.ts
  • src/__tests__/main/parsers/codex-output-parser.test.ts
  • src/__tests__/main/process-manager/spawners/ChildProcessSpawner.test.ts
  • src/__tests__/main/stats/aggregations.test.ts
  • src/__tests__/main/stats/auto-run.test.ts
  • src/__tests__/main/stats/data-management.test.ts
  • src/__tests__/main/stats/integration.test.ts
  • src/__tests__/main/stats/paths.test.ts
  • src/__tests__/main/stats/query-events.test.ts
  • src/__tests__/main/stats/stats-db.test.ts
  • src/__tests__/main/web-server/web-server-factory.test.ts
  • src/main/debug-package/collectors/group-chats.ts
  • src/main/debug-package/collectors/storage.ts
  • src/main/group-chat/group-chat-agent.ts
  • src/main/group-chat/group-chat-moderator.ts
  • src/main/group-chat/group-chat-router.ts
  • src/main/history-manager.ts
  • src/main/index.ts
  • src/main/ipc/handlers/agentSessions.ts
  • src/main/ipc/handlers/director-notes.ts
  • src/main/ipc/handlers/git.ts
  • src/main/ipc/handlers/groupChat.ts
  • src/main/ipc/handlers/history.ts
  • src/main/ipc/handlers/process.ts
  • src/main/ipc/handlers/tabNaming.ts
  • src/main/parsers/codex-output-parser.ts
  • src/main/parsers/index.ts
  • src/main/process-manager/ProcessManager.ts
  • src/main/process-manager/spawners/ChildProcessSpawner.ts
  • src/main/process-manager/spawners/PtySpawner.ts
  • src/main/process-manager/utils/imageUtils.ts
  • src/main/stats/aggregations.ts
  • src/main/stats/singleton.ts
  • src/main/stats/stats-db.ts
  • src/main/utils/context-groomer.ts
  • src/main/utils/wslDetector.ts
  • src/main/web-server/WebServer.ts
  • src/main/web-server/managers/CallbackRegistry.ts
  • src/main/web-server/routes/apiRoutes.ts
  • src/main/web-server/types.ts
  • src/main/web-server/web-server-factory.ts
  • src/renderer/components/AICommandsPanel.tsx
  • src/renderer/components/AgentSessionsBrowser.tsx
  • src/renderer/components/AutoRun.tsx
  • src/renderer/components/CsvTableRenderer.tsx
  • src/renderer/components/ExecutionQueueIndicator.tsx
  • src/renderer/components/FilePreview.tsx
  • src/renderer/components/SessionList.tsx
  • src/renderer/components/SessionListItem.tsx
  • src/renderer/components/Toast.tsx
Comment on lines +459 to 493
for (const parsed of parsedSessions) {
if (!parsed) {
continue;
}

try {
const data: HistoryFileData = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
let modified = false;

for (const entry of data.entries) {
if (entry.agentSessionId === agentSessionId && entry.sessionName !== sessionName) {
entry.sessionName = sessionName;
modified = true;
updatedCount++;
}
let sessionUpdatedCount = 0;
for (const entry of parsed.data.entries) {
if (entry.agentSessionId === agentSessionId && entry.sessionName !== sessionName) {
entry.sessionName = sessionName;
sessionUpdatedCount++;
updatedCount++;
}
}

if (modified) {
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
if (sessionUpdatedCount > 0) {
try {
await fs.promises.writeFile(
parsed.filePath,
JSON.stringify(parsed.data, null, 2),
'utf-8'
);
logger.debug(
`Updated ${updatedCount} entries for agentSessionId ${agentSessionId} in session ${sessionId}`,
`Updated ${sessionUpdatedCount} entries for agentSessionId ${agentSessionId} in session ${parsed.sessionId}`,
LOG_CONTEXT
);
} catch (error) {
logger.warn(
`Failed to update sessionName in session ${parsed.sessionId}: ${error}`,
LOG_CONTEXT
);
captureException(error, { operation: 'history:updateSessionNameWrite', sessionId: parsed.sessionId });
}
} catch (error) {
logger.warn(`Failed to update sessionName in session ${sessionId}: ${error}`, LOG_CONTEXT);
captureException(error, { operation: 'history:updateSessionName', sessionId });

break;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

updateSessionNameByClaudeSessionId stops after the first updated file.

Line 492 breaks the outer loop after one matching session file, so later files with the same agentSessionId are not updated.

💡 Proposed fix
 			if (sessionUpdatedCount > 0) {
 				try {
 					await fs.promises.writeFile(
 						parsed.filePath,
 						JSON.stringify(parsed.data, null, 2),
 						'utf-8'
 					);
 					logger.debug(
 						`Updated ${sessionUpdatedCount} entries for agentSessionId ${agentSessionId} in session ${parsed.sessionId}`,
 						LOG_CONTEXT
 					);
 				} catch (error) {
 					logger.warn(
 						`Failed to update sessionName in session ${parsed.sessionId}: ${error}`,
 						LOG_CONTEXT
 					);
 					captureException(error, { operation: 'history:updateSessionNameWrite', sessionId: parsed.sessionId });
 				}
-
-				break;
 			}
 		}
📝 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
for (const parsed of parsedSessions) {
if (!parsed) {
continue;
}
try {
const data: HistoryFileData = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
let modified = false;
for (const entry of data.entries) {
if (entry.agentSessionId === agentSessionId && entry.sessionName !== sessionName) {
entry.sessionName = sessionName;
modified = true;
updatedCount++;
}
let sessionUpdatedCount = 0;
for (const entry of parsed.data.entries) {
if (entry.agentSessionId === agentSessionId && entry.sessionName !== sessionName) {
entry.sessionName = sessionName;
sessionUpdatedCount++;
updatedCount++;
}
}
if (modified) {
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
if (sessionUpdatedCount > 0) {
try {
await fs.promises.writeFile(
parsed.filePath,
JSON.stringify(parsed.data, null, 2),
'utf-8'
);
logger.debug(
`Updated ${updatedCount} entries for agentSessionId ${agentSessionId} in session ${sessionId}`,
`Updated ${sessionUpdatedCount} entries for agentSessionId ${agentSessionId} in session ${parsed.sessionId}`,
LOG_CONTEXT
);
} catch (error) {
logger.warn(
`Failed to update sessionName in session ${parsed.sessionId}: ${error}`,
LOG_CONTEXT
);
captureException(error, { operation: 'history:updateSessionNameWrite', sessionId: parsed.sessionId });
}
} catch (error) {
logger.warn(`Failed to update sessionName in session ${sessionId}: ${error}`, LOG_CONTEXT);
captureException(error, { operation: 'history:updateSessionName', sessionId });
break;
}
for (const parsed of parsedSessions) {
if (!parsed) {
continue;
}
let sessionUpdatedCount = 0;
for (const entry of parsed.data.entries) {
if (entry.agentSessionId === agentSessionId && entry.sessionName !== sessionName) {
entry.sessionName = sessionName;
sessionUpdatedCount++;
updatedCount++;
}
}
if (sessionUpdatedCount > 0) {
try {
await fs.promises.writeFile(
parsed.filePath,
JSON.stringify(parsed.data, null, 2),
'utf-8'
);
logger.debug(
`Updated ${sessionUpdatedCount} entries for agentSessionId ${agentSessionId} in session ${parsed.sessionId}`,
LOG_CONTEXT
);
} catch (error) {
logger.warn(
`Failed to update sessionName in session ${parsed.sessionId}: ${error}`,
LOG_CONTEXT
);
captureException(error, { operation: 'history:updateSessionNameWrite', sessionId: parsed.sessionId });
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/history-manager.ts` around lines 459 - 493, The loop in
updateSessionNameByClaudeSessionId stops after updating the first matching file
because of the stray "break" after the write; remove that "break" so the outer
loop over parsedSessions continues and updates all parsed.data.entries matching
agentSessionId. Keep the per-file try/catch and logging (references:
parsedSessions, parsed, parsed.filePath, parsed.data.entries, agentSessionId,
sessionName, sessionUpdatedCount, updatedCount) so each file is written and
errors are captured independently.
session={session}
index={i}
selectedIndex={selectedIndex}
isSelected={i === selectedIndex}
Copy link

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:

fd SessionListItem.tsx src/renderer/components

Repository: RunMaestro/Maestro

Length of output: 105


🏁 Script executed:

cd src/renderer/components && cat -n AgentSessionsBrowser.tsx | sed -n '1485,1500p'

Repository: RunMaestro/Maestro

Length of output: 852


🏁 Script executed:

cd src/renderer/components && cat -n SessionListItem.tsx | sed -n '40,60p'

Repository: RunMaestro/Maestro

Length of output: 894


🏁 Script executed:

cd src/renderer/components && cat -n SessionListItem.tsx | sed -n '44,80p'

Repository: RunMaestro/Maestro

Length of output: 1900


🏁 Script executed:

cd src/renderer/components && cat -n AgentSessionsBrowser.tsx | sed -n '1488,1510p'

Repository: RunMaestro/Maestro

Length of output: 1157


🏁 Script executed:

cd src/renderer/components && cat -n SessionListItem.tsx | sed -n '80,90p'

Repository: RunMaestro/Maestro

Length of output: 406


Props mismatch with SessionListItemProps interface causes TypeScript compilation error.

The SessionListItemProps interface requires index: number and selectedIndex: number props, but this call site passes isSelected: boolean instead and omits the required index and selectedIndex props. This will cause a TypeScript error.

Either update the interface to replace index and selectedIndex with isSelected, or pass the required props from the call site.

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

In `@src/renderer/components/AgentSessionsBrowser.tsx` at line 1491, The call to
SessionListItem currently passes isSelected={i === selectedIndex} but the
SessionListItemProps interface expects index: number and selectedIndex: number;
fix by changing the call site to pass the required props (e.g. index={i}
selectedIndex={selectedIndex}) and remove the isSelected prop, or alternatively
update the SessionListItemProps interface and the SessionListItem component to
accept isSelected: boolean instead of index/selectedIndex—choose one approach
and make corresponding updates to the SessionListItem component and its usage to
keep props consistent (refer to SessionListItemProps, SessionListItem,
selectedIndex, index, isSelected).
Comment on lines +152 to +160
const parts = text.split(searchRegex);
if (parts.length === 1) return text;
// Use running character offset as key to guarantee uniqueness across
// identical substrings appearing at different positions.
let offset = 0;
return parts.map((part) => {
return parts.map((part, index) => {
const key = offset;
offset += part.length;
return regex.test(part) ? (
const isMatch = index % 2 === 1;
Copy link

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:

cat -n src/renderer/components/CsvTableRenderer.tsx | sed -n '140,170p'

Repository: RunMaestro/Maestro

Length of output: 1041


🏁 Script executed:

python3 << 'EOF'
import re

# Test cases to see if split can produce empty segments
test_cases = [
    ("hello world", r"\s+", "space separated"),
    ("a,b,c", r",", "comma separated"),
    ("a,,b", r",", "consecutive delimiters"),
    ("hello", r"l", "substring match"),
    ("test", r"e|s", "alternation"),
    ("aaa", r"a", "all chars match"),
    ("aaa", r"a*", "zero-width match"),
]

print("Testing split() behavior with various regex patterns:\n")
for text, pattern, desc in test_cases:
    parts = re.split(pattern, text)
    print(f"Text: '{text}' | Pattern: {pattern} | {desc}")
    print(f"  Parts: {parts}")
    print(f"  Empty parts present: {'' in parts}")
    
    # Simulate the offset key calculation
    offset = 0
    offsets = []
    for part in parts:
        offsets.append(offset)
        offset += len(part)
    print(f"  Offset sequence: {offsets}")
    print(f"  Duplicate offsets: {len(offsets) != len(set(offsets))}")
    print()
EOF

Repository: RunMaestro/Maestro

Length of output: 1206


🏁 Script executed:

rg "highlightMatches" src/renderer/components/CsvTableRenderer.tsx -A 2 -B 2

Repository: RunMaestro/Maestro

Length of output: 369


🏁 Script executed:

rg "searchMatchRegex" src/renderer/components/CsvTableRenderer.tsx -B 3 -A 3

Repository: RunMaestro/Maestro

Length of output: 627


🏁 Script executed:

rg "REGEX_ESCAPE_REGEX" src/renderer/components/CsvTableRenderer.tsx -B 2 -A 2

Repository: RunMaestro/Maestro

Length of output: 406


Use a collision-safe key for highlighted segments.

The key={offset} at line 158 can duplicate when split() yields empty segments (e.g., when the match occurs at the string start, end, or when consecutive matches appear). This causes React key collisions and potential rendering issues.

Proposed fix
-		const key = offset;
+		const key = `${offset}-${index}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/CsvTableRenderer.tsx` around lines 152 - 160, The
current key generation in CsvTableRenderer (using offset derived from
text.split(searchRegex)) can collide when split yields empty strings; update the
key for the mapped segments to be collision-safe by combining the running offset
with the index (or another stable discriminator) so each element's key is unique
(e.g., use `${offset}-${index}` or similar) when mapping parts in the function
that splits text with searchRegex and computes offset and isMatch.
Comment on lines +121 to +173
const styles = useMemo(() => {
return {
container: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
},
textMain: {
color: theme.colors.textMain,
},
textAccent: {
color: theme.colors.accent,
},
textError: {
color: theme.colors.error,
},
divider: {
borderColor: theme.colors.border,
},
submenu: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
},
submenuBelowLeft: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
top: 0,
left: '100%',
marginLeft: 4,
},
submenuBelowRight: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
top: 0,
right: '100%',
marginRight: 4,
},
submenuAboveLeft: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
bottom: 0,
left: '100%',
marginLeft: 4,
},
submenuAboveRight: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
bottom: 0,
right: '100%',
marginRight: 4,
},
};
}, [theme.colors, session.contextUsage, contextWarningYellowThreshold, contextWarningRedThreshold]);

Copy link

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:

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

Repository: RunMaestro/Maestro

Length of output: 6971


🏁 Script executed:

grep -n "contextWarningYellowThreshold\|contextWarningRedThreshold" src/renderer/components/SessionList.tsx

Repository: RunMaestro/Maestro

Length of output: 961


🏁 Script executed:

sed -n '88,107p' src/renderer/components/SessionList.tsx

Repository: RunMaestro/Maestro

Length of output: 365


🏁 Script executed:

sed -n '121,172p' src/renderer/components/SessionList.tsx | grep -i "contextWarning\|contextUsage"

Repository: RunMaestro/Maestro

Length of output: 163


🏁 Script executed:

sed -n '121,172p' src/renderer/components/SessionList.tsx

Repository: RunMaestro/Maestro

Length of output: 1296


Remove undefined identifiers from useMemo dependency array.

Line 172 references contextWarningYellowThreshold, contextWarningRedThreshold, and session.contextUsage in the dependency array, but these identifiers do not exist in the SessionContextMenu function scope and are not used in the styles object. This will cause a runtime error.

Proposed fix
-	}, [theme.colors, session.contextUsage, contextWarningYellowThreshold, contextWarningRedThreshold]);
+	}, [theme.colors]);
📝 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
const styles = useMemo(() => {
return {
container: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
},
textMain: {
color: theme.colors.textMain,
},
textAccent: {
color: theme.colors.accent,
},
textError: {
color: theme.colors.error,
},
divider: {
borderColor: theme.colors.border,
},
submenu: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
},
submenuBelowLeft: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
top: 0,
left: '100%',
marginLeft: 4,
},
submenuBelowRight: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
top: 0,
right: '100%',
marginRight: 4,
},
submenuAboveLeft: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
bottom: 0,
left: '100%',
marginLeft: 4,
},
submenuAboveRight: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
bottom: 0,
right: '100%',
marginRight: 4,
},
};
}, [theme.colors, session.contextUsage, contextWarningYellowThreshold, contextWarningRedThreshold]);
const styles = useMemo(() => {
return {
container: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
},
textMain: {
color: theme.colors.textMain,
},
textAccent: {
color: theme.colors.accent,
},
textError: {
color: theme.colors.error,
},
divider: {
borderColor: theme.colors.border,
},
submenu: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
},
submenuBelowLeft: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
top: 0,
left: '100%',
marginLeft: 4,
},
submenuBelowRight: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
top: 0,
right: '100%',
marginRight: 4,
},
submenuAboveLeft: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
bottom: 0,
left: '100%',
marginLeft: 4,
},
submenuAboveRight: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
bottom: 0,
right: '100%',
marginRight: 4,
},
};
}, [theme.colors]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/SessionList.tsx` around lines 121 - 173, The useMemo
for styles includes undefined identifiers (session.contextUsage,
contextWarningYellowThreshold, contextWarningRedThreshold) in its dependency
array which will throw at runtime; update the dependency array for the useMemo
that computes styles (the useMemo that returns container/textMain/textAccent/...
in SessionContextMenu / styles) to only include the actual used values (e.g.,
theme.colors or theme) and remove session.contextUsage,
contextWarningYellowThreshold, and contextWarningRedThreshold from the array.
Comment on lines 1573 to 1714
const sessionListStyles = useMemo(() => {
return {
root: {
container: {
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
'--tw-ring-color': theme.colors.accent,
},
headerBorder: {
borderColor: theme.colors.border,
},
iconAccent: {
color: theme.colors.accent,
},
textMain: {
color: theme.colors.textMain,
},
borderDim: {
color: theme.colors.textDim,
},
borderError: {
color: theme.colors.error,
},
badgeColor: {
color: '#FFD700',
},
menuButtonColor: {
color: theme.colors.textDim,
},
actionDivider: {
borderColor: theme.colors.border,
},
bottomDivider: {
borderColor: theme.colors.border,
},
bottomPrimaryAction: {
backgroundColor: theme.colors.accent,
color: theme.colors.accentForeground,
},
bottomCollapse: {
borderColor: theme.colors.border,
},
sidebarFilterInput: {
borderColor: theme.colors.accent,
color: theme.colors.textMain,
},
sectionHeaderAccent: {
color: theme.colors.accent,
},
sectionHeaderDim: {
color: theme.colors.textDim,
},
sectionHeaderAccentFill: theme.colors.accent,
sectionBorderAccent: {
borderColor: theme.colors.accent,
},
sectionBorder: {
borderColor: theme.colors.border,
},
groupDelete: {
color: theme.colors.error,
},
newGroupButton: {
backgroundColor: `${theme.colors.accent}20`,
color: theme.colors.accent,
border: `1px solid ${theme.colors.accent}40`,
},
dropZone: {
borderColor: theme.colors.accent,
color: theme.colors.textDim,
backgroundColor: `${theme.colors.accent}10`,
},
unreadBadge: {
backgroundColor: theme.colors.error,
},
skinnyDotFallback: {
border: `1.5px solid ${theme.colors.textDim}`,
backgroundColor: 'transparent',
},
skinnyTooltip: {
minWidth: '240px',
backgroundColor: theme.colors.bgSidebar,
border: `1px solid ${theme.colors.border}`,
},
live: {
overlay: {
width: '280px',
},
panel: {
backgroundColor: theme.colors.bgSidebar,
border: `1px solid ${theme.colors.border}`,
},
panelSectionBorder: {
borderColor: theme.colors.border,
},
panelTextDim: {
color: theme.colors.textDim,
},
installBanner: {
backgroundColor: theme.colors.bgActivity,
},
customPortInput: {
backgroundColor: theme.colors.bgActivity,
borderColor: theme.colors.border,
color: theme.colors.textMain,
},
customPortCaption: {
color: theme.colors.textDim,
opacity: 0.7,
},
urlPillContainer: {
backgroundColor: theme.colors.bgActivity,
},
tabInactiveText: {
color: theme.colors.textDim,
},
overlayMenuPanel: {
backgroundColor: theme.colors.bgSidebar,
border: `1px solid ${theme.colors.border}`,
maxHeight: 'calc(100vh - 90px)',
},
copyActionButton: {
color: theme.colors.textDim,
},
openBrowserLocal: {
color: '#4ade80',
borderColor: 'rgba(74, 222, 128, 0.3)',
},
openBrowserRemote: {
color: '#60a5fa',
borderColor: 'rgba(96, 165, 250, 0.3)',
},
webIcon: {
color: theme.colors.textDim,
},
},
badge: {
color: theme.colors.accent,
},
},
};
}, [theme.colors]);
Copy link

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:

wc -l src/renderer/components/SessionList.tsx

Repository: RunMaestro/Maestro

Length of output: 106


🏁 Script executed:

# First, let's see the sessionListStyles definition and the lines around 1573-1714
sed -n '1573,1714p' src/renderer/components/SessionList.tsx | head -50

Repository: RunMaestro/Maestro

Length of output: 1210


🏁 Script executed:

# Search for all occurrences of "sessionListStyles.skinny" in the file
rg 'sessionListStyles\.skinny' src/renderer/components/SessionList.tsx -n -A 2 -B 2

Repository: RunMaestro/Maestro

Length of output: 772


🏁 Script executed:

# Check the structure of sessionListStyles - look for where 'skinny' property is defined
rg 'skinny:' src/renderer/components/SessionList.tsx -n -A 5

Repository: RunMaestro/Maestro

Length of output: 44


🏁 Script executed:

# Also check the full sessionListStyles object structure
sed -n '1573,1750p' src/renderer/components/SessionList.tsx

Repository: RunMaestro/Maestro

Length of output: 5327


Fix incorrect style object path references in skinny mode.

The code accesses sessionListStyles.skinny.* on lines 3103, 3119, and 3129, but this property does not exist in the sessionListStyles object. All style definitions, including skinnyDotFallback, unreadBadge, and skinnyTooltip, are nested under root. Using the non-existent skinny path results in undefined property access and breaks rendering in collapsed mode.

Change all occurrences to use sessionListStyles.root.* instead:

🐛 Required fix
-														...sessionListStyles.skinny.skinnyDotFallback,
+														...sessionListStyles.root.skinnyDotFallback,
@@
-											style={sessionListStyles.skinny.unreadBadge}
+											style={sessionListStyles.root.unreadBadge}
@@
-										...sessionListStyles.skinny.skinnyTooltip,
+										...sessionListStyles.root.skinnyTooltip,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/SessionList.tsx` around lines 1573 - 1714, The review
points out that sessionListStyles.skinny.* is undefined; update all references
that read sessionListStyles.skinny.skinnyDotFallback,
sessionListStyles.skinny.unreadBadge, and sessionListStyles.skinny.skinnyTooltip
(used in collapsed/skinny mode) to instead read
sessionListStyles.root.skinnyDotFallback, sessionListStyles.root.unreadBadge,
and sessionListStyles.root.skinnyTooltip so they reference the actual style
definitions returned by the useMemo that defines sessionListStyles.
@greptile-apps
Copy link

greptile-apps bot commented Mar 1, 2026

Greptile Summary

This PR bundles two independent workstreams: (1) a useMemo-based style extraction and React.memo wrapping across several renderer components to reduce inline style object allocations per render, and (2) a full async migration of HistoryManager's file I/O from synchronous fs calls to fs.promises, with corresponding IPC handler and test updates.

The SessionListItem memoization is clean and correct. However, SessionList.tsx contains two behavioral regressions in its useMemo refactors:

  • Context bar freezesSessionTooltipContent.tooltipStyles computes contextUsage.width and contextUsage.backgroundColor from session.contextUsage, contextWarningYellowThreshold, and contextWarningRedThreshold, but the dependency array only lists [theme.colors]. The progress bar will never update after initial render.
  • Collapsed pill status colors are transparent — In renderCollapsedPill, collapsedPillStyles.segment.base ({ backgroundColor: 'transparent' }) is spread after backgroundColor: getStatusColor(s.state, theme), silently overriding the dynamic status color for all non-batch, non-fallback sessions.

The async HistoryManager migration is well-executed, but a large number of test names in history-manager.test.ts were replaced with the generic 'should run async path', losing the intent of each scenario.

Confidence Score: 2/5

  • Not safe to merge — two visual regressions in SessionList.tsx will affect users in production.
  • The frozen context bar and the transparent collapsed-pill status colors are both user-visible regressions introduced directly by the memoization refactor. The PR description states "no behavioral change intended," but these bugs contradict that claim.
  • src/renderer/components/SessionList.tsx requires close attention — both the SessionTooltipContent useMemo dependency array (line 710) and the renderCollapsedPill spread logic (around line 1176) need fixes before merging.

Important Files Changed

Filename Overview
src/renderer/components/SessionList.tsx Multiple useMemo refactors — contains two logic bugs: missing dependencies in SessionTooltipContent.tooltipStyles cause the context bar to freeze; spread of collapsedPillStyles.segment.base silently overrides getStatusColor, making all collapsed-pill status colors transparent. Also has a spurious dependency array in SessionContextMenu.styles.
src/renderer/components/SessionListItem.tsx Wrapped in React.memo and extracted per-style useMemo hooks. Dependencies are correct and the memoization intent is sound — no functional regressions identified.
src/renderer/components/AICommandsPanel.tsx Styles consolidated into a single commandStyles useMemo with [theme] dependency; sortedCommands memoized. Functional equivalence maintained, though the PR introduces inconsistent indentation throughout the JSX in this file.
src/main/history-manager.ts Synchronous fs calls replaced with async fs.promises equivalents; public methods converted to async. A private listSessionsWithHistorySync is retained for internal sync use. Well-structured async migration; callers in IPC handlers correctly await all methods.
src/tests/main/history-manager.test.ts Tests updated to await async methods and mock fs.promises. However, a large number of descriptive test names (e.g. "should return entries from session file") were replaced with the generic "should run async path", significantly reducing readability of test output.
src/tests/main/ipc/handlers/agentSessions.test.ts Expanded with new tests for agentSessions:getGlobalStats, including a 30-second TTL cache validation test and a cache-expiry refresh test. Correctly mocks fs/promises and statsCache.
src/main/ipc/handlers/history.ts All calls to HistoryManager methods properly updated to await their now-async counterparts. No functional regressions.
src/main/ipc/handlers/director-notes.ts Updated to await listSessionsWithHistory and getEntries in the unified history handler. Correct async propagation.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["session.contextUsage changes"] --> B{"useMemo dep array\ncontains contextUsage?"}
    B -- "No (bug)" --> C["tooltipStyles NOT recomputed\n→ bar width frozen at initial value"]
    B -- "Yes (fix)" --> D["tooltipStyles recomputed\n→ bar width updates correctly"]

    E["s.state changes → getStatusColor"] --> F{"collapsedPillStyles.segment.base\nspread order"}
    F -- "After getStatusColor (bug)" --> G["backgroundColor overridden\nwith 'transparent'"]
    F -- "Before getStatusColor (fix)" --> H["backgroundColor = status color\n(correct)"]

    style C fill:#f66,color:#fff
    style G fill:#f66,color:#fff
    style D fill:#6a6,color:#fff
    style H fill:#6a6,color:#fff
Loading

Last reviewed commit: 117a782

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

56 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

</div>
<div className="text-xs" style={{ color: theme.colors.textDim }}>
<div className="text-xs" style={styles.textDim}>
View application logs
Copy link

Choose a reason for hiding this comment

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

Missing useMemo dependencies — context bar will freeze

tooltipStyles.contextUsage computes its width and backgroundColor using session.contextUsage, contextWarningYellowThreshold, and contextWarningRedThreshold, but none of these are listed in the useMemo dependency array (only theme.colors is). This means the context progress bar will be permanently stuck at the value it had on first render; neither the width nor the color will update when context usage changes.

Suggested change
View application logs
}, [theme.colors, session.contextUsage, contextWarningYellowThreshold, contextWarningRedThreshold]);
@greptile-apps
Copy link

greptile-apps bot commented Mar 1, 2026

Additional Comments (3)

src/renderer/components/SessionList.tsx
Spurious useMemo dependencies in SessionContextMenu

session.contextUsage, contextWarningYellowThreshold, and contextWarningRedThreshold appear in the dependency array but are never referenced inside the styles computation — only theme.colors is actually used. These extra dependencies cause the memoized value to be unnecessarily recalculated whenever those props change.

	}, [theme.colors]);

src/renderer/components/SessionList.tsx
Spread of collapsedPillStyles.segment.base silently overwrites getStatusColor

In JavaScript object literals, a spread applied after an explicit property overrides that property. Here, collapsedPillStyles.segment.base is { backgroundColor: 'transparent' }, so it overwrites backgroundColor: getStatusColor(s.state, theme). Every non-batch, non-fallback segment in a collapsed pill will now render with a transparent background instead of its status color, making it invisible.

The base object is redundant here and should be removed:

								: {
										backgroundColor: getStatusColor(s.state, theme),
									}),

src/__tests__/main/history-manager.test.ts
Generic test names lose meaningful descriptions

Dozens of test it() descriptions have been replaced with the generic string 'should run async path' throughout this file. The original descriptions ('should return entries from session file', 'should return false if entry is not found', etc.) directly convey the scenario being tested and make failing test output immediately actionable. Generic names make it hard to identify what broke at a glance.

This same pattern also appears throughout the renamed tests in this file — e.g., 'should run async path' replaces 'should return true when migration marker exists', 'should remove an entry by id and return true', and many others. All the renamed test descriptions should be restored or updated to accurately describe the scenario.

Copy link

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (10)
src/renderer/components/CsvTableRenderer.tsx (1)

300-337: ⚠️ Potential issue | 🟡 Minor

Add keyboard accessibility to sortable column headers.

The headers have onClick handlers for sorting but lack tabIndex and keyboard event handlers, making them inaccessible to keyboard users.

Proposed fix
 {headerRow.map((header, i) => (
 	<th
 		key={i}
 		onClick={() => handleHeaderClick(i)}
+		onKeyDown={(e) => {
+			if (e.key === 'Enter' || e.key === ' ') {
+				e.preventDefault();
+				handleHeaderClick(i);
+			}
+		}}
+		tabIndex={0}
+		role="button"
 		style={{

As per coding guidelines: "Add tabIndex attribute and focus event handlers when implementing components that need keyboard focus."

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

In `@src/renderer/components/CsvTableRenderer.tsx` around lines 300 - 337, The
header <th> elements rendered in the headerRow mapping are clickable via
handleHeaderClick but lack keyboard accessibility; add tabIndex={0} and a
keyboard event handler that calls handleHeaderClick(i) when Enter or Space is
pressed (e.g., an onKeyDown that checks for key === 'Enter' || key === ' ' /
keyCode 13/32), and also add role="button" (and optionally onFocus/onBlur for
focus styling) so sortable headers are reachable and operable by keyboard users;
update references around headerRow, handleHeaderClick, and sort to ensure
behavior and visual focus state match the mouse click interaction.
src/renderer/components/SessionList.tsx (1)

174-225: ⚠️ Potential issue | 🔴 Critical

Variables left, top, ready used before declaration.

The menuStyles useMemo (lines 174-208) references left, top, and ready in both the computed object and the dependency array, but these are not declared until line 225 via useContextMenuPosition. This causes a temporal dead zone (TDZ) error at runtime.

🐛 Proposed fix — move the hook call before the useMemo
 	// Use ref to avoid re-registering listener when onDismiss changes
 	const onDismissRef = useRef(onDismiss);
 	onDismissRef.current = onDismiss;

+	// Measure menu and adjust position to stay within viewport
+	const { left, top, ready } = useContextMenuPosition(menuRef, x, y);
+
 	const styles = useMemo(() => {
 		return {
 			// ... styles definition
 		};
-	}, [theme.colors, session.contextUsage, contextWarningYellowThreshold, contextWarningRedThreshold]);
+	}, [theme.colors]);

 	const menuStyles = useMemo(() => {
 		// ... menuStyles definition using left, top, ready
 	}, [
 		left,
 		top,
 		ready,
 		// ... other deps
 	]);

 	// Close on click outside
 	useClickOutside(menuRef, onDismiss);

 	// Close on Escape - stable listener that never re-registers
 	useEffect(() => {
 		// ...
 	}, []);

-	// Measure menu and adjust position to stay within viewport
-	const { left, top, ready } = useContextMenuPosition(menuRef, x, y);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/SessionList.tsx` around lines 174 - 225, menuStyles'
useMemo references left, top and ready before they are declared which causes a
TDZ; move the call to useContextMenuPosition(menuRef, x, y) so that const {
left, top, ready } is declared before the useMemo that computes menuStyles, then
update the useMemo dependency array to keep left, top and ready (and existing
submenuPosition & style refs) so React hooks/readers see the values in scope;
ensure useClickOutside(menuRef, onDismiss) and the Escape useEffect remain in
place and that no other references to left/top/ready precede the moved hook.
src/__tests__/main/stats/stats-db.test.ts (8)

761-766: ⚠️ Potential issue | 🔴 Critical

Missing await for async getAvailableBackups() method.

Same issue as above - the async method needs to be awaited.

🐛 Proposed fix
-			const backups = db.getAvailableBackups();
+			const backups = await db.getAvailableBackups();
 			expect(backups).toHaveLength(3);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/stats/stats-db.test.ts` around lines 761 - 766, The test
calls the async method getAvailableBackups() without awaiting it; update the
test to await db.getAvailableBackups() (e.g., assign const backups = await
db.getAvailableBackups()) so the promise resolves before assertions. Locate the
call to getAvailableBackups() in the test block and add the await keyword and
ensure the enclosing test function is async if not already.

826-828: ⚠️ Potential issue | 🔴 Critical

Missing await for async restoreFromBackup() method.

🐛 Proposed fix
-			db.restoreFromBackup('/path/to/backup');
+			await db.restoreFromBackup('/path/to/backup');
 
 			expect(mockDb.close).toHaveBeenCalled();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/stats/stats-db.test.ts` around lines 826 - 828, The test
calls the async method restoreFromBackup() without awaiting it, causing the
expectation on mockDb.close to run early; update the test so restoreFromBackup()
is awaited (or return its promise) before asserting
expect(mockDb.close).toHaveBeenCalled(), ensuring the test function is marked
async if needed and referencing the restoreFromBackup() call and the
mockDb.close assertion in the test.

836-842: ⚠️ Potential issue | 🔴 Critical

Missing await for async restoreFromBackup() method.

🐛 Proposed fix
-			db.restoreFromBackup('/path/to/backup.db');
+			await db.restoreFromBackup('/path/to/backup.db');
 
-			expect(mockFsCopyFileSync).toHaveBeenCalledWith(
+			expect(mockFsCopyFile).toHaveBeenCalledWith(

Note: The assertion should also likely check mockFsCopyFile (async) instead of mockFsCopyFileSync since restoreFromBackup now uses fsp.copyFile.

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

In `@src/__tests__/main/stats/stats-db.test.ts` around lines 836 - 842, The test
calls the async method restoreFromBackup() without awaiting it and still asserts
against the sync mock; change the test to await
db.restoreFromBackup('/path/to/backup.db') and update the expectation to check
the async mock (mockFsCopyFile) was called with '/path/to/backup.db' and a
destination containing 'stats.db' (restoreFromBackup uses fsp.copyFile, so
assert mockFsCopyFile rather than mockFsCopyFileSync).

778-781: ⚠️ Potential issue | 🔴 Critical

Missing await for async getAvailableBackups() method.

🐛 Proposed fix
-			const backups = db.getAvailableBackups();
+			const backups = await db.getAvailableBackups();
 			expect(backups).toHaveLength(1);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/stats/stats-db.test.ts` around lines 778 - 781, The test
is calling the async method getAvailableBackups() without awaiting it; change
the test to await db.getAvailableBackups() (e.g., const backups = await
db.getAvailableBackups()) and ensure the test function is declared async so the
await is valid; update any surrounding test callback to async and keep the rest
of the assertions unchanged.

849-853: ⚠️ Potential issue | 🔴 Critical

Missing await for async restoreFromBackup() method.

🐛 Proposed fix
-			db.restoreFromBackup('/path/to/backup.db');
+			await db.restoreFromBackup('/path/to/backup.db');
 
 			// Should attempt to unlink WAL and SHM files
-			expect(mockFsUnlinkSync).toHaveBeenCalled();
+			expect(mockFsUnlink).toHaveBeenCalled();

Note: Should also check mockFsUnlink (async) instead of mockFsUnlinkSync.

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

In `@src/__tests__/main/stats/stats-db.test.ts` around lines 849 - 853, The test
calls the async method restoreFromBackup() without awaiting it and then asserts
unlink behavior; change the test to await db.restoreFromBackup(...) to ensure
the promise resolves before assertions and replace the sync assertion
mockFsUnlinkSync with the async mock assertion mockFsUnlink (or
mockFsUnlink.mockResolvedValue...) so the test verifies the asynchronous unlink
path; update references to restoreFromBackup and mockFsUnlink/mockFsUnlinkSync
in the test accordingly.

952-954: ⚠️ Potential issue | 🔴 Critical

Missing await for async backupDatabase() method.

backupDatabase() now returns Promise<BackupResult> but the test doesn't await it.

🐛 Proposed fix
 			mockDb.pragma.mockClear();
-			db.backupDatabase();
+			await db.backupDatabase();
 
 			expect(mockDb.pragma).toHaveBeenCalledWith('wal_checkpoint(TRUNCATE)');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/stats/stats-db.test.ts` around lines 952 - 954, The test
calls db.backupDatabase() but doesn't await the async method; update the test to
await db.backupDatabase() (and optionally capture the Promise result if needed)
before asserting
expect(mockDb.pragma).toHaveBeenCalledWith('wal_checkpoint(TRUNCATE)'); ensure
the call to backupDatabase() in the test for stats-db.test.ts is awaited so the
async work completes prior to the expectation.

978-980: ⚠️ Potential issue | 🔴 Critical

Missing await for async backupDatabase() method.

🐛 Proposed fix
-			db.backupDatabase();
+			await db.backupDatabase();
 
 			expect(callOrder).toEqual(['checkpoint', 'copy']);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/stats/stats-db.test.ts` around lines 978 - 980, The test
calls db.backupDatabase() without awaiting it, so make the test await the async
backupDatabase() call (change db.backupDatabase(); to await
db.backupDatabase();) and ensure the surrounding test function is declared async
(or returns the Promise) so the expect(callOrder).toEqual(['checkpoint','copy'])
runs after completion; update references to backupDatabase and the test function
signature accordingly.

799-803: ⚠️ Potential issue | 🔴 Critical

Missing await for async getAvailableBackups() method.

🐛 Proposed fix
-			const backups = db.getAvailableBackups();
+			const backups = await db.getAvailableBackups();
 			expect(backups[0].date).toBe('2026-02-01');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/stats/stats-db.test.ts` around lines 799 - 803, The test
calls the async method getAvailableBackups() without awaiting it; update the
test to await db.getAvailableBackups() and ensure the surrounding test function
is declared async so backups resolves before assertions (reference the
getAvailableBackups() call and the backups variable in this test block).
♻️ Duplicate comments (5)
src/renderer/components/AutoRun.tsx (1)

1514-1520: ⚠️ Potential issue | 🟡 Minor

Pass trimmedSearchQuery instead of searchQuery to searchHighlight.

The match counting at line 1073 uses trimmedSearchQuery to build the regex, but the highlighting at line 1516 passes the untrimmed searchQuery. This mismatch means searching for " foo " (with spaces) will count matches for "foo" but attempt to highlight the literal string with spaces, causing inconsistent behavior.

🔧 Suggested fix
 searchHighlight: {
-	query: searchQuery,
+	query: trimmedSearchQuery,
 	currentMatchIndex,
 	onMatchRendered: handleMatchRendered,
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/AutoRun.tsx` around lines 1514 - 1520, The
searchHighlight object is using the untrimmed variable searchQuery while match
counting uses trimmedSearchQuery, causing mismatched highlights for queries with
surrounding whitespace; update the searchHighlight call (the object passed where
containerRef: previewRef, searchHighlight: {...}) to use trimmedSearchQuery for
query (i.e., pass trimmedSearchQuery instead of searchQuery) and leave
currentMatchIndex and onMatchRendered (handleMatchRendered) unchanged so
counting and highlighting use the same trimmed input.
src/renderer/components/SessionList.tsx (4)

172-172: ⚠️ Potential issue | 🔴 Critical

Remove undefined identifiers from useMemo dependency array.

The dependency array references session.contextUsage, contextWarningYellowThreshold, and contextWarningRedThreshold, but these identifiers are either unused in the computed styles (session.contextUsage) or not defined in SessionContextMenu's scope (the threshold variables). This causes a runtime ReferenceError.

🐛 Proposed fix
-	}, [theme.colors, session.contextUsage, contextWarningYellowThreshold, contextWarningRedThreshold]);
+	}, [theme.colors]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/SessionList.tsx` at line 172, The useMemo dependency
array in SessionList.tsx (within the SessionContextMenu / useMemo call) includes
undefined or unused identifiers (session.contextUsage,
contextWarningYellowThreshold, contextWarningRedThreshold) causing a
ReferenceError; update the dependency array to only include the actual values
referenced inside the memoized callback (e.g., theme.colors and any properties
of session that the callback reads), removing session.contextUsage and the two
threshold names that are not defined in this scope so the dependencies match the
used variables.

3220-3220: ⚠️ Potential issue | 🔴 Critical

Invalid path sessionListStyles.skinny.unreadBadge — should be .root..

Same issue as above: sessionListStyles has no skinny property. This breaks the unread notification badge in collapsed sidebar mode.

🐛 Proposed fix
-											style={sessionListStyles.skinny.unreadBadge}
+											style={sessionListStyles.root.unreadBadge}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/SessionList.tsx` at line 3220, The unread badge is
referencing a non-existent style path sessionListStyles.skinny.unreadBadge;
update the reference to the correct style location
sessionListStyles.root.unreadBadge (or move the unreadBadge into the .root
object) so the collapsed sidebar uses the valid style key; change any other uses
of sessionListStyles.skinny to sessionListStyles.root and ensure the unreadBadge
entry exists on sessionListStyles.root in the SessionList component.

1798-1811: ⚠️ Potential issue | 🔴 Critical

Invalid style object path: sessionListStyles.skinny does not exist.

Lines 1798-1805 and 1807-1811 reference sessionListStyles.skinny.skinnyDotFallback and sessionListStyles.skinny.skinnyTooltip, but sessionListStyles (defined at lines 1599-1757) only has a root property. The skinny path is undefined, causing runtime errors.

🐛 Proposed fix
 			skinny: {
 				dot: {
 					activeByState: skinnyDotActiveByState,
 					inactiveByState: skinnyDotInactiveByState,
 					fallback: {
 						active: {
-							...sessionListStyles.skinny.skinnyDotFallback,
+							...sessionListStyles.root.skinnyDotFallback,
 							opacity: 1,
 						},
 						inactive: {
-							...sessionListStyles.skinny.skinnyDotFallback,
+							...sessionListStyles.root.skinnyDotFallback,
 							opacity: 0.25,
 						},
 					},
 				},
 				tooltip: {
-					...sessionListStyles.skinny.skinnyTooltip,
+					...sessionListStyles.root.skinnyTooltip,
 					left: '80px',
 					...(tooltipPosition ? { top: `${tooltipPosition.y}px` } : {}),
 				},
 			},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/SessionList.tsx` around lines 1798 - 1811,
sessionListStyles.skinny is undefined causing runtime errors; either add a
skinny object to the sessionListStyles definition with skinnyDotFallback and
skinnyTooltip keys, or update the references to point to the existing path (e.g.
replace sessionListStyles.skinny.skinnyDotFallback and
sessionListStyles.skinny.skinnyTooltip with
sessionListStyles.root.skinnyDotFallback and
sessionListStyles.root.skinnyTooltip). Adjust the code around tooltipPosition
(still use tooltipPosition ? { top: `${tooltipPosition.y}px` } : {}) and ensure
the names skinnyDotFallback and skinnyTooltip match exactly what you add or
reference.

920-988: ⚠️ Potential issue | 🟠 Major

tooltipStyles memo has incomplete dependencies — causes stale context styles.

The memoized tooltipStyles computes contextUsageColor (lines 921-926) using session.contextUsage and the threshold props, and directly uses session.contextUsage for the width style (line 972). However, the dependency array only includes [theme.colors].

When session.contextUsage, contextWarningYellowThreshold, or contextWarningRedThreshold change, the memo won't recalculate, leaving stale context usage colors and bar widths.

🐛 Proposed fix
-	}, [theme.colors]);
+	}, [
+		theme.colors,
+		session.contextUsage,
+		contextWarningYellowThreshold,
+		contextWarningRedThreshold,
+	]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/SessionList.tsx` around lines 920 - 988, The useMemo
for tooltipStyles is missing dependencies so contextUsageColor and the width
(`session.contextUsage`) can become stale; update the dependency array for the
tooltipStyles memo (where tooltipStyles is defined and getContextColor is
called) to include session.contextUsage, contextWarningYellowThreshold, and
contextWarningRedThreshold in addition to theme.colors so the styles (including
the contextUsage width and color) recompute when those values change.
🧹 Nitpick comments (2)
src/renderer/components/FilePreview.tsx (1)

869-880: Remove redundant regex copies.

These three memoized values (searchRegexForCodeSearch, searchRegexForMarkdownSearch, searchRegexForEdit) are all identical copies of searchRegex. The lastIndex state concern doesn't apply here because matchAll (used by getMatches) always starts from index 0 and returns all matches without mutating the regex's state.

You can use searchRegex directly in all three contexts and remove these intermediate memos.

♻️ Suggested simplification
 const contentSearchMatches = useMemo(() => {
 	return getMatches(file?.content ?? '', searchRegex);
 }, [searchRegex, file?.content]);
 const contentSearchMatchCount = contentSearchMatches.length;
-const searchRegexForCodeSearch = useMemo(() => {
-	if (!searchRegex) return null;
-	return new RegExp(searchRegex.source, searchRegex.flags);
-}, [searchRegex]);
-const searchRegexForMarkdownSearch = useMemo(() => {
-	if (!searchRegex) return null;
-	return new RegExp(searchRegex.source, searchRegex.flags);
-}, [searchRegex]);
-const searchRegexForEdit = useMemo(() => {
-	if (!searchRegex) return null;
-	return new RegExp(searchRegex.source, searchRegex.flags);
-}, [searchRegex]);

Then update usages to reference searchRegex directly (e.g., in the effects and editModeSearchMatches).

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

In `@src/renderer/components/FilePreview.tsx` around lines 869 - 880, Remove the
three redundant memoized regex copies (searchRegexForCodeSearch,
searchRegexForMarkdownSearch, searchRegexForEdit) and replace their usages with
the original searchRegex; update any references in effects, getMatches calls,
and editModeSearchMatches logic to use searchRegex directly since matchAll does
not mutate lastIndex, and delete the useMemo blocks for those three symbols to
simplify the component.
src/main/stats/stats-db.ts (1)

95-98: Minor: Redundant Promise.resolve() in async function.

In an async function, a bare return; implicitly returns a resolved promise. The explicit Promise.resolve() is unnecessary.

🔧 Suggested simplification
 async initialize(): Promise<void> {
 	if (this.initialized) {
-		return Promise.resolve();
+		return;
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/stats/stats-db.ts` around lines 95 - 98, The initialize method
(async initialize()) contains a redundant Promise.resolve(); replace the
explicit Promise.resolve() with a simple bare return (or remove the return
entirely) so the async function relies on its implicit resolved Promise; update
the initialize() implementation where the early-return checks this.initialized
to use return; instead of return Promise.resolve();.
🤖 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/__tests__/main/stats/stats-db.test.ts`:
- Around line 817-818: The test is calling the async method restoreFromBackup on
db without awaiting it, so the assertion checks a Promise instead of its
resolved boolean; update the test to await
db.restoreFromBackup('/path/to/nonexistent/backup') (or return the Promise) and
ensure the test function is declared async so the expect(result).toBe(false)
asserts the resolved value of restoreFromBackup.
- Around line 741-742: The test is calling the async method
getAvailableBackups() without awaiting it, so change the test to handle the
Promise by marking the test callback async (or returning the Promise) and
awaiting db.getAvailableBackups(); then assert against the resolved value
(expect(await db.getAvailableBackups()).toEqual([]) or store into a variable via
const backups = await db.getAvailableBackups() before asserting). Ensure
getAvailableBackups() is referenced exactly by name in the updated test.

In `@src/renderer/components/SessionList.tsx`:
- Around line 535-559: The useMemo for styles in HamburgerMenuContent currently
lists undefined dependencies (session.contextUsage,
contextWarningYellowThreshold, contextWarningRedThreshold); update the
dependency array to only include the actual values in scope—at minimum
theme.colors—so change the dependencies for the styles useMemo in
HamburgerMenuContent (the styles constant defined via useMemo) to [theme.colors]
(or any other props actually used) and remove the erroneous
session/contextThreshold identifiers.

---

Outside diff comments:
In `@src/__tests__/main/stats/stats-db.test.ts`:
- Around line 761-766: The test calls the async method getAvailableBackups()
without awaiting it; update the test to await db.getAvailableBackups() (e.g.,
assign const backups = await db.getAvailableBackups()) so the promise resolves
before assertions. Locate the call to getAvailableBackups() in the test block
and add the await keyword and ensure the enclosing test function is async if not
already.
- Around line 826-828: The test calls the async method restoreFromBackup()
without awaiting it, causing the expectation on mockDb.close to run early;
update the test so restoreFromBackup() is awaited (or return its promise) before
asserting expect(mockDb.close).toHaveBeenCalled(), ensuring the test function is
marked async if needed and referencing the restoreFromBackup() call and the
mockDb.close assertion in the test.
- Around line 836-842: The test calls the async method restoreFromBackup()
without awaiting it and still asserts against the sync mock; change the test to
await db.restoreFromBackup('/path/to/backup.db') and update the expectation to
check the async mock (mockFsCopyFile) was called with '/path/to/backup.db' and a
destination containing 'stats.db' (restoreFromBackup uses fsp.copyFile, so
assert mockFsCopyFile rather than mockFsCopyFileSync).
- Around line 778-781: The test is calling the async method
getAvailableBackups() without awaiting it; change the test to await
db.getAvailableBackups() (e.g., const backups = await db.getAvailableBackups())
and ensure the test function is declared async so the await is valid; update any
surrounding test callback to async and keep the rest of the assertions
unchanged.
- Around line 849-853: The test calls the async method restoreFromBackup()
without awaiting it and then asserts unlink behavior; change the test to await
db.restoreFromBackup(...) to ensure the promise resolves before assertions and
replace the sync assertion mockFsUnlinkSync with the async mock assertion
mockFsUnlink (or mockFsUnlink.mockResolvedValue...) so the test verifies the
asynchronous unlink path; update references to restoreFromBackup and
mockFsUnlink/mockFsUnlinkSync in the test accordingly.
- Around line 952-954: The test calls db.backupDatabase() but doesn't await the
async method; update the test to await db.backupDatabase() (and optionally
capture the Promise result if needed) before asserting
expect(mockDb.pragma).toHaveBeenCalledWith('wal_checkpoint(TRUNCATE)'); ensure
the call to backupDatabase() in the test for stats-db.test.ts is awaited so the
async work completes prior to the expectation.
- Around line 978-980: The test calls db.backupDatabase() without awaiting it,
so make the test await the async backupDatabase() call (change
db.backupDatabase(); to await db.backupDatabase();) and ensure the surrounding
test function is declared async (or returns the Promise) so the
expect(callOrder).toEqual(['checkpoint','copy']) runs after completion; update
references to backupDatabase and the test function signature accordingly.
- Around line 799-803: The test calls the async method getAvailableBackups()
without awaiting it; update the test to await db.getAvailableBackups() and
ensure the surrounding test function is declared async so backups resolves
before assertions (reference the getAvailableBackups() call and the backups
variable in this test block).

In `@src/renderer/components/CsvTableRenderer.tsx`:
- Around line 300-337: The header <th> elements rendered in the headerRow
mapping are clickable via handleHeaderClick but lack keyboard accessibility; add
tabIndex={0} and a keyboard event handler that calls handleHeaderClick(i) when
Enter or Space is pressed (e.g., an onKeyDown that checks for key === 'Enter' ||
key === ' ' / keyCode 13/32), and also add role="button" (and optionally
onFocus/onBlur for focus styling) so sortable headers are reachable and operable
by keyboard users; update references around headerRow, handleHeaderClick, and
sort to ensure behavior and visual focus state match the mouse click
interaction.

In `@src/renderer/components/SessionList.tsx`:
- Around line 174-225: menuStyles' useMemo references left, top and ready before
they are declared which causes a TDZ; move the call to
useContextMenuPosition(menuRef, x, y) so that const { left, top, ready } is
declared before the useMemo that computes menuStyles, then update the useMemo
dependency array to keep left, top and ready (and existing submenuPosition &
style refs) so React hooks/readers see the values in scope; ensure
useClickOutside(menuRef, onDismiss) and the Escape useEffect remain in place and
that no other references to left/top/ready precede the moved hook.

---

Duplicate comments:
In `@src/renderer/components/AutoRun.tsx`:
- Around line 1514-1520: The searchHighlight object is using the untrimmed
variable searchQuery while match counting uses trimmedSearchQuery, causing
mismatched highlights for queries with surrounding whitespace; update the
searchHighlight call (the object passed where containerRef: previewRef,
searchHighlight: {...}) to use trimmedSearchQuery for query (i.e., pass
trimmedSearchQuery instead of searchQuery) and leave currentMatchIndex and
onMatchRendered (handleMatchRendered) unchanged so counting and highlighting use
the same trimmed input.

In `@src/renderer/components/SessionList.tsx`:
- Line 172: The useMemo dependency array in SessionList.tsx (within the
SessionContextMenu / useMemo call) includes undefined or unused identifiers
(session.contextUsage, contextWarningYellowThreshold,
contextWarningRedThreshold) causing a ReferenceError; update the dependency
array to only include the actual values referenced inside the memoized callback
(e.g., theme.colors and any properties of session that the callback reads),
removing session.contextUsage and the two threshold names that are not defined
in this scope so the dependencies match the used variables.
- Line 3220: The unread badge is referencing a non-existent style path
sessionListStyles.skinny.unreadBadge; update the reference to the correct style
location sessionListStyles.root.unreadBadge (or move the unreadBadge into the
.root object) so the collapsed sidebar uses the valid style key; change any
other uses of sessionListStyles.skinny to sessionListStyles.root and ensure the
unreadBadge entry exists on sessionListStyles.root in the SessionList component.
- Around line 1798-1811: sessionListStyles.skinny is undefined causing runtime
errors; either add a skinny object to the sessionListStyles definition with
skinnyDotFallback and skinnyTooltip keys, or update the references to point to
the existing path (e.g. replace sessionListStyles.skinny.skinnyDotFallback and
sessionListStyles.skinny.skinnyTooltip with
sessionListStyles.root.skinnyDotFallback and
sessionListStyles.root.skinnyTooltip). Adjust the code around tooltipPosition
(still use tooltipPosition ? { top: `${tooltipPosition.y}px` } : {}) and ensure
the names skinnyDotFallback and skinnyTooltip match exactly what you add or
reference.
- Around line 920-988: The useMemo for tooltipStyles is missing dependencies so
contextUsageColor and the width (`session.contextUsage`) can become stale;
update the dependency array for the tooltipStyles memo (where tooltipStyles is
defined and getContextColor is called) to include session.contextUsage,
contextWarningYellowThreshold, and contextWarningRedThreshold in addition to
theme.colors so the styles (including the contextUsage width and color)
recompute when those values change.

---

Nitpick comments:
In `@src/main/stats/stats-db.ts`:
- Around line 95-98: The initialize method (async initialize()) contains a
redundant Promise.resolve(); replace the explicit Promise.resolve() with a
simple bare return (or remove the return entirely) so the async function relies
on its implicit resolved Promise; update the initialize() implementation where
the early-return checks this.initialized to use return; instead of return
Promise.resolve();.

In `@src/renderer/components/FilePreview.tsx`:
- Around line 869-880: Remove the three redundant memoized regex copies
(searchRegexForCodeSearch, searchRegexForMarkdownSearch, searchRegexForEdit) and
replace their usages with the original searchRegex; update any references in
effects, getMatches calls, and editModeSearchMatches logic to use searchRegex
directly since matchAll does not mutate lastIndex, and delete the useMemo blocks
for those three symbols to simplify the component.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d6bd0bc and c228521.

📒 Files selected for processing (12)
  • src/__tests__/main/ipc/handlers/git.test.ts
  • src/__tests__/main/stats/stats-db.test.ts
  • src/main/ipc/handlers/git.ts
  • src/main/stats/migrations.ts
  • src/main/stats/schema.ts
  • src/main/stats/stats-db.ts
  • src/renderer/components/AutoRun.tsx
  • src/renderer/components/CsvTableRenderer.tsx
  • src/renderer/components/FilePreview.tsx
  • src/renderer/components/SessionList.tsx
  • src/renderer/components/SessionListItem.tsx
  • src/shared/stats-types.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/renderer/components/SessionListItem.tsx
  • src/tests/main/ipc/handlers/git.test.ts
Comment on lines 741 to 742
const backups = db.getAvailableBackups();
expect(backups).toEqual([]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing await for async getAvailableBackups() method.

getAvailableBackups() now returns Promise<Array<...>> but the test doesn't await it. This will cause the assertion to compare against a Promise object rather than the resolved array.

🐛 Proposed fix
-			const backups = db.getAvailableBackups();
-			expect(backups).toEqual([]);
+			const backups = await db.getAvailableBackups();
+			expect(backups).toEqual([]);
📝 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
const backups = db.getAvailableBackups();
expect(backups).toEqual([]);
const backups = await db.getAvailableBackups();
expect(backups).toEqual([]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/stats/stats-db.test.ts` around lines 741 - 742, The test
is calling the async method getAvailableBackups() without awaiting it, so change
the test to handle the Promise by marking the test callback async (or returning
the Promise) and awaiting db.getAvailableBackups(); then assert against the
resolved value (expect(await db.getAvailableBackups()).toEqual([]) or store into
a variable via const backups = await db.getAvailableBackups() before asserting).
Ensure getAvailableBackups() is referenced exactly by name in the updated test.
Comment on lines 817 to 818
const result = db.restoreFromBackup('/path/to/nonexistent/backup');
expect(result).toBe(false);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Missing await for async restoreFromBackup() method.

restoreFromBackup() now returns Promise<boolean> but the test doesn't await it. The assertion will always pass because a Promise object is truthy/falsy regardless of the resolved value.

🐛 Proposed fix
-			const result = db.restoreFromBackup('/path/to/nonexistent/backup');
-			expect(result).toBe(false);
+			const result = await db.restoreFromBackup('/path/to/nonexistent/backup');
+			expect(result).toBe(false);
📝 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
const result = db.restoreFromBackup('/path/to/nonexistent/backup');
expect(result).toBe(false);
const result = await db.restoreFromBackup('/path/to/nonexistent/backup');
expect(result).toBe(false);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/main/stats/stats-db.test.ts` around lines 817 - 818, The test
is calling the async method restoreFromBackup on db without awaiting it, so the
assertion checks a Promise instead of its resolved boolean; update the test to
await db.restoreFromBackup('/path/to/nonexistent/backup') (or return the
Promise) and ensure the test function is declared async so the
expect(result).toBe(false) asserts the resolved value of restoreFromBackup.
Comment on lines +535 to +559
const styles = useMemo(() => {
return {
icon: {
color: theme.colors.accent,
},
textMain: {
color: theme.colors.textMain,
},
textDim: {
color: theme.colors.textDim,
},
divider: {
borderColor: theme.colors.border,
},
shortcutBadge: {
backgroundColor: theme.colors.bgActivity,
color: theme.colors.textDim,
},
};
}, [
theme.colors,
session.contextUsage,
contextWarningYellowThreshold,
contextWarningRedThreshold,
]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Undefined identifiers in HamburgerMenuContent styles dependency array.

The styles useMemo references session.contextUsage, contextWarningYellowThreshold, and contextWarningRedThreshold in its dependency array (lines 554-559), but none of these exist in HamburgerMenuContent's scope. The component only receives theme, onNewAgentSession, openWizard, startTour, and setMenuOpen as props. This appears to be copy-paste residue from SessionContextMenu.

🐛 Proposed fix
 	}, [
-		theme.colors,
-		session.contextUsage,
-		contextWarningYellowThreshold,
-		contextWarningRedThreshold,
-	]);
+		theme.colors,
+	]);
📝 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
const styles = useMemo(() => {
return {
icon: {
color: theme.colors.accent,
},
textMain: {
color: theme.colors.textMain,
},
textDim: {
color: theme.colors.textDim,
},
divider: {
borderColor: theme.colors.border,
},
shortcutBadge: {
backgroundColor: theme.colors.bgActivity,
color: theme.colors.textDim,
},
};
}, [
theme.colors,
session.contextUsage,
contextWarningYellowThreshold,
contextWarningRedThreshold,
]);
const styles = useMemo(() => {
return {
icon: {
color: theme.colors.accent,
},
textMain: {
color: theme.colors.textMain,
},
textDim: {
color: theme.colors.textDim,
},
divider: {
borderColor: theme.colors.border,
},
shortcutBadge: {
backgroundColor: theme.colors.bgActivity,
color: theme.colors.textDim,
},
};
}, [
theme.colors,
]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/SessionList.tsx` around lines 535 - 559, The useMemo
for styles in HamburgerMenuContent currently lists undefined dependencies
(session.contextUsage, contextWarningYellowThreshold,
contextWarningRedThreshold); update the dependency array to only include the
actual values in scope—at minimum theme.colors—so change the dependencies for
the styles useMemo in HamburgerMenuContent (the styles constant defined via
useMemo) to [theme.colors] (or any other props actually used) and remove the
erroneous session/contextThreshold identifiers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant