Files
deepseek-harness/packages/api/workspace-controller/src/client/service.ts
T
Tianyi Cui 5f773a0ded feat(workspace): restore archived sessions from a settings page
Sessions could be archived from the sidebar row menu but never restored:
the registry-global archive set was a one-way display filter with no
inverse RPC and no surface listing its members.

WorkspaceRegistry.unarchiveSession drops one id from archivedSessionIds in
one durable write on the same operation chain as archive, with no
session-existence probe because a removal cannot introduce an unknown
referent. The Workspace controller exposes it as workspace.unarchiveSession
returning the complete archive set, which the existing archived follow
increment already propagates, so no frame type and no persisted field
changes.

The new client package dsh-client-ui-settings-unarchive-sessions owns the
restore surface: one localized settings.section page listing the archive
set joined with loaded session summaries, newest archive first, searchable
by title or workspace, with one Unarchive action per row.
2026-09-13 16:45:45 +08:00

143 lines
5.1 KiB
TypeScript

/** React-free Client Workspace service and command facade. */
import { Service, type Context } from '@deepseek-ai/cordis'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { RemoteFailure } from '@deepseek-ai/dsh-typert-protocol'
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
import type { WorkspaceView } from '../types.ts'
import type { ClientWorkspaceModel, WorkspaceSnapshot } from './model.ts'
/** Structured create failure for callers that distinguish Host business errors. */
export class WorkspaceCreateError extends Error {
override readonly name = 'WorkspaceCreateError'
/** @param rpcError - Host business or folded carrier failure. */
constructor(readonly rpcError: RemoteFailure) {
super(`workspace create failed: ${rpcError.code}: ${rpcError.message}`)
}
}
/** Bare observable source for the Workspace Controller snapshot. */
export interface WorkspaceSource {
/** Read the identity-stable current snapshot. */
getSnapshot(): WorkspaceSnapshot
/**
* Subscribe to snapshot changes.
* @param listener - invalidation callback.
* @returns unsubscribe function.
*/
subscribe(listener: () => void): () => void
}
/** Workspace Controller's Client service face. */
export interface IWorkspaces {
/** Host-authoritative Workspace rows, order, archive set, and follow lifecycle. */
readonly list: WorkspaceSource
/**
* Register an existing path as a Workspace.
* @param input - Host create payload.
* @returns the created or idempotently resolved Workspace.
*/
create(input: { path: string }): Promise<WorkspaceView>
/**
* Rename a Workspace.
* @param workspaceId - target Workspace.
* @param title - new display title.
* @returns the renamed Workspace.
*/
rename(workspaceId: WorkspaceId, title: string): Promise<WorkspaceView>
/**
* Delete a Workspace registration without deleting Sessions or files.
* @param workspaceId - target Workspace.
*/
delete(workspaceId: WorkspaceId): Promise<void>
/**
* Move a Workspace within the Host registry order.
* @param workspaceId - Workspace to move.
* @param beforeWorkspaceId - anchor Workspace; omitted appends.
*/
insertBefore(workspaceId: WorkspaceId, beforeWorkspaceId?: WorkspaceId): Promise<void>
/**
* Archive a Session from Workspace grouping surfaces.
* @param sessionId - Session to archive.
*/
archiveSession(sessionId: SessionId): Promise<void>
/**
* Unarchive a Session from the archived Session list.
* @param sessionId - Session to unarchive.
*/
unarchiveSession(sessionId: SessionId): Promise<void>
/**
* Move a Session within one Workspace account.
* @param workspaceId - owning Workspace.
* @param sessionId - Session to move.
* @param beforeSessionId - anchor Session; omitted appends.
* @returns the changed Workspace.
*/
insertSessionBefore(
workspaceId: WorkspaceId,
sessionId: SessionId,
beforeSessionId?: SessionId,
): Promise<WorkspaceView>
}
/** Owns the bare Workspace snapshot and Workspace-only commands. */
export class WorkspaceController extends Service implements IWorkspaces {
readonly list: WorkspaceSource
/**
* @param ctx - Client root Context.
* @param model - Remote-backed Workspace state model.
*/
constructor(ctx: Context, private readonly model: ClientWorkspaceModel) {
super(ctx, 'workspaces')
this.list = model
}
async create(input: { path: string }): Promise<WorkspaceView> {
const result = await this.model.create(input)
if (!result.ok) throw new WorkspaceCreateError(result.error)
return result.value.workspace
}
async rename(workspaceId: WorkspaceId, title: string): Promise<WorkspaceView> {
const result = await this.model.rename(workspaceId, title)
if (!result.ok) throw commandError('rename', result.error)
return result.value.workspace
}
async delete(workspaceId: WorkspaceId): Promise<void> {
const result = await this.model.delete(workspaceId)
if (!result.ok) throw commandError('delete', result.error)
}
async insertBefore(workspaceId: WorkspaceId, beforeWorkspaceId?: WorkspaceId): Promise<void> {
const result = await this.model.insertBefore(workspaceId, beforeWorkspaceId)
if (!result.ok) throw commandError('reorder', result.error)
}
async archiveSession(sessionId: SessionId): Promise<void> {
const result = await this.model.archiveSession(sessionId)
if (!result.ok) throw commandError('session archive', result.error)
}
async unarchiveSession(sessionId: SessionId): Promise<void> {
const result = await this.model.unarchiveSession(sessionId)
if (!result.ok) throw commandError('session unarchive', result.error)
}
async insertSessionBefore(
workspaceId: WorkspaceId,
sessionId: SessionId,
beforeSessionId?: SessionId,
): Promise<WorkspaceView> {
const result = await this.model.insertSessionBefore(workspaceId, sessionId, beforeSessionId)
if (!result.ok) throw commandError('move', result.error)
return result.value.workspace
}
}
function commandError(operation: string, failure: RemoteFailure): Error {
return new Error(`workspace ${operation} failed: ${failure.code}: ${failure.message}`)
}