|
|
@@ -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<SessionId>
|
|
|
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<SessionId> = 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<RpcResult<{ archivedSessionIds: SessionId[] }>> {
|
|
|
+ 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<HostFrame>): 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
|