diff --git a/src/client/src/controllers/sessionController.ts b/src/client/src/controllers/sessionController.ts index f55b04c..4fd3dfc 100644 --- a/src/client/src/controllers/sessionController.ts +++ b/src/client/src/controllers/sessionController.ts @@ -6,6 +6,7 @@ import { readChatHistoryCache, mergeChatHistory, writeChatHistoryCache, type Raw import { applyTranscriptEvent } from "../chatTranscript"; import { isShellInput } from "../inputModes"; import { GlobalSessionSocket, SessionSocket, type SessionUiEvent } from "../sessionSocket"; +import { markSessionArchived, selectionAfterArchivingSession } from "./sessionSelection"; import type { GetState, SetState, UpdateUrl } from "./types"; export class SessionController { @@ -158,10 +159,15 @@ export class SessionController { if (!session) return; try { await api.archive(session.id); - this.replaceSession({ ...session, archived: true, archivedAt: new Date().toISOString() }); - if (this.getState().selectedSession?.id === session.id) { - this.socket.close(); - this.setState({ status: undefined, activity: undefined }); + const state = this.getState(); + const sessions = markSessionArchived(state.sessions, session.id, new Date().toISOString()); + const selectionChange = selectionAfterArchivingSession(sessions, state.selectedSession?.id, session.id); + this.setState({ sessions }); + + if (selectionChange.type === "select") await this.selectSession(selectionChange.session); + else if (selectionChange.type === "clear") { + this.clearActiveSession(); + this.updateUrl(); } } catch (error) { this.setState({ error: String(error) }); diff --git a/src/client/src/controllers/sessionSelection.test.ts b/src/client/src/controllers/sessionSelection.test.ts new file mode 100644 index 0000000..cfe527c --- /dev/null +++ b/src/client/src/controllers/sessionSelection.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import type { SessionInfo } from "../api"; +import { markSessionArchived, selectionAfterArchivingSession } from "./sessionSelection"; + +describe("markSessionArchived", () => { + it("marks the matching session archived without mutating the original", () => { + const sessions = [testSession("s1"), testSession("s2")]; + + const next = markSessionArchived(sessions, "s1", "later"); + + expect(next).toEqual([{ ...sessions[0], archived: true, archivedAt: "later" }, sessions[1]]); + expect(sessions[0]?.archived).toBeUndefined(); + }); +}); + +describe("selectionAfterArchivingSession", () => { + it("leaves selection unchanged when archiving an unselected session", () => { + expect(selectionAfterArchivingSession([testSession("s1"), testSession("s2")], "s1", "s2")).toEqual({ type: "unchanged" }); + }); + + it("selects the first active session when archiving the selected session", () => { + const s2 = testSession("s2"); + + expect(selectionAfterArchivingSession([testSession("s1"), s2], "s1", "s1")).toEqual({ type: "select", session: s2 }); + }); + + it("skips archived sessions when choosing the next selected session", () => { + const s3 = testSession("s3"); + + expect(selectionAfterArchivingSession([testSession("s1"), { ...testSession("s2"), archived: true }, s3], "s1", "s1")).toEqual({ type: "select", session: s3 }); + }); + + it("clears selection when no active session remains", () => { + expect(selectionAfterArchivingSession([testSession("s1")], "s1", "s1")).toEqual({ type: "clear" }); + }); +}); + +function testSession(id: string): SessionInfo { + return { id, path: `/tmp/project/.pi/sessions/${id}`, cwd: "/tmp/project", created: "now", modified: "now", messageCount: 0, firstMessage: "" }; +} diff --git a/src/client/src/controllers/sessionSelection.ts b/src/client/src/controllers/sessionSelection.ts new file mode 100644 index 0000000..d1ce117 --- /dev/null +++ b/src/client/src/controllers/sessionSelection.ts @@ -0,0 +1,17 @@ +import type { SessionInfo } from "../api"; + +export type ArchiveSelectionChange = + | { type: "unchanged" } + | { type: "select"; session: SessionInfo } + | { type: "clear" }; + +export function markSessionArchived(sessions: SessionInfo[], sessionId: string, archivedAt: string): SessionInfo[] { + return sessions.map((session) => session.id === sessionId ? { ...session, archived: true, archivedAt } : session); +} + +export function selectionAfterArchivingSession(sessions: SessionInfo[], selectedSessionId: string | undefined, archivedSessionId: string): ArchiveSelectionChange { + if (selectedSessionId !== archivedSessionId) return { type: "unchanged" }; + + const nextSession = sessions.find((session) => session.id !== archivedSessionId && session.archived !== true); + return nextSession === undefined ? { type: "clear" } : { type: "select", session: nextSession }; +}