diff --git a/packages/client/connection/src/client/fixture.ts b/packages/client/connection/src/client/fixture.ts index 45445cc9cf..49f0aaf9dc 100644 --- a/packages/client/connection/src/client/fixture.ts +++ b/packages/client/connection/src/client/fixture.ts @@ -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) diff --git a/packages/client/connection/tests/fake-api.ts b/packages/client/connection/tests/fake-api.ts index 0d58800279..d599de1be7 100644 --- a/packages/client/connection/tests/fake-api.ts +++ b/packages/client/connection/tests/fake-api.ts @@ -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 diff --git a/packages/client/locale/tests/language-row.spec.tsx b/packages/client/locale/tests/language-row.spec.tsx index 2fdc5d5f45..223b9761b2 100644 --- a/packages/client/locale/tests/language-row.spec.tsx +++ b/packages/client/locale/tests/language-row.spec.tsx @@ -21,7 +21,7 @@ function emptySessions() { } function emptyWorkspaces() { const store = createSnapshotStore({ - items: [], state: 'idle', phase: 'ready', error: null, + items: [], archivedSessionIds: new Set(), state: 'idle', phase: 'ready', error: null, baselinesReady: true, recentWorkspaceId: undefined, }) return bindSnapshotSelector(store) diff --git a/packages/client/runtime/README.i18n.yaml b/packages/client/runtime/README.i18n.yaml index f42e4f09d0..60fcf831b4 100644 --- a/packages/client/runtime/README.i18n.yaml +++ b/packages/client/runtime/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/runtime/README.md -README.md: 9f2b165f1a98dcecfa3ab82386da9b094cfd2f54 -README.zh.md: 3ed047e65d3bddc14c3b6b84f327bbeebf805d4b +README.md: 4c9122d87bfb0ea2d66478de03c69975b0577ab7 +README.zh.md: 1ee4731964ea75de29da47137933852581fc45b3 diff --git a/packages/client/runtime/README.md b/packages/client/runtime/README.md index 9f2b165f1a..4c9122d87b 100644 --- a/packages/client/runtime/README.md +++ b/packages/client/runtime/README.md @@ -10,6 +10,8 @@ Workspace and Session lists have independent monotone `pending` → `ready` base `WorkspacesService.delete(workspaceId)` removes the registration from the client projection after the successful unary response; the matching `host/workspace-removed` frame is idempotent and synchronizes other tabs. Session state and the current Session selection are independent, so accounted Sessions immediately project under Ungrouped after their Workspace disappears. +`WorkspaceListState.archivedSessionIds` mirrors the Host's registry-global archive set (a `ReadonlySet` replaced only when membership changes). It is full-snapshot state: the `workspace.list` baseline, the `archiveSession` unary echo, and the `host/archived-sessions-changed` frame each install the complete set. `WorkspacesService.archiveSession(sessionId)` archives over the wire and, when the archived session is the current selection, clears it into the New Session view state; grouping surfaces hide members everywhere while the session rows stay in the list store. + SlotsService gives the renderer separate bare observables for `useSessions` and `useWorkspaces`; web-react creates the hooks. Workspace business state does not enter `SessionListState` or an entry store. `SessionsService.search(query, signal)` is a stateless one-shot action over the `session.search` RPC. It returns ranked session/snippet pairs without putting query, loading, or error state into the shared Session list, so each UI owner controls debounce, cancellation, stale-response suppression, and fallback presentation. `searchResultLimit` re-exposes `SESSION_SEARCH_RESULT_LIMIT` — the bound the response schema itself enforces — as injected presentation data, so client plugins do not duplicate it. It is a protocol constant rather than per-connection state, so the connection handle does not carry it. diff --git a/packages/client/runtime/README.zh.md b/packages/client/runtime/README.zh.md index 3ed047e65d..1ee4731964 100644 --- a/packages/client/runtime/README.zh.md +++ b/packages/client/runtime/README.zh.md @@ -10,6 +10,8 @@ Workspace 和 Session 列表各自具有单调的 `pending` → `ready` 基线 `WorkspacesService.delete(workspaceId)` 在一元响应成功后从客户端投影中移除注册记录;对应的 `host/workspace-removed` 帧具有幂等性,并负责同步其他标签页。Session 状态与当前 Session selection 相互独立,因此 Workspace 消失后,其已纳入客户端投影的 Session 会立即投影到 Ungrouped 下。 +`WorkspaceListState.archivedSessionIds` 镜像 Host 的注册表级全局归档集合(一个 `ReadonlySet`,仅在成员变化时才替换)。它是全快照状态:`workspace.list` 基线、`archiveSession` 一元回声和 `host/archived-sessions-changed` 帧各自安装完整集合。`WorkspacesService.archiveSession(sessionId)` 通过 wire 归档;当被归档的会话正是当前 selection 时,将其清空为 New Session 视图状态。各分组视图在所有位置隐藏集合成员,而会话行本身仍留在列表 store 中。 + SlotsService 分别为 renderer 提供 `useSessions` 与 `useWorkspaces` 的裸 observable;web-react 创建钩子。Workspace 业务状态不会进入 `SessionListState` 或配置项 store。 `SessionsService.search(query, signal)` 是基于 `session.search` RPC 的无状态单次操作。它返回经过排序的会话/snippet 对,但不会将查询条件、加载状态或错误状态写入共享 Session 列表,因此每个 UI 所有者都自行负责防抖、取消、抑制陈旧响应和回退呈现。`searchResultLimit` 将 `SESSION_SEARCH_RESULT_LIMIT`——即响应 schema 自身强制执行的上限——作为注入的呈现数据重新公开,使客户端插件无需复制该值。它是协议常量而非逐连接状态,因此连接 handle 不携带它。 diff --git a/packages/client/runtime/src/client/contract/workspaces.ts b/packages/client/runtime/src/client/contract/workspaces.ts index 9238ea5fd0..3e64ef3717 100644 --- a/packages/client/runtime/src/client/contract/workspaces.ts +++ b/packages/client/runtime/src/client/contract/workspaces.ts @@ -76,4 +76,11 @@ export interface IWorkspaces { * @returns the updated Workspace view. */ insertSessionBefore(workspaceId: WorkspaceId, sessionId: SessionId, beforeSessionId?: SessionId): Promise + /** + * Archive a session into the registry-global set (hidden from grouping + * surfaces; session log and accounting slot remain). Archiving the current + * session clears the selection into the New Session view state. + * @param sessionId - session to archive. + */ + archiveSession(sessionId: SessionId): Promise } diff --git a/packages/client/runtime/src/client/workspaces/manager.ts b/packages/client/runtime/src/client/workspaces/manager.ts index ce4198cd01..0eddae8445 100644 --- a/packages/client/runtime/src/client/workspaces/manager.ts +++ b/packages/client/runtime/src/client/workspaces/manager.ts @@ -14,6 +14,8 @@ export type WorkspaceListPhase = 'pending' | 'ready' /** Immutable workspace-list snapshot. */ export interface WorkspaceListSnapshot { items: readonly WorkspaceView[] + /** Registry-global archive set (hidden from grouping surfaces; accounting slots retained). */ + archivedSessionIds: ReadonlySet state: 'idle' | 'loading' | 'error' phase: WorkspaceListPhase error: RpcError | null @@ -28,6 +30,9 @@ export class WorkspaceManager { private items: Workspace[] = [] private itemViewsSource: readonly Workspace[] | null = null private itemViewsCache: readonly WorkspaceView[] = [] + // Full-snapshot state (list response / unary response / changed frame all + // carry the complete set), so deltas never merge — installs replace. + private archivedSessionIds: ReadonlySet = new Set() private state: WorkspaceListSnapshot['state'] = 'idle' private phase: WorkspaceListPhase = 'pending' private error: RpcError | null = null @@ -77,6 +82,7 @@ export class WorkspaceManager { items = items.filter(workspace => !this.removedIds.has(workspace.workspaceId)) for (const delta of frames) items = applyWorkspaceDelta(items, delta) this.installViews(items) + this.installArchived(result.value.archivedSessionIds) this.state = 'idle' this.phase = 'ready' } else { @@ -158,6 +164,18 @@ export class WorkspaceManager { return result } + /** + * Archive one session in the registry-global set, then install the + * returned full set without waiting for the changed frame. + * @param sessionId - session to archive. + * @returns the wire result. + */ + async archiveSession(sessionId: SessionId): Promise> { + const { result } = await this.api.workspace.archiveSession({ sessionId }) + if (result.ok) this.installArchived(result.value.archivedSessionIds) + return result + } + /** * Host-frame entry. Non-workspace frames are ignored so the runtime can * fan one host stream out to both object managers. @@ -166,6 +184,9 @@ export class WorkspaceManager { handleHostEnvelope(envelope: RpcRequest): void { if (envelope.payload.type === 'host/workspace-changed') this.upsert(envelope.payload.workspace) else if (envelope.payload.type === 'host/workspace-removed') this.remove(envelope.payload.workspaceId) + else if (envelope.payload.type === 'host/archived-sessions-changed') { + this.installArchived(envelope.payload.archivedSessionIds) + } } /** Re-pull the baseline after each connection generation. */ @@ -194,12 +215,21 @@ export class WorkspaceManager { private buildSnapshot(): WorkspaceListSnapshot { return { items: this.itemViews(), + archivedSessionIds: this.archivedSessionIds, state: this.state, phase: this.phase, error: this.error, } } + /** Replace the archive set when membership actually changed (set identity backs Object.is short-circuits). */ + private installArchived(archivedSessionIds: readonly SessionId[]): void { + if (archivedSessionIds.length === this.archivedSessionIds.size + && archivedSessionIds.every(id => this.archivedSessionIds.has(id))) return + this.archivedSessionIds = new Set(archivedSessionIds) + this.notifier.markDirty() + } + /** Upsert one Host view, optionally retaining the local object that materialized it. */ private upsert(view: WorkspaceView, identity?: Workspace): void { if (this.removedIds.has(view.workspaceId)) return diff --git a/packages/client/runtime/src/client/workspaces/service.ts b/packages/client/runtime/src/client/workspaces/service.ts index 1dd3319e79..f3125e6494 100644 --- a/packages/client/runtime/src/client/workspaces/service.ts +++ b/packages/client/runtime/src/client/workspaces/service.ts @@ -14,6 +14,12 @@ import { WorkspaceManager, type WorkspaceListPhase } from './manager.ts' /** Workspace list plus the two-baseline readiness and default-target projection. */ export interface WorkspaceListState { items: readonly WorkspaceView[] + /** + * Registry-global archive set: grouping surfaces hide these sessions + * everywhere (workspace groups and the ungrouped bucket) while their + * session logs and workspace accounting slots remain. + */ + archivedSessionIds: ReadonlySet state: 'idle' | 'loading' | 'error' phase: WorkspaceListPhase error: RpcError | null @@ -58,7 +64,7 @@ export class WorkspacesService implements IWorkspaces { constructor(ctx: Context, private readonly api: IApiClient, private readonly sessions: SessionsPort) { this.manager = new WorkspaceManager(api) this.list = createSnapshotStore({ - items: [], state: 'idle', phase: 'pending', error: null, + items: [], archivedSessionIds: new Set(), state: 'idle', phase: 'pending', error: null, baselinesReady: false, recentWorkspaceId: undefined, }) this.manager.subscribe(() => { this.project() }) @@ -249,6 +255,18 @@ export class WorkspacesService implements IWorkspaces { if (!result.ok) throw new Error(`workspace delete failed: ${result.error.code}: ${result.error.message}`) } + /** + * Archive a session into the registry-global set. When the archived + * session is the current one, the selection is cleared into the New + * Session view state — a hidden row must not stay open behind the list. + * @param sessionId - session to archive. + */ + async archiveSession(sessionId: SessionId): Promise { + const result = await this.manager.archiveSession(sessionId) + if (!result.ok) throw new Error(`session archive failed: ${result.error.code}: ${result.error.message}`) + if (this.sessions.list.getSnapshot().current === sessionId) this.sessions.clear() + } + /** * Move a session within its Workspace's manual order (DOM-insertBefore-like). * @param workspaceId - owning workspace. @@ -293,6 +311,7 @@ export class WorkspacesService implements IWorkspaces { const baselinesReady = workspace.phase === 'ready' && sessions.phase === 'ready' this.list.set({ items: workspace.items, + archivedSessionIds: workspace.archivedSessionIds, state: workspace.state, phase: workspace.phase, error: workspace.error, diff --git a/packages/client/runtime/tests/fake-api.ts b/packages/client/runtime/tests/fake-api.ts index 06d948ae83..9cf7971049 100644 --- a/packages/client/runtime/tests/fake-api.ts +++ b/packages/client/runtime/tests/fake-api.ts @@ -140,7 +140,10 @@ export class FakeApiClient implements IApiClient { openPath: (payload: unknown) => this.record('host.openPath', payload, this.onOpenPath(payload)), } - onWorkspaceList: (payload: unknown) => Promise> = () => Promise.resolve(ok({ items: [] })) + // The archive-set field defaults at the binding below so list stubs keep + // the pre-archive `{ items }` shape; a stub carrying the field wins. + onWorkspaceList: (payload: unknown) => Promise> = + () => Promise.resolve(ok({ items: [] })) onWorkspaceCreate: (payload: unknown) => Promise> = () => Promise.resolve(ok({ workspace: fakeWorkspace('fk-ws'), created: true })) @@ -153,13 +156,22 @@ export class FakeApiClient implements IApiClient { onWorkspaceInsertSessionBefore: (payload: unknown) => Promise> = () => Promise.resolve(ok({ workspace: fakeWorkspace('fk-ws') })) + onWorkspaceArchiveSession: (payload: unknown) => Promise> = + payload => Promise.resolve(ok({ archivedSessionIds: [(payload as { sessionId: SessionId }).sessionId] })) + readonly workspace: IApiClient['workspace'] = { - list: (payload: unknown) => this.record('workspace.list', payload, this.onWorkspaceList(payload)), + list: (payload: unknown) => this.record('workspace.list', payload, this.onWorkspaceList(payload).then(response => ( + response.result.ok + ? { ...response, result: { ok: true as const, value: { archivedSessionIds: [] as never[], ...response.result.value } } } + : response + )) as ReturnType), create: (payload: unknown) => this.record('workspace.create', payload, this.onWorkspaceCreate(payload)), rename: (payload: unknown) => this.record('workspace.rename', payload, this.onWorkspaceRename(payload)), delete: (payload: unknown) => this.record('workspace.delete', payload, this.onWorkspaceDelete(payload)), insertSessionBefore: (payload: unknown) => this.record('workspace.insertSessionBefore', payload, this.onWorkspaceInsertSessionBefore(payload)), + archiveSession: (payload: unknown) => + this.record('workspace.archiveSession', payload, this.onWorkspaceArchiveSession(payload)), } // Payloads stay `unknown` (lint-lane note above); response rows are the real diff --git a/packages/client/runtime/tests/workspaces-service.spec.ts b/packages/client/runtime/tests/workspaces-service.spec.ts index 7fd6934827..2bd462e5ee 100644 --- a/packages/client/runtime/tests/workspaces-service.spec.ts +++ b/packages/client/runtime/tests/workspaces-service.spec.ts @@ -285,6 +285,52 @@ describe('WorkspacesService', () => { })) await expect(workspaces.delete(wid('ghost'))).rejects.toThrow(/workspace-not-found: gone/) }) + + it('archives a session, projects the set from the response, list, and frame, and clears only the current one', async () => { + const ctx = new Context() + const api = new FakeApiClient() + const sessions = new SessionsService(ctx, api) + const workspaces = new WorkspacesService(ctx, api, sessions) + api.onList = () => Promise.resolve(ok({ + items: [ + { sessionId: sid('s-open'), updatedAt: 2, running: false, blank: false }, + { sessionId: sid('s-idle'), updatedAt: 1, running: false, blank: false }, + ], + }) as never) + await sessions.refresh() + sessions.open(sid('s-open')) + + // Archiving a non-current session installs the unary echo and keeps the selection. + await expect(workspaces.archiveSession(sid('s-idle'))).resolves.toBeUndefined() + expect(api.callsOf('workspace.archiveSession')).toEqual([{ sessionId: 's-idle' }]) + expect([...workspaces.list.getSnapshot().archivedSessionIds]).toEqual(['s-idle']) + expect(sessions.list.getSnapshot().current).toBe('s-open') + + // Archiving the current session clears it into the New Session view state. + api.onWorkspaceArchiveSession = () => Promise.resolve(ok({ archivedSessionIds: [sid('s-idle'), sid('s-open')] })) + await workspaces.archiveSession(sid('s-open')) + expect([...workspaces.list.getSnapshot().archivedSessionIds]).toEqual(['s-idle', 's-open']) + expect(sessions.list.getSnapshot().current).toBeUndefined() + + // A Host failure leaves the set and the selection untouched. + api.onWorkspaceArchiveSession = () => Promise.resolve(err({ + code: 'session-not-found', message: 'no session ghost', details: { sessionId: sid('ghost') }, + })) + await expect(workspaces.archiveSession(sid('ghost'))).rejects.toThrow(/session-not-found/) + expect([...workspaces.list.getSnapshot().archivedSessionIds]).toEqual(['s-idle', 's-open']) + + // The changed frame and the list baseline both re-install the full set. + workspaces.handleHostEnvelope({ + rpcId: 'frame' as never, + payload: { type: 'host/archived-sessions-changed', archivedSessionIds: [sid('s-idle')] }, + } as never) + // Frame installs ride the notifier's microtask batch before projecting. + await new Promise(resolve => setTimeout(resolve, 0)) + expect([...workspaces.list.getSnapshot().archivedSessionIds]).toEqual(['s-idle']) + api.onWorkspaceList = () => Promise.resolve(ok({ items: [], archivedSessionIds: [sid('s-open')] }) as never) + await workspaces.refresh() + expect([...workspaces.list.getSnapshot().archivedSessionIds]).toEqual(['s-open']) + }) }) describe('startInitialSelection', () => { diff --git a/packages/client/test-runtime/src/fixtures.ts b/packages/client/test-runtime/src/fixtures.ts index 4219d233e2..d81adf7698 100644 --- a/packages/client/test-runtime/src/fixtures.ts +++ b/packages/client/test-runtime/src/fixtures.ts @@ -73,6 +73,7 @@ export function conversationSnapshot(sessionId: SessionId): ConversationSnapshot export function workspaceListState(): WorkspaceListState { return { items: [], + archivedSessionIds: new Set(), state: 'idle', phase: 'ready', error: null, diff --git a/packages/client/test-runtime/src/workspaces.ts b/packages/client/test-runtime/src/workspaces.ts index 6c1a9d0aad..fab209c96c 100644 --- a/packages/client/test-runtime/src/workspaces.ts +++ b/packages/client/test-runtime/src/workspaces.ts @@ -186,4 +186,21 @@ export class TestWorkspaces implements IWorkspaces { if (stub !== undefined) return await (stub(workspaceId, sessionId, beforeSessionId) as Promise) return { workspaceId, title: '', path: '', sessionIds: [sessionId] } as unknown as WorkspaceView } + + /** + * Archive a session (recorded). The default mirrors the production face's + * observable effect: the id joins the list state's archive set. + * @param sessionId - session to archive. + */ + async archiveSession(sessionId: SessionId): Promise { + this.calls.push({ method: 'archiveSession', args: [sessionId] }) + const stub = this.stubs.get('archiveSession') + if (stub !== undefined) { + await (stub(sessionId) as Promise) + return + } + await this.update((draft) => { + draft.archivedSessionIds = new Set([...draft.archivedSessionIds, sessionId]) + }) + } } diff --git a/packages/client/ui-conversation/tests/chat-code-subcalls.spec.tsx b/packages/client/ui-conversation/tests/chat-code-subcalls.spec.tsx index 49eeec29fd..16d070b04d 100644 --- a/packages/client/ui-conversation/tests/chat-code-subcalls.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-code-subcalls.spec.tsx @@ -128,7 +128,7 @@ async function bench(snapshot: ConversationSnapshot) { ctx.provide('sessions', sessionsFake) const workspaces = { list: createSnapshotStore({ - items: [], state: 'idle', phase: 'ready', error: null, + items: [], archivedSessionIds: new Set(), state: 'idle', phase: 'ready', error: null, baselinesReady: true, recentWorkspaceId: undefined, }), startSession: vi.fn(), diff --git a/packages/client/ui-conversation/tests/chat-view.spec.tsx b/packages/client/ui-conversation/tests/chat-view.spec.tsx index bfaab93375..b59335fa3a 100644 --- a/packages/client/ui-conversation/tests/chat-view.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-view.spec.tsx @@ -94,7 +94,7 @@ function emptySessions() { function emptyWorkspaces() { const store = createSnapshotStore({ - items: [], state: 'idle', phase: 'ready', error: null, + items: [], archivedSessionIds: new Set(), state: 'idle', phase: 'ready', error: null, baselinesReady: true, recentWorkspaceId: undefined, }) return bindSnapshotSelector(store) diff --git a/packages/client/ui-conversation/tests/gate-branch-tails.spec.tsx b/packages/client/ui-conversation/tests/gate-branch-tails.spec.tsx index 354b94ecdd..192470abfc 100644 --- a/packages/client/ui-conversation/tests/gate-branch-tails.spec.tsx +++ b/packages/client/ui-conversation/tests/gate-branch-tails.spec.tsx @@ -74,7 +74,7 @@ describe('render branch tails', () => { const emptyList = createSnapshotStore( { ids: [], byId: {}, current: undefined, phase: 'ready' }) const emptyWorkspaces = createSnapshotStore({ - items: [], state: 'idle', phase: 'ready', error: null, + items: [], archivedSessionIds: new Set(), state: 'idle', phase: 'ready', error: null, baselinesReady: true, recentWorkspaceId: undefined, }) const view = render( @@ -111,7 +111,7 @@ describe('render branch tails', () => { const emptyList = createSnapshotStore( { ids: [], byId: {}, current: undefined, phase: 'ready' }) const emptyWorkspaces = createSnapshotStore({ - items: [], state: 'idle', phase: 'ready', error: null, + items: [], archivedSessionIds: new Set(), state: 'idle', phase: 'ready', error: null, baselinesReady: true, recentWorkspaceId: undefined, }) const view = render( diff --git a/packages/client/ui-conversation/tests/input-bar.spec.tsx b/packages/client/ui-conversation/tests/input-bar.spec.tsx index 6831b1a5a0..1974881357 100644 --- a/packages/client/ui-conversation/tests/input-bar.spec.tsx +++ b/packages/client/ui-conversation/tests/input-bar.spec.tsx @@ -96,7 +96,7 @@ function bench(over?: BenchOptions) { ids: [], byId: {}, current: undefined, phase: 'ready', })), useWorkspaces: bindSnapshotSelector(createSnapshotStore({ - items: [], state: 'idle', phase: 'ready', error: null, + items: [], archivedSessionIds: new Set(), state: 'idle', phase: 'ready', error: null, baselinesReady: true, recentWorkspaceId: undefined, })), useProjection: ((key: string, selector?: (v: unknown) => unknown) => diff --git a/packages/client/ui-conversation/tests/input-matrix.spec.tsx b/packages/client/ui-conversation/tests/input-matrix.spec.tsx index f619d380a0..4901ec2f9f 100644 --- a/packages/client/ui-conversation/tests/input-matrix.spec.tsx +++ b/packages/client/ui-conversation/tests/input-matrix.spec.tsx @@ -39,7 +39,7 @@ function mountBar(shell: SessionInputShell, over?: { running?: boolean; disabled ids: [], byId: {}, current: undefined, phase: 'ready', })), useWorkspaces: bindSnapshotSelector(createSnapshotStore({ - items: [], state: 'idle', phase: 'ready', error: null, + items: [], archivedSessionIds: new Set(), state: 'idle', phase: 'ready', error: null, baselinesReady: true, recentWorkspaceId: undefined, })), useProjection: (() => undefined), diff --git a/packages/client/ui-conversation/tests/input-scenarios.spec.tsx b/packages/client/ui-conversation/tests/input-scenarios.spec.tsx index 60b02795fe..167b9c96af 100644 --- a/packages/client/ui-conversation/tests/input-scenarios.spec.tsx +++ b/packages/client/ui-conversation/tests/input-scenarios.spec.tsx @@ -125,7 +125,7 @@ async function scopedBench(register?: (slash: SlashService) => void) { ids: [], byId: {}, current: undefined, phase: 'ready', })), useWorkspaces: bindSnapshotSelector(createSnapshotStore({ - items: [], state: 'idle', phase: 'ready', error: null, + items: [], archivedSessionIds: new Set(), state: 'idle', phase: 'ready', error: null, baselinesReady: true, recentWorkspaceId: undefined, })), useProjection: (() => undefined), diff --git a/packages/client/ui-conversation/tests/skeleton.spec.tsx b/packages/client/ui-conversation/tests/skeleton.spec.tsx index a37074bd07..2669cbb8ab 100644 --- a/packages/client/ui-conversation/tests/skeleton.spec.tsx +++ b/packages/client/ui-conversation/tests/skeleton.spec.tsx @@ -62,7 +62,7 @@ function workspace(id = 'w1'): WorkspaceView { } const workspaceState = (items: readonly WorkspaceView[]): WorkspaceListState => ({ - items, state: 'idle', phase: 'ready', error: null, + items, archivedSessionIds: new Set(), state: 'idle', phase: 'ready', error: null, baselinesReady: true, recentWorkspaceId: undefined, }) diff --git a/packages/client/ui-conversation/tests/terminal-card.spec.tsx b/packages/client/ui-conversation/tests/terminal-card.spec.tsx index 213897e0f5..29f246d603 100644 --- a/packages/client/ui-conversation/tests/terminal-card.spec.tsx +++ b/packages/client/ui-conversation/tests/terminal-card.spec.tsx @@ -429,7 +429,7 @@ describe('DetailsPanel Output section', () => { phase: 'ready', }) const workspaces = createSnapshotStore({ - items: [], state: 'idle', phase: 'ready', error: null, + items: [], archivedSessionIds: new Set(), state: 'idle', phase: 'ready', error: null, baselinesReady: true, recentWorkspaceId: undefined, }) return render( @@ -607,7 +607,7 @@ describe('DetailsPanel Output section', () => { useSessions={bindSnapshotSelector(createSnapshotStore( { ids: [], byId: {}, current: undefined, phase: 'ready' }))} useWorkspaces={bindSnapshotSelector(createSnapshotStore({ - items: [], state: 'idle', phase: 'ready', error: null, + items: [], archivedSessionIds: new Set(), state: 'idle', phase: 'ready', error: null, baselinesReady: true, recentWorkspaceId: undefined, }))} useInput={(() => { throw new Error('unused') })} diff --git a/packages/client/ui-layout/tests/app-frame.spec.tsx b/packages/client/ui-layout/tests/app-frame.spec.tsx index ab8a11706d..d30cd17877 100644 --- a/packages/client/ui-layout/tests/app-frame.spec.tsx +++ b/packages/client/ui-layout/tests/app-frame.spec.tsx @@ -77,7 +77,7 @@ function mountFrame() { return sel(sessionState) }) as never const workspaceState: WorkspaceListState = { - items: [], state: 'idle', phase: 'ready', error: null, + items: [], archivedSessionIds: new Set(), state: 'idle', phase: 'ready', error: null, baselinesReady: baselinesReady.current, recentWorkspaceId: undefined, } const element = () => ( diff --git a/packages/client/ui-theme/tests/appearance-row.spec.tsx b/packages/client/ui-theme/tests/appearance-row.spec.tsx index f21fb26bdd..00cc0e3052 100644 --- a/packages/client/ui-theme/tests/appearance-row.spec.tsx +++ b/packages/client/ui-theme/tests/appearance-row.spec.tsx @@ -27,7 +27,7 @@ function emptySessions() { } function emptyWorkspaces() { const store = createSnapshotStore({ - items: [], state: 'idle', phase: 'ready', error: null, + items: [], archivedSessionIds: new Set(), state: 'idle', phase: 'ready', error: null, baselinesReady: true, recentWorkspaceId: undefined, }) return bindSnapshotSelector(store) diff --git a/packages/client/ui-trajectory/tests/views.spec.tsx b/packages/client/ui-trajectory/tests/views.spec.tsx index 5c1e6c691d..d31b43d3e6 100644 --- a/packages/client/ui-trajectory/tests/views.spec.tsx +++ b/packages/client/ui-trajectory/tests/views.spec.tsx @@ -113,7 +113,7 @@ function emptySessions() { function emptyWorkspaces() { const store = createSnapshotStore({ - items: [], state: 'idle', phase: 'ready', error: null, baselinesReady: true, + items: [], archivedSessionIds: new Set(), state: 'idle', phase: 'ready', error: null, baselinesReady: true, recentWorkspaceId: undefined, }) return bindSnapshotSelector(store)