Merge remote-tracking branch 'origin/master' into worktree/custom-deepseek-models

# Conflicts:
#	apps/web/tests/snapshots/message-actions/ui.expected.md
#	apps/web/tests/snapshots/seeded-history/command-row.expected.md
#	apps/web/tests/snapshots/seeded-history/ui.expected.md
#	packages/client/ui-model/README.i18n.yaml
#	packages/client/ui-model/README.md
#	packages/client/ui-model/README.zh.md
#	packages/client/ui-model/tests/model-select.spec.tsx
#	packages/host/apiproxy/README.i18n.yaml
This commit is contained in:
Yichen Jiang
2026-08-03 16:20:55 +08:00
1371 changed files with 125172 additions and 10229 deletions

View File

@@ -1,8 +1,8 @@
/**
* commands domain contract: the web catalog/dispatch face of the host command
* registry (`ctx.commands`). Both methods address one session's agent via
* `sessionId` — every served session has an Agent (Session+Agent are born
* together), so there is no agent-less surface on this wire.
* registry (`ctx.commands`). Both methods address an ordinary session's Agent
* via `sessionId`, resuming it when cold. Session-backed subagents reject with
* `agent-busy` and retain their dedicated continuation owner.
*/
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
@@ -27,7 +27,8 @@ export interface CommandDescriptor {
export interface CommandsApi {
/**
* Lists the addressed agent's effective command catalog (name-sorted,
* globals plus its scoped shadows).
* globals plus its scoped shadows). Session-backed subagents reject with
* `agent-busy`.
*/
list(request: RpcRequest<{ sessionId: SessionId }>): Promise<RpcResponse<{ commands: readonly CommandDescriptor[] }>>
@@ -42,6 +43,7 @@ export interface CommandsApi {
* pairing id, letting the issuing client correlate this acknowledgment
* with that flow node. The signal rides beside the request, never on the
* wire: the fetch carrier's request signal cancels the running handler.
* Session-backed subagents reject with `agent-busy` before dispatch.
*/
execute(request: RpcRequest<{ sessionId: SessionId; line: string }>, signal: AbortSignal):
Promise<RpcResponse<{ matched: boolean; commandId?: CommandId }>>

View File

@@ -54,6 +54,7 @@ export const muxFrameSchema = z.discriminatedUnion('type', [
sessionId: sessionIdSchema,
items: z.array(z.object({
id: inboxItemIdSchema,
placement: z.union([z.literal('queued'), z.literal('steering')]),
message: messageSchema,
})),
}),
@@ -65,7 +66,14 @@ export const muxFrameSchema = z.discriminatedUnion('type', [
/** HostFrame union (payload slot of a host-stream ServerRequest). */
export const hostFrameSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('host/session-added'), sessionId: sessionIdSchema, blank: z.boolean(), parentSessionId: sessionIdSchema.optional(), cwd: z.string().optional() }),
z.object({
type: z.literal('host/session-added'),
sessionId: sessionIdSchema,
blank: z.boolean(),
parentSessionId: sessionIdSchema.optional(),
origin: z.literal('subagent').optional(),
cwd: z.string().optional(),
}),
z.object({ type: z.literal('host/session-removed'), sessionId: sessionIdSchema }),
z.object({ type: z.literal('host/session-status'), sessionId: sessionIdSchema, running: z.boolean() }),
z.object({ type: z.literal('host/agent-error'), sessionId: sessionIdSchema, message: z.string() }),

View File

@@ -32,10 +32,12 @@ export type ToolEventView =
| { for: 'call'; view: ToolCallView }
| { for: 'result'; view: ToolResultView }
/** One pending queued occurrence in an authoritative queue snapshot. */
/** One pending inbox occurrence in the authoritative `session/queue` snapshot. */
export interface QueuedInboxItem {
/** Agent-owned occurrence identity used by queue mutations. */
/** Agent-owned occurrence identity; queue mutations address only `queued` items. */
id: InboxItemId
/** Agent-resolved FIFO placement; clients render queued and steering items on different surfaces. */
placement: 'queued' | 'steering'
/** Complete pending message; it is not durable until the Agent claims it. */
message: Message
}
@@ -71,11 +73,12 @@ export type MuxFrame =
| { type: 'question/requested'; sessionId: SessionId; questions: AskUserQuestionItem[] }
| { type: 'question/resolved'; sessionId: SessionId; questionRpcId: RpcId; outcome: 'answered' | 'cancelled' }
/**
* Complete transient queue state after every enqueue, mutation, claim, or
* Complete transient inbox state after every enqueue, mutation, claim, or
* discard. Pending work is not model-visible and therefore has no durable
* session event; the whole snapshot makes edit, deletion, cancel, and
* reconnect converge through one authoritative signal. Pending steering is
* outside this Web queue projection.
* reconnect converge through one authoritative signal. `session/queue`
* covers both resolved placements: queued items render
* in QueueDock, while pending steering renders at the conversation tail.
*/
| { type: 'session/queue'; sessionId: SessionId; items: QueuedInboxItem[] }
/**
@@ -90,9 +93,9 @@ export type MuxFrame =
| { type: 'stream/error'; error: RpcError }
/**
* Host stream frames. session-added carries the lineage anchor, the project
* cwd, and the blank bit (the list-summary fields a client cannot wait for a
* refresh to learn); the frame fires at session/created, so blank is
* Host stream frames. session-added carries the lineage anchor, product
* origin, project cwd, and blank bit (the list-summary fields a client cannot
* wait for a refresh to learn); the frame fires at session/created, so blank is
* constantly true — clients flip it on the session's first
* `host/session-status(running:true)` (a blank session never runs), and a
* reconnecting client takes `session.list`'s summary.blank as authoritative.
@@ -106,7 +109,14 @@ export type MuxFrame =
* workspace-changed — `workspace.list` re-baselines it on reconnect).
*/
export type HostFrame =
| { type: 'host/session-added'; sessionId: SessionId; blank: boolean; parentSessionId?: SessionId; cwd?: string }
| {
type: 'host/session-added'
sessionId: SessionId
blank: boolean
parentSessionId?: SessionId
origin?: 'subagent'
cwd?: string
}
| { type: 'host/session-removed'; sessionId: SessionId }
| { type: 'host/session-status'; sessionId: SessionId; running: boolean }
| { type: 'host/agent-error'; sessionId: SessionId; message: string }

View File

@@ -22,7 +22,11 @@ export interface GoalRef {
readonly revision: number
}
/** Goal-domain unary methods (every mutation resolves the session's agent and applies one CAS-guarded verb). */
/**
* Goal-domain unary methods. Every mutation resolves an ordinary session's
* Agent and applies one CAS-guarded verb; session-backed subagents reject with
* `agent-busy`.
*/
export interface GoalsApi {
/** Create and arm a goal. */
create(request: RpcRequest<{ sessionId: SessionId; objective: string; maxGoalRounds?: number }>):

View File

@@ -9,6 +9,7 @@ import type { HostApi } from './host.ts'
import type { WorkspaceApi } from './workspace.ts'
import type { CommandsApi } from './commands.ts'
import type { SkillsApi } from './skills.ts'
import type { SubagentsApi } from './subagents.ts'
import type { EventsApi } from './events.ts'
import type { GoalsApi } from './goals.ts'
import type { SettingsApi } from './settings.ts'
@@ -19,6 +20,7 @@ import type { ClientResponse, RpcReceipt } from './rpc.ts'
/** Root interface of the unified API surface. New client-request domain = one new file pair + one field here + one map row. */
export interface ApiProxy {
sessions: SessionsApi
subagents: SubagentsApi
host: HostApi
workspace: WorkspaceApi
commands: CommandsApi
@@ -39,6 +41,9 @@ export type {
SessionsApi, SessionSummary,
} from './sessions.ts'
export type { DirectoryEntry, DirectoryListing, HostApi } from './host.ts'
export type {
SubagentAddress, SubagentCatalog, SubagentListEntry, SubagentPromptReceipt, SubagentsApi,
} from './subagents.ts'
export type { WorkspaceApi, WorkspaceId, WorkspaceView } from './workspace.ts'
export type { CommandsApi, CommandDescriptor } from './commands.ts'
export type { SkillsApi, SkillEntry } from './skills.ts'

View File

@@ -13,6 +13,7 @@ import type { GoalsApi } from './goals.ts'
import type { SettingsApi } from './settings.ts'
import type { CredentialsApi } from './credentials.ts'
import type { LlmApi } from './llm.ts'
import type { SubagentsApi } from './subagents.ts'
import type { RpcResponse } from './rpc.ts'
/**
@@ -32,6 +33,9 @@ export interface RpcMethodMap {
'session.prompt': SessionsApi['prompt']
'session.updateQueue': SessionsApi['updateQueue']
'session.cancel': SessionsApi['cancel']
'subagent.list': SubagentsApi['list']
'subagent.history': SubagentsApi['history']
'subagent.prompt': SubagentsApi['prompt']
'host.describe': HostApi['describe']
'host.pickDirectory': HostApi['pickDirectory']
'host.listDirectory': HostApi['listDirectory']

View File

@@ -48,6 +48,7 @@ export const rpcErrorSchema: z.ZodType<RpcError> = z.discriminatedUnion('code',
z.object({ code: z.literal('directory-picker-unavailable'), message: z.string(), details: z.object({ capability: z.string() }) }),
z.object({ code: z.literal('agent-busy'), message: z.string(), details: z.object({ reason: z.string() }) }),
z.object({ code: z.literal('queue-item-not-found'), message: z.string(), details: z.object({ itemId: z.string() }) }),
z.object({ code: z.literal('steer-unavailable'), message: z.string(), details: z.object({ itemId: z.string() }) }),
z.object({ code: z.literal('command-error'), message: z.string(), details: z.object({}) }),
z.object({ code: z.literal('unknown-command'), message: z.string(), details: z.object({}) }),
z.object({ code: z.literal('settings-rejected'), message: z.string(), details: z.object({ ns: z.string() }) }),
@@ -56,6 +57,16 @@ export const rpcErrorSchema: z.ZodType<RpcError> = z.discriminatedUnion('code',
z.object({ code: z.literal('credential-rejected'), message: z.string(), details: z.object({ ref: z.string() }) }),
z.object({ code: z.literal('title-invalid'), message: z.string(), details: z.object({ sessionId: z.string() }) }),
z.object({ code: z.literal('fork-unavailable'), message: z.string(), details: z.object({ sessionId: z.string() }) }),
z.object({ code: z.literal('subagent-parent-unavailable'), message: z.string(), details: z.object({ parentSessionId: z.string() }) }),
z.object({ code: z.literal('subagent-not-found'), message: z.string(), details: z.object({ parentSessionId: z.string(), childSessionId: z.string() }) }),
z.object({ code: z.literal('subagent-catalog-diagnostic'), message: z.string(), details: z.object({
parentSessionId: z.string(),
childSessionId: z.string(),
reason: z.union([z.literal('corrupt'), z.literal('unsupported'), z.literal('unavailable')]),
}) }),
z.object({ code: z.literal('subagent-not-resumable'), message: z.string(), details: z.object({ childSessionId: z.string() }) }),
z.object({ code: z.literal('subagent-unauthorized'), message: z.string(), details: z.object({ childSessionId: z.string() }) }),
z.object({ code: z.literal('subagent-delivery-unavailable'), message: z.string(), details: z.object({ childSessionId: z.string() }) }),
z.object({ code: z.literal('internal'), message: z.string(), details: z.object({}) }),
]) as unknown as z.ZodType<RpcError>

View File

@@ -46,6 +46,7 @@ export interface RpcErrorDetailsMap {
'directory-picker-unavailable': { capability: string }
'agent-busy': { reason: string }
'queue-item-not-found': { itemId: InboxItemId }
'steer-unavailable': { itemId: InboxItemId }
/** A known slash command reported a usage/state error; the message is the command's own text. */
'command-error': {}
/** A leading-/ prompt named no registered command; the message names the token. */
@@ -71,6 +72,16 @@ export interface RpcErrorDetailsMap {
'credential-rejected': { ref: string }
'title-invalid': { sessionId: SessionId }
'fork-unavailable': { sessionId: SessionId }
'subagent-parent-unavailable': { parentSessionId: SessionId }
'subagent-not-found': { parentSessionId: SessionId; childSessionId: SessionId }
'subagent-catalog-diagnostic': {
parentSessionId: SessionId
childSessionId: SessionId
reason: 'corrupt' | 'unsupported' | 'unavailable'
}
'subagent-not-resumable': { childSessionId: SessionId }
'subagent-unauthorized': { childSessionId: SessionId }
'subagent-delivery-unavailable': { childSessionId: SessionId }
'internal': {}
}

View File

@@ -53,6 +53,7 @@ export const sessionSummarySchema = z.object({
running: z.boolean(),
blank: z.boolean(),
parentSessionId: sessionIdSchema.optional(),
origin: z.literal('subagent').optional(),
cwd: z.string().optional(),
projections: z.lazy(() => sessionProjectionsBlockSchema).optional(),
}) as unknown as z.ZodType<Wire<SessionSummary>>
@@ -193,10 +194,10 @@ export const toolEventViewSchema = z.discriminatedUnion('for', [
]) as unknown as z.ZodType<ToolEventView>
/** One session.history item: the session event plus its optional host-computed tool view. */
export const historyEntrySchema = z.object({
export const historyEntrySchema: z.ZodType<Wire<HistoryEntry>> = z.object({
event: sessionEventSchema,
view: toolEventViewSchema.optional(),
}) satisfies z.ZodType<Wire<HistoryEntry>>
}) as unknown as z.ZodType<Wire<HistoryEntry>>
/**
* Projection baseline passthrough: `values` stays a wide record — each value
@@ -210,11 +211,11 @@ export const sessionProjectionsBlockSchema = z.object({
}) as unknown as z.ZodType<SessionProjectionsBlock>
/** session.history response value (projections rides the tail page only). */
export const sessionHistoryValueSchema = z.object({
export const sessionHistoryValueSchema: z.ZodType<Wire<ResponseValue<'session.history'>>> = z.object({
events: z.array(historyEntrySchema),
hasMore: z.boolean(),
projections: sessionProjectionsBlockSchema.optional(),
}) satisfies z.ZodType<Wire<ResponseValue<'session.history'>>>
})
/** session.models request payload. */
export const sessionModelsRequestSchema = z.object({
@@ -267,6 +268,7 @@ export const sessionUpdateQueueRequestSchema = z.object({
action: z.discriminatedUnion('kind', [
z.object({ kind: z.literal('edit'), content: z.array(contentBlockSchema) }),
z.object({ kind: z.literal('remove') }),
z.object({ kind: z.literal('steer') }),
]),
}) as unknown as z.ZodType<RequestPayload<'session.updateQueue'>>

View File

@@ -127,6 +127,7 @@ export interface SessionModels {
export type QueueAction =
| { kind: 'edit'; content: ContentBlock[] }
| { kind: 'remove' }
| { kind: 'steer' }
/** Session list entry (v1 builds no index: list does readdir+stat). */
export interface SessionSummary {
@@ -151,6 +152,8 @@ export interface SessionSummary {
blank: boolean
/** fork/spawn lineage (session.header.parentSession passthrough); absent for root sessions. */
parentSessionId?: SessionId
/** Coarse durable origin used by navigation surfaces; never proves resumability. */
origin?: 'subagent'
/** Session working directory (header.cwd passthrough); absent when unrecorded. */
cwd?: string
/**
@@ -215,17 +218,22 @@ export interface SessionsApi {
* the client needs a fresh baseline already pulls the tail page, and
* loadOlder (the only beforeSeq path) is the only path that never needs one.
* A deployment without the registry serves histories without the block.
* Reading history uses an attached Session or persistence inspection and
* never resumes or publishes an Agent.
*/
history(request: RpcRequest<{ sessionId: SessionId; beforeSeq?: number; maxMessages?: number }>):
Promise<RpcResponse<{ events: HistoryEntry[]; hasMore: boolean; projections?: SessionProjectionsBlock }>>
/** Reads a fresh advisory model directory for this session. Provider lookups run independently. */
/**
* Reads a fresh advisory model directory for an ordinary session. Provider
* lookups run independently; subagents reject with `agent-busy`.
*/
models(request: RpcRequest<{ sessionId: SessionId }>): Promise<RpcResponse<SessionModels>>
/**
* Selects the complete target for this session. Exact model metadata
* validates an optional reasoning effort, while catalog membership remains
* advisory.
* advisory. Session-backed subagents reject with `agent-busy`.
*/
selectModel(request: RpcRequest<{
sessionId: SessionId
@@ -241,6 +249,7 @@ export interface SessionsApi {
* normalized accepted title and the title event's seq return so the caller
* can settle its projection cell without waiting for the push frame. A
* title that normalizes to empty fails with `title-invalid`.
* Session-backed subagents reject with `agent-busy`.
*/
rename(request: RpcRequest<{ sessionId: SessionId; title: string }>):
Promise<RpcResponse<{ title: string; seq: number }>>
@@ -261,23 +270,31 @@ export interface SessionsApi {
* falls back to the source's last completed turn. An in-log anchor whose
* turn is still open fails with `fork-unavailable` instead of clipping to
* an earlier turn. The child inherits the source cwd, latest logged model
* target, workspace attachment, and `parentSessionId` lineage; the seed
* prefix carries the source title.
* target and `parentSessionId` lineage; the seed prefix carries the source
* title. Reading the source uses attached state or persistence inspection
* without acquiring an Agent. Workspace attachment follows the source
* directly, or the nearest workspace-owning ancestor when the source is a
* subagent.
*/
fork(request: RpcRequest<{ sessionId: SessionId; atSeq?: number }>):
Promise<RpcResponse<{ sessionId: SessionId }>>
/** Sends a message. content is core's ContentBlock[] verbatim; mode maps 1:1 — queue→send, steer→steer. */
/** Sends a message to an ordinary session Agent. Session-backed subagents reject with `agent-busy` and use `subagent.prompt`. */
prompt(request: RpcRequest<{ sessionId: SessionId; mode: 'queue' | 'steer'; content: ContentBlock[] }>):
Promise<RpcResponse<{ accepted: true; command?: { kind: 'success'; text?: string } }>>
/**
* Edits or removes one pending queued occurrence.
* Edits, removes, or strictly steers one pending queued occurrence on an ordinary session.
* Session-backed subagents reject with `agent-busy`.
*/
updateQueue(request: RpcRequest<{ sessionId: SessionId; itemId: InboxItemId; action: QueueAction }>):
Promise<RpcResponse<{ accepted: true }>>
/** Stops: clears both FIFOs + aborts the current step (1:1 with agent.cancel). */
/**
* Stops an ordinary session's active turn, preserving pending inbox work
* that resumes in FIFO order after cancellation settles. Session-backed
* subagents reject with `agent-busy`.
*/
cancel(request: RpcRequest<{ sessionId: SessionId }>): Promise<RpcResponse<{ accepted: true }>>
}

View File

@@ -0,0 +1,77 @@
/** Zod schemas for the browser-safe subagent domain. */
import { z } from 'zod'
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { RequestPayload, ResponseValue } from './rpc-map.ts'
import type { Wire } from './rpc.schema.ts'
import {
contentBlockSchema, historyEntrySchema, sessionIdSchema, sessionProjectionsBlockSchema,
} from './sessions.schema.ts'
import type { SubagentListEntry } from './subagents.ts'
/** Healthy and diagnostic durable catalog rows. */
export const subagentListEntrySchema = z.union([
z.object({
kind: z.literal('child'),
id: sessionIdSchema,
mode: z.literal('one-shot'),
activity: z.union([z.literal('running'), z.literal('inactive')]),
hasChildren: z.boolean(),
label: z.string().optional(),
}),
z.object({
kind: z.literal('child'),
id: sessionIdSchema,
mode: z.literal('continuable'),
activity: z.union([z.literal('running'), z.literal('inactive')]),
hasChildren: z.boolean(),
label: z.string(),
}),
z.object({
kind: z.literal('diagnostic'),
id: sessionIdSchema,
reason: z.union([z.literal('corrupt'), z.literal('unsupported'), z.literal('unavailable')]),
}),
]) satisfies z.ZodType<Wire<SubagentListEntry>>
/** subagent.list request payload. */
export const subagentListRequestSchema = z.object({
parentSessionId: sessionIdSchema,
}) satisfies z.ZodType<Wire<RequestPayload<'subagent.list'>>>
/** subagent.list response value. */
export const subagentListValueSchema = z.object({
entries: z.array(subagentListEntrySchema),
parentAvailable: z.boolean(),
}) satisfies z.ZodType<Wire<ResponseValue<'subagent.list'>>>
/** subagent.history request payload. */
export const subagentHistoryRequestSchema = z.object({
parentSessionId: sessionIdSchema,
childSessionId: sessionIdSchema,
mode: z.union([z.literal('one-shot'), z.literal('continuable')]),
beforeSeq: z.number().int().nonnegative().optional(),
maxMessages: z.number().int().positive().optional(),
}) satisfies z.ZodType<Wire<RequestPayload<'subagent.history'>>>
/** subagent.history response value. */
export const subagentHistoryValueSchema = z.object({
events: z.array(historyEntrySchema),
hasMore: z.boolean(),
projections: sessionProjectionsBlockSchema.optional(),
}) as unknown as z.ZodType<Wire<ResponseValue<'subagent.history'>>>
/** subagent.prompt request payload. */
export const subagentPromptRequestSchema = z.object({
parentSessionId: sessionIdSchema,
childSessionId: sessionIdSchema,
mode: z.literal('continuable'),
content: z.array(contentBlockSchema),
}) as unknown as z.ZodType<RequestPayload<'subagent.prompt'>>
const messageIdSchema = z.string() as unknown as z.ZodType<MessageId>
/** subagent.prompt response value. */
export const subagentPromptValueSchema = z.object({
messageId: messageIdSchema,
}) satisfies z.ZodType<Wire<ResponseValue<'subagent.prompt'>>>

View File

@@ -0,0 +1,96 @@
/**
* Browser-safe subagent domain contract. Persisted transcript reads never
* activate an Agent, while continuable prompts route through the exact live
* direct parent into the child's Agent inbox.
*/
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { SessionId } from '@deepseek-ai/dsh-session/types'
import type { RpcRequest, RpcResponse } from './rpc.ts'
import type { HistoryEntry, SessionProjectionsBlock } from './sessions.ts'
/** Complete durable direct-child catalog row. */
export type SubagentListEntry =
| {
kind: 'child'
id: SessionId
/** Whether the child Agent driver is running at the Host sampling boundary. */
activity: 'running' | 'inactive'
/** Whether a direct descendant has durable `origin: 'subagent'`. */
hasChildren: boolean
} & (
| {
mode: 'one-shot'
label?: string
}
| {
mode: 'continuable'
label: string
}
)
| {
kind: 'diagnostic'
id: SessionId
reason: 'corrupt' | 'unsupported' | 'unavailable'
}
/** Inbox identity returned once the continuation accepts one human message. */
export interface SubagentPromptReceipt {
messageId: MessageId
}
/** Durable parent/child address that selects subagent transport in the client. */
export type SubagentAddress =
& {
parentSessionId: SessionId
childSessionId: SessionId
}
& (
| { mode: 'one-shot' }
| { mode: 'continuable' }
)
/** Complete direct-child catalog plus the delivery-time parent availability hint. */
export interface SubagentCatalog {
entries: SubagentListEntry[]
parentAvailable: boolean
}
/** Subagent-domain unary methods. */
export interface SubagentsApi {
/**
* Lists direct session-backed children without loading either side. Parent
* availability is a hint; continuable prompt performs the authoritative
* check.
*/
list(
request: RpcRequest<{ parentSessionId: SessionId }>,
signal?: AbortSignal,
): Promise<RpcResponse<SubagentCatalog>>
/**
* Reads one healthy catalog child's persisted raw log with ordinary
* message-aligned pagination and render intents, without Agent activation.
*/
history(
request: RpcRequest<SubagentAddress & { beforeSeq?: number; maxMessages?: number }>,
signal?: AbortSignal,
): Promise<RpcResponse<{
events: HistoryEntry[]
hasMore: boolean
projections?: SessionProjectionsBlock
}>>
/**
* Delivers human content to a continuable child through the exact live
* parent's continuation owner. Success identifies the message accepted by
* the child's FIFO inbox; later execution is independent of this request.
*/
prompt(
request: RpcRequest<
Extract<SubagentAddress, { mode: 'continuable' }> & { content: ContentBlock[] }
>,
signal: AbortSignal,
): Promise<RpcResponse<SubagentPromptReceipt>>
}

View File

@@ -22,7 +22,7 @@ export interface WorkspaceView {
workspaceId: WorkspaceId
/** Canonical directory path (host-side realpath canon). */
path: string
/** Unique display title (defaults to the path basename at create). */
/** Display title (defaults to the path basename at create). */
title: string
/**
* Sessions accounted under this workspace, in manually owned order
@@ -53,8 +53,8 @@ export interface WorkspaceApi {
* root before registering. Either spelling resolving to a directory already
* owned by a workspace returns that workspace (`created: false`) for the
* existing-folder spelling. Create-by-name rejects an existing title with
* `workspace-name-conflict`; a new path whose basename duplicates another
* Workspace title is rejected by the registry with the same code.
* `workspace-name-conflict`; path adoption allows distinct canonical paths
* whose basenames produce the same display title.
* A new name-created workspace uses `name` as both directory name and title;
* a path-created workspace uses the registry's basename title default.
*/