Files
deepseek-harness/packages/api/workspace-controller/src/index.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

135 lines
4.6 KiB
TypeScript

/** Host Workspace Remote owner: explicit commands and reconnect-safe state. */
import { Context } from '@deepseek-ai/cordis'
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
import { WorkspaceCommands } from './commands.ts'
import { DirectoryPickerController } from './directory-picker.ts'
import { WorkspaceFeed } from './feed.ts'
import type {
WorkspaceArchiveSessionRequest,
WorkspaceArchiveValue,
WorkspaceCreateRequest,
WorkspaceCreateValue,
WorkspaceDeleteRequest,
WorkspaceDeleteValue,
WorkspaceFollowFrame,
WorkspaceInsertBeforeRequest,
WorkspaceInsertSessionBeforeRequest,
WorkspaceOrderValue,
WorkspaceRenameRequest,
WorkspaceUnarchiveSessionRequest,
WorkspaceValue,
} from './types.ts'
export type * from './types.ts'
export { DirectoryPickerController } from './directory-picker.ts'
declare module '@deepseek-ai/cordis' {
interface Context {
/** Host Workspace business API and Remote namespace owner. */
workspaceController: WorkspaceController
}
}
/** Host service backing the generated `ctx.remote.workspace` namespace. */
export class WorkspaceController extends TypertRemoteService {
static inject = ['typert', 'workspaceRegistry']
private readonly commands: WorkspaceCommands
private readonly feed: WorkspaceFeed
/** @param ctx - Host context containing the Workspace registry. */
constructor(ctx: Context) {
super(ctx, 'workspaceController', { namespace: 'workspace' })
this.commands = new WorkspaceCommands(ctx)
this.feed = new WorkspaceFeed(ctx)
// This package is the Loader entry for both Remote owners it hosts: the
// directory-picking seam is abstract and never an entry itself. The child
// stays pending until a picking backend is composed, so a host without one
// registers no picking namespace instead of answering an unservable verb.
ctx.plugin(DirectoryPickerController)
}
/**
* Create or idempotently resolve one Workspace over an existing directory.
* @param request - directory path to register.
* @returns the Workspace and whether this call created it.
*/
@Remote('create')
create(request: WorkspaceCreateRequest): Promise<WorkspaceCreateValue> {
return this.commands.create(request)
}
/**
* Rename one Workspace to a unique non-blank title.
* @param request - Workspace identity and proposed title.
* @returns the updated Workspace projection.
*/
@Remote('rename')
rename(request: WorkspaceRenameRequest): Promise<WorkspaceValue> {
return this.commands.rename(request)
}
/**
* Remove one Workspace registration while retaining files and Sessions.
* @param request - Workspace identity to remove.
* @returns deletion confirmation.
*/
@Remote('delete')
delete(request: WorkspaceDeleteRequest): Promise<WorkspaceDeleteValue> {
return this.commands.delete(request)
}
/**
* Move one Workspace within the registry display order.
* @param request - moved Workspace and optional anchor.
* @returns the complete resulting Workspace order.
*/
@Remote('insertBefore')
insertBefore(request: WorkspaceInsertBeforeRequest): Promise<WorkspaceOrderValue> {
return this.commands.insertBefore(request)
}
/**
* Move one accounted Session within a Workspace.
* @param request - Workspace, Session, and optional anchor identities.
* @returns the updated Workspace projection.
*/
@Remote('insertSessionBefore')
insertSessionBefore(request: WorkspaceInsertSessionBeforeRequest): Promise<WorkspaceValue> {
return this.commands.insertSessionBefore(request)
}
/**
* Hide one known Session from Workspace grouping surfaces.
* @param request - Session identity to archive.
* @returns the complete resulting archive set.
*/
@Remote('archiveSession')
archiveSession(request: WorkspaceArchiveSessionRequest): Promise<WorkspaceArchiveValue> {
return this.commands.archiveSession(request)
}
/**
* Restore one archived Session to Workspace grouping surfaces.
* @param request - Session identity to unarchive.
* @returns the complete resulting archive set.
*/
@Remote('unarchiveSession')
unarchiveSession(request: WorkspaceUnarchiveSessionRequest): Promise<WorkspaceArchiveValue> {
return this.commands.unarchiveSession(request)
}
/**
* Stream a complete Workspace baseline followed by ordered increments.
* @param signal - generation cancellation.
* @returns baseline followed by ordered Workspace increments.
*/
@Remote({ mode: 'stream' })
follow(signal: AbortSignal): AsyncIterable<WorkspaceFollowFrame> {
return this.feed.follow(signal)
}
}
export default WorkspaceController