From bc602ff5eab67da27036eee74a0d1aeb2f4699aa Mon Sep 17 00:00:00 2001 From: weishu Date: Sat, 3 Jan 2026 10:31:54 +0800 Subject: [PATCH] fix: archive session --- cli/src/agent/sessionBase.ts | 1 + server/src/sync/syncEngine.ts | 5 +++++ server/src/web/routes/sessions.ts | 15 +++++++++++++++ web/src/api/client.ts | 7 +++++++ web/src/components/SessionHeader.tsx | 4 ++-- web/src/components/SessionList.tsx | 4 ++-- web/src/hooks/mutations/useSessionActions.ts | 20 +++++++++++++++++++- 7 files changed, 51 insertions(+), 5 deletions(-) diff --git a/cli/src/agent/sessionBase.ts b/cli/src/agent/sessionBase.ts index 1dc382a1..365e2ae9 100644 --- a/cli/src/agent/sessionBase.ts +++ b/cli/src/agent/sessionBase.ts @@ -58,6 +58,7 @@ export class AgentSessionBase { this.keepAliveInterval = setInterval(() => { this.client.keepAlive(this.thinking, this.mode, this.getKeepAliveRuntime()); }, 2000); + } onThinkingChange = (thinking: boolean) => { diff --git a/server/src/sync/syncEngine.ts b/server/src/sync/syncEngine.ts index 4e057249..ddcd5bb8 100644 --- a/server/src/sync/syncEngine.ts +++ b/server/src/sync/syncEngine.ts @@ -728,6 +728,11 @@ export class SyncEngine { await this.sessionRpc(sessionId, 'abort', { reason: 'User aborted via Telegram Bot' }) } + async archiveSession(sessionId: string): Promise { + await this.sessionRpc(sessionId, 'killSession', {}) + this.handleSessionEnd({ sid: sessionId, time: Date.now() }) + } + async switchSession(sessionId: string, to: 'remote' | 'local'): Promise { await this.sessionRpc(sessionId, 'switch', { to }) } diff --git a/server/src/web/routes/sessions.ts b/server/src/web/routes/sessions.ts index f162f430..53e21443 100644 --- a/server/src/web/routes/sessions.ts +++ b/server/src/web/routes/sessions.ts @@ -132,6 +132,21 @@ export function createSessionsRoutes(getSyncEngine: () => SyncEngine | null): Ho return c.json({ ok: true }) }) + app.post('/sessions/:id/archive', async (c) => { + const engine = requireSyncEngine(c, getSyncEngine) + if (engine instanceof Response) { + return engine + } + + const sessionResult = requireSessionFromParam(c, engine, { requireActive: true }) + if (sessionResult instanceof Response) { + return sessionResult + } + + await engine.archiveSession(sessionResult.sessionId) + return c.json({ ok: true }) + }) + app.post('/sessions/:id/switch', async (c) => { const engine = requireSyncEngine(c, getSyncEngine) if (engine instanceof Response) { diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 85df5000..9da94fa2 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -239,6 +239,13 @@ export class ApiClient { }) } + async archiveSession(sessionId: string): Promise { + await this.request(`/api/sessions/${encodeURIComponent(sessionId)}/archive`, { + method: 'POST', + body: JSON.stringify({}) + }) + } + async switchSession(sessionId: string): Promise { await this.request(`/api/sessions/${encodeURIComponent(sessionId)}/switch`, { method: 'POST', diff --git a/web/src/components/SessionHeader.tsx b/web/src/components/SessionHeader.tsx index 53e6ea14..da6f1b06 100644 --- a/web/src/components/SessionHeader.tsx +++ b/web/src/components/SessionHeader.tsx @@ -74,7 +74,7 @@ export function SessionHeader(props: { const [archiveOpen, setArchiveOpen] = useState(false) const [deleteOpen, setDeleteOpen] = useState(false) - const { abortSession, renameSession, deleteSession, isPending } = useSessionActions(api, session.id) + const { archiveSession, renameSession, deleteSession, isPending } = useSessionActions(api, session.id) const handleDelete = async () => { await deleteSession() @@ -168,7 +168,7 @@ export function SessionHeader(props: { description={`Are you sure you want to archive "${title}"? This will disconnect the active session.`} confirmLabel="Archive" confirmingLabel="Archiving..." - onConfirm={abortSession} + onConfirm={archiveSession} isPending={isPending} destructive /> diff --git a/web/src/components/SessionList.tsx b/web/src/components/SessionList.tsx index 794c8253..996da4b6 100644 --- a/web/src/components/SessionList.tsx +++ b/web/src/components/SessionList.tsx @@ -177,7 +177,7 @@ function SessionItem(props: { const [archiveOpen, setArchiveOpen] = useState(false) const [deleteOpen, setDeleteOpen] = useState(false) - const { abortSession, renameSession, deleteSession, isPending } = useSessionActions(api, s.id) + const { archiveSession, renameSession, deleteSession, isPending } = useSessionActions(api, s.id) const longPressHandlers = useLongPress({ onLongPress: () => { @@ -273,7 +273,7 @@ function SessionItem(props: { description={`Are you sure you want to archive "${sessionName}"? This will disconnect the active session.`} confirmLabel="Archive" confirmingLabel="Archiving..." - onConfirm={abortSession} + onConfirm={archiveSession} isPending={isPending} destructive /> diff --git a/web/src/hooks/mutations/useSessionActions.ts b/web/src/hooks/mutations/useSessionActions.ts index bc54476d..440dccd3 100644 --- a/web/src/hooks/mutations/useSessionActions.ts +++ b/web/src/hooks/mutations/useSessionActions.ts @@ -22,6 +22,7 @@ function toModelMode(mode: ModelMode): ModelModeValue { export function useSessionActions(api: ApiClient | null, sessionId: string | null): { abortSession: () => Promise + archiveSession: () => Promise switchSession: () => Promise setPermissionMode: (mode: PermissionMode) => Promise setModelMode: (mode: ModelMode) => Promise @@ -47,6 +48,16 @@ export function useSessionActions(api: ApiClient | null, sessionId: string | nul onSuccess: () => void invalidateSession(), }) + const archiveMutation = useMutation({ + mutationFn: async () => { + if (!api || !sessionId) { + throw new Error('Session unavailable') + } + await api.archiveSession(sessionId) + }, + onSuccess: () => void invalidateSession(), + }) + const switchMutation = useMutation({ mutationFn: async () => { if (!api || !sessionId) { @@ -104,11 +115,18 @@ export function useSessionActions(api: ApiClient | null, sessionId: string | nul return { abortSession: abortMutation.mutateAsync, + archiveSession: archiveMutation.mutateAsync, switchSession: switchMutation.mutateAsync, setPermissionMode: permissionMutation.mutateAsync, setModelMode: modelMutation.mutateAsync, renameSession: renameMutation.mutateAsync, deleteSession: deleteMutation.mutateAsync, - isPending: abortMutation.isPending || switchMutation.isPending || permissionMutation.isPending || modelMutation.isPending || renameMutation.isPending || deleteMutation.isPending, + isPending: abortMutation.isPending + || archiveMutation.isPending + || switchMutation.isPending + || permissionMutation.isPending + || modelMutation.isPending + || renameMutation.isPending + || deleteMutation.isPending, } }