auto-claude: 225-bulk-delete-and-archive-chat-history#1829
auto-claude: 225-bulk-delete-and-archive-chat-history#1829
Conversation
…ession and InsightsSessionSummary Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ete and archive Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ionStorage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ionManager Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…lers.ts Add handlers for bulk delete, archive, bulk archive, and unarchive sessions. Update list sessions handler to accept includeArchived param. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e/archive methods Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…o insights-store Add deleteSessions, archiveSession, archiveSessions, unarchiveSession helpers and update loadInsightsSessions to accept optional includeArchived parameter. Also update ElectronAPI types and browser mocks for new methods. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… bulk delete and archive Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ive support to ChatHistorySidebar Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ySidebar props Add showArchived state, import bulk store helpers (deleteSessions, archiveSession, archiveSessions, unarchiveSession), create handler functions, add useEffect to reload sessions when showArchived changes, and pass all new props to ChatHistorySidebar. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ions (qa-requested) Make handleArchiveSession, handleUnarchiveSession, handleDeleteSessions, and handleArchiveSessions async, await store calls, then reload sessions with showArchived flag to preserve archived session visibility. QA Fix Session: 2
📝 WalkthroughWalkthroughAdds archiving, unarchiving, and bulk deletion for Insights sessions across storage, manager, service, IPC, preload API, renderer store, UI, types, mocks, and i18n; Changes
Sequence DiagramsequenceDiagram
participant User as User (UI)
participant Sidebar as ChatHistorySidebar
participant Insights as Insights Component
participant Store as Insights Store
participant API as InsightsAPI (preload)
participant IPC as IPC Handler
participant Service as InsightsService
participant Manager as SessionManager
participant Storage as SessionStorage
User->>Sidebar: select sessions & trigger bulk archive/delete
Sidebar->>Insights: onArchiveSessions / onDeleteSessions(sessionIds)
Insights->>Store: archiveSessions / deleteSessions(projectId, sessionIds)
Store->>API: archiveInsightsSessions / deleteInsightsSessions(projectId, sessionIds)
API->>IPC: invokeIpc(INSIGHTS_ARCHIVE_SESSIONS / INSIGHTS_DELETE_SESSIONS, projectId, sessionIds)
IPC->>Service: archiveSessions / deleteSessions(projectPath, sessionIds)
Service->>Manager: archiveSessions / deleteSessions(projectPath, sessionIds)
Manager->>Storage: archiveSessions / deleteSessions(projectPath, sessionIds)
Storage-->>Manager: {archivedIds/deletedIds, failedIds}
Manager-->>Service: {archivedIds/deletedIds, failedIds}
Service-->>IPC: {archivedIds/deletedIds, failedIds}
IPC-->>API: {success, data}
API-->>Store: result received
Store->>Store: loadInsightsSessions(projectId, includeArchived)
Store->>API: listInsightsSessions(projectId, includeArchived)
API->>IPC: invokeIpc(INSIGHTS_LIST_SESSIONS, projectId, includeArchived)
IPC->>Service: listSessions(projectPath, includeArchived)
Service->>Manager: listSessions(projectPath, includeArchived)
Manager->>Storage: listSessions(projectPath, includeArchived)
Storage-->>Manager: sessions[]
Manager-->>Service: sessions[]
Service-->>IPC: sessions[]
IPC-->>API: {success, data: sessions[]}
API-->>Store: Sessions updated
Store-->>Insights: Sessions updated
Insights-->>Sidebar: UI refreshed
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @AndyMik90, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the chat history management within the Insights feature by introducing robust bulk operations. Users can now efficiently manage their chat sessions by selecting multiple entries for either permanent deletion or archiving. The archiving functionality provides a way to declutter the active session list while retaining access to older conversations through a dedicated toggle, improving overall user experience and organization. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request successfully implements bulk delete and archive functionality for the chat history. The changes are well-structured across the frontend, main process, and shared types. My review includes suggestions to improve maintainability by reducing code duplication, enhancing error handling by logging caught exceptions, and increasing efficiency by avoiding redundant data reloads. I also pointed out a minor issue with a redundant event handler in the UI.
| } catch { | ||
| return false; | ||
| } |
There was a problem hiding this comment.
This catch block is empty, which means any error during the save operation will be silently swallowed. While the function correctly returns false, logging the error would be beneficial for debugging purposes. Consider adding console.error to log the exception.
} catch (error) {
console.error(`[SessionStorage] Failed to archive session ${sessionId}:`, error);
return false;
}| // Check if current cached session was among archived | ||
| const currentSession = this.sessions.get(projectId); | ||
| if (currentSession && result.archivedIds.includes(currentSession.id)) { | ||
| this.sessions.delete(projectId); | ||
|
|
||
| const remaining = this.listSessions(projectPath); | ||
| if (remaining.length > 0) { | ||
| this.switchSession(projectId, projectPath, remaining[0].id); | ||
| } else { | ||
| this.storage.clearCurrentSessionId(projectPath); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
The logic for handling the auto-switching of the current session if it's archived or deleted is duplicated across archiveSession, deleteSessions, and archiveSessions. To improve maintainability and adhere to the DRY principle, consider extracting this logic into a private helper method. For example: private handleCurrentSessionRemoval(projectId: string, projectPath: string): void.
| > | ||
| <Checkbox | ||
| checked={isSelected} | ||
| onCheckedChange={() => onToggleSelect()} |
There was a problem hiding this comment.
The onCheckedChange handler on the Checkbox component is redundant because its parent div already has an onClick handler that serves the same purpose. This could result in onToggleSelect being called twice for each click on the checkbox. To avoid this and simplify the code, you can remove this line.
| export async function deleteSessions(projectId: string, sessionIds: string[]): Promise<{ success: boolean; failedIds?: string[] }> { | ||
| const result = await window.electronAPI.deleteInsightsSessions(projectId, sessionIds); | ||
| if (result.success) { | ||
| await loadInsightsSession(projectId); | ||
| return { success: true }; | ||
| } | ||
| return { success: false, failedIds: result.data?.failedIds }; | ||
| } |
There was a problem hiding this comment.
The call to loadInsightsSession(projectId) on line 319 reloads the session list, but it doesn't respect the showArchived state from the UI. This leads to redundant reloads in the component that calls this function.
To make this more efficient, you could:
- Update this function and other new data-mutating functions (
archiveSession,unarchiveSession,archiveSessions) to accept anincludeArchivedflag. - Pass this flag down to
loadInsightsSessionand subsequently toloadInsightsSessions. - Update the component-level handlers to only make one call to the store function, passing the
showArchivedstate.
This will make the data flow more efficient and remove redundant API calls.
There was a problem hiding this comment.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/frontend/src/main/insights/session-storage.ts (1)
153-201: 🧹 Nitpick | 🔵 Trivial
listSessionsreads all session files from disk on every call — consider the cost during bulk operations.Both
deleteSessionsandarchiveSessionsinSessionManagercallthis.listSessions(projectPath)after mutation to find the next session to switch to. SincelistSessionsperforms a full directory scan and parses every JSON file, this means bulk-deleting N sessions still triggers a full re-read. For a small number of sessions this is fine, but it could become a bottleneck as session count grows.Consider caching the session list or passing remaining sessions from the bulk result to avoid re-scanning.
🤖 Fix all issues with AI agents
In `@apps/frontend/src/main/insights/session-manager.ts`:
- Around line 108-180: Duplicate "auto-switch on current session removal" logic
should be extracted into a single private helper (e.g., private
autoSwitchIfNeeded(projectId: string, projectPath: string, affectedIds: string |
string[])) that accepts the projectId, projectPath and either a sessionId or
array of sessionIds; implement the logic that checks
this.sessions.get(projectId), removes the cached entry if its id is in
affectedIds, calls listSessions(projectPath) and either switchSession(projectId,
projectPath, remaining[0].id) or storage.clearCurrentSessionId(projectPath).
Replace the repeated blocks in archiveSession, deleteSessions, archiveSessions
(and any deleteSession if present) with a call to this new helper (for batch ops
pass the array), keeping existing return values from storage operations
unchanged.
In `@apps/frontend/src/main/ipc-handlers/insights-handlers.ts`:
- Around line 263-323: The bulk-delete and bulk-archive IPC handlers
(IPC_CHANNELS.INSIGHTS_DELETE_SESSIONS and
IPC_CHANNELS.INSIGHTS_ARCHIVE_SESSIONS) always return success:true even when all
items failed; update the handlers to inspect the returned result from
insightsService.deleteSessions and insightsService.archiveSessions and if
result.failedIds.length === sessionIds.length return { success: false, error:
"Failed to delete/archive sessions", data: result } (or similar error text),
otherwise keep returning success:true with data so partial successes remain
true; modify only those handlers (deleteSessions and archiveSessions call sites)
to set the top-level success flag based on total failure vs partial/full
success.
In `@apps/frontend/src/renderer/components/ChatHistorySidebar.tsx`:
- Around line 48-53: The bulk-operation props onDeleteSessions and
onArchiveSessions are typed to return void so callers like handleBulkDelete and
handleBulkArchive don't await async work; change the prop signatures in
ChatHistorySidebar (onDeleteSessions?: (sessionIds: string[]) => Promise<void>
and onArchiveSessions?: (sessionIds: string[]) => Promise<void>) and then update
callers (e.g., handleBulkDelete and handleBulkArchive in Insights.tsx) to await
the returned Promise before closing dialogs/clearing selection so IPC calls and
session reloads complete first.
- Around line 335-349: Replace the misleading translation keys used for the
single-session delete dialog in ChatHistorySidebar.tsx: instead of calling
t('insights.bulkDeleteTitle') and t('insights.bulkDeleteDescription', { count: 1
}) for the single-delete AlertDialogTitle/Description, switch to a dedicated or
neutral key (e.g., t('insights.deleteSessionTitle') and
t('insights.deleteSessionDescription') or
t('insights.deleteConfirmTitle'/'insights.deleteConfirmDescription')); add those
new i18n entries to the translations and update the AlertDialogTitle and
AlertDialogDescription usages accordingly (references: AlertDialogTitle,
AlertDialogDescription, handleDelete, deleteSessionId).
- Around line 476-489: The checkbox is toggling twice because both the wrapper
div's onClick and the Checkbox's onCheckedChange call onToggleSelect; remove the
duplicate by deleting the onCheckedChange prop from the Checkbox so only the
div's onClick (which calls e.stopPropagation() then onToggleSelect()) handles
selection; ensure the Checkbox still receives checked={isSelected} so visual
state remains correct and leave the wrapper div's onClick and stopPropagation
intact (referencing Checkbox and onToggleSelect in the ChatHistorySidebar
component).
In `@apps/frontend/src/renderer/components/Insights.tsx`:
- Around line 152-155: The useEffect that calls loadInsightsSessions(projectId,
showArchived) duplicates the initial call already made by
loadInsightsSession(projectId), causing two IPC calls on mount; fix by skipping
the effect's first run (use a ref-based "didMount" guard) so the effect only
runs when showArchived or projectId change after mount, referencing the
useEffect that depends on [projectId, showArchived] and the functions
loadInsightsSession and loadInsightsSessions; alternatively, remove the internal
loadInsightsSessions call from loadInsightsSession and rely solely on this
effect—pick one approach and implement it consistently.
- Around line 213-231: The component handlers (handleArchiveSession,
handleUnarchiveSession, handleDeleteSessions, handleArchiveSessions) are
redundantly calling loadInsightsSessions after calling store helpers that
already reload; remove the extra await loadInsightsSessions(projectId,
showArchived) lines from each handler so the store helpers (archiveSession,
unarchiveSession, deleteSessions, archiveSessions) remain the single source of
truth for reloading, or alternatively pass showArchived into the store helpers
if you need the filter applied there—either remove the component-level reloads
or move showArchived into the store, but do not call loadInsightsSessions twice.
In `@apps/frontend/src/renderer/lib/mocks/insights-mock.ts`:
- Around line 72-92: The archive mocks (archiveInsightsSession,
archiveInsightsSessions) do not update mock session state and
listInsightsSessions ignores _includeArchived; update archiveInsightsSession and
archiveInsightsSessions to set archivedAt = new Date().toISOString() on matching
entries in mockInsightsSessions (use session id matching) and return those ids
in data.archivedIds, update unarchiveInsightsSession to clear archivedAt for the
given session id and return success, and modify listInsightsSessions to filter
out archived sessions when _includeArchived is false (or include them when true)
so the mock reflects archived state; reference the functions
archiveInsightsSession, archiveInsightsSessions, unarchiveInsightsSession,
listInsightsSessions and the mockInsightsSessions array when making changes.
In `@apps/frontend/src/renderer/stores/insights-store.ts`:
- Around line 316-350: The store helpers deleteSessions, archiveSession,
archiveSessions, and unarchiveSession currently call
loadInsightsSession(projectId) after success causing duplicate reloads and
flicker; remove those internal reload calls (the await
loadInsightsSession(projectId) lines) so these functions only call the IPC
(window.electronAPI.*) and return their success/failedIds results, and keep the
responsibility for reloading (via loadInsightsSessions(projectId,
includeArchived)) in the caller (e.g., Insights.tsx handlers like
handleDeleteSessions/handleArchiveSessions) where includeArchived is known;
ensure function signatures and returned shapes remain the same.
| /** | ||
| * Archive a session | ||
| */ | ||
| archiveSession(projectId: string, projectPath: string, sessionId: string): boolean { | ||
| const success = this.storage.archiveSession(projectPath, sessionId); | ||
| if (!success) return false; | ||
|
|
||
| // If this was the current session, auto-switch | ||
| const currentSession = this.sessions.get(projectId); | ||
| if (currentSession?.id === sessionId) { | ||
| this.sessions.delete(projectId); | ||
|
|
||
| const remaining = this.listSessions(projectPath); | ||
| if (remaining.length > 0) { | ||
| this.switchSession(projectId, projectPath, remaining[0].id); | ||
| } else { | ||
| this.storage.clearCurrentSessionId(projectPath); | ||
| } | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Unarchive a session | ||
| */ | ||
| unarchiveSession(projectPath: string, sessionId: string): boolean { | ||
| return this.storage.unarchiveSession(projectPath, sessionId); | ||
| } | ||
|
|
||
| /** | ||
| * Delete multiple sessions | ||
| */ | ||
| deleteSessions(projectId: string, projectPath: string, sessionIds: string[]): { deletedIds: string[]; failedIds: string[] } { | ||
| const result = this.storage.deleteSessions(projectPath, sessionIds); | ||
|
|
||
| // Check if current cached session was among deleted | ||
| const currentSession = this.sessions.get(projectId); | ||
| if (currentSession && result.deletedIds.includes(currentSession.id)) { | ||
| this.sessions.delete(projectId); | ||
|
|
||
| const remaining = this.listSessions(projectPath); | ||
| if (remaining.length > 0) { | ||
| this.switchSession(projectId, projectPath, remaining[0].id); | ||
| } else { | ||
| this.storage.clearCurrentSessionId(projectPath); | ||
| } | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| /** | ||
| * Archive multiple sessions | ||
| */ | ||
| archiveSessions(projectId: string, projectPath: string, sessionIds: string[]): { archivedIds: string[]; failedIds: string[] } { | ||
| const result = this.storage.archiveSessions(projectPath, sessionIds); | ||
|
|
||
| // Check if current cached session was among archived | ||
| const currentSession = this.sessions.get(projectId); | ||
| if (currentSession && result.archivedIds.includes(currentSession.id)) { | ||
| this.sessions.delete(projectId); | ||
|
|
||
| const remaining = this.listSessions(projectPath); | ||
| if (remaining.length > 0) { | ||
| this.switchSession(projectId, projectPath, remaining[0].id); | ||
| } else { | ||
| this.storage.clearCurrentSessionId(projectPath); | ||
| } | ||
| } | ||
|
|
||
| return result; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Duplicated "auto-switch on current session removal" logic across four methods.
The pattern of clearing cache, listing remaining sessions, and switching (or clearing the current session pointer) is repeated in deleteSession, archiveSession, deleteSessions, and archiveSessions. Consider extracting a private helper like handleCurrentSessionRemoved(projectId, projectPath) to reduce duplication and ensure consistency if the logic evolves.
♻️ Suggested helper extraction
+ /**
+ * If the current cached session was removed/archived, switch to next available or clear.
+ */
+ private autoSwitchIfNeeded(projectId: string, projectPath: string, removedSessionId: string): void {
+ const currentSession = this.sessions.get(projectId);
+ if (currentSession?.id !== removedSessionId) return;
+
+ this.sessions.delete(projectId);
+ const remaining = this.listSessions(projectPath);
+ if (remaining.length > 0) {
+ this.switchSession(projectId, projectPath, remaining[0].id);
+ } else {
+ this.storage.clearCurrentSessionId(projectPath);
+ }
+ }Then each method simply calls this.autoSwitchIfNeeded(projectId, projectPath, sessionId) or iterates over the affected IDs.
🤖 Prompt for AI Agents
In `@apps/frontend/src/main/insights/session-manager.ts` around lines 108 - 180,
Duplicate "auto-switch on current session removal" logic should be extracted
into a single private helper (e.g., private autoSwitchIfNeeded(projectId:
string, projectPath: string, affectedIds: string | string[])) that accepts the
projectId, projectPath and either a sessionId or array of sessionIds; implement
the logic that checks this.sessions.get(projectId), removes the cached entry if
its id is in affectedIds, calls listSessions(projectPath) and either
switchSession(projectId, projectPath, remaining[0].id) or
storage.clearCurrentSessionId(projectPath). Replace the repeated blocks in
archiveSession, deleteSessions, archiveSessions (and any deleteSession if
present) with a call to this new helper (for batch ops pass the array), keeping
existing return values from storage operations unchanged.
| // Delete multiple sessions | ||
| ipcMain.handle( | ||
| IPC_CHANNELS.INSIGHTS_DELETE_SESSIONS, | ||
| async (_, projectId: string, sessionIds: string[]): Promise<IPCResult<{ deletedIds: string[]; failedIds: string[] }>> => { | ||
| const project = projectStore.getProject(projectId); | ||
| if (!project) { | ||
| return { success: false, error: "Project not found" }; | ||
| } | ||
|
|
||
| const result = insightsService.deleteSessions(projectId, project.path, sessionIds); | ||
| return { success: true, data: result }; | ||
| } | ||
| ); | ||
|
|
||
| // Archive a session | ||
| ipcMain.handle( | ||
| IPC_CHANNELS.INSIGHTS_ARCHIVE_SESSION, | ||
| async (_, projectId: string, sessionId: string): Promise<IPCResult> => { | ||
| const project = projectStore.getProject(projectId); | ||
| if (!project) { | ||
| return { success: false, error: "Project not found" }; | ||
| } | ||
|
|
||
| const success = insightsService.archiveSession(projectId, project.path, sessionId); | ||
| if (success) { | ||
| return { success: true }; | ||
| } | ||
| return { success: false, error: "Failed to archive session" }; | ||
| } | ||
| ); | ||
|
|
||
| // Archive multiple sessions | ||
| ipcMain.handle( | ||
| IPC_CHANNELS.INSIGHTS_ARCHIVE_SESSIONS, | ||
| async (_, projectId: string, sessionIds: string[]): Promise<IPCResult<{ archivedIds: string[]; failedIds: string[] }>> => { | ||
| const project = projectStore.getProject(projectId); | ||
| if (!project) { | ||
| return { success: false, error: "Project not found" }; | ||
| } | ||
|
|
||
| const result = insightsService.archiveSessions(projectId, project.path, sessionIds); | ||
| return { success: true, data: result }; | ||
| } | ||
| ); | ||
|
|
||
| // Unarchive a session | ||
| ipcMain.handle( | ||
| IPC_CHANNELS.INSIGHTS_UNARCHIVE_SESSION, | ||
| async (_, projectId: string, sessionId: string): Promise<IPCResult> => { | ||
| const project = projectStore.getProject(projectId); | ||
| if (!project) { | ||
| return { success: false, error: "Project not found" }; | ||
| } | ||
|
|
||
| const success = insightsService.unarchiveSession(project.path, sessionId); | ||
| if (success) { | ||
| return { success: true }; | ||
| } | ||
| return { success: false, error: "Failed to unarchive session" }; | ||
| } | ||
| ); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
New bulk and archive IPC handlers follow existing patterns well.
The new handlers are consistent with the established project-existence-check-then-delegate pattern. One minor observation: INSIGHTS_DELETE_SESSIONS and INSIGHTS_ARCHIVE_SESSIONS always return { success: true, data: result } even when result.failedIds contains all IDs (i.e., total failure). The single-session counterparts (INSIGHTS_ARCHIVE_SESSION, INSIGHTS_UNARCHIVE_SESSION) differentiate success/failure via the success field. This asymmetry means the renderer store treats bulk total-failure as success. It's not a blocker since failedIds is available in the response, but worth keeping in mind for error handling on the UI side.
🤖 Prompt for AI Agents
In `@apps/frontend/src/main/ipc-handlers/insights-handlers.ts` around lines 263 -
323, The bulk-delete and bulk-archive IPC handlers
(IPC_CHANNELS.INSIGHTS_DELETE_SESSIONS and
IPC_CHANNELS.INSIGHTS_ARCHIVE_SESSIONS) always return success:true even when all
items failed; update the handlers to inspect the returned result from
insightsService.deleteSessions and insightsService.archiveSessions and if
result.failedIds.length === sessionIds.length return { success: false, error:
"Failed to delete/archive sessions", data: result } (or similar error text),
otherwise keep returning success:true with data so partial successes remain
true; modify only those handlers (deleteSessions and archiveSessions call sites)
to set the top-level success flag based on total failure vs partial/full
success.
| {/* Single delete confirmation dialog */} | ||
| <AlertDialog open={!!deleteSessionId} onOpenChange={() => setDeleteSessionId(null)}> | ||
| <AlertDialogContent> | ||
| <AlertDialogHeader> | ||
| <AlertDialogTitle>Delete conversation?</AlertDialogTitle> | ||
| <AlertDialogTitle>{t('insights.bulkDeleteTitle')}</AlertDialogTitle> | ||
| <AlertDialogDescription> | ||
| This will permanently delete this conversation and all its messages. | ||
| This action cannot be undone. | ||
| {t('insights.bulkDeleteDescription', { count: 1 })} | ||
| </AlertDialogDescription> | ||
| </AlertDialogHeader> | ||
| <AlertDialogFooter> | ||
| <AlertDialogCancel>Cancel</AlertDialogCancel> | ||
| <AlertDialogAction onClick={handleDelete}>Delete</AlertDialogAction> | ||
| <AlertDialogCancel>{t('actions.cancel')}</AlertDialogCancel> | ||
| <AlertDialogAction onClick={handleDelete}>{t('actions.delete')}</AlertDialogAction> | ||
| </AlertDialogFooter> | ||
| </AlertDialogContent> | ||
| </AlertDialog> |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Reusing bulkDeleteTitle key for single-session delete is misleading.
The single-delete confirmation dialog (lines 335-349) uses t('insights.bulkDeleteTitle') and t('insights.bulkDeleteDescription', { count: 1 }). While this works functionally if the translation handles pluralization, the key name bulkDeleteTitle is semantically confusing for a single-item delete. Consider using a dedicated key (e.g., insights.deleteSessionTitle) or a shared key with a neutral name (e.g., insights.deleteConfirmTitle).
🤖 Prompt for AI Agents
In `@apps/frontend/src/renderer/components/ChatHistorySidebar.tsx` around lines
335 - 349, Replace the misleading translation keys used for the single-session
delete dialog in ChatHistorySidebar.tsx: instead of calling
t('insights.bulkDeleteTitle') and t('insights.bulkDeleteDescription', { count: 1
}) for the single-delete AlertDialogTitle/Description, switch to a dedicated or
neutral key (e.g., t('insights.deleteSessionTitle') and
t('insights.deleteSessionDescription') or
t('insights.deleteConfirmTitle'/'insights.deleteConfirmDescription')); add those
new i18n entries to the translations and update the AlertDialogTitle and
AlertDialogDescription usages accordingly (references: AlertDialogTitle,
AlertDialogDescription, handleDelete, deleteSessionId).
| deleteInsightsSessions: async (_projectId: string, sessionIds: string[]) => { | ||
| for (const sessionId of sessionIds) { | ||
| const index = mockInsightsSessions.findIndex(s => s.id === sessionId); | ||
| if (index !== -1) { | ||
| mockInsightsSessions.splice(index, 1); | ||
| } | ||
| } | ||
| return { success: true, data: { deletedIds: sessionIds, failedIds: [] } }; | ||
| }, | ||
|
|
||
| archiveInsightsSession: async (_projectId: string, _sessionId: string) => { | ||
| return { success: true }; | ||
| }, | ||
|
|
||
| archiveInsightsSessions: async (_projectId: string, sessionIds: string[]) => { | ||
| return { success: true, data: { archivedIds: sessionIds, failedIds: [] } }; | ||
| }, | ||
|
|
||
| unarchiveInsightsSession: async (_projectId: string, _sessionId: string) => { | ||
| return { success: true }; | ||
| }, |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Mock archive operations don't update archivedAt on sessions.
The archiveInsightsSession and archiveInsightsSessions mocks return { success: true } but don't set archivedAt on the mock session data. Similarly, listInsightsSessions ignores _includeArchived. This means the mock won't reflect archived state in the UI during browser-mode development. Not a production issue, but worth noting for mock fidelity.
🤖 Prompt for AI Agents
In `@apps/frontend/src/renderer/lib/mocks/insights-mock.ts` around lines 72 - 92,
The archive mocks (archiveInsightsSession, archiveInsightsSessions) do not
update mock session state and listInsightsSessions ignores _includeArchived;
update archiveInsightsSession and archiveInsightsSessions to set archivedAt =
new Date().toISOString() on matching entries in mockInsightsSessions (use
session id matching) and return those ids in data.archivedIds, update
unarchiveInsightsSession to clear archivedAt for the given session id and return
success, and modify listInsightsSessions to filter out archived sessions when
_includeArchived is false (or include them when true) so the mock reflects
archived state; reference the functions archiveInsightsSession,
archiveInsightsSessions, unarchiveInsightsSession, listInsightsSessions and the
mockInsightsSessions array when making changes.
AndyMik90
left a comment
There was a problem hiding this comment.
🤖 Auto Claude PR Review
Merge Verdict: 🟠 NEEDS REVISION
🟠 Needs revision - 8 issue(s) require attention.
Branch is out of date with base branch. Update branch first - if no conflicts arise, you can merge. If merge conflicts arise, resolve them and run follow-up review again. 8 issue(s) must be addressed (2 required, 6 recommended). 2 non-blocking suggestion(s) to consider.
Risk Assessment
| Factor | Level | Notes |
|---|---|---|
| Complexity | High | Based on lines changed |
| Security Impact | None | Based on security findings |
| Scope Coherence | Good | Based on structural review |
🚨 Blocking Issues (Must Fix)
- Branch Out of Date: PR branch is behind the base branch and needs to be updated
Findings Summary
- High: 2 issue(s)
- Medium: 6 issue(s)
- Low: 2 issue(s)
Generated by Auto Claude PR Review
Findings (10 selected of 10 total)
🟡 [26fc012b1a8a] [MEDIUM] Session eviction logic duplicated 4 times across methods
📁 apps/frontend/src/main/insights/session-manager.ts:86
The pattern 'check if current cached session was affected → delete from cache → list remaining sessions → switch to first or clear pointer' is copy-pasted across deleteSession (lines 86-106), archiveSession (lines 111-129), deleteSessions (lines 141-158), and archiveSessions (lines 163-180). All four methods contain the identical 8-line block. This means any bug fix or behavior change (e.g., switching to the most recent session instead of the first) must be applied in 4 places.
Suggested fix:
Extract a private helper method like `private evictCurrentSessionIfAffected(projectId: string, projectPath: string, affectedIds: string[]): void` that encapsulates the cache eviction and auto-switch logic, then call it from all four methods.
🟡 [47f1cd29f97f] [MEDIUM] Double session reload causes redundant IPC calls and stale intermediate state
📁 apps/frontend/src/renderer/components/Insights.tsx:213
Each handler (handleArchiveSession, handleUnarchiveSession, handleDeleteSessions, handleArchiveSessions) calls the store helper (e.g., archiveSession) which internally calls loadInsightsSession → loadInsightsSessions(projectId) WITHOUT the showArchived parameter (defaults to false). Then the handler calls loadInsightsSessions(projectId, showArchived) again. This causes: (1) Two redundant IPC roundtrips per operation, and (2) When showArchived is true, the first reload briefly hides archived sessions from the sidebar before the second reload adds them back, causing a visible UI flicker. Verified by reading loadInsightsSession at insights-store.ts:226-235 which calls loadInsightsSessions(projectId) at line 234 without includeArchived.
Suggested fix:
Either: (A) Remove the loadInsightsSessions calls from inside the store helpers (archiveSession, deleteSessions, etc.) and let the component handle the reload with the correct showArchived flag, or (B) Add an includeArchived parameter to the store helpers so they can pass it through to loadInsightsSessions. Option A is simpler and avoids the double-call.
🟡 [8d8521719c31] [MEDIUM] Date formatting uses hardcoded English strings violating i18n requirement
📁 apps/frontend/src/renderer/components/ChatHistorySidebar.tsx:154
The formatDate function uses hardcoded English strings 'Today', 'Yesterday', and 'days ago' instead of i18n translation keys. The project's CLAUDE.md rules state: 'All frontend user-facing text MUST use react-i18next translation keys. Never hardcode strings in JSX/TSX.' The codebase already has a 'time' namespace with keys like 'time.daysAgo' that could be leveraged. Similarly, line 516 has hardcoded English pluralization: message{session.messageCount !== 1 ? 's' : ''}.
Suggested fix:
Use i18n keys for the date labels: t('time.today'), t('time.yesterday'), t('time.daysAgo', { count: diffDays }), and for message count: t('insights.messageCount', { count: session.messageCount }). Add missing keys to en/common.json and fr/common.json.
🟡 [f88fea7f97d3] [MEDIUM] Async bulk operations are fire-and-forget with no error handling
📁 apps/frontend/src/renderer/components/ChatHistorySidebar.tsx:139
handleBulkDelete and handleBulkArchive call the prop callbacks (onDeleteSessions, onArchiveSessions) without awaiting them. The prop types are declared as (sessionIds: string[]) => void but the actual implementations in Insights.tsx are async functions. This means: (1) UI state is cleared (selectedIds, dialog) before operations complete - user sees success before it happens, (2) If the async operations throw, the promise rejection is unhandled, and (3) If the operations fail, the user has already lost their selection state with no way to retry.
Suggested fix:
Change the prop types to return `Promise<void>`, make the handlers async, await the callbacks, and only clear state after success. Consider wrapping in try/catch to handle failures gracefully:
onDeleteSessions?: (sessionIds: string[]) => Promise;
...
const handleBulkDelete = async () => {
if (selectedIds.size > 0 && onDeleteSessions) {
await onDeleteSessions(Array.from(selectedIds));
setSelectedIds(new Set());
setBulkDeleteOpen(false);
}
};
🔵 [ddffc97d8164] [LOW] archivedAt not deserialized in loadSessionById, inconsistent with other date fields
📁 apps/frontend/src/main/insights/session-storage.ts:35
loadSessionById converts createdAt and updatedAt from JSON strings to Date objects (lines 37-38), but does not convert the new archivedAt field. After JSON.parse, archivedAt remains a string. While this doesn't cause a runtime error (listSessions creates new Date from it at line 188, and truthiness checks work on strings), it's inconsistent with the type definition which declares archivedAt as Date. Any code path that loads a session via loadSessionById and checks session.archivedAt instanceof Date would get false. | The loadSessionById method converts createdAt and updatedAt from JSON strings to Date objects (lines 37-38) but does not perform the same conversion for the new archivedAt field. After JSON.parse, archivedAt remains a string (e.g., '2026-02-14T12:00:00.000Z') while the TypeScript type declares it as Date | undefined. This doesn't currently cause crashes because: (1) truthiness checks work on strings, (2) listSessions wraps it in new Date(). However, it violates the type contract and could cause runtime errors if any future code calls Date methods (like .getTime()) on the loaded archivedAt value.
Suggested fix:
Add archivedAt deserialization after the other date conversions:
if (session.archivedAt) {
session.archivedAt = new Date(session.archivedAt);
}
🟠 [f94472aa7592] [HIGH] Session reload paths lose archived sessions when showArchived is true
📁 apps/frontend/src/renderer/stores/insights-store.ts:234
The loadInsightsSession() function (line 234) always calls loadInsightsSessions(projectId) WITHOUT passing includeArchived. This overwrites the Zustand sessions list to exclude archived sessions. Multiple store helpers chain through this function: deleteSession (line 300), deleteSessions (line 319), archiveSession (line 328), archiveSessions (line 337), unarchiveSession (line 346). Additionally, renameSession (line 310) directly calls loadInsightsSessions(projectId) without includeArchived, and the onInsightsSessionUpdated listener (line 477) does the same.
The new bulk handlers in Insights.tsx partially mitigate this by calling loadInsightsSessions(projectId, showArchived) after the store function, but handleDeleteSession (single delete, line 205-207) and handleRenameSession (line 209-211) do NOT do this follow-up reload. The onInsightsSessionUpdated listener (line 477) also cannot access React's showArchived state.
Reproduction: Toggle 'Show Archived' on → delete or rename any single session → all archived sessions vanish from the sidebar with no automatic recovery (the useEffect on showArchived only fires when showArchived changes, not after operations). The same happens when receiving a chat response that triggers onInsightsSessionUpdated.
Suggested fix:
Either: (1) Have `loadInsightsSession` accept an optional `includeArchived` parameter and pass it through to `loadInsightsSessions`, updating all callers (the Insights.tsx handlers, the onSessionUpdated listener) to pass `showArchived`. Or (2) Store `showArchived` in the Zustand store so `loadInsightsSession` and the session-updated listener can read it from shared state instead of relying on the React component's local state.
🟠 [85546bb1a4c1] [HIGH] Missing i18n key: actions.cancel does not exist
📁 apps/frontend/src/renderer/components/ChatHistorySidebar.tsx:345
The code uses t('actions.cancel') but the actions namespace in common.json only has save, apply, delete, and settings — no cancel key. This will render as the raw key string 'actions.cancel' instead of 'Cancel'. The established codebase pattern for cancel buttons in AlertDialogs is t('common:buttons.cancel') or t('buttons.cancel') (used in Worktrees.tsx:1001, WorktreeCleanupDialog.tsx:82, KanbanBoard.tsx:1596, QueueSettingsModal.tsx:137, TaskEditDialog.tsx:281, etc.). Searched Grep('actions.*cancel', 'en/common.json') - confirmed key does not exist. Searched Grep('buttons.cancel', 'en/common.json') - found at line 59.
Suggested fix:
Change both occurrences (lines 345 and 375) from `t('actions.cancel')` to `t('buttons.cancel')` to match the established codebase pattern and use an existing key.
🟡 [e6b6204cb4e9] [MEDIUM] Using aria-label keys as visible button text instead of selection namespace
📁 apps/frontend/src/renderer/components/ChatHistorySidebar.tsx:253
The 'Select All' and 'Clear Selection' links use t('accessibility.selectAllAriaLabel') and t('accessibility.clearSelectionAriaLabel') as visible button text. In IdeationHeader.tsx, these keys are correctly used only for aria-label attributes and tooltip content (not visible text). The established pattern for visible selection text is the selection namespace: Worktrees.tsx uses t('common:selection.selectAll') and t('common:selection.clearSelection') for visible button text. Using *AriaLabel keys as visible text is inconsistent with how the codebase separates accessibility labels from display text. Searched Grep('selection.selectAll|selection.clearSelection', 'Worktrees.tsx') - found on line 524.
Suggested fix:
Use `t('selection.selectAll')` and `t('selection.clearSelection')` for visible button text, consistent with Worktrees.tsx. Reserve `accessibility.*AriaLabel` keys for aria-label attributes.
🟡 [c8cef15265cb] [MEDIUM] [NEEDS REVIEW] Duplicate i18n keys: insights.selectMode/exitSelectMode duplicates selection.select/done
📁 apps/frontend/src/renderer/components/ChatHistorySidebar.tsx:198
The PR introduces insights.selectMode ('Select') and insights.exitSelectMode ('Done') for the selection mode toggle button. However, the codebase already has selection.select ('Select') and selection.done ('Done') in the generic selection namespace, used by Worktrees.tsx:500 for the same purpose. This creates duplicate i18n keys with identical English text for the same UI concept. Searched Grep('selection.done|selection.select', 'Worktrees.tsx') - confirmed usage on line 500.
Suggested fix:
Use `t('selection.done')` and `t('selection.select')` from the existing generic `selection` namespace, consistent with Worktrees.tsx, instead of creating duplicate insights-specific keys.
🔵 [165af4d61dd1] [LOW] Unused duplicate i18n key: selection.archiveSelected never referenced
📁 apps/frontend/src/shared/i18n/locales/en/common.json:119
The PR adds selection.archiveSelected ('Archive Selected') at line 119, but only insights.archiveSelected (line 433) is actually used in the code (ChatHistorySidebar.tsx:329). The selection.archiveSelected key is dead code. Searched Grep('selection.archiveSelected', 'src/') - no matches found. Also verified in fr/common.json where the same duplicate exists at line 119.
Suggested fix:
Remove `selection.archiveSelected` from both en/common.json and fr/common.json since it's unused. The code correctly uses `insights.archiveSelected` instead.
This review was generated by Auto Claude.
| const handleDeleteSessions = async (sessionIds: string[]) => { | ||
| await deleteSessions(projectId, sessionIds); | ||
| await loadInsightsSessions(projectId, showArchived); | ||
| }; | ||
|
|
||
| const handleArchiveSessions = async (sessionIds: string[]) => { | ||
| await archiveSessions(projectId, sessionIds); | ||
| await loadInsightsSessions(projectId, showArchived); | ||
| }; |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
AndyMik90
left a comment
There was a problem hiding this comment.
🤖 Auto Claude PR Review
🟠 Follow-up Review: Needs Revision
🟠 Needs revision - 1 unresolved finding(s) from previous review.
Resolution Status
- ✅ Resolved: 0 previous findings addressed
- ❌ Unresolved: 1 previous findings remain
- 🆕 New Issues: 0 new findings in recent changes
Finding Validation
- 🔍 Dismissed as False Positives: 0 findings were re-investigated and found to be incorrect
- ✓ Confirmed Valid: 10 findings verified as genuine issues
- 👤 Needs Human Review: 0 findings require manual verification
🚨 Blocking Issues
- Branch Out of Date: PR branch is behind the base branch and needs to be updated
- quality: [UNRESOLVED] Async bulk operations are fire-and-forget with no error handling
Verdict
[Recovered via extraction] Branch is now up-to-date but none of the 10 code findings have been addressed. The only commit was a merge from develop with no code fixes. All 10 previous findings were confirmed valid by finding-validator. CI is passing (all 20 checks green).
Review Process
Agents invoked: resolution-verifier, comment-analyzer, finding-validator
This is an AI-generated follow-up review using parallel specialist analysis with finding validation.
Findings (1 selected of 1 total)
🟡 [f88fea7f97d3] [MEDIUM] [UNRESOLVED] Async bulk operations are fire-and-forget with no error handling
📁 apps/frontend/src/renderer/components/ChatHistorySidebar.tsx:139
handleBulkDelete and handleBulkArchive call the prop callbacks (onDeleteSessions, onArchiveSessions) without awaiting them. The prop types are declared as (sessionIds: string[]) => void but the actual implementations in Insights.tsx are async functions. This means: (1) UI state is cleared (selectedIds, dialog) before operations complete - user sees success before it happens, (2) If the async operations throw, the promise rejection is unhandled, and (3) If the operations fail, the user has already lost their selection state with no way to retry.
Suggested fix:
Change the prop types to return `Promise<void>`, make the handlers async, await the callbacks, and only clear state after success. Consider wrapping in try/catch to handle failures gracefully:
onDeleteSessions?: (sessionIds: string[]) => Promise;
...
const handleBulkDelete = async () => {
if (selectedIds.size > 0 && onDeleteSessions) {
await onDeleteSessions(Array.from(selectedIds));
setSelectedIds(new Set());
setBulkDeleteOpen(false);
}
};
This review was generated by Auto Claude.
Fixed fire-and-forget async operations that were clearing UI state before operations completed. Changes: - Updated prop types to return Promise<void> for async callbacks - Made handleBulkDelete and handleBulkArchive async functions - Added await for all async callback invocations - Added try/catch error handling with console logging - Updated SessionItem prop types for onArchive/onUnarchive - Made single archive/unarchive arrow functions async This ensures UI state is only cleared after operations complete successfully and prevents unhandled promise rejections. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@apps/frontend/src/renderer/components/ChatHistorySidebar.tsx`:
- Around line 307-308: The onArchive/onUnarchive prop handlers are wrapped in
redundant async/await wrappers even though onArchiveSession and
onUnarchiveSession already return Promise<void>; remove the unnecessary wrappers
by passing a direct arrow that calls the existing async callbacks (use
onArchive={onArchiveSession ? () => onArchiveSession(session.id) : undefined}
and similarly for onUnarchive with onUnarchiveSession and session.id) so you
don't add extra async/await indirection around
onArchiveSession/onUnarchiveSession.
- Around line 139-164: handleBulkDelete and handleBulkArchive currently catch
and log errors then re-throw, which leads to unhandled promise rejections when
invoked from React onClick handlers; remove the "throw error" lines in both
functions and instead surface failures by setting component state or invoking a
toast/error handler (e.g., call a provided showToast or set an error state)
after logging so the UI can display the error without re-throwing; update
handleBulkDelete and handleBulkArchive to swallow the exception after reporting
it to the user.
- Around line 551-565: The DropdownMenuItem onSelect currently calls onArchive
and onUnarchive directly (in ChatHistorySidebar), which return Promise<void> and
can cause unhandled rejections; wrap each call in a small handler (e.g., a
wrapper function used for onSelect) that invokes the Promise and catches errors
(logging them consistently with the project's bulk operation handler), e.g.,
replace direct onSelect={onUnarchive} and onSelect={onArchive} with onSelect={()
=> { onUnarchive()?.catch(err => /* log same way as bulk ops */) }} (and same
for onArchive) so rejections are handled and logged.
AndyMik90
left a comment
There was a problem hiding this comment.
🤖 Auto Claude PR Review
🟠 Follow-up Review: Needs Revision
🟠 Needs revision - 6 blocking issue(s) require fixes.
Resolution Status
- ✅ Resolved: 1 previous findings addressed
- ❌ Unresolved: 0 previous findings remain
- 🆕 New Issues: 8 new findings in recent changes
Finding Validation
- 🔍 Dismissed as False Positives: 0 findings were re-investigated and found to be incorrect
- ✓ Confirmed Valid: 7 findings verified as genuine issues
- 👤 Needs Human Review: 0 findings require manual verification
🚨 Blocking Issues
- Branch Out of Date: PR branch is behind the base branch and needs to be updated
- quality: Checkbox double-toggle causes selection clicks to be no-ops
- quality: handleBulkDelete re-throws error causing unhandled promise rejection
- quality: handleBulkArchive re-throws error causing unhandled promise rejection
- quality: Bulk delete/archive IPC handlers always return success:true even with partial failures
- security: Session ID used in file paths without sanitization (defense-in-depth)
Verdict
CI Status: ✅ All 20 CI checks passing — no CI blockers.
Previous Finding Resolution: The original finding (f88fea7f97d3 — async bulk operations fire-and-forget) is resolved. Props are now typed as Promise, handlers properly await with try/catch, and callers in Insights.tsx await operations correctly. Good progress.
However, 1 HIGH and 4 MEDIUM severity findings were confirmed valid by finding-validator, blocking merge:
-
[HIGH] NEW-003 — Checkbox double-toggle is a no-op: Both the wrapper div's onClick and the Checkbox's onCheckedChange call onToggleSelect(), causing a double-toggle that cancels itself out. Users literally cannot select sessions for bulk operations — the core feature of this fix. This is a functional bug that must be fixed.
-
[MEDIUM] NEW-001/002 — Re-throw causes unhandled promise rejections: handleBulkDelete (line 148) and handleBulkArchive (line 161) catch errors then re-throw from React onClick handlers, producing unhandled promise rejections. The fix is simple: remove the
throw error;lines. -
[MEDIUM] NEW-005 — Partial failures silently reported as success: Bulk delete/archive IPC handlers always return success:true even when failedIds is non-empty. Users get no indication of partial failures.
-
[MEDIUM] NEW-004 — Session ID path traversal: Defense-in-depth gap where session IDs are used in file paths without sanitization. Lower practical risk in Electron.
Recommended fixes (in priority order):
- Remove onToggleSelect() from wrapper div onClick, keep only e.stopPropagation() (NEW-003 — functional bug)
- Remove
throw error;from handleBulkDelete and handleBulkArchive (NEW-001/002 — quick fix) - Check failedIds.length in IPC handlers before returning success:true (NEW-005)
- Validate sessionId format in getSessionPath (NEW-004 — optional, follow-up acceptable)
Review Process
Agents invoked: resolution-verifier, new-code-reviewer, comment-analyzer, finding-validator
This is an AI-generated follow-up review using parallel specialist analysis with finding validation.
Findings (8 selected of 8 total)
🟠 [NEW-003] [HIGH] Checkbox double-toggle causes selection clicks to be no-ops
📁 apps/frontend/src/renderer/components/ChatHistorySidebar.tsx:489
Both the wrapper div's onClick and the Checkbox's onCheckedChange call onToggleSelect(). When clicking the Checkbox, Radix fires onCheckedChange first, then the click bubbles to the wrapper div. With React 18 batching and functional updaters, the first call toggles the selection and the second toggles it back, making checkbox clicks effectively no-ops. Users cannot select sessions for bulk operations.
Suggested fix:
Remove onToggleSelect() from the wrapper div's onClick. Keep only e.stopPropagation() to prevent row selection, and let the Checkbox's onCheckedChange handle the toggle:
<div onClick={(e) => e.stopPropagation()} className="shrink-0">
<Checkbox checked={isSelected} onCheckedChange={() => onToggleSelect()} className="h-4 w-4" />
</div>
🟡 [NEW-001] [MEDIUM] handleBulkDelete re-throws error causing unhandled promise rejection
📁 apps/frontend/src/renderer/components/ChatHistorySidebar.tsx:148
handleBulkDelete catches errors, logs them, then re-throws. Since it's called from AlertDialogAction onClick (a React event handler), the re-thrown error becomes an unhandled promise rejection. React onClick handlers return void and do not catch returned promises.
Suggested fix:
Remove 'throw error;' on line 148. The error is already logged to console. Optionally add user-facing error feedback (toast/state) instead of re-throwing.
🟡 [NEW-002] [MEDIUM] handleBulkArchive re-throws error causing unhandled promise rejection
📁 apps/frontend/src/renderer/components/ChatHistorySidebar.tsx:161
Same pattern as NEW-001. handleBulkArchive catches, logs, then re-throws from a Button onClick handler, producing an unhandled promise rejection on failure.
Suggested fix:
Remove 'throw error;' on line 161. The error is already logged.
🟡 [NEW-005] [MEDIUM] Bulk delete/archive IPC handlers always return success:true even with partial failures
📁 apps/frontend/src/main/ipc-handlers/insights-handlers.ts:273
INSIGHTS_DELETE_SESSIONS and INSIGHTS_ARCHIVE_SESSIONS handlers always return { success: true, data: result } regardless of whether result.failedIds is non-empty. The store functions check result.success (always true) and discard failure information. Users get no indication of partial failures.
Suggested fix:
Check result.failedIds.length in the IPC handler and return success:false when there are failures, or propagate failedIds in the store functions even on success.
🟡 [NEW-004] [MEDIUM] Session ID used in file paths without sanitization (defense-in-depth)
📁 apps/frontend/src/main/insights/session-storage.ts:0
Session IDs from the renderer are interpolated into file paths via path.join() without validation. A crafted session ID containing '../' could traverse outside the sessions directory. Practical risk is limited in an Electron desktop app but this is a defense-in-depth gap.
Suggested fix:
Validate sessionId matches expected pattern (e.g., /^session-\d+$/) in getSessionPath or at the IPC handler level.
🔵 [NEW-006] [LOW] Dropdown archive/unarchive handlers fire-and-forget with no error catch
📁 apps/frontend/src/renderer/components/ChatHistorySidebar.tsx:553
Async archive/unarchive handlers passed to Radix DropdownMenuItem.onSelect, which discards returned promises. If the IPC call or loadInsightsSessions rejects, it produces an unhandled promise rejection.
Suggested fix:
Wrap the handlers with try/catch: onArchive={() => { onArchiveSession(session.id).catch(err => console.error('Archive failed:', err)); }}
🔵 [NEW-007] [LOW] Selection state not pruned when sessions list changes
📁 apps/frontend/src/renderer/components/ChatHistorySidebar.tsx:90
selectedIds Set may contain IDs of sessions that no longer exist. The useEffect only clears on showArchived change, not when sessions change. The button counter shows selectedIds.size which could be misleading after failed operations.
Suggested fix:
Add a useEffect that prunes selectedIds when sessions change, or compute displayed count from sessions.filter(s => selectedIds.has(s.id)).length.
🔵 [CMT-002] [LOW] [FROM COMMENTS] Redundant async/await wrappers on archive/unarchive callbacks
📁 apps/frontend/src/renderer/components/ChatHistorySidebar.tsx:307
Lines 307-308 wrap already-async callbacks in unnecessary async/await: 'async () => await onArchiveSession(session.id)' is equivalent to '() => onArchiveSession(session.id)' since onArchiveSession already returns Promise. This is a trivial code hygiene concern from CodeRabbit.
Suggested fix:
Simplify to: onArchive={onArchiveSession ? () => onArchiveSession(session.id) : undefined}
This review was generated by Auto Claude.
…lity Comprehensive code quality improvements addressing all review comments: **Critical Fixes:** - Fixed double-toggle bug where checkbox called onToggleSelect twice - Added accessibility attributes (role, tabIndex, onKeyDown) to interactive div **Error Handling:** - Removed error re-throws in async event handlers to prevent unhandled rejections - Added error logging to empty catch blocks in session-storage.ts - Added error handling with .catch() for dropdown menu promise rejections - Added logging for partial failures in bulk operations **Code Quality:** - Replaced non-null assertion (!) with explicit null check in session-manager.ts - Removed redundant async/await wrappers in archive/unarchive handlers - Removed duplicate loadInsightsSession calls from store functions - Added skip-first-run guard to prevent double load on component mount - Added clarifying comment for showArchived useEffect dependency **Performance:** - Eliminated redundant IPC calls by removing duplicate session reloads - Fixed flicker issue caused by double reload with different filters All changes maintain backward compatibility and improve user experience. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| }; | ||
|
|
||
| const handleArchiveSessions = async (sessionIds: string[]) => { | ||
| const result = await archiveSessions(projectId, sessionIds); | ||
| await loadInsightsSessions(projectId, showArchived); | ||
|
|
||
| // Log partial failures for debugging | ||
| if (result.failedIds && result.failedIds.length > 0) { | ||
| console.warn(`Failed to archive ${result.failedIds.length} session(s):`, result.failedIds); | ||
| } | ||
| }; |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
There was a problem hiding this comment.
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 (3)
apps/frontend/src/renderer/components/Insights.tsx (1)
316-322: 🧹 Nitpick | 🔵 TrivialHardcoded sidebar toggle title attributes.
Lines 322 (
title={showSidebar ? 'Hide sidebar' : 'Show sidebar'}) contain hardcoded English strings. This is a pre-existing issue in the file, but since this PR touches the sidebar toggle area and adds i18n for other new strings, it would be consistent to localize these too.Based on learnings, pre-existing i18n issues can be deferred to future i18n cleanup passes. Flagging for awareness only.
Also applies to: 334-336
apps/frontend/src/main/insights/session-storage.ts (1)
163-180: 🧹 Nitpick | 🔵 TrivialMinor: title generation runs before the archive filter, wasting work for skipped sessions.
The title generation block (lines 169-175) runs before the archive check at lines 177-180. For archived sessions that will be skipped, this is unnecessary work.
Proposed fix: move the filter before title generation
const session = JSON.parse(content) as InsightsSession; + // Skip archived sessions unless explicitly included + if (!includeArchived && session.archivedAt) { + continue; + } + // Generate title if not present let title = session.title; if (!title && session.messages.length > 0) { const firstUserMessage = session.messages.find(m => m.role === 'user'); title = firstUserMessage ? this.generateTitle(firstUserMessage.content) : 'Untitled Conversation'; } - // Skip archived sessions unless explicitly included - if (!includeArchived && session.archivedAt) { - continue; - }apps/frontend/src/renderer/stores/insights-store.ts (1)
226-235:⚠️ Potential issue | 🟠 Major
loadInsightsSessionalways reloads session list withoutincludeArchived, breakingshowArchivedstate.
loadInsightsSessioncallsloadInsightsSessions(projectId)at line 234 without forwarding theincludeArchivedflag. This is called from:
- The mount effect in
Insights.tsx(line 147)deleteSession(line 300) — single-session delete via the dropdown menuIn both cases, if the user has
showArchivedtoggled on, archived sessions will momentarily vanish from the sidebar because the reload uses the defaultincludeArchived = undefined(falsy).For (1), the
isFirstRunguard inInsights.tsxmitigates the issue on mount — but not onprojectIdchange.For (2), the single
deleteSessionstore function reloads without respectingshowArchived, andhandleDeleteSessioninInsights.tsx(line 210-212) doesn't perform its own reload to correct this.Consider either:
- Adding an
includeArchivedparameter toloadInsightsSession, or- Not calling
loadInsightsSessionsfrom withinloadInsightsSession— let callers handle it.
🤖 Fix all issues with AI agents
In `@apps/frontend/src/renderer/components/ChatHistorySidebar.tsx`:
- Around line 474-489: The row currently always has role="button" and tabIndex=0
but in selection mode its onClick/onKeyDown are disabled; update the div in
ChatHistorySidebar.tsx to conditionally set accessibility and handlers: when
isSelectionMode is false keep role="button"/tabIndex and the existing
onSelect/keyboard behavior, and when isSelectionMode is true either remove
role/tabIndex (so only the nested checkbox is focusable) or (preferred) set
role="checkbox" and wire onClick and onKeyDown (Space/Enter) to toggle selection
by calling the existing onSelect (or the component's selection toggle handler)
so activating the row toggles the checkbox; ensure ARIA checked state is kept in
sync with the nested checkbox.
In `@apps/frontend/src/renderer/components/Insights.tsx`:
- Around line 218-246: The handlers handleArchiveSession,
handleUnarchiveSession, handleDeleteSessions, and handleArchiveSessions lack
try/catch around their awaited IPC/store calls which can lead to unhandled
promise rejections; wrap the body of each of these functions in a try/catch,
call the same cleanup reload (loadInsightsSessions(projectId, showArchived)) in
finally or after success as appropriate, and in the catch log the error via
console.error or processLogger (including context like sessionId(s) and which
operation) and surface a user-facing error if needed; ensure you keep existing
partial-failure logging (result.failedIds) inside the try block so it only runs
on success.
- Around line 152-160: The current isFirstRun ref is shared across the component
lifecycle causing duplicate calls to loadInsightsSessions when projectId changes
while showArchived is true; update the logic so the "skip first run" guard is
per-projectId (e.g., reset isFirstRun.current when projectId changes) OR alter
loadInsightsSession to respect the showArchived context and avoid calling
loadInsightsSessions(projectId) when showArchived is active; target the
isFirstRun ref usage in the useEffect block and the loadInsightsSession /
loadInsightsSessions call sites to ensure only one load happens per
projectId+showArchived change.
…ionality Comprehensive fixes addressing 4 critical and accessibility issues: **Critical Bug Fixes:** 1. Fixed UI state mismatch when active session is archived/deleted - Added loadInsightsSession() call after bulk operations - Ensures frontend stays in sync when backend auto-switches sessions 2. Fixed race condition causing flicker on projectId change - Added prevProjectId ref to reset skip flag per-projectId - Prevents duplicate loadInsightsSessions calls when showArchived is true 3. Added comprehensive error handling to all bulk handlers - Wrapped all async operations in try/catch blocks - Added detailed error logging with session IDs and context - Prevents unhandled promise rejections from IPC failures **Accessibility Improvements:** 4. Fixed selection mode accessibility in SessionItem - Removed redundant checkbox onCheckedChange to prevent double-toggle - Made parent row fully interactive with role="checkbox" in selection mode - Added aria-checked attribute for screen reader support - Wired row onClick and keyboard handlers to toggle selection - Users can now activate selection via row click/keyboard or nested checkbox All changes maintain backward compatibility and improve robustness. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
| {isSelectionMode ? ( | ||
| <div className="shrink-0"> | ||
| <Checkbox | ||
| checked={isSelected} | ||
| className="h-4 w-4" | ||
| /> | ||
| </div> |
There was a problem hiding this comment.
Bug: The Checkbox component lacks an onCheckedChange handler and relies on a parent's onClick via event bubbling, which is an unreliable and inconsistent pattern.
Severity: HIGH
Suggested Fix
Add an onCheckedChange={onToggleSelect} handler and onClick={(e) => e.stopPropagation()} to the Checkbox component. This ensures the component handles its own state changes directly and prevents the parent's onClick from also firing, creating a more robust and predictable interaction.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location: apps/frontend/src/renderer/components/ChatHistorySidebar.tsx#L494-L500
Potential issue: The `Checkbox` component in `SessionItem` is a controlled component
using `checked={isSelected}` but lacks a corresponding `onCheckedChange` handler.
Instead, it relies on click events bubbling up to a parent `div`'s `onClick` handler to
toggle its state. This implementation is fragile and inconsistent with other parts of
the codebase where checkboxes use `onCheckedChange` and `e.stopPropagation()`. This can
lead to unresponsive checkboxes or inconsistent behavior, especially if the underlying
Radix UI library's event handling changes.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@apps/frontend/src/renderer/components/ChatHistorySidebar.tsx`:
- Around line 318-342: Bulk archive currently calls handleBulkArchive
immediately from the archive Button (onArchiveSessions present) while bulk
delete opens an AlertDialog; add the same confirmation flow for archiving by
replacing the direct onClick with opening a confirmation state (e.g.,
setBulkArchiveOpen) and only invoke handleBulkArchive when the user confirms, or
if you intentionally want immediate archive, document that decision in a comment
next to handleBulkArchive/onArchiveSessions; reference the archive Button,
handleBulkArchive, onArchiveSessions, and the setBulkDeleteOpen/AlertDialog
pattern to replicate the modal/confirm behavior.
In `@apps/frontend/src/renderer/components/Insights.tsx`:
- Around line 275-277: The showArchived state currently persists across project
switches; when projectId changes you should reset showArchived to a sensible
default (e.g., false) so the archived-toggle doesn't carry over or appear
out-of-sync. Locate the effect that watches projectId (the existing useEffect
around the projectId handling) and add a call to setShowArchived(false) inside
that effect (or update that effect to include showArchived reset logic) so the
toggle is cleared on project change; ensure the handler handleToggleShowArchived
and the showArchived state keep their current names and behavior otherwise.
- Around line 152-163: The current useEffect in Insights.tsx uses prevProjectId
and isFirstRun to skip running loadInsightsSessions when projectId changes,
which prevents loading archived sessions for the new project; update the effect
to only skip the initial mount by removing prevProjectId logic and changing the
condition to if (isFirstRun.current) { isFirstRun.current = false; return; } so
that loadInsightsSessions(projectId, showArchived) runs on any subsequent
projectId or showArchived change; ensure you keep the isFirstRun ref and the
call site loadInsightsSessions(projectId, showArchived) intact (and remove the
redundant loadInsightsSession call elsewhere if you follow the alternative
approach).
| {/* Bulk action toolbar */} | ||
| {isSelectionMode && selectedIds.size > 0 && ( | ||
| <div className="flex items-center gap-2 border-t border-border px-3 py-2"> | ||
| <Button | ||
| variant="destructive" | ||
| size="sm" | ||
| className="flex-1 text-xs" | ||
| onClick={() => setBulkDeleteOpen(true)} | ||
| > | ||
| <Trash2 className="mr-1.5 h-3.5 w-3.5" /> | ||
| {t('selection.deleteSelected')} ({selectedIds.size}) | ||
| </Button> | ||
| {onArchiveSessions && ( | ||
| <Button | ||
| variant="secondary" | ||
| size="sm" | ||
| className="flex-1 text-xs" | ||
| onClick={handleBulkArchive} | ||
| > | ||
| <Archive className="mr-1.5 h-3.5 w-3.5" /> | ||
| {t('insights.archiveSelected')} ({selectedIds.size}) | ||
| </Button> | ||
| )} | ||
| </div> | ||
| )} |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Bulk archive has no confirmation dialog, but bulk delete does — intentional?
handleBulkArchive fires immediately on click (line 335) without a confirmation step, while bulk delete shows an AlertDialog. Since archiving is recoverable (unarchive exists), this is likely intentional but worth confirming — accidentally archiving 50 sessions has a non-trivial recovery cost.
🤖 Prompt for AI Agents
In `@apps/frontend/src/renderer/components/ChatHistorySidebar.tsx` around lines
318 - 342, Bulk archive currently calls handleBulkArchive immediately from the
archive Button (onArchiveSessions present) while bulk delete opens an
AlertDialog; add the same confirmation flow for archiving by replacing the
direct onClick with opening a confirmation state (e.g., setBulkArchiveOpen) and
only invoke handleBulkArchive when the user confirms, or if you intentionally
want immediate archive, document that decision in a comment next to
handleBulkArchive/onArchiveSessions; reference the archive Button,
handleBulkArchive, onArchiveSessions, and the setBulkDeleteOpen/AlertDialog
pattern to replicate the modal/confirm behavior.
| // Reload sessions when showArchived changes (skip first run to avoid duplicate load) | ||
| // Reset the skip flag when projectId changes to avoid duplicate loads on project switch | ||
| const prevProjectId = useRef(projectId); | ||
| const isFirstRun = useRef(true); | ||
| useEffect(() => { | ||
| if (isFirstRun.current || prevProjectId.current !== projectId) { | ||
| isFirstRun.current = false; | ||
| prevProjectId.current = projectId; | ||
| return; | ||
| } | ||
| loadInsightsSessions(projectId, showArchived); | ||
| }, [projectId, showArchived]); |
There was a problem hiding this comment.
When projectId changes while showArchived is true, archived sessions are not loaded for the new project.
When the user switches projects with the "Show archived" toggle on:
- Line 147's effect calls
loadInsightsSession(projectId), which internally loads sessions withoutincludeArchived. - This effect skips (line 157 condition is true), so
loadInsightsSessions(projectId, showArchived)never fires.
Result: the sidebar loses archived sessions despite the toggle being active. The user must toggle off/on to recover.
Proposed fix: fire the archived-aware load after skipping the duplicate
const prevProjectId = useRef(projectId);
const isFirstRun = useRef(true);
useEffect(() => {
if (isFirstRun.current) {
isFirstRun.current = false;
prevProjectId.current = projectId;
return;
}
- if (prevProjectId.current !== projectId) {
+ if (prevProjectId.current !== projectId && !showArchived) {
prevProjectId.current = projectId;
return;
}
+ if (prevProjectId.current !== projectId) {
+ prevProjectId.current = projectId;
+ }
loadInsightsSessions(projectId, showArchived);
}, [projectId, showArchived]);Alternatively, always reload with showArchived on project switch and remove the redundant load from loadInsightsSession:
useEffect(() => {
if (isFirstRun.current) {
isFirstRun.current = false;
prevProjectId.current = projectId;
return;
}
+ prevProjectId.current = projectId;
loadInsightsSessions(projectId, showArchived);
- }, [projectId, showArchived]);
+ }, [projectId, showArchived]);The second approach is simpler — it only skips the initial mount (which is already covered by loadInsightsSession) and lets all subsequent projectId/showArchived changes go through.
🤖 Prompt for AI Agents
In `@apps/frontend/src/renderer/components/Insights.tsx` around lines 152 - 163,
The current useEffect in Insights.tsx uses prevProjectId and isFirstRun to skip
running loadInsightsSessions when projectId changes, which prevents loading
archived sessions for the new project; update the effect to only skip the
initial mount by removing prevProjectId logic and changing the condition to if
(isFirstRun.current) { isFirstRun.current = false; return; } so that
loadInsightsSessions(projectId, showArchived) runs on any subsequent projectId
or showArchived change; ensure you keep the isFirstRun ref and the call site
loadInsightsSessions(projectId, showArchived) intact (and remove the redundant
loadInsightsSession call elsewhere if you follow the alternative approach).
| const handleToggleShowArchived = () => { | ||
| setShowArchived(prev => !prev); | ||
| }; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider resetting showArchived on projectId change.
showArchived persists across project switches. If a user toggles it on for one project and switches, the toggle remains active for the new project (and per the issue above, may not even take effect). Consider resetting it in the projectId effect at line 146–150, or accept this as intentional cross-project behavior and document the decision.
🤖 Prompt for AI Agents
In `@apps/frontend/src/renderer/components/Insights.tsx` around lines 275 - 277,
The showArchived state currently persists across project switches; when
projectId changes you should reset showArchived to a sensible default (e.g.,
false) so the archived-toggle doesn't carry over or appear out-of-sync. Locate
the effect that watches projectId (the existing useEffect around the projectId
handling) and add a call to setShowArchived(false) inside that effect (or update
that effect to include showArchived reset logic) so the toggle is cleared on
project change; ensure the handler handleToggleShowArchived and the showArchived
state keep their current names and behavior otherwise.
AndyMik90
left a comment
There was a problem hiding this comment.
🤖 Auto Claude PR Review
🟠 Follow-up Review: Needs Revision
🟠 Needs revision - 3 unresolved finding(s) from previous review.
Resolution Status
- ✅ Resolved: 5 previous findings addressed
- ❌ Unresolved: 3 previous findings remain
- 🆕 New Issues: 5 new findings in recent changes
Finding Validation
- 🔍 Dismissed as False Positives: 1 findings were re-investigated and found to be incorrect
- ✓ Confirmed Valid: 9 findings verified as genuine issues
- 👤 Needs Human Review: 1 findings require manual verification
🚨 Blocking Issues
- Branch Out of Date: PR branch is behind the base branch and needs to be updated
- quality: [UNRESOLVED] Bulk delete/archive IPC handlers always return success:true even with partial failures
- security: [UNRESOLVED] Session ID used in file paths without sanitization (defense-in-depth)
- quality: Archive/delete operations do not refresh current session state
- quality: Race condition between effects when projectId changes with showArchived=true
- quality: [FROM COMMENTS] [FROM SENTRY BOT] Active session not reloaded after bulk/archive operations
Verdict
Good progress: 5 of 8 previous findings resolved. CI is all green (20/20 checks passing). However, several blocking issues remain:
HIGH: Bulk delete/archive and single archive/unarchive operations do not refresh the current session state (NR-001/NR-004). When the active session is deleted or archived, the chat area continues showing stale messages. This is a user-facing bug confirmed by Sentry bot, CodeRabbit, and code inspection.
MEDIUM (unresolved): NEW-005 — IPC handlers for INSIGHTS_DELETE_SESSIONS and INSIGHTS_ARCHIVE_SESSIONS still always return success: true even when failedIds is non-empty. The store functions then discard failedIds on the success: true branch, making the Insights.tsx partial-failure logging dead code.
MEDIUM (unresolved): NEW-004 — Session IDs are still used in file paths without sanitization (defense-in-depth concern).
MEDIUM (new): CMT-002 — Race condition: when projectId changes after initial render with showArchived=true, two concurrent loadInsightsSessions calls fire with conflicting filter states.
The LOW findings (stale selection state, archivedAt Date conversion, accessibility) are not blocking but should be addressed.
Review Process
Agents invoked: resolution-verifier, new-code-reviewer, comment-analyzer, finding-validator
This is an AI-generated follow-up review using parallel specialist analysis with finding validation.
Findings (8 selected of 8 total)
🟡 [NEW-005] [MEDIUM] [UNRESOLVED] Bulk delete/archive IPC handlers always return success:true even with partial failures
📁 apps/frontend/src/main/ipc-handlers/insights-handlers.ts:273
INSIGHTS_DELETE_SESSIONS and INSIGHTS_ARCHIVE_SESSIONS handlers always return { success: true, data: result } regardless of whether result.failedIds is non-empty. The store functions check result.success (always true) and discard failure information. Users get no indication of partial failures.
Resolution note: insights-handlers.ts line 273: return { success: true, data: result }; — still unconditional success:true. insights-store.ts line 319: return { success: true }; — discards failedIds when success is true. The else branch (returning failedIds) is dead code.
Suggested fix:
Check result.failedIds.length in the IPC handler and return success:false when there are failures, or propagate failedIds in the store functions even on success.
🟡 [NEW-004] [MEDIUM] [UNRESOLVED] Session ID used in file paths without sanitization (defense-in-depth)
📁 apps/frontend/src/main/insights/session-storage.ts:0
Session IDs from the renderer are interpolated into file paths via path.join() without validation. A crafted session ID containing '../' could traverse outside the sessions directory. Practical risk is limited in an Electron desktop app but this is a defense-in-depth gap.
Resolution note: paths.ts line 30: return path.join(this.getSessionsDir(projectPath), ${sessionId}.json); — no validation. No sanitization found anywhere in the IPC handler → service → storage → paths chain.
Suggested fix:
Validate sessionId matches expected pattern (e.g., /^session-\d+$/) in getSessionPath or at the IPC handler level.
🔵 [NEW-007] [LOW] [UNRESOLVED] Selection state not pruned when sessions list changes
📁 apps/frontend/src/renderer/components/ChatHistorySidebar.tsx:90
selectedIds Set may contain IDs of sessions that no longer exist. The useEffect only clears on showArchived change, not when sessions change. The button counter shows selectedIds.size which could be misleading after failed operations.
Resolution note: Only useEffect clearing selectedIds depends on [showArchived] (line 91-93). No pruning when sessions prop changes. Stale IDs can remain in selectedIds causing count mismatch in toolbar.
Suggested fix:
Add a useEffect that prunes selectedIds when sessions change, or compute displayed count from sessions.filter(s => selectedIds.has(s.id)).length.
🟠 [NR-001] [HIGH] Archive/delete operations do not refresh current session state
📁 apps/frontend/src/renderer/components/Insights.tsx:218
handleArchiveSession, handleUnarchiveSession, handleDeleteSessions, and handleArchiveSessions only call loadInsightsSessions() (sidebar list refresh) but never loadInsightsSession() (current session refresh). When the active session is among those deleted/archived, the chat area continues displaying stale messages. The single-delete path (deleteSession store helper) correctly calls loadInsightsSession(). Also confirmed by Sentry bot reporting UI state mismatch.
Suggested fix:
Add await loadInsightsSession(projectId) before or after loadInsightsSessions() in all four handlers. This refreshes both the current session state and the sidebar list.
🟡 [CMT-002] [MEDIUM] Race condition between effects when projectId changes with showArchived=true
📁 apps/frontend/src/renderer/components/Insights.tsx:153
The isFirstRun ref is only true on first render and never resets when projectId changes. When projectId changes: Effect 1 calls loadInsightsSession(projectId) which internally calls loadInsightsSessions(projectId) WITHOUT includeArchived. Effect 2 (since isFirstRun is now false) calls loadInsightsSessions(projectId, showArchived). When showArchived=true, these two concurrent calls produce conflicting session lists — the winner depends on IPC response ordering.
Suggested fix:
Either (1) reset isFirstRun when projectId changes by tracking previous projectId, (2) make Effect 2 depend only on showArchived (not projectId), or (3) consolidate both effects into a single effect that handles initial load and archive filter coherently.
🔵 [NR-003] [LOW] archivedAt not converted from string to Date in loadSessionById
📁 apps/frontend/src/main/insights/session-storage.ts:36
loadSessionById converts createdAt, updatedAt, and message timestamps from JSON strings to Date objects but omits the new archivedAt field. After loading from disk, archivedAt will be a string instead of Date, violating the InsightsSession type. The listSessions method does correctly convert archivedAt. Currently a latent bug since archivedAt is only used for truthiness checks.
Suggested fix:
Add after line 38: if (session.archivedAt) session.archivedAt = new Date(session.archivedAt);
🔵 [CMT-003] [LOW] Accessibility: role='button' with no interactivity in selection mode
📁 apps/frontend/src/renderer/components/ChatHistorySidebar.tsx:476
In selection mode, the SessionItem row div retains role='button' and tabIndex={0} but onClick is undefined and onKeyDown is disabled. Screen readers announce it as an interactive button that does nothing when activated. Users must navigate to the nested Checkbox to toggle selection.
Suggested fix:
Conditionally set role and tabIndex: role={isSelectionMode ? undefined : 'button'} tabIndex={isSelectionMode ? -1 : 0}. Alternatively, wire onClick/onKeyDown to toggle selection in selection mode.
🟠 [CMT-SENTRY-001] [HIGH] [FROM COMMENTS] [FROM SENTRY BOT] Active session not reloaded after bulk/archive operations
📁 apps/frontend/src/renderer/components/Insights.tsx:218
Sentry bot reported that bulk archive/delete handlers in Insights.tsx don't reload the current session, causing a UI state mismatch when the active session is modified. This was independently confirmed by new-code-reviewer (NR-001) and comment-analyzer. The fix is to call loadInsightsSession(projectId) in addition to loadInsightsSessions().
This review was generated by Auto Claude.
Base Branch
developbranch (required for all feature/fix PRs)main(hotfix only - maintainers)Description
This PR implements bulk delete and archive functionality for the Insights chat history sidebar. Users can now select multiple chat sessions and perform bulk delete (permanent removal) or bulk archive (recoverable) operations. Archive moves sessions to a hidden state with an "Show archived" toggle to filter visibility, and includes unarchive functionality for recovery.
Related Issue
Closes #225
Type of Change
Area
Commit Message Format
Follow conventional commits:
<type>: <subject>Types: feat, fix, docs, style, refactor, test, chore
Example:
feat: add user authentication systemAI Disclosure
Tool(s) used: Claude Code
Testing level:
Untested -- AI output not yet verified
Lightly tested -- ran the app / spot-checked key paths
Fully tested -- all tests pass, manually verified behavior
I understand what this PR does and how the underlying code works
Checklist
developbranchPlatform Testing Checklist
CRITICAL: This project supports Windows, macOS, and Linux. Platform-specific bugs are a common source of breakage.
platform/module instead of directprocess.platformchecksfindExecutable()or platform abstractions)If you only have access to one OS: CI now tests on all platforms. Ensure all checks pass before submitting.
CI/Testing Requirements
Screenshots
Feature Toggle
use_feature_nameBreaking Changes
Breaking: No
Details:
Backward compatible. All existing single-session delete and rename functionality preserved. Archive is additive (new field
archivedAtoptional on types).Change Context
Branch Information
Git Diff Summary
14 files changed, 730 insertions(+), 85 deletions(-)
Files Changed
apps/frontend/src/shared/types/insights.ts- AddedarchivedAtoptional fieldapps/frontend/src/shared/constants/ipc.ts- Added 4 new IPC channels for bulk operationsapps/frontend/src/main/insights/session-storage.ts- Implemented archive/unarchive/bulk delete/bulk archive methodsapps/frontend/src/main/insights/session-manager.ts- Added cache management for archive operationsapps/frontend/src/main/insights-service.ts- Added delegation methods for new operationsapps/frontend/src/main/ipc-handlers/insights-handlers.ts- Registered new IPC handlersapps/frontend/src/preload/api/modules/insights-api.ts- Extended preload API with bulk operationsapps/frontend/src/renderer/stores/insights-store.ts- Added store helper functions for bulk operationsapps/frontend/src/renderer/components/ChatHistorySidebar.tsx- Major UI update with multi-select, bulk toolbar, archive toggleapps/frontend/src/renderer/components/Insights.tsx- Wired new handler props to sidebarapps/frontend/src/shared/i18n/locales/en/common.json- English translation keysapps/frontend/src/shared/i18n/locales/fr/common.json- French translation keysSummary by CodeRabbit
New Features
UI / Copy