feat(web-shell): manage sessions from the sidebar (archive, unarchive, delete) (#6293)

Add an Archive quick action and a "..." overflow menu (Rename / Archive / Delete) to each session row in the web-shell sidebar, plus a collapsible "Archived" section that lazily lists archived sessions with Restore / Delete. Thread the daemon's existing archiveState filter and archive/unarchive endpoints through the webui workspace facade and the useDaemonSessions hook; rename stays limited to the current live session.
This commit is contained in:
Shaojin Wen 2026-07-04 12:27:22 +08:00 committed by GitHub
parent 2a6a9514e3
commit c37cb23ccc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 863 additions and 48 deletions

View file

@ -58,6 +58,30 @@ export function createDaemonWorkspaceActions({
);
},
async archiveSession(sessionId: string) {
const client = requireClient(getClient, 'Archive session failed');
const result = await withActionTimeout(
client.archiveSessionsData([sessionId]),
'Archive session timed out',
);
if (result.errors.length > 0) {
throw new Error(result.errors[0].error);
}
return result.archived.length > 0 || result.alreadyArchived.length > 0;
},
async unarchiveSession(sessionId: string) {
const client = requireClient(getClient, 'Unarchive session failed');
const result = await withActionTimeout(
client.unarchiveSessionsData([sessionId]),
'Unarchive session timed out',
);
if (result.errors.length > 0) {
throw new Error(result.errors[0].error);
}
return result.unarchived.length > 0 || result.alreadyActive.length > 0;
},
async loadMcpStatus() {
const client = requireClient(getClient, 'Load MCP status failed');
return withActionTimeout(

View file

@ -5,6 +5,7 @@
*/
import { useCallback } from 'react';
import type { DaemonSessionArchiveState } from '@qwen-code/sdk/daemon';
import { useOptionalDaemonActions } from '../../session/DaemonSessionProvider.js';
import { useDaemonWorkspace } from '../DaemonWorkspaceProvider.js';
import type { DaemonResourceOptions } from '../types.js';
@ -12,15 +13,17 @@ import { useDaemonResource } from './useDaemonResource.js';
export interface DaemonSessionsOptions extends DaemonResourceOptions {
pageSize?: number;
/** Which session directory to list. Defaults to the daemon's `active`. */
archiveState?: DaemonSessionArchiveState;
}
export function useDaemonSessions(options: DaemonSessionsOptions = {}) {
const { pageSize, ...resourceOptions } = options;
const { pageSize, archiveState, ...resourceOptions } = options;
const workspace = useDaemonWorkspace();
const sessionActions = useOptionalDaemonActions();
const load = useCallback(
() => workspace.actions.listSessions({ pageSize }),
[pageSize, workspace.actions],
() => workspace.actions.listSessions({ pageSize, archiveState }),
[archiveState, pageSize, workspace.actions],
);
const workspaceReady = !!workspace.workspaceCwd;
const result = useDaemonResource(load, {
@ -44,6 +47,22 @@ export function useDaemonSessions(options: DaemonSessionsOptions = {}) {
},
[workspace.actions, reload],
);
const archiveSession = useCallback(
async (sessionId: string) => {
const archived = await workspace.actions.archiveSession(sessionId);
if (archived) reload();
return archived;
},
[workspace.actions, reload],
);
const unarchiveSession = useCallback(
async (sessionId: string) => {
const unarchived = await workspace.actions.unarchiveSession(sessionId);
if (unarchived) reload();
return unarchived;
},
[workspace.actions, reload],
);
return {
...result,
sessions: result.data ?? [],
@ -53,5 +72,7 @@ export function useDaemonSessions(options: DaemonSessionsOptions = {}) {
releaseSession: sessionActions?.releaseSession,
deleteSession,
deleteSessions,
archiveSession,
unarchiveSession,
};
}

View file

@ -29,6 +29,7 @@ import type {
DaemonMcpRestartResult,
DaemonMcpManageAction,
DaemonMcpManageResult,
DaemonSessionArchiveState,
DaemonUpdateAgentRequest,
DaemonWorkspaceAgentDetail,
DaemonWorkspaceAgentsStatus,
@ -146,6 +147,7 @@ export interface DaemonWorkspaceActions {
// Sessions
listSessions(options?: {
pageSize?: number;
archiveState?: DaemonSessionArchiveState;
}): Promise<DaemonSessionSummary[]>;
deleteSession(sessionId: string): Promise<boolean>;
deleteSessions(sessionIds: string[]): Promise<{
@ -153,6 +155,14 @@ export interface DaemonWorkspaceActions {
notFound: string[];
errors: Array<{ sessionId: string; error: string }>;
}>;
/**
* Move a session to the archived directory. Idempotent: an
* already-archived session resolves `true`. Rejects if the daemon
* reports a per-session error (e.g. an archive/unarchive conflict).
*/
archiveSession(sessionId: string): Promise<boolean>;
/** Restore an archived session to the active directory. Idempotent. */
unarchiveSession(sessionId: string): Promise<boolean>;
// MCP
loadMcpStatus(): Promise<DaemonWorkspaceMcpStatus>;