Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
f1305d5
auto-claude: subtask-1-1 - Add archivedAt optional field to InsightsS…
AndyMik90 Feb 14, 2026
2382865
auto-claude: subtask-1-2 - Add new IPC channel constants for bulk del…
AndyMik90 Feb 14, 2026
9379470
auto-claude: subtask-2-1 - Add archive/unarchive/bulk methods to Sess…
AndyMik90 Feb 14, 2026
48b14c8
auto-claude: subtask-2-2 - Add archive/unarchive/bulk methods to Sess…
AndyMik90 Feb 14, 2026
b00fb64
auto-claude: subtask-3-1 - Add delegation methods to InsightsService
AndyMik90 Feb 14, 2026
af22f05
auto-claude: subtask-3-2 - Register new IPC handlers in insights-hand…
AndyMik90 Feb 14, 2026
f78d672
auto-claude: subtask-3-3 - Extend preload InsightsAPI with bulk delet…
AndyMik90 Feb 14, 2026
aef3fa0
auto-claude: subtask-4-1 - Add bulk delete/archive helper functions t…
AndyMik90 Feb 14, 2026
2913d99
auto-claude: subtask-5-1 - Add i18n translation keys for chat history…
AndyMik90 Feb 14, 2026
835b47e
auto-claude: subtask-5-2 - Add selection mode, bulk actions, and arch…
AndyMik90 Feb 14, 2026
5f8747d
auto-claude: subtask-5-3 - Update Insights.tsx to wire new ChatHistor…
AndyMik90 Feb 14, 2026
f8a9890
fix: pass showArchived flag when reloading sessions after bulk operat…
AndyMik90 Feb 14, 2026
dc8df1c
Merge branch 'develop' into auto-claude/225-bulk-delete-and-archive-c…
AndyMik90 Feb 14, 2026
4e053fd
fix: properly await async bulk operations in ChatHistorySidebar
AndyMik90 Feb 15, 2026
4f23ecd
fix: address all PR review findings for bulk delete/archive functiona…
AndyMik90 Feb 15, 2026
fc484f5
fix: address all new PR review findings for bulk delete/archive funct…
AndyMik90 Feb 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions apps/frontend/src/main/insights-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ export class InsightsService extends EventEmitter {
/**
* List all sessions for a project
*/
listSessions(projectPath: string): InsightsSessionSummary[] {
return this.sessionManager.listSessions(projectPath);
listSessions(projectPath: string, includeArchived = false): InsightsSessionSummary[] {
return this.sessionManager.listSessions(projectPath, includeArchived);
}

/**
Expand All @@ -95,6 +95,34 @@ export class InsightsService extends EventEmitter {
return this.sessionManager.deleteSession(projectId, projectPath, sessionId);
}

/**
* Archive a session
*/
archiveSession(projectId: string, projectPath: string, sessionId: string): boolean {
return this.sessionManager.archiveSession(projectId, projectPath, sessionId);
}

/**
* Unarchive a session
*/
unarchiveSession(projectPath: string, sessionId: string): boolean {
return this.sessionManager.unarchiveSession(projectPath, sessionId);
}

/**
* Delete multiple sessions
*/
deleteSessions(projectId: string, projectPath: string, sessionIds: string[]): { deletedIds: string[]; failedIds: string[] } {
return this.sessionManager.deleteSessions(projectId, projectPath, sessionIds);
}

/**
* Archive multiple sessions
*/
archiveSessions(projectId: string, projectPath: string, sessionIds: string[]): { archivedIds: string[]; failedIds: string[] } {
return this.sessionManager.archiveSessions(projectId, projectPath, sessionIds);
}

/**
* Rename a session
*/
Expand Down
83 changes: 79 additions & 4 deletions apps/frontend/src/main/insights/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ export class SessionManager {
*/
loadSession(projectId: string, projectPath: string): InsightsSession | null {
// Check in-memory cache first
if (this.sessions.has(projectId)) {
return this.sessions.get(projectId)!;
const cachedSession = this.sessions.get(projectId);
if (cachedSession) {
return cachedSession;
}

// Migrate old format if needed
Expand All @@ -40,10 +41,10 @@ export class SessionManager {
/**
* List all sessions for a project
*/
listSessions(projectPath: string): InsightsSessionSummary[] {
listSessions(projectPath: string, includeArchived = false): InsightsSessionSummary[] {
// Migrate old format if needed
this.storage.migrateOldSession(projectPath);
return this.storage.listSessions(projectPath);
return this.storage.listSessions(projectPath, includeArchived);
}

/**
Expand Down Expand Up @@ -105,6 +106,80 @@ export class SessionManager {
return true;
}

/**
* 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);
}
}

Comment on lines +167 to +179
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

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.

return result;
}
Comment on lines +109 to +181
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 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.


/**
* Rename a session
*/
Expand Down
81 changes: 79 additions & 2 deletions apps/frontend/src/main/insights/session-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,76 @@ export class SessionStorage {
writeFileSync(sessionPath, JSON.stringify(session, null, 2), 'utf-8');
}

/**
* Archive a session
*/
archiveSession(projectPath: string, sessionId: string): boolean {
const session = this.loadSessionById(projectPath, sessionId);
if (!session) return false;

try {
session.archivedAt = new Date();
this.saveSession(projectPath, session);
return true;
} catch (error) {
console.error(`[SessionStorage] Failed to archive session ${sessionId}:`, error);
return false;
}
}

/**
* Unarchive a session
*/
unarchiveSession(projectPath: string, sessionId: string): boolean {
const session = this.loadSessionById(projectPath, sessionId);
if (!session) return false;

try {
delete session.archivedAt;
this.saveSession(projectPath, session);
return true;
} catch (error) {
console.error(`[SessionStorage] Failed to unarchive session ${sessionId}:`, error);
return false;
}
}

/**
* Delete multiple sessions
*/
deleteSessions(projectPath: string, sessionIds: string[]): { deletedIds: string[]; failedIds: string[] } {
const deletedIds: string[] = [];
const failedIds: string[] = [];

for (const sessionId of sessionIds) {
if (this.deleteSession(projectPath, sessionId)) {
deletedIds.push(sessionId);
} else {
failedIds.push(sessionId);
}
}

return { deletedIds, failedIds };
}

/**
* Archive multiple sessions
*/
archiveSessions(projectPath: string, sessionIds: string[]): { archivedIds: string[]; failedIds: string[] } {
const archivedIds: string[] = [];
const failedIds: string[] = [];

for (const sessionId of sessionIds) {
if (this.archiveSession(projectPath, sessionId)) {
archivedIds.push(sessionId);
} else {
failedIds.push(sessionId);
}
}

return { archivedIds, failedIds };
}

/**
* Delete a session from disk
*/
Expand All @@ -82,7 +152,7 @@ export class SessionStorage {
/**
* List all sessions for a project
*/
listSessions(projectPath: string): InsightsSessionSummary[] {
listSessions(projectPath: string, includeArchived = false): InsightsSessionSummary[] {
const sessionsDir = this.paths.getSessionsDir(projectPath);
if (!existsSync(sessionsDir)) return [];

Expand All @@ -104,13 +174,20 @@ export class SessionStorage {
: 'Untitled Conversation';
}

// Skip archived sessions unless explicitly included
if (!includeArchived && session.archivedAt) {
continue;
}

sessions.push({
id: session.id,
projectId: session.projectId,
title: title || 'New Conversation',
messageCount: session.messages.length,
modelConfig: session.modelConfig,
createdAt: new Date(session.createdAt),
updatedAt: new Date(session.updatedAt)
updatedAt: new Date(session.updatedAt),
...(session.archivedAt ? { archivedAt: new Date(session.archivedAt) } : {})
});
} catch {
// Skip invalid session files
Expand Down
66 changes: 64 additions & 2 deletions apps/frontend/src/main/ipc-handlers/insights-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,17 +249,79 @@ export function registerInsightsHandlers(getMainWindow: () => BrowserWindow | nu
// List all sessions for a project
ipcMain.handle(
IPC_CHANNELS.INSIGHTS_LIST_SESSIONS,
async (_, projectId: string): Promise<IPCResult<InsightsSessionSummary[]>> => {
async (_, projectId: string, includeArchived?: boolean): Promise<IPCResult<InsightsSessionSummary[]>> => {
const project = projectStore.getProject(projectId);
if (!project) {
return { success: false, error: "Project not found" };
}

const sessions = insightsService.listSessions(project.path);
const sessions = insightsService.listSessions(project.path, includeArchived ?? false);
return { success: true, data: sessions };
}
);

// 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" };
}
);
Comment on lines +263 to +323
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 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.


// Create a new session
ipcMain.handle(
IPC_CHANNELS.INSIGHTS_NEW_SESSION,
Expand Down
22 changes: 19 additions & 3 deletions apps/frontend/src/preload/api/modules/insights-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,14 @@ export interface InsightsAPI {
description: string,
metadata?: TaskMetadata
) => Promise<IPCResult<Task>>;
listInsightsSessions: (projectId: string) => Promise<IPCResult<InsightsSessionSummary[]>>;
listInsightsSessions: (projectId: string, includeArchived?: boolean) => Promise<IPCResult<InsightsSessionSummary[]>>;
newInsightsSession: (projectId: string) => Promise<IPCResult<InsightsSession>>;
switchInsightsSession: (projectId: string, sessionId: string) => Promise<IPCResult<InsightsSession | null>>;
deleteInsightsSession: (projectId: string, sessionId: string) => Promise<IPCResult>;
deleteInsightsSessions: (projectId: string, sessionIds: string[]) => Promise<IPCResult<{ deletedIds: string[]; failedIds: string[] }>>;
archiveInsightsSession: (projectId: string, sessionId: string) => Promise<IPCResult>;
archiveInsightsSessions: (projectId: string, sessionIds: string[]) => Promise<IPCResult<{ archivedIds: string[]; failedIds: string[] }>>;
unarchiveInsightsSession: (projectId: string, sessionId: string) => Promise<IPCResult>;
renameInsightsSession: (projectId: string, sessionId: string, newTitle: string) => Promise<IPCResult>;
updateInsightsModelConfig: (projectId: string, sessionId: string, modelConfig: InsightsModelConfig) => Promise<IPCResult>;

Expand Down Expand Up @@ -69,8 +73,8 @@ export const createInsightsAPI = (): InsightsAPI => ({
): Promise<IPCResult<Task>> =>
invokeIpc(IPC_CHANNELS.INSIGHTS_CREATE_TASK, projectId, title, description, metadata),

listInsightsSessions: (projectId: string): Promise<IPCResult<InsightsSessionSummary[]>> =>
invokeIpc(IPC_CHANNELS.INSIGHTS_LIST_SESSIONS, projectId),
listInsightsSessions: (projectId: string, includeArchived?: boolean): Promise<IPCResult<InsightsSessionSummary[]>> =>
invokeIpc(IPC_CHANNELS.INSIGHTS_LIST_SESSIONS, projectId, includeArchived),

newInsightsSession: (projectId: string): Promise<IPCResult<InsightsSession>> =>
invokeIpc(IPC_CHANNELS.INSIGHTS_NEW_SESSION, projectId),
Expand All @@ -81,6 +85,18 @@ export const createInsightsAPI = (): InsightsAPI => ({
deleteInsightsSession: (projectId: string, sessionId: string): Promise<IPCResult> =>
invokeIpc(IPC_CHANNELS.INSIGHTS_DELETE_SESSION, projectId, sessionId),

deleteInsightsSessions: (projectId: string, sessionIds: string[]): Promise<IPCResult<{ deletedIds: string[]; failedIds: string[] }>> =>
invokeIpc(IPC_CHANNELS.INSIGHTS_DELETE_SESSIONS, projectId, sessionIds),

archiveInsightsSession: (projectId: string, sessionId: string): Promise<IPCResult> =>
invokeIpc(IPC_CHANNELS.INSIGHTS_ARCHIVE_SESSION, projectId, sessionId),

archiveInsightsSessions: (projectId: string, sessionIds: string[]): Promise<IPCResult<{ archivedIds: string[]; failedIds: string[] }>> =>
invokeIpc(IPC_CHANNELS.INSIGHTS_ARCHIVE_SESSIONS, projectId, sessionIds),

unarchiveInsightsSession: (projectId: string, sessionId: string): Promise<IPCResult> =>
invokeIpc(IPC_CHANNELS.INSIGHTS_UNARCHIVE_SESSION, projectId, sessionId),

renameInsightsSession: (projectId: string, sessionId: string, newTitle: string): Promise<IPCResult> =>
invokeIpc(IPC_CHANNELS.INSIGHTS_RENAME_SESSION, projectId, sessionId, newTitle),

Expand Down
Loading
Loading