Files
deepseek-harness/packages/host/apiproxy/src/api/rpc.schema.ts
imccyu ea8b1178cd feat(web): session list one-list, hover card, row menus, rename, manual ordering
Sidebar session list grows the figma 239-10458 feature set and the
workspace/session browsing region moves wholesale into ui-workspace:

- Group-by menu (WorkSpace / In one list): flat mode lists every session
  top-level, strictly newest-first; the choice persists across reloads.
- Session rows get a 500ms hover detail card (title / relative time /
  status line) and a ... menu (Rename / Fork session / Delete session,
  visual-only for now); workspace headers get ... with Rename (wired) and
  Delete workspace (visual-only).
- workspace.rename RPC: trims, rejects duplicate titles on the create
  chain (workspace-name-conflict), no-op on same title; modal dialog with
  client-side duplicate pre-check.
- workspace.insertSessionBefore RPC (DOM-insertBefore semantics, omitted
  anchor appends): HTML5 drag reorder of root sessions inside a workspace
  group; order truth stays host-side, the view refreshes from the
  response/changed frame.
- Activity pinning removed: the session/event touchSession chain is gone;
  workspace accounts are manually owned (new sessions prepend, explicit
  reordering only). Contracts and tests updated, api catalog regenerated.
- ui-sidebar reduced to the column shell (brand, fold state machine, New
  Session, Settings) exposing one sidebar.workspaces hole with a two-fact
  owner share {wide, expandSidebar}; ui-workspace owns the whole region
  (header, search, grouped/flat lists, dialogs, drag) plus the picker via
  a shared WorkspaceCreateFlow. The old sidebar.workspace picker slot and
  its deferral indirection are gone.
- ui-primitives: Menu gains label entries, danger rows, and
  closeOnPointerLeave; new HoverCard (portaled, open-delay, disabled
  guard). Hover card and row menu never coexist.
2026-07-26 00:55:17 +08:00

105 lines
5.4 KiB
TypeScript

/**
* Message-layer zod schemas: the four wire full forms + error body +
* carrier receipt. The payload slot is unknown in the full-form schemas — business payloads
* get a second parse dispatched by method (two-level parse discipline).
* Brand cast point: rpcIdSchema, and only there.
*/
import { z } from 'zod'
import type { z as zCore } from 'zod'
type ZodIssue = zCore.core.$ZodIssue
import type { ClientRequest, ClientResponse, RpcError, RpcId, RpcReceipt, ServerRequest, ServerResponse } from './rpc.ts'
/**
* Wire widening of a contract type: widens every property (deeply) to `original | undefined`.
* The repo enables exactOptionalPropertyTypes while zod `.optional()` outputs `T | undefined`,
* so `satisfies z.ZodType<ContractType>` is unusable across the board; anchoring is always
* written `satisfies z.ZodType<Wire<ContractType>>` — the widening only adds undefined, so
* missing fields / wrong types still fail to compile. On the JSON wire, "absent" and
* "value undefined" serialize identically, so the widening loses no validation semantics.
*/
export type Wire<T> = T extends readonly (infer E)[] ? Wire<E>[]
: T extends object ? { [K in keyof T]: Wire<T[K]> | undefined }
: T
/**
* RpcId: one brand cast after shape validation (the only cast point in this
* file). No min-length: the id is an opaque echo token, and rejecting shapes
* here would only turn a correlatable error report into a client-side parse
* failure (the handler substitutes a sentinel when a request's id is unreadable).
*/
export const rpcIdSchema = z.string() as unknown as z.ZodType<RpcId>
/** Error body: discriminated by code, per-branch details aligned to RpcErrorDetailsMap; details is required. */
export const rpcErrorSchema: z.ZodType<RpcError> = z.discriminatedUnion('code', [
z.object({ code: z.literal('bad-request'), message: z.string(), details: z.object({ issues: z.array(z.custom<ZodIssue>()) }) }),
z.object({ code: z.literal('cancelled'), message: z.string(), details: z.object({}) }),
z.object({ code: z.literal('session-not-found'), message: z.string(), details: z.object({ sessionId: z.string() }) }),
z.object({ code: z.literal('session-conflict'), message: z.string(), details: z.object({ sessionId: z.string(), requestedCwd: z.string(), existingCwd: z.string().optional() }) }),
z.object({ code: z.literal('workspace-attach-failed'), message: z.string(), details: z.object({ sessionId: z.string(), workspaceId: z.string() }) }),
z.object({ code: z.literal('workspace-not-found'), message: z.string(), details: z.object({ workspaceId: z.string() }) }),
z.object({ code: z.literal('workspace-invalid-path'), message: z.string(), details: z.object({ path: z.string() }) }),
z.object({ code: z.literal('workspace-name-conflict'), message: z.string(), details: z.object({ name: z.string() }) }),
z.object({ code: z.literal('workspace-move-invalid'), message: z.string(), details: z.object({ workspaceId: z.string(), sessionId: z.string(), beforeSessionId: z.string().optional() }) }),
z.object({ code: z.literal('agent-busy'), message: z.string(), details: z.object({ reason: z.string() }) }),
z.object({ code: z.literal('internal'), message: z.string(), details: z.object({}) }),
]) as unknown as z.ZodType<RpcError>
/**
* Business success/failure result schema (generic, reusable).
* @param value - Schema for the business value.
* @returns Schema for RpcResult<T>.
*/
export function rpcResultSchema<T>(value: z.ZodType<T>): z.ZodUnion<readonly [z.ZodType, z.ZodType]> {
return z.union([
z.object({ ok: z.literal(true), value }),
z.object({ ok: z.literal(false), error: rpcErrorSchema }),
])
}
// ---- The four wire full-form schemas (payload/result.value slots stay wide — business layer does the second parse) ----
/** ClientRequest full form (payload stays wide — the business layer runs the second parse). */
export const clientRequestSchema = z.object({
type: z.literal('client-request'),
rpcId: rpcIdSchema,
method: z.string(),
payload: z.unknown(),
}) as unknown as z.ZodType<ClientRequest>
/** ServerResponse full form (result.value stays wide). */
export const serverResponseSchema = z.object({
type: z.literal('server-response'),
rpcId: rpcIdSchema,
result: rpcResultSchema(z.unknown()),
}) as unknown as z.ZodType<ServerResponse>
/** ServerRequest full form (payload stays wide). */
export const serverRequestSchema = z.object({
type: z.literal('server-request'),
rpcId: rpcIdSchema,
method: z.string(),
payload: z.unknown(),
}) as unknown as z.ZodType<ServerRequest>
/** ClientResponse full form (result.value stays wide). */
export const clientResponseSchema = z.object({
type: z.literal('client-response'),
rpcId: rpcIdSchema,
result: rpcResultSchema(z.unknown()),
}) as unknown as z.ZodType<ClientResponse>
/** Wire full-form union (discriminated by type). */
export const rpcMessageSchema = z.discriminatedUnion('type', [
clientRequestSchema as unknown as z.ZodObject<z.ZodRawShape>,
serverResponseSchema as unknown as z.ZodObject<z.ZodRawShape>,
serverRequestSchema as unknown as z.ZodObject<z.ZodRawShape>,
clientResponseSchema as unknown as z.ZodObject<z.ZodRawShape>,
])
/** Carrier receipt schema. */
export const rpcReceiptSchema = z.union([
z.object({ accepted: z.literal(true) }),
z.object({ accepted: z.literal(false), reason: z.union([z.literal('not-pending'), z.literal('bad-response')]) }),
]) satisfies z.ZodType<Wire<RpcReceipt>>