feat(workspace): support persistent workspace ordering
This commit is contained in:
@@ -25,7 +25,7 @@ import { isUserInvocable } from '@deepseek-ai/dsh-skill'
|
||||
import type { Workspace, WorkspaceRecord } from '@deepseek-ai/dsh-workspace'
|
||||
import {
|
||||
workspaceDomainState, workspaceRecord, WorkspaceId as brandWorkspaceId,
|
||||
WorkspaceMoveInvalidError, WorkspaceUnknownSessionError,
|
||||
WorkspaceMoveInvalidError, WorkspaceOrderInvalidError, WorkspaceUnknownSessionError,
|
||||
} from '@deepseek-ai/dsh-workspace'
|
||||
// Type-only: brings the `ctx.tools` Context merge into this program (viewFor reads presenters).
|
||||
import {
|
||||
@@ -2671,6 +2671,20 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
return ok(request, { deleted: true as const })
|
||||
},
|
||||
|
||||
async insertBefore(request) {
|
||||
const { workspaceId, beforeWorkspaceId } = request.payload
|
||||
try {
|
||||
const workspaceIds = await ctx.workspace.insertBefore(
|
||||
brandWorkspaceId(workspaceId),
|
||||
beforeWorkspaceId === undefined ? undefined : brandWorkspaceId(beforeWorkspaceId),
|
||||
)
|
||||
return ok(request, { workspaceIds: [...workspaceIds] })
|
||||
} catch (error: unknown) {
|
||||
if (!(error instanceof WorkspaceOrderInvalidError)) throw error
|
||||
return workspaceNotFound(request, error.workspaceId)
|
||||
}
|
||||
},
|
||||
|
||||
async insertSessionBefore(request) {
|
||||
const { payload } = request
|
||||
const workspace = ctx.workspace.get(brandWorkspaceId(payload.workspaceId))
|
||||
@@ -3370,6 +3384,7 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
const committedWorkspaceIds = new Set(
|
||||
ctx.workspace.list().map(workspace => String(workspace.id)),
|
||||
)
|
||||
let committedWorkspaceOrder = ctx.workspace.list().map(workspace => workspaceView(workspace).workspaceId)
|
||||
// Frame-dedup baseline, same posture as committedWorkspaceIds: the
|
||||
// stream opens against the current set; workspace.list re-baselines
|
||||
// reconnecting clients, so only later changes need frames.
|
||||
@@ -3401,6 +3416,9 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
if (change.table === '') {
|
||||
if (change.operation !== 'put') return
|
||||
const state = workspaceDomainState.parse(change.value)
|
||||
const orderChanged = state.workspaceIds.length === committedWorkspaceOrder.length
|
||||
&& state.workspaceIds.every(workspaceId => committedWorkspaceIds.has(String(workspaceId)))
|
||||
&& state.workspaceIds.some((workspaceId, index) => workspaceId !== committedWorkspaceOrder[index])
|
||||
for (const workspaceId of state.workspaceIds) {
|
||||
if (committedWorkspaceIds.has(workspaceId)) continue
|
||||
const workspace = ctx.workspace.get(workspaceId)
|
||||
@@ -3410,6 +3428,13 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
committedWorkspaceIds.add(workspaceId)
|
||||
queue.push(frame({ type: 'host/workspace-changed', workspace: workspaceView(workspace) }))
|
||||
}
|
||||
committedWorkspaceOrder = [...state.workspaceIds]
|
||||
if (orderChanged) {
|
||||
queue.push(frame({
|
||||
type: 'host/workspace-order-changed',
|
||||
workspaceIds: [...state.workspaceIds],
|
||||
}))
|
||||
}
|
||||
if (state.archivedSessionIds.length !== archivedSessionIds.length
|
||||
|| state.archivedSessionIds.some((id, index) => id !== archivedSessionIds[index])) {
|
||||
archivedSessionIds = state.archivedSessionIds
|
||||
|
||||
@@ -83,6 +83,7 @@ export const hostFrameSchema = z.discriminatedUnion('type', [
|
||||
z.object({ type: z.literal('host/agent-error'), sessionId: sessionIdSchema, message: z.string() }),
|
||||
z.object({ type: z.literal('host/workspace-changed'), workspace: workspaceViewSchema }),
|
||||
z.object({ type: z.literal('host/workspace-removed'), workspaceId: workspaceIdSchema }),
|
||||
z.object({ type: z.literal('host/workspace-order-changed'), workspaceIds: z.array(workspaceIdSchema) }),
|
||||
z.object({ type: z.literal('host/archived-sessions-changed'), archivedSessionIds: z.array(sessionIdSchema) }),
|
||||
z.object({ type: z.literal('host/commands-changed') }),
|
||||
z.object({ type: z.literal('host/session-preset-changed'), sessionId: sessionIdSchema, agentPreset: z.string() }),
|
||||
|
||||
@@ -119,7 +119,8 @@ export type MuxFrame =
|
||||
* workspace mutation (create/attach/order change — the client upserts, while
|
||||
* `workspace.list` provides the reconnect baseline); workspace-removed is the
|
||||
* committed registration-deletion increment and never implies directory or
|
||||
* session-log deletion; archived-sessions-changed pushes the full registry
|
||||
* session-log deletion; workspace-order-changed pushes the complete durable
|
||||
* registry order after a reorder; archived-sessions-changed pushes the full registry
|
||||
* archive set after every durable change (same full-snapshot posture as
|
||||
* workspace-changed — `workspace.list` re-baselines it on reconnect).
|
||||
*/
|
||||
@@ -139,6 +140,7 @@ export type HostFrame =
|
||||
| { type: 'host/agent-error'; sessionId: SessionId; message: string }
|
||||
| { type: 'host/workspace-changed'; workspace: WorkspaceView }
|
||||
| { type: 'host/workspace-removed'; workspaceId: WorkspaceView['workspaceId'] }
|
||||
| { type: 'host/workspace-order-changed'; workspaceIds: WorkspaceView['workspaceId'][] }
|
||||
| { type: 'host/archived-sessions-changed'; archivedSessionIds: SessionId[] }
|
||||
/**
|
||||
* The command registry changed (`commands/change` passthrough). Pure
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface RpcMethodMap {
|
||||
'workspace.create': WorkspaceApi['create']
|
||||
'workspace.rename': WorkspaceApi['rename']
|
||||
'workspace.delete': WorkspaceApi['delete']
|
||||
'workspace.insertBefore': WorkspaceApi['insertBefore']
|
||||
'workspace.insertSessionBefore': WorkspaceApi['insertSessionBefore']
|
||||
'workspace.archiveSession': WorkspaceApi['archiveSession']
|
||||
'command.list': CommandsApi['list']
|
||||
|
||||
@@ -66,6 +66,17 @@ export const workspaceDeleteValueSchema = z.object({
|
||||
deleted: z.literal(true),
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'workspace.delete'>>>
|
||||
|
||||
/** workspace.insertBefore request payload (anchor omitted = append to end). */
|
||||
export const workspaceInsertBeforeRequestSchema = z.object({
|
||||
workspaceId: workspaceIdSchema,
|
||||
beforeWorkspaceId: workspaceIdSchema.optional(),
|
||||
}) satisfies z.ZodType<Wire<RequestPayload<'workspace.insertBefore'>>>
|
||||
|
||||
/** workspace.insertBefore response value: the complete durable display order. */
|
||||
export const workspaceInsertBeforeValueSchema = z.object({
|
||||
workspaceIds: z.array(workspaceIdSchema),
|
||||
}) satisfies z.ZodType<Wire<ResponseValue<'workspace.insertBefore'>>>
|
||||
|
||||
/** workspace.insertSessionBefore request payload (anchor omitted = append to end). */
|
||||
export const workspaceInsertSessionBeforeRequestSchema = z.object({
|
||||
workspaceId: workspaceIdSchema,
|
||||
|
||||
@@ -73,6 +73,15 @@ export interface WorkspaceApi {
|
||||
delete(request: RpcRequest<{ workspaceId: WorkspaceId }>):
|
||||
Promise<RpcResponse<{ deleted: true }>>
|
||||
|
||||
/**
|
||||
* Moves one Workspace within the registry display order,
|
||||
* DOM-insertBefore-like. An omitted anchor appends to the end.
|
||||
*/
|
||||
insertBefore(request: RpcRequest<{
|
||||
workspaceId: WorkspaceId
|
||||
beforeWorkspaceId?: WorkspaceId
|
||||
}>): Promise<RpcResponse<{ workspaceIds: WorkspaceId[] }>>
|
||||
|
||||
/**
|
||||
* Moves an accounted session within its workspace's manual order,
|
||||
* DOM-insertBefore-like: with `beforeSessionId` the session is inserted
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
workspaceArchiveSessionValueSchema,
|
||||
workspaceCreateValueSchema,
|
||||
workspaceDeleteValueSchema,
|
||||
workspaceInsertBeforeValueSchema,
|
||||
workspaceInsertSessionBeforeValueSchema,
|
||||
workspaceListValueSchema,
|
||||
workspaceRenameValueSchema,
|
||||
@@ -117,6 +118,7 @@ export interface IApiClient {
|
||||
create(payload: RequestPayload<'workspace.create'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'workspace.create'>>>
|
||||
rename(payload: RequestPayload<'workspace.rename'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'workspace.rename'>>>
|
||||
delete(payload: RequestPayload<'workspace.delete'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'workspace.delete'>>>
|
||||
insertBefore(payload: RequestPayload<'workspace.insertBefore'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'workspace.insertBefore'>>>
|
||||
insertSessionBefore(payload: RequestPayload<'workspace.insertSessionBefore'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'workspace.insertSessionBefore'>>>
|
||||
archiveSession(payload: RequestPayload<'workspace.archiveSession'>, signal?: AbortSignal): Promise<RpcResponse<ResponseValue<'workspace.archiveSession'>>>
|
||||
}
|
||||
@@ -198,6 +200,7 @@ const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType<Wire<ResponseV
|
||||
'workspace.create': workspaceCreateValueSchema,
|
||||
'workspace.rename': workspaceRenameValueSchema,
|
||||
'workspace.delete': workspaceDeleteValueSchema,
|
||||
'workspace.insertBefore': workspaceInsertBeforeValueSchema,
|
||||
'workspace.insertSessionBefore': workspaceInsertSessionBeforeValueSchema,
|
||||
'workspace.archiveSession': workspaceArchiveSessionValueSchema,
|
||||
'command.list': commandListValueSchema,
|
||||
@@ -452,6 +455,7 @@ export abstract class AbstractApiClient implements IApiClient {
|
||||
create: (payload, signal) => this.callUnary('workspace.create', payload, signal),
|
||||
rename: (payload, signal) => this.callUnary('workspace.rename', payload, signal),
|
||||
delete: (payload, signal) => this.callUnary('workspace.delete', payload, signal),
|
||||
insertBefore: (payload, signal) => this.callUnary('workspace.insertBefore', payload, signal),
|
||||
insertSessionBefore: (payload, signal) => this.callUnary('workspace.insertSessionBefore', payload, signal),
|
||||
archiveSession: (payload, signal) => this.callUnary('workspace.archiveSession', payload, signal),
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
workspaceArchiveSessionRequestSchema,
|
||||
workspaceCreateRequestSchema,
|
||||
workspaceDeleteRequestSchema,
|
||||
workspaceInsertBeforeRequestSchema,
|
||||
workspaceInsertSessionBeforeRequestSchema,
|
||||
workspaceListRequestSchema,
|
||||
workspaceRenameRequestSchema,
|
||||
@@ -113,6 +114,7 @@ const UNARY_ROUTES: UnaryRoutes = {
|
||||
'workspace.create': { schema: workspaceCreateRequestSchema, invoke: (api, r) => api.workspace.create(r) },
|
||||
'workspace.rename': { schema: workspaceRenameRequestSchema, invoke: (api, r) => api.workspace.rename(r) },
|
||||
'workspace.delete': { schema: workspaceDeleteRequestSchema, invoke: (api, r) => api.workspace.delete(r) },
|
||||
'workspace.insertBefore': { schema: workspaceInsertBeforeRequestSchema, invoke: (api, r) => api.workspace.insertBefore(r) },
|
||||
'workspace.insertSessionBefore': { schema: workspaceInsertSessionBeforeRequestSchema, invoke: (api, r) => api.workspace.insertSessionBefore(r) },
|
||||
'workspace.archiveSession': { schema: workspaceArchiveSessionRequestSchema, invoke: (api, r) => api.workspace.archiveSession(r) },
|
||||
'command.list': { schema: commandListRequestSchema, invoke: (api, r) => api.commands.list(r) },
|
||||
|
||||
Reference in New Issue
Block a user