feat(client-runtime): project the archive set and the archiveSession action

WorkspaceListState gains archivedSessionIds (ReadonlySet, replaced only
on membership change), installed as full snapshots from the list
baseline, the unary echo, and the changed frame. Archiving the current
session clears the selection into the New Session view state. Test
doubles (test-runtime, fake APIs, fixture client) follow the widened
IWorkspaces/IApiClient faces.
This commit is contained in:
imccyu
2026-07-31 02:40:14 +08:00
committed by imccyu
parent 53ceab53b8
commit c764ed7e64
24 changed files with 177 additions and 21 deletions

View File

@@ -969,6 +969,9 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
updatedAt: fixtureEpoch,
}]
let nextWorkspace = 1
// Registry-global archive set mirroring the host: archived sessions keep
// their workspace accounting slot and only grouping surfaces hide them.
const archivedSessionIds: SessionId[] = []
// In-memory browse tree behind the fixture's `browse` picker capability —
// deterministic content mirroring the design mock so assembled Web tests
@@ -1623,7 +1626,10 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
openPath: request => ok(request, { opened: true as const }),
},
workspace: {
list: request => ok(request, { items: workspaces.map(w => ({ ...w })) }),
list: request => ok(request, {
items: workspaces.map(w => ({ ...w })),
archivedSessionIds: [...archivedSessionIds],
}),
create: (request) => {
const { path, name } = request.payload
const target = path ?? `/tmp/fixture-workspaces/${name ?? ''}`
@@ -1709,6 +1715,16 @@ export function createFixtureApi(options: FixtureOptions = {}): ApiProxy {
}
return ok(request, { workspace: { ...workspace } })
},
archiveSession: (request) => {
const missing = requireSession(request)
if (missing !== undefined) return missing
const { sessionId } = request.payload
if (!archivedSessionIds.includes(sessionId)) {
archivedSessionIds.push(sessionId)
emitHost({ type: 'host/archived-sessions-changed', archivedSessionIds: [...archivedSessionIds] })
}
return ok(request, { archivedSessionIds: [...archivedSessionIds] })
},
},
commands: {
// The catalog mirrors one session's effective view (every fixture
@@ -2089,6 +2105,7 @@ export class FixtureApiClient extends AbstractApiClient {
case 'workspace.rename': return this.api.workspace.rename(request)
case 'workspace.delete': return this.api.workspace.delete(request)
case 'workspace.insertSessionBefore': return this.api.workspace.insertSessionBefore(request)
case 'workspace.archiveSession': return this.api.workspace.archiveSession(request)
case 'command.list': return this.api.commands.list(request)
case 'command.execute': return this.api.commands.execute(request, signal)
case 'skill.list': return this.api.skills.list(request)

View File

@@ -122,7 +122,7 @@ export class FakeApiClient implements IApiClient {
}
readonly workspace: IApiClient['workspace'] = {
list: (payload: unknown) => this.record('workspace.list', payload, Promise.resolve(ok({ items: [] }))),
list: (payload: unknown) => this.record('workspace.list', payload, Promise.resolve(ok({ items: [], archivedSessionIds: [] }))),
create: (payload: unknown) => this.record('workspace.create', payload, Promise.resolve(ok({
workspace: { workspaceId: 'fk-ws' as never, path: '/f/ws', title: 'ws', sessionIds: [], createdAt: '0', updatedAt: '0' },
created: true,
@@ -134,6 +134,9 @@ export class FakeApiClient implements IApiClient {
insertSessionBefore: (payload: unknown) => this.record('workspace.insertSessionBefore', payload, Promise.resolve(ok({
workspace: { workspaceId: 'fk-ws' as never, path: '/f/ws', title: 'ws', sessionIds: [], createdAt: '0', updatedAt: '0' },
}))),
archiveSession: (payload: unknown) => this.record('workspace.archiveSession', payload, Promise.resolve(ok({
archivedSessionIds: [(payload as { sessionId: SessionId }).sessionId],
}))),
}
// Payloads stay `unknown` (lint-lane note above); response rows are the real