Adopt async file IO for history and stats#385
Adopt async file IO for history and stats#385openasocket wants to merge 22 commits intoRunMaestro:mainfrom
Conversation
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
📝 WalkthroughWalkthroughThis PR systematically optimizes UX performance by converting synchronous filesystem operations to async throughout the main process, wrapping React components in React.memo and extracting expensive computations to memoized hooks, and adding database compound indexes for query performance. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
|
@greptile review |
|
✅ Actions performedReview triggered.
|
Greptile SummaryThis PR implements a comprehensive performance optimization sweep across React rendering, main process I/O, and database queries. All changes are behavior-preserving and follow established patterns from Key improvements:
Test coverage: All changes include comprehensive test updates. The async conversions required updating 15+ test files to use The Confidence Score: 5/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Performance Optimizations] --> B[React Layer]
A --> C[Main Process]
A --> D[Database Layer]
B --> B1[React.memo wraps]
B --> B2[useMemo for computations]
B --> B3[Style extraction]
B1 --> B1a[SessionListItem]
B1 --> B1b[ExecutionQueueIndicator]
B1 --> B1c[14 UsageDashboard charts]
B2 --> B2a[Session list sorting]
B2 --> B2b[Regex pre-compilation]
B2 --> B2c[Date parsing cache]
B3 --> B3a[Inline styles → useMemo]
C --> C1[Async File I/O]
C --> C2[Query Parallelization]
C --> C3[Session Discovery]
C1 --> C1a[history-manager.ts]
C1 --> C1b[stats-db.ts]
C1 --> C1c[imageUtils.ts]
C2 --> C2a[Git handlers - Promise.all]
C3 --> C3a[Batched scanning]
C3 --> C3b[30s cache TTL]
D --> D1[Compound Indexes]
D --> D2[Async Init/Vacuum]
D1 --> D1a[time+agent]
D1 --> D1b[time+project]
D1 --> D1c[time+source]
Last reviewed commit: f1e54db |
There was a problem hiding this comment.
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 (11)
src/renderer/components/UsageDashboard/LongestAutoRunsTable.tsx (1)
128-138:⚠️ Potential issue | 🟠 MajorError handling swallows exceptions instead of bubbling to Sentry.
The
catchblock logs toconsole.errorand then continues, which violates the coding guideline requiring unexpected errors to bubble up to Sentry for tracking. For a data-fetching failure that the user cannot recover from directly, either let it bubble or explicitly report it via Sentry utilities.Proposed fix using Sentry utilities
+import { captureException } from '../../../utils/sentry'; + const fetchData = useCallback(async () => { setLoading(true); try { const autoRunSessions = await window.maestro.stats.getAutoRunSessions(timeRange); setSessions(autoRunSessions); } catch (err) { - console.error('Failed to fetch Auto Run sessions for table:', err); + captureException(err, { extra: { context: 'LongestAutoRunsTable fetchData', timeRange } }); + // Optionally set an error state to show user feedback } finally { setLoading(false); } }, [timeRange]);As per coding guidelines: "Do NOT silently swallow exceptions with try-catch-console.error blocks. Let unhandled exceptions bubble up to Sentry for error tracking."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/UsageDashboard/LongestAutoRunsTable.tsx` around lines 128 - 138, The fetchData function currently swallows errors in its catch by only calling console.error; replace that with error reporting to Sentry or rethrowing so exceptions are not silently ignored. Update the catch in fetchData (wrapping window.maestro.stats.getAutoRunSessions) to call the project's Sentry utility (e.g., Sentry.captureException or the app's captureError helper) with the caught err and a helpful message, then rethrow or allow the error to bubble instead of solely logging; ensure setLoading(false) still runs in finally and preserve existing setSessions behavior.src/renderer/components/SessionList.tsx (1)
1799-1813:⚠️ Potential issue | 🟡 MinorInconsistent indentation in button element.
The changed lines (1799-1800, 1804-1805) have different indentation than the unchanged lines within the same element (1801-1803, 1806). The
onClickhandler body andtitleattribute are more deeply indented than other attributes.As per coding guidelines,
src/**/*.{ts,tsx,js,jsx}: Use tabs for indentation, not spaces. Always match existing file indentation when editing.Suggested fix to align indentation
{/* Drawer handle at bottom - click to collapse */} - <button - onClick={(e) => { - e.stopPropagation(); - onToggleWorktreeExpanded(session.id); - }} - className="w-full flex items-center justify-center gap-1.5 py-0.5 text-[9px] font-medium hover:opacity-80 transition-opacity cursor-pointer" - style={sessionListStyles.worktreeCollapseButton} - title="Click to collapse worktrees" - > + <button + onClick={(e) => { + e.stopPropagation(); + onToggleWorktreeExpanded(session.id); + }} + className="w-full flex items-center justify-center gap-1.5 py-0.5 text-[9px] font-medium hover:opacity-80 transition-opacity cursor-pointer" + style={sessionListStyles.worktreeCollapseButton} + title="Click to collapse worktrees" + >🤖 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 1799 - 1813, Fix the inconsistent indentation in the button JSX inside SessionList component: re-indent the onClick handler body, title attribute, and surrounding attributes to match the file's existing tab-based indentation style (use tabs, not spaces) so all attributes and children lines align with the rest of the button element; specifically update the JSX for the button that calls onToggleWorktreeExpanded(session.id) and uses sessionListStyles.worktreeCollapseButton, GitBranch, ChevronUp, and worktreeChildren so the attribute lines and inner lines have uniform tab indentation consistent with the file.src/renderer/components/AICommandsPanel.tsx (1)
209-259:⚠️ Potential issue | 🟡 MinorAdd tabIndex/outline-none to interactive controls.
The updated buttons still omit the focus requirements in renderer components. Please applytabIndexandoutline-none(and replicate to other buttons in this panel).Suggested patch
<button + tabIndex={0} onClick={() => setVariablesExpanded(!variablesExpanded)} - className="w-full px-3 py-2 flex items-center justify-between hover:bg-white/5 transition-colors" + className="w-full px-3 py-2 flex items-center justify-between hover:bg-white/5 transition-colors outline-none" > @@ <button + tabIndex={0} onClick={() => setIsCreating(true)} - className="flex items-center gap-2 px-4 py-2 rounded text-sm font-medium transition-all" + className="flex items-center gap-2 px-4 py-2 rounded text-sm font-medium transition-all outline-none" >As per coding guidelines:
src/renderer/**/*.{tsx,jsx}: Add tabIndex={0} or tabIndex={-1} and outline-none class to ensure focus works correctly. Use ref={(el) => el?.focus()} for auto-focus in React components.🤖 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 209 - 259, The interactive controls (the toggle button that calls setVariablesExpanded and the "Add Command" button that calls setIsCreating) are missing keyboard focus attributes; add tabIndex={0} and the Tailwind class "outline-none" to these button elements (and other buttons in this panel, including the variable items if they should be interactive) so they are focusable, and where a control should auto-focus (e.g., when opening creation UI setIsCreating true) use a ref callback like ref={(el) => el?.focus()} on that element; ensure you update the JSX around the variables toggle (variablesExpanded, setVariablesExpanded), TEMPLATE_VARIABLES_GENERAL items, and the Add Command button to include these changes.src/renderer/components/UsageDashboard/ChartSkeletons.tsx (1)
47-77:⚠️ Potential issue | 🟡 MinorAdd focus attributes to all skeleton root containers.
These skeleton components render top-level containers missingtabIndex,outline-none, and autofocus refs required by the renderer component guidelines. Apply focus attributes to each of the following skeleton containers:
SummaryCardsSkeleton(lines 52-57)AgentComparisonChartSkeleton(lines 86-89)SourceDistributionChartSkeleton(lines 133-136)ActivityHeatmapSkeleton(lines 183-186)DurationTrendsChartSkeleton(lines 264-267)AutoRunStatsSkeleton(lines 332-336)DashboardSkeleton(line 370)🔧 Example update
- <div - className="grid gap-4" + <div + className="grid gap-4 outline-none" + tabIndex={-1} + ref={(el) => el?.focus()} style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`, }} data-testid="summary-cards-skeleton"Per coding guidelines: "src/renderer/**/*.{tsx,jsx}: Add tabIndex={0} or tabIndex={-1} and outline-none class to ensure focus works correctly. Use ref={(el) => el?.focus()} for auto-focus in React components."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/UsageDashboard/ChartSkeletons.tsx` around lines 47 - 77, The root containers of several skeleton components (SummaryCardsSkeleton, AgentComparisonChartSkeleton, SourceDistributionChartSkeleton, ActivityHeatmapSkeleton, DurationTrendsChartSkeleton, AutoRunStatsSkeleton, DashboardSkeleton) lack focus attributes; update each root <div> to include a focusable/tabbable setup by adding tabIndex={0} or tabIndex={-1} as appropriate, the "outline-none" class, and a ref that auto-focuses (ref={el => el?.focus()}) per renderer guidelines so keyboard/focus handling works; modify the root container in the listed components (e.g., the outer div in SummaryCardsSkeleton) to include these three changes.src/renderer/components/ExecutionQueueIndicator.tsx (1)
137-146:⚠️ Potential issue | 🟡 MinorAdd explicit focus affordances to comply with renderer accessibility rules.
The indicator button is interactive but lacks the required explicit focus attributes/classes. Please add a tabIndex and outline-none class here.
🔧 Proposed fix
<button ref={containerRef} onClick={onClick} + tabIndex={0} className="w-full mb-2 px-3 py-2 rounded-lg border flex items-center gap-2 text-sm transition-all hover:opacity-90 outline-none" style={{ backgroundColor: theme.colors.bgActivity, borderColor: theme.colors.border, color: theme.colors.textMain, }} >As per coding guidelines, all renderer components must include tabIndex and outline-none to ensure focus works correctly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/ExecutionQueueIndicator.tsx` around lines 137 - 146, The ExecutionQueueIndicator button lacks explicit focus affordances; update the JSX for the button element (the one using ref={containerRef} and onClick={onClick} inside the ExecutionQueueIndicator component) to include tabIndex={0} and add the "outline-none" class to its className so keyboard focus is enabled while preserving existing styles and classes.src/main/stats/stats-db.ts (1)
175-182:⚠️ Potential issue | 🟡 MinorHandle unexpected fs errors explicitly in size/path checks.
getDatabaseSizeandpathExistscurrently treat any error as “missing,” which can mask permission/IO failures and skip maintenance. Consider only suppressing ENOENT and rethrowing others.🛠️ Suggested guard for ENOENT-only handling
- async getDatabaseSize(): Promise<number> { - try { - const stats = await StatsDB.fsPromises.stat(this.dbPath); - return stats.size; - } catch { - return 0; - } - } + async getDatabaseSize(): Promise<number> { + try { + const stats = await StatsDB.fsPromises.stat(this.dbPath); + return stats.size; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return 0; + } + throw error; + } + } @@ - private async pathExists(targetPath: string): Promise<boolean> { - try { - await StatsDB.fsPromises.access(targetPath, fs.constants.F_OK); - return true; - } catch { - return false; - } - } + private async pathExists(targetPath: string): Promise<boolean> { + try { + await StatsDB.fsPromises.access(targetPath, fs.constants.F_OK); + return true; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return false; + } + throw error; + } + }As per coding guidelines, handle only expected/recoverable errors explicitly with specific error codes and let unexpected errors bubble up to Sentry.
Also applies to: 832-838
🤖 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 175 - 182, The current getDatabaseSize (using StatsDB.fsPromises.stat on this.dbPath) and pathExists swallow all errors; change both to catch errors explicitly and only suppress ENOENT: in getDatabaseSize, after awaiting StatsDB.fsPromises.stat(this.dbPath) catch the error, if error.code === 'ENOENT' return 0, otherwise rethrow the error; similarly in pathExists (the function that checks this.dbPath via fsPromises.access/stat) catch and return false only for error.code === 'ENOENT' and rethrow other errors so unexpected permission/IO failures surface to Sentry.src/main/history-manager.ts (1)
209-330:⚠️ Potential issue | 🟠 MajorSerialize async read‑modify‑write operations to avoid lost history entries.
Concurrent IPC calls to
addEntry,deleteEntry,updateEntry,clearSession, andupdateSessionNameByClaudeSessionIdcan interleave on the same session, causing last-write-wins data loss. Implement per-session write serialization using a Promise queue (as suggested in the comment) to preserve operation ordering.🧵 Suggested per-session write queue (example with addEntry)
@@ export class HistoryManager { private historyDir: string; private legacyFilePath: string; private migrationMarkerPath: string; private configDir: string; private watcher: fs.FSWatcher | null = null; + private writeQueues = new Map<string, Promise<unknown>>(); @@ + private enqueueWrite<T>(sessionId: string, task: () => Promise<T>): Promise<T> { + const prev = this.writeQueues.get(sessionId) ?? Promise.resolve(); + const next = prev.then(task, task); + const cleanup = next.finally(() => { + if (this.writeQueues.get(sessionId) === cleanup) { + this.writeQueues.delete(sessionId); + } + }); + this.writeQueues.set(sessionId, cleanup); + return next; + } @@ - async addEntry(sessionId: string, projectPath: string, entry: HistoryEntry): Promise<void> { - const filePath = this.getSessionFilePath(sessionId); - let data: HistoryFileData; - - if (await this.fileExists(filePath)) { - try { - data = JSON.parse(await fsPromises.readFile(filePath, 'utf-8')); - } catch { - data = { version: HISTORY_VERSION, sessionId, projectPath, entries: [] }; - } - } else { - data = { version: HISTORY_VERSION, sessionId, projectPath, entries: [] }; - } - - // Add to beginning (most recent first) - data.entries.unshift(entry); - - // Trim to max entries - if (data.entries.length > MAX_ENTRIES_PER_SESSION) { - data.entries = data.entries.slice(0, MAX_ENTRIES_PER_SESSION); - } - - // Update projectPath if it changed - data.projectPath = projectPath; - - try { - await fsPromises.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8'); - logger.debug(`Added history entry for session ${sessionId}`, LOG_CONTEXT); - } catch (error) { - logger.error(`Failed to write history for session ${sessionId}: ${error}`, LOG_CONTEXT); - captureException(error, { operation: 'history:write', sessionId }); - } - } + async addEntry(sessionId: string, projectPath: string, entry: HistoryEntry): Promise<void> { + return this.enqueueWrite(sessionId, async () => { + const filePath = this.getSessionFilePath(sessionId); + let data: HistoryFileData; + + if (await this.fileExists(filePath)) { + try { + data = JSON.parse(await fsPromises.readFile(filePath, 'utf-8')); + } catch { + data = { version: HISTORY_VERSION, sessionId, projectPath, entries: [] }; + } + } else { + data = { version: HISTORY_VERSION, sessionId, projectPath, entries: [] }; + } + + // Add to beginning (most recent first) + data.entries.unshift(entry); + + // Trim to max entries + if (data.entries.length > MAX_ENTRIES_PER_SESSION) { + data.entries = data.entries.slice(0, MAX_ENTRIES_PER_SESSION); + } + + // Update projectPath if it changed + data.projectPath = projectPath; + + try { + await fsPromises.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8'); + logger.debug(`Added history entry for session ${sessionId}`, LOG_CONTEXT); + } catch (error) { + logger.error(`Failed to write history for session ${sessionId}: ${error}`, LOG_CONTEXT); + captureException(error, { operation: 'history:write', sessionId }); + } + }); + }Apply the same queuing pattern to
deleteEntry,updateEntry,clearSession, andupdateSessionNameByClaudeSessionId.🤖 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 209 - 330, Concurrent read-modify-write operations on the same session can interleave and lose entries; implement a per-session Promise queue to serialize operations by adding a Map<string, Promise<any>> (e.g., sessionQueues) and, in each mutating method (addEntry, deleteEntry, updateEntry, clearSession and updateSessionNameByClaudeSessionId), replace the direct body with an enqueued task: set sessionQueues[sessionId] = (sessionQueues[sessionId] || Promise.resolve()).then(() => { /* original logic */ }); ensure the enqueued callback returns the final Promise so callers await completion, catch/log errors inside the task, and finally clear the map entry when the chain settles to avoid memory leaks; keep the existing read/write/error handling but run it inside the queued task to preserve ordering.src/main/ipc/handlers/tabNaming.ts (1)
197-263:⚠️ Potential issue | 🟡 MinorClean up listeners/timeouts on spawn failure to avoid leaks.
Ifspawnrejects, the timeout and listeners remain registered and may fire after the Promise is already rejected.🧹 Proposed fix
- (async () => { - try { - await processManager.spawn({ - sessionId, - toolType: config.agentType, - cwd, - command, - args: finalArgs, - prompt: fullPrompt, - customEnvVars, - sendPromptViaStdin: shouldSendPromptViaStdin, - }); - } catch (error) { - reject(error); - } - })(); + (async () => { + try { + await processManager.spawn({ + sessionId, + toolType: config.agentType, + cwd, + command, + args: finalArgs, + prompt: fullPrompt, + customEnvVars, + sendPromptViaStdin: shouldSendPromptViaStdin, + }); + } catch (error) { + if (!resolved) { + resolved = true; + clearTimeout(timeoutId); + processManager.off('data', onData); + processManager.off('exit', onExit); + } + reject(error); + } + })();🤖 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 197 - 263, If processManager.spawn rejects, the timeout and listeners (onData/onExit) are left registered causing leaks; update the spawn error handler inside the async IIFE so it clears the timeout (timeoutId), removes listeners via processManager.off('data', onData) and processManager.off('exit', onExit), and ensures the promise is settled exactly once by checking/setting the resolved flag before rejecting (or resolving) with the error; reference processManager.spawn, onData, onExit, timeoutId, resolved and sessionId to locate and modify the handler.src/main/web-server/routes/apiRoutes.ts (1)
326-338:⚠️ Potential issue | 🟠 MajorCapture history failures via Sentry instead of swallowing the error.
The new async
getHistorycan reject; the current catch returns a 500 without Sentry reporting. Please capture (or rethrow) unexpected errors so they aren’t lost.Suggested fix
+import { captureException } from '../../utils/sentry'; ... } catch (error: any) { + captureException(error, { + tags: { context: 'WebServer:API /history' }, + extra: { projectPath, sessionId }, + }); return reply.code(500).send({ error: 'Internal Server Error', message: `Failed to fetch history: ${error.message}`, timestamp: Date.now(), }); }As per coding guidelines, “Do NOT silently swallow exceptions with try-catch-console.error blocks. Let unhandled exceptions bubble up to Sentry for error tracking. Use Sentry utilities (captureException, captureMessage) from 'src/utils/sentry' for explicit error reporting with context.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/web-server/routes/apiRoutes.ts` around lines 326 - 338, The catch block around this.callbacks.getHistory in the route handler currently returns a 500 without reporting the error; update the handler to call the Sentry utilities (import captureException and/or captureMessage from src/utils/sentry) when an unexpected error occurs in getHistory (or rethrow after reporting) so failures are reported to Sentry; specifically, in the catch for the getHistory call (references: getHistory, this.callbacks.getHistory) call captureException(error, { extra: { projectPath, sessionId } }) or captureMessage with context before sending the reply (or rethrow) to ensure the error is captured by Sentry.src/main/process-manager/utils/imageUtils.ts (2)
33-45:⚠️ Potential issue | 🟠 MajorReport write failures to Sentry instead of only logging.
The catch block logs and returns null, which can mask unexpected IO errors without Sentry visibility. Please capture exceptions (or rethrow unexpected ones) with context.
Suggested fix
+import { captureException } from '../../utils/sentry'; ... } catch (error) { + captureException(error, { + tags: { context: 'ProcessManager:saveImageToTempFile' }, + extra: { tempPath }, + }); logger.error('[ProcessManager] Failed to save image to temp file', 'ProcessManager', { error: String(error), }); return null; }As per coding guidelines, “Do NOT silently swallow exceptions with try-catch-console.error blocks. Let unhandled exceptions bubble up to Sentry for error tracking. Use Sentry utilities (captureException, captureMessage) from 'src/utils/sentry' for explicit error reporting with context.”
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/process-manager/utils/imageUtils.ts` around lines 33 - 45, The catch block around Buffer.from(parsed.base64) / fsPromises.writeFile currently only logs and returns null; change it to capture the exception with the Sentry helper and let it bubble instead of silently swallowing it: import captureException from 'src/utils/sentry', call captureException(error, { extra: { tempPath, parsedBase64Preview: parsed.base64?.slice(0,100), attemptedSize: buffer?.length } }) (or similar contextual fields) inside the catch, keep or augment the logger.error, and then rethrow the caught error (throw error) so it surfaces to calling code/Sentry rather than returning null. Ensure you reference parsed.base64, tempPath and the write operation (fsPromises.writeFile) when adding context.
22-36:⚠️ Potential issue | 🟠 MajorError handling must use Sentry for explicit error reporting.
The function correctly awaits at its call site (ChildProcessSpawner.ts:112), but the catch block (lines 41-46) silently swallows the exception using only
logger.error. Per coding guidelines, use Sentry utilities fromsrc/utils/sentryto explicitly report file write failures with context, then return null or rethrow as appropriate.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/process-manager/utils/imageUtils.ts` around lines 22 - 36, The catch block in saveImageToTempFile currently only logs the error; update it to import the Sentry utilities from src/utils/sentry and call the appropriate reporting function (e.g., captureException or reportError) inside the catch that surrounds fsPromises.writeFile, passing the caught exception plus context (function name saveImageToTempFile, index, tempPath, ext, and parsed.mediaType or a safe subset of dataUrl info) so failures are explicitly reported to Sentry, then return null (preserving current behavior) instead of silently swallowing the error.
🧹 Nitpick comments (7)
src/renderer/components/UsageDashboard/LongestAutoRunsTable.tsx (1)
229-235: Inline style objects recreated on each row render.The
onMouseEnter/onMouseLeavehandlers setbackgroundColordirectly on the element, which is fine for hover state. However, the initialstyleobject on lines 225-228 is recreated every render for every row. For a table capped at 25 rows this is minor, but if you want to squeeze out more perf, consider CSS:nth-child(even)or:hoverpseudo-classes instead of JS-driven hover and alternating styles.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/UsageDashboard/LongestAutoRunsTable.tsx` around lines 229 - 235, The per-row inline style and the JS hover handlers (onMouseEnter/onMouseLeave) in LongestAutoRunsTable recreate style objects on every render; replace the JS-driven alternating-row and hover styling with CSS rules instead (use a class on the row element and define :nth-child(even) for alternating background and :hover for hover state using theme colors or CSS variables derived from theme.colors), or if you must keep inline styles, memoize the base style per index (e.g., compute once using a stable helper) and remove the onMouseEnter/onMouseLeave backgroundColor mutations so the row style is static and not recreated on each render.src/renderer/components/UsageDashboard/EmptyState.tsx (1)
36-36: Consider extracting inline styles to constants.The inline style objects (
{ color: theme.colors.textDim },{ opacity: 0.3 }, etc.) create new references on each render, which can cause unnecessary child re-renders even when values haven't changed. For full memoization benefit, consider extracting static styles and usinguseMemofor theme-dependent ones.♻️ Optional: Extract inline styles
+import { memo, useMemo } from 'react'; -import { memo } from 'react'; import { BarChart3 } from 'lucide-react'; import type { Theme } from '../../types'; +const illustrationStyle = { opacity: 0.3 }; +const decorativeSvgStyle = { opacity: 0.5 }; + export const EmptyState = memo(function EmptyState({ theme, title = 'No usage data yet', message = 'Start using Maestro to see your stats!', }: EmptyStateProps) { + const textDimStyle = useMemo(() => ({ color: theme.colors.textDim }), [theme.colors.textDim]); + const textMainStyle = useMemo(() => ({ color: theme.colors.textMain }), [theme.colors.textMain]); + return ( <div className="h-full flex flex-col items-center justify-center gap-4" - style={{ color: theme.colors.textDim }} + style={textDimStyle} data-testid="usage-dashboard-empty" > {/* Subtle chart illustration */} - <div className="relative" style={{ opacity: 0.3 }}> + <div className="relative" style={illustrationStyle}>Also applies to: 40-40, 49-49, 59-59
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/UsageDashboard/EmptyState.tsx` at line 36, The inline style objects in the EmptyState component (e.g., style={{ color: theme.colors.textDim }}, style={{ opacity: 0.3 }}, and other similar inline styles) should be extracted so they don't get re-created on every render; move static styles to module-level constants and wrap theme-dependent style objects in a useMemo inside the EmptyState function (referencing theme) and replace the inline style props on the relevant JSX elements with those memoized constants (look for the style props in EmptyState.tsx around the color/opacity usages).src/renderer/components/QRCode.tsx (1)
57-61: Consider reporting error to Sentry instead of only logging to console.The error is caught and displayed to the user, which is good. However, per coding guidelines, consider using Sentry utilities from
src/utils/sentryfor error reporting with context, rather than only logging to console. This would help track QR generation failures in production.Suggested change
.catch((err) => { - console.error('Failed to generate QR code:', err); + // QR generation failure is recoverable - show error UI setError('Failed to generate QR code'); setDataUrl(null); });Or if tracking is desired:
import { captureException } from '@/utils/sentry'; // ... .catch((err) => { captureException(err, { extra: { value, size } }); setError('Failed to generate QR code'); setDataUrl(null); });As per coding guidelines: "Do NOT silently swallow exceptions with try-catch-console.error blocks" and "Use Sentry utilities (captureException, captureMessage) from 'src/utils/sentry' for explicit error reporting with context."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/QRCode.tsx` around lines 57 - 61, Replace the console.error-only catch in QRCode.tsx with a Sentry report: import captureException from src/utils/sentry and call captureException(err, { extra: { value, size } }) (or appropriate local variables used for QR generation) inside the .catch block, then keep the existing setError('Failed to generate QR code') and setDataUrl(null) so the UI behavior is unchanged; ensure the import and the use of captureException reference the same names used elsewhere (captureException) and include relevant contextual fields.src/renderer/components/UsageDashboard/AutoRunStats.tsx (1)
90-130: Consider memoizing MetricCard for additional gains.
MetricCardis rendered 6 times within this component. Since the parent is now memoized, wrappingMetricCardwithReact.memocould prevent unnecessary re-renders when only individual card props change.♻️ Optional: Memoize MetricCard
+const MetricCard = memo(function MetricCard({ icon, label, value, subValue, theme }: MetricCardProps) { -function MetricCard({ icon, label, value, subValue, theme }: MetricCardProps) { return ( <div className="p-4 rounded-lg flex items-start gap-3" // ... rest unchanged </div> ); -} +});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/components/UsageDashboard/AutoRunStats.tsx` around lines 90 - 130, MetricCard is a plain functional component rendered multiple times so wrap it with React.memo to avoid unnecessary re-renders; update the MetricCard export/definition to use React.memo(MetricCard) (or assign const MemoMetricCard = React.memo(MetricCard) and use that) and ensure any props used (icon, label, value, subValue, theme) are stable or provide a custom comparison function to React.memo if needed to control re-rendering.src/renderer/components/SessionList.tsx (1)
2002-2003: Misleading comment about effect dependencies.The comment on lines 2002-2003 states "Keep dependencies limited to sessionFilter and sessions" but the dependency array at line 2040 only includes
[sessionFilter]. Thesessionsvariable is used in the effect body (line 2008) but intentionally excluded from dependencies.The current behavior is likely intentional (only react to filter changes, using latest
sessionsfrom that render), but the comment should be updated to accurately reflect this design choice.Suggested comment fix
- // Note: Keep dependencies limited to sessionFilter and sessions to avoid unnecessary effect churn + // Note: Intentionally depends only on sessionFilter - sessions is accessed from the render closure + // when the filter changes, ensuring fresh session data without re-running on every session update useEffect(() => {Also applies to: 2040-2040
🤖 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 2002 - 2003, The existing comment above the useEffect in SessionList is misleading: it says "Keep dependencies limited to sessionFilter and sessions" while the dependency array only includes [sessionFilter] and sessions is intentionally omitted; update the comment to clearly state that the effect only runs when sessionFilter changes and that sessions is intentionally read from the current render (i.e., excluded from dependencies on purpose) so future readers won't assume sessions should be in the array; reference the useEffect block, the sessionFilter variable, and the sessions variable when making this clarification.src/renderer/components/AutoRun.tsx (1)
690-699: Minor:searchQueryis redundant insearchRegexdependencies.
escapedSearchQueryalready depends onsearchQuery, so including both in the dependency array is unnecessary. This doesn't cause bugs, just a small clarity nit.♻️ Optional cleanup
const searchRegex = useMemo(() => { if (!searchQuery.trim()) { return null; } return new RegExp(escapedSearchQuery, 'gi'); - }, [escapedSearchQuery, searchQuery]); + }, [escapedSearchQuery]);🤖 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 690 - 699, The useMemo for searchRegex redundantly lists searchQuery in its dependency array even though escapedSearchQuery already updates from searchQuery; update the useMemo for searchRegex (the function named searchRegex created with useMemo) to depend only on escapedSearchQuery (remove searchQuery from the dependency array) so the memo is correctly tied to the derived value.src/main/debug-package/collectors/group-chats.ts (1)
54-86: Consider parallelizing file processing for improved collection speed.The current implementation processes group chat files sequentially. For debug package collection with many files, parallel processing could speed this up:
♻️ Optional: Parallel file processing
- for (const file of files) { - if (!file.endsWith('.json') || file.endsWith('.log.json')) { - continue; - } - - const filePath = path.join(groupChatsPath, file); - - try { - const content = await fsPromises.readFile(filePath, 'utf-8'); - // ... rest of processing - } 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) => { + const filePath = path.join(groupChatsPath, file); + try { + const content = await fsPromises.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, + } as GroupChatInfo; + } catch { + return null; + } + }) + ); + + return results.filter((r): r is GroupChatInfo => r !== null);🤖 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 54 - 86, The current for-loop in the group chat collector processes files sequentially (iterating over files, reading with fsPromises.readFile, parsing JSON, calling countMessages, and pushing into groupChats), which is slow for many files; refactor collect logic (the loop that builds chatInfo) to run in parallel by mapping files to async tasks and using Promise.all (or a bounded concurrency utility like p-limit) to run readFile/JSON.parse/countMessages concurrently, ensure each task catches/parses its own errors so a single failure doesn't reject the entire Promise.all, and finally aggregate successful chatInfo results into the groupChats array.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.ai-audit/annotations.jsonl:
- Line 11: The entry with session_id "1a945b20-4249-4534-a69b-91262e1d9afa" is a
start event without a corresponding end; add a matching JSONL end event object
for that session (type "session", event "end", same session_id and
environment_hash, a later timestamp and same assurance_level/description as
appropriate) so the start/end pair is complete, or remove the incomplete start
line if the session should not be recorded.
In `@src/main/history-manager.ts`:
- Around line 179-185: The fileExists method currently treats any
fsPromises.access error as "missing"; update fileExists to only return false
when the caught error has code 'ENOENT' and rethrow all other errors so
permission/IO errors surface to Sentry. Specifically, inside fileExists (which
calls fsPromises.access with fs.constants.F_OK), inspect the caught error's code
and return false for 'ENOENT' only—otherwise throw the error. Keep the async
signature and behavior unchanged for the ENOENT case.
In `@src/main/ipc/handlers/agentSessions.ts`:
- Around line 241-278: The catch blocks in the session discovery flow (around
fs.access, fs.readdir and inside processProjectDir) are swallowing all errors;
update each catch to only handle expected filesystem error codes (e.g., ENOENT,
ENOTDIR, EACCES) and rethrow any other errors so they bubble up (use the same
pattern for the other catches noted around lines 327-395). In places where you
intentionally handle a recoverable error, log/report it using the Sentry helpers
(captureException or captureMessage from src/utils/sentry) with context (include
claudeProjectsDir, projectDir or filePath and the operation like
fs.stat/fs.readdir), and for all other unknown error.code values rethrow the
error instead of returning empty results (affecting functions/processes
including processProjectDir, the initial fs.access block, and the per-file
fs.stat handling).
In `@src/main/parsers/codex-output-parser.ts`:
- Around line 246-250: The empty .catch on loadCodexConfigCached() swallows all
errors; update the catch in the promise chain that calls
loadCodexConfigCached().then(...).catch(...) to explicitly ignore only expected
"config not found" errors (check error.code === 'ENOENT' or error.message
contains 'not found' or similar), and for any other error call your logger (or
console.error) with context (e.g., "Failed to load Codex config" and the error)
before rethrowing or returning; reference the loadCodexConfigCached and
this.applyCodexConfig call sites to locate where to add the error discrimination
and logging.
In `@src/main/utils/context-groomer.ts`:
- Around line 320-342: The GroomingProcessManager.spawn signature is currently
synchronous but the code calls await processManager.spawn(...); update the
GroomingProcessManager interface method spawn to return Promise<{ pid: number;
success?: boolean } | null> (i.e., make it async) to match the usage in
context-groomer where processManager.spawn is awaited; then adjust any
implementing classes/types to return a Promise as well so implementations
conform to the new async signature (refer to the spawn method on
GroomingProcessManager and the usage processManager.spawn in the spawning
block).
In `@src/main/web-server/types.ts`:
- Around line 294-297: Update the test callback implementations to satisfy the
GetHistoryCallback signature which returns Promise<HistoryEntry[]>: locate uses
in CallbackRegistry.test.ts (the functions passed as history callbacks) and
change their returns from raw arrays (e.g., () => []) to promise-returning ones
(e.g., async () => [] or () => Promise.resolve([])); ensure any helper mocks
used by CallbackRegistry.getHistory() are updated similarly so the type matches
GetHistoryCallback and avoids returning non-Promise values.
In `@src/renderer/components/FilePreview.tsx`:
- Around line 673-680: The regex is built from the raw searchQuery while
hasSearchQuery uses trimmedSearchQuery, causing mismatches; update the useMemo
for searchRegex to construct the RegExp from trimmedSearchQuery (not
searchQuery) so the gating (hasSearchQuery) and pattern creation align—adjust
the dependency list to include trimmedSearchQuery and keep
escapeRegexCharacters, useMemo, trimmedSearchQuery, and hasSearchQuery as the
referenced symbols.
- Around line 289-297: The CODE_FENCE_BOUNDARY_REGEX and HEADING_CAPTURE_REGEX
don't accept the up-to-3 leading spaces allowed by CommonMark, so update
CODE_FENCE_BOUNDARY_REGEX (used in FilePreview) to allow optional 0–3 leading
spaces before the fence marker (e.g., prefix with a non-capturing group like (?:
{0,3})) and update HEADING_CAPTURE_REGEX to allow 0–3 leading spaces before the
ATX hashes (e.g., ^(?: {0,3})(#{1,6})\s+(.+)$) while preserving multiline
behavior; keep other regexes (MARKDOWN_OPEN_TASK_REGEX,
MARKDOWN_CLOSED_TASK_REGEX, etc.) as-is.
---
Outside diff comments:
In `@src/main/history-manager.ts`:
- Around line 209-330: Concurrent read-modify-write operations on the same
session can interleave and lose entries; implement a per-session Promise queue
to serialize operations by adding a Map<string, Promise<any>> (e.g.,
sessionQueues) and, in each mutating method (addEntry, deleteEntry, updateEntry,
clearSession and updateSessionNameByClaudeSessionId), replace the direct body
with an enqueued task: set sessionQueues[sessionId] = (sessionQueues[sessionId]
|| Promise.resolve()).then(() => { /* original logic */ }); ensure the enqueued
callback returns the final Promise so callers await completion, catch/log errors
inside the task, and finally clear the map entry when the chain settles to avoid
memory leaks; keep the existing read/write/error handling but run it inside the
queued task to preserve ordering.
In `@src/main/ipc/handlers/tabNaming.ts`:
- Around line 197-263: If processManager.spawn rejects, the timeout and
listeners (onData/onExit) are left registered causing leaks; update the spawn
error handler inside the async IIFE so it clears the timeout (timeoutId),
removes listeners via processManager.off('data', onData) and
processManager.off('exit', onExit), and ensures the promise is settled exactly
once by checking/setting the resolved flag before rejecting (or resolving) with
the error; reference processManager.spawn, onData, onExit, timeoutId, resolved
and sessionId to locate and modify the handler.
In `@src/main/process-manager/utils/imageUtils.ts`:
- Around line 33-45: The catch block around Buffer.from(parsed.base64) /
fsPromises.writeFile currently only logs and returns null; change it to capture
the exception with the Sentry helper and let it bubble instead of silently
swallowing it: import captureException from 'src/utils/sentry', call
captureException(error, { extra: { tempPath, parsedBase64Preview:
parsed.base64?.slice(0,100), attemptedSize: buffer?.length } }) (or similar
contextual fields) inside the catch, keep or augment the logger.error, and then
rethrow the caught error (throw error) so it surfaces to calling code/Sentry
rather than returning null. Ensure you reference parsed.base64, tempPath and the
write operation (fsPromises.writeFile) when adding context.
- Around line 22-36: The catch block in saveImageToTempFile currently only logs
the error; update it to import the Sentry utilities from src/utils/sentry and
call the appropriate reporting function (e.g., captureException or reportError)
inside the catch that surrounds fsPromises.writeFile, passing the caught
exception plus context (function name saveImageToTempFile, index, tempPath, ext,
and parsed.mediaType or a safe subset of dataUrl info) so failures are
explicitly reported to Sentry, then return null (preserving current behavior)
instead of silently swallowing the error.
In `@src/main/stats/stats-db.ts`:
- Around line 175-182: The current getDatabaseSize (using
StatsDB.fsPromises.stat on this.dbPath) and pathExists swallow all errors;
change both to catch errors explicitly and only suppress ENOENT: in
getDatabaseSize, after awaiting StatsDB.fsPromises.stat(this.dbPath) catch the
error, if error.code === 'ENOENT' return 0, otherwise rethrow the error;
similarly in pathExists (the function that checks this.dbPath via
fsPromises.access/stat) catch and return false only for error.code === 'ENOENT'
and rethrow other errors so unexpected permission/IO failures surface to Sentry.
In `@src/main/web-server/routes/apiRoutes.ts`:
- Around line 326-338: The catch block around this.callbacks.getHistory in the
route handler currently returns a 500 without reporting the error; update the
handler to call the Sentry utilities (import captureException and/or
captureMessage from src/utils/sentry) when an unexpected error occurs in
getHistory (or rethrow after reporting) so failures are reported to Sentry;
specifically, in the catch for the getHistory call (references: getHistory,
this.callbacks.getHistory) call captureException(error, { extra: { projectPath,
sessionId } }) or captureMessage with context before sending the reply (or
rethrow) to ensure the error is captured by Sentry.
In `@src/renderer/components/AICommandsPanel.tsx`:
- Around line 209-259: The interactive controls (the toggle button that calls
setVariablesExpanded and the "Add Command" button that calls setIsCreating) are
missing keyboard focus attributes; add tabIndex={0} and the Tailwind class
"outline-none" to these button elements (and other buttons in this panel,
including the variable items if they should be interactive) so they are
focusable, and where a control should auto-focus (e.g., when opening creation UI
setIsCreating true) use a ref callback like ref={(el) => el?.focus()} on that
element; ensure you update the JSX around the variables toggle
(variablesExpanded, setVariablesExpanded), TEMPLATE_VARIABLES_GENERAL items, and
the Add Command button to include these changes.
In `@src/renderer/components/ExecutionQueueIndicator.tsx`:
- Around line 137-146: The ExecutionQueueIndicator button lacks explicit focus
affordances; update the JSX for the button element (the one using
ref={containerRef} and onClick={onClick} inside the ExecutionQueueIndicator
component) to include tabIndex={0} and add the "outline-none" class to its
className so keyboard focus is enabled while preserving existing styles and
classes.
In `@src/renderer/components/SessionList.tsx`:
- Around line 1799-1813: Fix the inconsistent indentation in the button JSX
inside SessionList component: re-indent the onClick handler body, title
attribute, and surrounding attributes to match the file's existing tab-based
indentation style (use tabs, not spaces) so all attributes and children lines
align with the rest of the button element; specifically update the JSX for the
button that calls onToggleWorktreeExpanded(session.id) and uses
sessionListStyles.worktreeCollapseButton, GitBranch, ChevronUp, and
worktreeChildren so the attribute lines and inner lines have uniform tab
indentation consistent with the file.
In `@src/renderer/components/UsageDashboard/ChartSkeletons.tsx`:
- Around line 47-77: The root containers of several skeleton components
(SummaryCardsSkeleton, AgentComparisonChartSkeleton,
SourceDistributionChartSkeleton, ActivityHeatmapSkeleton,
DurationTrendsChartSkeleton, AutoRunStatsSkeleton, DashboardSkeleton) lack focus
attributes; update each root <div> to include a focusable/tabbable setup by
adding tabIndex={0} or tabIndex={-1} as appropriate, the "outline-none" class,
and a ref that auto-focuses (ref={el => el?.focus()}) per renderer guidelines so
keyboard/focus handling works; modify the root container in the listed
components (e.g., the outer div in SummaryCardsSkeleton) to include these three
changes.
In `@src/renderer/components/UsageDashboard/LongestAutoRunsTable.tsx`:
- Around line 128-138: The fetchData function currently swallows errors in its
catch by only calling console.error; replace that with error reporting to Sentry
or rethrowing so exceptions are not silently ignored. Update the catch in
fetchData (wrapping window.maestro.stats.getAutoRunSessions) to call the
project's Sentry utility (e.g., Sentry.captureException or the app's
captureError helper) with the caught err and a helpful message, then rethrow or
allow the error to bubble instead of solely logging; ensure setLoading(false)
still runs in finally and preserve existing setSessions behavior.
---
Nitpick comments:
In `@src/main/debug-package/collectors/group-chats.ts`:
- Around line 54-86: The current for-loop in the group chat collector processes
files sequentially (iterating over files, reading with fsPromises.readFile,
parsing JSON, calling countMessages, and pushing into groupChats), which is slow
for many files; refactor collect logic (the loop that builds chatInfo) to run in
parallel by mapping files to async tasks and using Promise.all (or a bounded
concurrency utility like p-limit) to run readFile/JSON.parse/countMessages
concurrently, ensure each task catches/parses its own errors so a single failure
doesn't reject the entire Promise.all, and finally aggregate successful chatInfo
results into the groupChats array.
In `@src/renderer/components/AutoRun.tsx`:
- Around line 690-699: The useMemo for searchRegex redundantly lists searchQuery
in its dependency array even though escapedSearchQuery already updates from
searchQuery; update the useMemo for searchRegex (the function named searchRegex
created with useMemo) to depend only on escapedSearchQuery (remove searchQuery
from the dependency array) so the memo is correctly tied to the derived value.
In `@src/renderer/components/QRCode.tsx`:
- Around line 57-61: Replace the console.error-only catch in QRCode.tsx with a
Sentry report: import captureException from src/utils/sentry and call
captureException(err, { extra: { value, size } }) (or appropriate local
variables used for QR generation) inside the .catch block, then keep the
existing setError('Failed to generate QR code') and setDataUrl(null) so the UI
behavior is unchanged; ensure the import and the use of captureException
reference the same names used elsewhere (captureException) and include relevant
contextual fields.
In `@src/renderer/components/SessionList.tsx`:
- Around line 2002-2003: The existing comment above the useEffect in SessionList
is misleading: it says "Keep dependencies limited to sessionFilter and sessions"
while the dependency array only includes [sessionFilter] and sessions is
intentionally omitted; update the comment to clearly state that the effect only
runs when sessionFilter changes and that sessions is intentionally read from the
current render (i.e., excluded from dependencies on purpose) so future readers
won't assume sessions should be in the array; reference the useEffect block, the
sessionFilter variable, and the sessions variable when making this
clarification.
In `@src/renderer/components/UsageDashboard/AutoRunStats.tsx`:
- Around line 90-130: MetricCard is a plain functional component rendered
multiple times so wrap it with React.memo to avoid unnecessary re-renders;
update the MetricCard export/definition to use React.memo(MetricCard) (or assign
const MemoMetricCard = React.memo(MetricCard) and use that) and ensure any props
used (icon, label, value, subValue, theme) are stable or provide a custom
comparison function to React.memo if needed to control re-rendering.
In `@src/renderer/components/UsageDashboard/EmptyState.tsx`:
- Line 36: The inline style objects in the EmptyState component (e.g., style={{
color: theme.colors.textDim }}, style={{ opacity: 0.3 }}, and other similar
inline styles) should be extracted so they don't get re-created on every render;
move static styles to module-level constants and wrap theme-dependent style
objects in a useMemo inside the EmptyState function (referencing theme) and
replace the inline style props on the relevant JSX elements with those memoized
constants (look for the style props in EmptyState.tsx around the color/opacity
usages).
In `@src/renderer/components/UsageDashboard/LongestAutoRunsTable.tsx`:
- Around line 229-235: The per-row inline style and the JS hover handlers
(onMouseEnter/onMouseLeave) in LongestAutoRunsTable recreate style objects on
every render; replace the JS-driven alternating-row and hover styling with CSS
rules instead (use a class on the row element and define :nth-child(even) for
alternating background and :hover for hover state using theme colors or CSS
variables derived from theme.colors), or if you must keep inline styles, memoize
the base style per index (e.g., compute once using a stable helper) and remove
the onMouseEnter/onMouseLeave backgroundColor mutations so the row style is
static and not recreated on each render.
| {"type":"session","event":"end","session_id":"f6904b0f-9eda-443e-bbef-dca3328d9838","timestamp":"2026-02-15T06:29:39.768Z","environment_hash":"1270eb4efee9b1422e9885aefd86d51b1e7690137ac6bf3323cf6e805a535493","assurance_level":"high","description":"codex agent session ended"} | ||
| {"type":"session","event":"start","session_id":"79b55eb5-d4c1-4af9-9a74-8e1d89ec7b11","timestamp":"2026-02-15T06:29:40.019Z","environment_hash":"1270eb4efee9b1422e9885aefd86d51b1e7690137ac6bf3323cf6e805a535493","assurance_level":"high","description":"codex agent session"} | ||
| {"type":"session","event":"end","session_id":"79b55eb5-d4c1-4af9-9a74-8e1d89ec7b11","timestamp":"2026-02-15T06:33:11.783Z","environment_hash":"1270eb4efee9b1422e9885aefd86d51b1e7690137ac6bf3323cf6e805a535493","assurance_level":"high","description":"codex agent session ended"} | ||
| {"type":"session","event":"start","session_id":"1a945b20-4249-4534-a69b-91262e1d9afa","timestamp":"2026-02-15T06:33:12.370Z","environment_hash":"1270eb4efee9b1422e9885aefd86d51b1e7690137ac6bf3323cf6e805a535493","assurance_level":"high","description":"codex agent session"} |
There was a problem hiding this comment.
Possible missing session end event (Line 11).
Line 11 records a start event for session 1a945b20-4249-4534-a69b-91262e1d9afa without a matching end event in this file. If this JSONL is meant to represent completed sessions, consider adding the corresponding end event or trimming the incomplete start.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.ai-audit/annotations.jsonl at line 11, The entry with session_id
"1a945b20-4249-4534-a69b-91262e1d9afa" is a start event without a corresponding
end; add a matching JSONL end event object for that session (type "session",
event "end", same session_id and environment_hash, a later timestamp and same
assurance_level/description as appropriate) so the start/end pair is complete,
or remove the incomplete start line if the session should not be recorded.
| private async fileExists(filePath: string): Promise<boolean> { | ||
| try { | ||
| await fsPromises.access(filePath, fs.constants.F_OK); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Don’t treat non-ENOENT access errors as “missing.”
fileExists currently returns false for any access error; this can hide permission/IO failures. Consider only suppressing ENOENT and rethrowing others.
🛠️ Suggested ENOENT-only handling
- private async fileExists(filePath: string): Promise<boolean> {
- try {
- await fsPromises.access(filePath, fs.constants.F_OK);
- return true;
- } catch {
- return false;
- }
- }
+ private async fileExists(filePath: string): Promise<boolean> {
+ try {
+ await fsPromises.access(filePath, fs.constants.F_OK);
+ return true;
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
+ return false;
+ }
+ throw error;
+ }
+ }As per coding guidelines, handle only expected/recoverable errors explicitly with specific error codes and let unexpected errors bubble up to Sentry.
📝 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.
| private async fileExists(filePath: string): Promise<boolean> { | |
| try { | |
| await fsPromises.access(filePath, fs.constants.F_OK); | |
| return true; | |
| } catch { | |
| return false; | |
| } | |
| private async fileExists(filePath: string): Promise<boolean> { | |
| try { | |
| await fsPromises.access(filePath, fs.constants.F_OK); | |
| return true; | |
| } catch (error) { | |
| if ((error as NodeJS.ErrnoException).code === 'ENOENT') { | |
| return false; | |
| } | |
| throw error; | |
| } | |
| } |
🤖 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 179 - 185, The fileExists method
currently treats any fsPromises.access error as "missing"; update fileExists to
only return false when the caught error has code 'ENOENT' and rethrow all other
errors so permission/IO errors surface to Sentry. Specifically, inside
fileExists (which calls fsPromises.access with fs.constants.F_OK), inspect the
caught error's code and return false for 'ENOENT' only—otherwise throw the
error. Keep the async signature and behavior unchanged for the ENOENT case.
| try { | ||
| await fs.access(claudeProjectsDir); | ||
| } catch { | ||
| setCachedSessionFiles('claude-code', files); | ||
| return files; | ||
| } | ||
|
|
||
| const projectDirs = await fs.readdir(claudeProjectsDir); | ||
|
|
||
| for (const projectDir of projectDirs) { | ||
| const processProjectDir = async (projectDir: string): Promise<SessionFileInfo[]> => { | ||
| const collected: SessionFileInfo[] = []; | ||
| const projectPath = path.join(claudeProjectsDir, projectDir); | ||
| try { | ||
| const stat = await fs.stat(projectPath); | ||
| if (!stat.isDirectory()) continue; | ||
| if (!stat.isDirectory()) return collected; | ||
| } catch { | ||
| return collected; | ||
| } | ||
|
|
||
| const dirFiles = await fs.readdir(projectPath); | ||
| const sessionFiles = dirFiles.filter((f) => f.endsWith('.jsonl')); | ||
| let dirFiles: string[]; | ||
| try { | ||
| dirFiles = await fs.readdir(projectPath); | ||
| } catch { | ||
| return collected; | ||
| } | ||
|
|
||
| for (const filename of sessionFiles) { | ||
| const filePath = path.join(projectPath, filename); | ||
| try { | ||
| const fileStat = await fs.stat(filePath); | ||
| // Skip 0-byte sessions (created but abandoned before any content was written) | ||
| if (fileStat.size === 0) continue; | ||
| const sessionKey = `${projectDir}/${filename.replace('.jsonl', '')}`; | ||
| files.push({ filePath, sessionKey, mtimeMs: fileStat.mtimeMs }); | ||
| } catch { | ||
| // Skip files we can't stat | ||
| } | ||
| for (const filename of dirFiles) { | ||
| if (!filename.endsWith('.jsonl')) continue; | ||
| const filePath = path.join(projectPath, filename); | ||
| try { | ||
| const fileStat = await fs.stat(filePath); | ||
| // Skip 0-byte sessions (created but abandoned before any content was written) | ||
| if (fileStat.size === 0) continue; | ||
| const sessionKey = `${projectDir}/${filename.replace('.jsonl', '')}`; | ||
| collected.push({ filePath, sessionKey, mtimeMs: fileStat.mtimeMs }); | ||
| } catch { | ||
| // Skip files we can't stat | ||
| } |
There was a problem hiding this comment.
Avoid swallowing filesystem errors during session discovery.
The new broad catch { ... } blocks return empty results for all errors, which can mask unexpected failures and violates the error-handling policy. Please filter for expected/recoverable error codes (e.g., ENOENT, ENOTDIR, EACCES) and rethrow anything else so it can be captured upstream.
🐛 Suggested pattern (apply similarly to other catch blocks)
- } catch {
- return collected;
- }
+ } catch (error) {
+ const code = (error as NodeJS.ErrnoException).code;
+ if (code === 'ENOENT' || code === 'ENOTDIR' || code === 'EACCES') {
+ return collected;
+ }
+ throw error;
+ }As per coding guidelines: Do NOT silently swallow exceptions with try-catch-console.error blocks. Handle only expected/recoverable errors explicitly with specific error codes. Let unexpected errors bubble up to Sentry. Use Sentry utilities (captureException, captureMessage) from 'src/utils/sentry' for explicit error reporting with context.
Also applies to: 327-395
🤖 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 241 - 278, The catch
blocks in the session discovery flow (around fs.access, fs.readdir and inside
processProjectDir) are swallowing all errors; update each catch to only handle
expected filesystem error codes (e.g., ENOENT, ENOTDIR, EACCES) and rethrow any
other errors so they bubble up (use the same pattern for the other catches noted
around lines 327-395). In places where you intentionally handle a recoverable
error, log/report it using the Sentry helpers (captureException or
captureMessage from src/utils/sentry) with context (include claudeProjectsDir,
projectDir or filePath and the operation like fs.stat/fs.readdir), and for all
other unknown error.code values rethrow the error instead of returning empty
results (affecting functions/processes including processProjectDir, the initial
fs.access block, and the per-file fs.stat handling).
| void loadCodexConfigCached() | ||
| .then((config) => { | ||
| this.applyCodexConfig(config); | ||
| }) | ||
| .catch(() => {}); |
There was a problem hiding this comment.
Silent error swallowing may hide unexpected failures.
The empty .catch(() => {}) at line 250 silently discards all errors from config loading. While missing config files are expected, unexpected errors (permissions, disk I/O) should be logged for debugging. Per coding guidelines, unexpected errors should not be silently swallowed.
🛠️ Suggested fix
void loadCodexConfigCached()
.then((config) => {
this.applyCodexConfig(config);
})
- .catch(() => {});
+ .catch((err) => {
+ // Config loading is optional - log for debugging but don't fail
+ if (process.env.NODE_ENV !== 'test') {
+ console.debug('[CodexOutputParser] Config load failed:', err.message);
+ }
+ });📝 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.
| void loadCodexConfigCached() | |
| .then((config) => { | |
| this.applyCodexConfig(config); | |
| }) | |
| .catch(() => {}); | |
| void loadCodexConfigCached() | |
| .then((config) => { | |
| this.applyCodexConfig(config); | |
| }) | |
| .catch((err) => { | |
| // Config loading is optional - log for debugging but don't fail | |
| if (process.env.NODE_ENV !== 'test') { | |
| console.debug('[CodexOutputParser] Config load failed:', err.message); | |
| } | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/parsers/codex-output-parser.ts` around lines 246 - 250, The empty
.catch on loadCodexConfigCached() swallows all errors; update the catch in the
promise chain that calls loadCodexConfigCached().then(...).catch(...) to
explicitly ignore only expected "config not found" errors (check error.code ===
'ENOENT' or error.message contains 'not found' or similar), and for any other
error call your logger (or console.error) with context (e.g., "Failed to load
Codex config" and the error) before rethrowing or returning; reference the
loadCodexConfigCached and this.applyCodexConfig call sites to locate where to
add the error discrimination and logging.
| const spawnProcess = 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; | ||
| } | ||
| if (!spawnResult || spawnResult.pid <= 0) { | ||
| cleanup(); | ||
| resolved = true; | ||
| reject(new Error(`Failed to spawn grooming process for ${agentType}`)); | ||
| return; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Search for ProcessManager and spawn method definitions
rg -n "interface.*ProcessManager|class.*ProcessManager" --type ts -A 5 | head -50
# Check the spawn method signature specifically
rg -n "spawn\s*\(" --type ts -B 2 -A 3 | grep -A 3 -B 2 "interface\|spawn"
# Look for the specific interface at line 46 in context-groomer.ts
head -50 src/main/utils/context-groomer.ts | tail -10Repository: RunMaestro/Maestro
Length of output: 50376
🏁 Script executed:
# Find and check the ProcessManager interface/class definition
fd -e ts | xargs rg -l "ProcessManager" | head -10Repository: RunMaestro/Maestro
Length of output: 477
🏁 Script executed:
# Check context-groomer.ts to see the interface definition around line 46
cat -n src/main/utils/context-groomer.ts | sed -n '40,55p'Repository: RunMaestro/Maestro
Length of output: 679
🏁 Script executed:
# Check if spawn is async in the actual ProcessManager implementation
rg -n "async spawn" --type ts -B 2 -A 10Repository: RunMaestro/Maestro
Length of output: 3372
Update GroomingProcessManager interface to reflect async spawn signature. The interface declares spawn() returning { pid: number; success?: boolean } | null, but the actual implementation (lines 321+) uses await, indicating spawn is async. The interface should return Promise<{ pid: number; success?: boolean } | null> to match the ProcessManager implementation.
🤖 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 320 - 342, The
GroomingProcessManager.spawn signature is currently synchronous but the code
calls await processManager.spawn(...); update the GroomingProcessManager
interface method spawn to return Promise<{ pid: number; success?: boolean } |
null> (i.e., make it async) to match the usage in context-groomer where
processManager.spawn is awaited; then adjust any implementing classes/types to
return a Promise as well so implementations conform to the new async signature
(refer to the spawn method on GroomingProcessManager and the usage
processManager.spawn in the spawning block).
| export type GetHistoryCallback = ( | ||
| projectPath?: string, | ||
| sessionId?: string | ||
| ) => import('../../shared/types').HistoryEntry[]; | ||
| ) => Promise<import('../../shared/types').HistoryEntry[]>; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find GetHistoryCallback implementers/usages and verify async return shapes.
rg -n --type=ts "GetHistoryCallback" src
rg -n --type=ts "getHistory\s*:" src/main/web-server
rg -n --type=ts "getHistory\s*=" src/main/web-serverRepository: RunMaestro/Maestro
Length of output: 2778
🏁 Script executed:
#!/bin/bash
# Check the actual implementations in test files
sed -n '205,215p' src/__tests__/main/web-server/managers/CallbackRegistry.test.ts
sed -n '560,600p' src/__tests__/main/web-server/managers/CallbackRegistry.test.ts
sed -n '370,385p' src/main/web-server/WebServer.tsRepository: RunMaestro/Maestro
Length of output: 2612
🏁 Script executed:
#!/bin/bash
# Find getHistory method in CallbackRegistry
sed -n '1,300p' src/main/web-server/managers/CallbackRegistry.ts | grep -A 20 "getHistory\s*("Repository: RunMaestro/Maestro
Length of output: 768
Fix type mismatch: GetHistoryCallback implementations don't return Promises as required by the type.
The type now requires Promise<HistoryEntry[]> return, but implementations in tests (e.g., () => [] at CallbackRegistry.test.ts:210, 567, 572, etc.) return arrays directly instead of Promises. While CallbackRegistry.getHistory() is async and wraps results at runtime, this creates a type safety violation. Update test implementations to return Promises (e.g., async () => [] or () => Promise.resolve([])).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/main/web-server/types.ts` around lines 294 - 297, Update the test
callback implementations to satisfy the GetHistoryCallback signature which
returns Promise<HistoryEntry[]>: locate uses in CallbackRegistry.test.ts (the
functions passed as history callbacks) and change their returns from raw arrays
(e.g., () => []) to promise-returning ones (e.g., async () => [] or () =>
Promise.resolve([])); ensure any helper mocks used by
CallbackRegistry.getHistory() are updated similarly so the type matches
GetHistoryCallback and avoids returning non-Promise values.
| const MARKDOWN_OPEN_TASK_REGEX = /^[\s]*[-*]\s*\[\s*\]/gm; | ||
| const MARKDOWN_CLOSED_TASK_REGEX = /^[\s]*[-*]\s*\[[xX]\]/gm; | ||
| const CODE_FENCE_BOUNDARY_REGEX = /^(`{3,}|~{3,})/; | ||
| const HEADING_CAPTURE_REGEX = /^(#{1,6})\s+(.+)$/; | ||
| const MARKDOWN_HIGHLIGHT_REGEX = /==([^=]+)==/g; | ||
| const INLINE_CODE_LANGUAGE_REGEX = /language-(\w+)/; | ||
| const REGEX_SPECIAL_CHARS = /[.*+?^${}()|[\]\\]/g; | ||
|
|
||
| const escapeRegexCharacters = (value: string): string => value.replace(REGEX_SPECIAL_CHARS, '\\$&'); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, verify the file exists and check the exact lines mentioned
head -n 300 src/renderer/components/FilePreview.tsx | tail -n 15Repository: RunMaestro/Maestro
Length of output: 614
🌐 Web query:
CommonMark specification indented code fences headings leading spaces
💡 Result:
In CommonMark, “leading spaces” matter because most block constructs allow up to 3 spaces of indentation, but 4 spaces generally switches interpretation to an indented code block.
- ATX headings (
#): the opening#may be preceded by up to 3 spaces; with 4 spaces, it’s not a heading (it becomes indented code / literal text). [1][2] - Setext headings:
- The content line must not have more than 3 leading spaces. [1]
- The underline (
---/===) may be preceded by up to 3 spaces; 4 spaces is too many. [1][2]
- Indented code blocks: any non-blank line with 4+ leading spaces is an indented code “chunk” (and the content is taken literally, not parsed as Markdown). [3]
- Fenced code blocks (``` / ~~~): the opening fence must be preceded by up to 3 spaces; 4 spaces is too many (the fence line is then just literal text inside an indented code block). Closing fences also only allow up to 3 spaces. [4][5]
Sources
- CommonMark Spec 0.30 – ATX + Setext heading indentation rules: (spec.commonmark.org)
- CommonMark Spec 0.30 – examples showing “four spaces is too many” for headings: (spec.commonmark.org)
- CommonMark Spec 0.30 – indented code block definition (4+ spaces): (spec.commonmark.org)
- CommonMark Spec 0.30 – fenced code blocks begin with fence indented up to 3 spaces: (spec.commonmark.org)
- CommonMark Spec 0.30 – examples showing 4-space-indented fences aren’t fences (and closing fence can’t be indented 4): (spec.commonmark.org)
Citations:
- 1: https://spec.commonmark.org/0.30/?utm_source=openai
- 2: https://spec.commonmark.org/0.30/
- 3: https://spec.commonmark.org/0.30/
- 4: https://spec.commonmark.org/0.30/
- 5: https://spec.commonmark.org/0.30/
🏁 Script executed:
# Find where CODE_FENCE_BOUNDARY_REGEX and HEADING_CAPTURE_REGEX are used
rg -n "CODE_FENCE_BOUNDARY_REGEX|HEADING_CAPTURE_REGEX" src/renderer/components/FilePreview.tsxRepository: RunMaestro/Maestro
Length of output: 277
🏁 Script executed:
# Get the function context where these regexes are used
sed -n '310,335p' src/renderer/components/FilePreview.tsxRepository: RunMaestro/Maestro
Length of output: 749
🏁 Script executed:
# Get more context - find the function signature and how 'lines' is created
sed -n '290,315p' src/renderer/components/FilePreview.tsx | head -n 30Repository: RunMaestro/Maestro
Length of output: 1029
🏁 Script executed:
# Check how extractHeadings is called - find callers of this function
rg -n "extractHeadings" src/renderer/components/FilePreview.tsx -A 2 -B 2Repository: RunMaestro/Maestro
Length of output: 461
🏁 Script executed:
# Check if there's any preprocessing of 'line' before regex testing
sed -n '307,335p' src/renderer/components/FilePreview.tsxRepository: RunMaestro/Maestro
Length of output: 878
Handle indented fences and headings in TOC parsing.
CommonMark allows up to 3 leading spaces before fenced code blocks and ATX headings. With the current regexes, lines with 1–3 leading spaces won't be detected, causing indented fences to go untracked and headings inside code fences to leak into the TOC. Update the regexes to allow optional leading whitespace.
🔧 Suggested regex fix
-const CODE_FENCE_BOUNDARY_REGEX = /^(`{3,}|~{3,})/;
-const HEADING_CAPTURE_REGEX = /^(#{1,6})\s+(.+)$/;
+const CODE_FENCE_BOUNDARY_REGEX = /^\s{0,3}(`{3,}|~{3,})/;
+const HEADING_CAPTURE_REGEX = /^\s{0,3}(#{1,6})\s+(.+)$/;🤖 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 289 - 297, The
CODE_FENCE_BOUNDARY_REGEX and HEADING_CAPTURE_REGEX don't accept the up-to-3
leading spaces allowed by CommonMark, so update CODE_FENCE_BOUNDARY_REGEX (used
in FilePreview) to allow optional 0–3 leading spaces before the fence marker
(e.g., prefix with a non-capturing group like (?: {0,3})) and update
HEADING_CAPTURE_REGEX to allow 0–3 leading spaces before the ATX hashes (e.g.,
^(?: {0,3})(#{1,6})\s+(.+)$) while preserving multiline behavior; keep other
regexes (MARKDOWN_OPEN_TASK_REGEX, MARKDOWN_CLOSED_TASK_REGEX, etc.) as-is.
| const trimmedSearchQuery = searchQuery.trim(); | ||
| const hasSearchQuery = trimmedSearchQuery.length > 0; | ||
| const searchRegex = useMemo(() => { | ||
| if (!hasSearchQuery) { | ||
| return null; | ||
| } | ||
| return new RegExp(escapeRegexCharacters(searchQuery), 'gi'); | ||
| }, [hasSearchQuery, searchQuery]); |
There was a problem hiding this comment.
Align searchRegex with the trimmed query.
hasSearchQuery is derived from the trimmed value, but the regex is built from the raw query. Leading/trailing spaces can make search look “active” while never matching. Consider building the regex from trimmedSearchQuery to keep gating and matching aligned.
🔧 Suggested tweak
-const searchRegex = useMemo(() => {
- if (!hasSearchQuery) {
- return null;
- }
- return new RegExp(escapeRegexCharacters(searchQuery), 'gi');
-}, [hasSearchQuery, searchQuery]);
+const searchRegex = useMemo(() => {
+ if (!hasSearchQuery) {
+ return null;
+ }
+ return new RegExp(escapeRegexCharacters(trimmedSearchQuery), 'gi');
+}, [hasSearchQuery, trimmedSearchQuery]);Also applies to: 2186-2213
🤖 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 673 - 680, The regex is
built from the raw searchQuery while hasSearchQuery uses trimmedSearchQuery,
causing mismatches; update the useMemo for searchRegex to construct the RegExp
from trimmedSearchQuery (not searchQuery) so the gating (hasSearchQuery) and
pattern creation align—adjust the dependency list to include trimmedSearchQuery
and keep escapeRegexCharacters, useMemo, trimmedSearchQuery, and hasSearchQuery
as the referenced symbols.
Closes #371
Summary
Systematic UX performance sweep targeting React rendering efficiency, main-process responsiveness, and database query performance. All changes are low-risk, behavior-preserving optimizations.
Changes
React Memoization (PERF-01 / PERF-02 / PERF-03)
React.memowraps for high-churn components:SessionListItem,ExecutionQueueIndicator,Toast,ConfirmModal,RenameTabModal,QRCode,QuickActionsModal,HistoryHelpModal,ContextWarningSash, and all 14UsageDashboardchart componentsuseMemofor expensive computations: session list sorts, AutoRun regex compilation, FilePreview regex/formatting, CsvTableRenderer regex, date tooltip formattingSessionListItem,SessionList,AICommandsPanelto prevent re-render allocationMain Process Async I/O (PERF-04 / PERF-05)
history-manager.ts: converted all syncfs.*calls tofs.promises.*, added async helper utilities, updated all IPC/director-notes/web-server callers to await the async flowsstats-db.ts/singleton.ts: async initialization, backup, vacuum, and directory creation; full test suite updatedgroup-chats.ts,storage.ts): async file readscodex-output-parser.ts: async config cachingimageUtils.ts: async temp file writeswslDetector.ts: async detectionuseStatshook: reuse shared debounced callback to reduce redundant fetchesDatabase & Query Performance (PERF-06)
stats-dbschema for common access patternsPromise.allfor independent git queries in IPC handlersVIBES Metadata
.ai-audit/directory with annotations, config, and manifest for AI code provenance trackingTest Plan
npx vitest run src/__tests__/main/history-manager.test.ts src/__tests__/main/ipc/handlers/history.test.ts src/__tests__/main/ipc/handlers/director-notes.test.ts src/__tests__/main/stats/*.test.tsnpx vitest run— full suite regression checknpm run buildsucceedsRisk Assessment
All changes are behavior-preserving:
React.memowraps cannot break behavior (only skip unnecessary re-renders)useMemocaches pure computations🤖 Generated with Claude Code
Summary by CodeRabbit
Performance Improvements
Bug Fixes & Refactoring