diff --git a/.agents/skills/dsh-prose-standard/SKILL.md b/.agents/skills/dsh-prose-standard/SKILL.md index faf234ae7c..37b070e466 100644 --- a/.agents/skills/dsh-prose-standard/SKILL.md +++ b/.agents/skills/dsh-prose-standard/SKILL.md @@ -7,6 +7,8 @@ description: Use when writing, reviewing, restoring, trimming, or auditing prose Write enough to preserve the contract, then remove reasoning transcripts, repetition, and decoration. This skill owns editorial judgment and required prose coverage; use [dsh-doc-standards](../dsh-doc-standards/SKILL.md) for placement, budgets, bilingual pairs, and documentation gates. It is guidance, not a script. +Comments describe non-obvious contracts or rationale that code cannot express; they do not restate what code already implies. + ## Inputs and exclusions Require an explicit `scope`. If it is missing, report the required input and stop; do not infer a repository-wide scope or begin an interview. diff --git a/apps/web/src/node-module-stub.ts b/apps/web/src/node-module-stub.ts index c64f307f7c..0a9b04ea5f 100644 --- a/apps/web/src/node-module-stub.ts +++ b/apps/web/src/node-module-stub.ts @@ -1,10 +1,6 @@ /** - * Browser stand-in for `node:module`, mapped by the vite alias in - * vite.config.ts (design §2.4). The vendored Loader's internal.ts imports - * `createRequire` at module scope but only calls it inside - * `ModuleLoader.fromInternal()`, whose version probe is compiled to the - * `"0.0.0"` define in the browser build — so this throw is a fail-loud - * tripwire for any path that would genuinely need Node's module machinery. + * Browser stand-in for `node:module`. `createRequire` is unreachable in the + * configured loader path and fails loud if that assumption changes. */ /** Throwing stand-in for node:module's createRequire (never reached in the browser boot). */ diff --git a/apps/web/tests/smoke-real.e2e.ts b/apps/web/tests/smoke-real.e2e.ts index 9cd34530be..a3d511df16 100644 --- a/apps/web/tests/smoke-real.e2e.ts +++ b/apps/web/tests/smoke-real.e2e.ts @@ -333,10 +333,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke const prompt = `Please answer this request carefully: explain event sourcing in two sentences, ending with exactly ${ROUND_DONE_MARKER}.` await input.fill(prompt) await input.press('Enter') - // startSession chain: session mounts, composer moves to the bottom. - // Regression pin (P0, 585671106): this send used to white-screen the tree - // (scope tag lost to a duplicate inlined runtime instance) — body going - // near-empty here means that class of bug is back. + // The first send must keep the session tree mounted; a near-empty body + // reveals a duplicate runtime bundle with incompatible scope tags. await page.waitForFunction(() => document.body.innerText.length > 50, undefined, { timeout: 15_000 }) expect(pageErrors).toEqual([]) await page.waitForFunction( diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 398bf4e7da..77ec3c155a 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -753,12 +753,12 @@ Source: [`packages/lsp/lsp-local/src/index.ts:85`](../packages/lsp/lsp-local/src Requires: `tools` ```ts config-catalog -/** Discriminated union of all supported MCP transport configurations. */ +/** Configuration for one stdio or Streamable HTTP MCP server. */ export type Config = StdioConfig | StreamableHttpConfig /** Config for connecting to an MCP server via a spawned child process over stdio. */ export interface StdioConfig { - /** Transport type: spawn a child process and communicate over stdio. */ + /** Selects child-process stdio transport. */ transport: 'stdio' /** * Stable local namespace for this server's model-facing tool names @@ -766,21 +766,21 @@ export interface StdioConfig { * unique across live mcp-client instances. */ serverName: string - /** Executable to spawn. */ + /** Executable used to start the server. */ command: string - /** Arguments passed to the command. */ + /** Arguments passed directly, without shell interpolation. */ args: string[] /** Extra env vars merged on top of scrubbed ambient env. */ env: Record /** Working directory for the child process. */ cwd: string - /** Timeout per callTool invocation (ms). */ + /** Per-tool-call timeout in milliseconds. */ toolCallTimeoutMs: number } /** Config for connecting to an MCP server over Streamable HTTP (SSE). */ export interface StreamableHttpConfig { - /** Transport type: connect to an MCP server over Streamable HTTP (SSE). */ + /** Selects Streamable HTTP transport. */ transport: 'streamable-http' /** * Stable local namespace for this server's model-facing tool names @@ -788,11 +788,11 @@ export interface StreamableHttpConfig { * unique across live mcp-client instances. */ serverName: string - /** MCP server URL. */ + /** MCP endpoint URL. */ url: string - /** Extra headers (e.g. auth tokens). */ + /** Additional headers attached to MCP requests. */ headers: Record - /** Timeout per callTool invocation (ms). */ + /** Per-tool-call timeout in milliseconds. */ toolCallTimeoutMs: number } ``` diff --git a/docs/core-data-structures/subagent.i18n.yaml b/docs/core-data-structures/subagent.i18n.yaml index d8d6a493d7..af4b1decda 100644 --- a/docs/core-data-structures/subagent.i18n.yaml +++ b/docs/core-data-structures/subagent.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -subagent.md: 0335a3f0780ae17b57ae730f5a49a269261c8073 -subagent.zh.md: dac48b624f6e0cfc28737e3e1a2774ba2d97e85b +subagent.md: 2497dbab9cfc8304eb7aaeba7109404ac614bbff +subagent.zh.md: 2d96e9bc635951746e72ed58a7c3638dc2598cc2 diff --git a/docs/core-data-structures/subagent.md b/docs/core-data-structures/subagent.md index 0335a3f078..2497dbab9c 100644 --- a/docs/core-data-structures/subagent.md +++ b/docs/core-data-structures/subagent.md @@ -19,16 +19,13 @@ A provider advertises its **start-time** features on a static descriptor the ser * is rejected with a typed error rather than accepted-then-ignored (the "fail loud, no silent * degradation" rule). These static flags cover features needed before a run exists; runtime * capabilities such as steering and resume are optional {@link SubagentRun} methods whose presence - * is the capability. + * is the capability. Each flag corresponds one-to-one to a {@link SubagentStartRequest} option: + * `depthLimit` to `maxDepth`; the other names match. */ interface SubagentCapabilities { - /** Honor {@link SubagentStartRequest.outputSchema} (structured final output). */ readonly outputSchema: boolean - /** Enforce {@link SubagentStartRequest.maxDepth} (recursion cap). */ readonly depthLimit: boolean - /** Enforce {@link SubagentStartRequest.toolFilter} (child tool scoping). */ readonly toolFilter: boolean - /** Honor {@link SubagentStartRequest.persona} (a per-child persona). */ readonly persona: boolean } ``` @@ -45,16 +42,12 @@ The tool layer builds this request from the model input and its own config; the * passes it to {@link SubagentProvider.start}. */ interface SubagentStartRequest { - /** The task/prompt for the child agent (a user message in the child session). */ + /** Content delivered as the child's user message. */ readonly prompt: ContentBlock[] /** - * The spawning ("parent") agent — the one whose tool call started this - * subagent. REQUIRED: in-process backends read `parent.session.header` for - * the working directory, the `parentSession` lineage to stamp on the child, - * and the parent's delegation depth. The out-of-process backend (ACP) reads - * exactly one field — the session header's cwd, the child's workspace when - * no deployment `cwd` override is configured; nothing else crosses the - * process boundary. + * The spawning agent. In-process providers derive workspace, lineage, and + * delegation depth from its durable session state. ACP reads only its cwd, + * and only when no deployment `cwd` override is configured. */ readonly parent: Agent /** @@ -65,7 +58,6 @@ interface SubagentStartRequest { * afterward. */ readonly signal: AbortSignal - /** Per-child agent options (model and plugin-defined extension fields). */ readonly agentOptions?: AgentOptions /** * Object-rooted JSON Schema within `assertObjectJsonSchema`'s enforced subset. Start rejects @@ -137,9 +129,9 @@ interface SubagentResult { interface SubagentStopReasonMap { /** The child finished its turn normally. */ completed: 'completed' - /** The run was cancelled by its request signal or by disposal. */ + /** Cancelled through the request signal or disposal. */ aborted: 'aborted' - /** The child failed (model error, transport error). */ + /** Model or transport failure. */ error: 'error' /** The child hit its token ceiling before finishing. */ 'max-tokens': 'max-tokens' @@ -180,9 +172,8 @@ interface SubagentRun { */ readonly result: Promise /** - * Cancel remaining work, reach child quiescence, and release the run's - * resources (in-process: dispose the owned agent and remove its session; - * ACP: kill and reap the subprocess). Idempotent. + * Cancel remaining work, reach child quiescence, and release resources. + * Idempotent. */ dispose(): Promise /** @@ -206,12 +197,9 @@ Each provider is a named child-agent transport, and multiple providers may coexi ```ts type-equiv /** - * A subagent backend: one transport for running a child agent (in-process - * spawn/fork, ACP to another process, …). Implementations register under a - * unique name via {@link SubagentService.registerProvider}; multiple providers - * coexist in one context (unlike the single-implementation bash seam). The - * Providers are trusted same-process implementations; callers treat their - * descriptors and returned values as borrowed immutable data. + * One registered transport for running child agents. Providers are trusted + * same-process implementations; callers treat descriptors and returned values + * as borrowed immutable data. */ interface SubagentProvider { /** Unique registry name (e.g. `spawn`, `fork`, `acp`). */ diff --git a/docs/core-data-structures/subagent.zh.md b/docs/core-data-structures/subagent.zh.md index dac48b624f..2d96e9bc63 100644 --- a/docs/core-data-structures/subagent.zh.md +++ b/docs/core-data-structures/subagent.zh.md @@ -19,16 +19,13 @@ subagent seam:一个 agent(智能体)将工作委派给子 agent。与 [ba * is rejected with a typed error rather than accepted-then-ignored (the "fail loud, no silent * degradation" rule). These static flags cover features needed before a run exists; runtime * capabilities such as steering and resume are optional {@link SubagentRun} methods whose presence - * is the capability. + * is the capability. Each flag corresponds one-to-one to a {@link SubagentStartRequest} option: + * `depthLimit` to `maxDepth`; the other names match. */ interface SubagentCapabilities { - /** Honor {@link SubagentStartRequest.outputSchema} (structured final output). */ readonly outputSchema: boolean - /** Enforce {@link SubagentStartRequest.maxDepth} (recursion cap). */ readonly depthLimit: boolean - /** Enforce {@link SubagentStartRequest.toolFilter} (child tool scoping). */ readonly toolFilter: boolean - /** Honor {@link SubagentStartRequest.persona} (a per-child persona). */ readonly persona: boolean } ``` @@ -45,16 +42,12 @@ interface SubagentCapabilities { * passes it to {@link SubagentProvider.start}. */ interface SubagentStartRequest { - /** The task/prompt for the child agent (a user message in the child session). */ + /** Content delivered as the child's user message. */ readonly prompt: ContentBlock[] /** - * The spawning ("parent") agent — the one whose tool call started this - * subagent. REQUIRED: in-process backends read `parent.session.header` for - * the working directory, the `parentSession` lineage to stamp on the child, - * and the parent's delegation depth. The out-of-process backend (ACP) reads - * exactly one field — the session header's cwd, the child's workspace when - * no deployment `cwd` override is configured; nothing else crosses the - * process boundary. + * The spawning agent. In-process providers derive workspace, lineage, and + * delegation depth from its durable session state. ACP reads only its cwd, + * and only when no deployment `cwd` override is configured. */ readonly parent: Agent /** @@ -65,7 +58,6 @@ interface SubagentStartRequest { * afterward. */ readonly signal: AbortSignal - /** Per-child agent options (model and plugin-defined extension fields). */ readonly agentOptions?: AgentOptions /** * Object-rooted JSON Schema within `assertObjectJsonSchema`'s enforced subset. Start rejects @@ -137,9 +129,9 @@ interface SubagentResult { interface SubagentStopReasonMap { /** The child finished its turn normally. */ completed: 'completed' - /** The run was cancelled by its request signal or by disposal. */ + /** Cancelled through the request signal or disposal. */ aborted: 'aborted' - /** The child failed (model error, transport error). */ + /** Model or transport failure. */ error: 'error' /** The child hit its token ceiling before finishing. */ 'max-tokens': 'max-tokens' @@ -182,9 +174,8 @@ interface SubagentRun { */ readonly result: Promise /** - * Cancel remaining work, reach child quiescence, and release the run's - * resources (in-process: dispose the owned agent and remove its session; - * ACP: kill and reap the subprocess). Idempotent. + * Cancel remaining work, reach child quiescence, and release resources. + * Idempotent. */ dispose(): Promise /** @@ -208,12 +199,9 @@ interface SubagentRun { ```ts type-equiv /** - * A subagent backend: one transport for running a child agent (in-process - * spawn/fork, ACP to another process, …). Implementations register under a - * unique name via {@link SubagentService.registerProvider}; multiple providers - * coexist in one context (unlike the single-implementation bash seam). The - * Providers are trusted same-process implementations; callers treat their - * descriptors and returned values as borrowed immutable data. + * One registered transport for running child agents. Providers are trusted + * same-process implementations; callers treat descriptors and returned values + * as borrowed immutable data. */ interface SubagentProvider { /** Unique registry name (e.g. `spawn`, `fork`, `acp`). */ diff --git a/examples/acp-agent/tests/acp.e2e.ts b/examples/acp-agent/tests/acp.e2e.ts index b51498e90c..16bf2fbf72 100644 --- a/examples/acp-agent/tests/acp.e2e.ts +++ b/examples/acp-agent/tests/acp.e2e.ts @@ -114,7 +114,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('acp-agent e2e: real prompt over }) expect(['end_turn', 'max_tokens']).toContain(res.stopReason) - // Verify the WORLD, not the agent's self-report: read the file from disk. + // Assert the filesystem effect independently of the model response. const proof = await readFile(join(workdir, 'proof.txt'), 'utf8') expect(proof).toContain('ACP_OK') diff --git a/examples/acp-agent/tests/hooks.e2e.ts b/examples/acp-agent/tests/hooks.e2e.ts index 49669e6b9b..b99d022c17 100644 --- a/examples/acp-agent/tests/hooks.e2e.ts +++ b/examples/acp-agent/tests/hooks.e2e.ts @@ -62,7 +62,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('acp-agent e2e: a PreToolUse hook // the model, not a turn failure). expect(['end_turn', 'max_tokens']).toContain(res.stopReason) - // Verify that the denied hook left no filesystem effect. + // Assert the denied operation independently of the model response. await expect(access(join(workdir, 'proof.txt'))).rejects.toThrow() // ACP publishes only the committed answer; hook/tool trace stays in the session log. diff --git a/packages/client/connection/src/client/index.ts b/packages/client/connection/src/client/index.ts index b017d1c9e2..673aa978da 100644 --- a/packages/client/connection/src/client/index.ts +++ b/packages/client/connection/src/client/index.ts @@ -1,10 +1,7 @@ /** - * Browser half of the wire consumer layer (contract: api-contracts v3 - * section 3; export inventory = v3 §3.2). The wire is this package's client - * half in its entirety — apply mounts ctx.connection: the shared api client - * plus the connection controller handle. Mode selection (?fixture) happens - * here so the rest of the client tree is mode-blind; the controller's sinks - * are wired by the runtime plugin (object layer), which injects this service. + * Browser wire client. The plugin selects fixture or HTTP transport, provides + * the shared API client, and lets the runtime object layer start the stream + * controller with its sinks. */ import type { Context } from 'cordis' import type { IApiClient } from './api.ts' @@ -23,9 +20,8 @@ export type { } from './api.ts' export { RpcId, AbstractApiClient, transportError } from './api.ts' -// ---- Connection loop types (part of the ConnectionHandle.start contract; -// the controller class itself stays package-internal — apply owns the loop, -// tests reach it via src) ---- +// Connection loop types are public through ConnectionHandle.start; the +// controller remains package-internal. export type { ConnectionConfig, ConnectionSinks, ConnectionState } diff --git a/packages/client/connection/src/index.ts b/packages/client/connection/src/index.ts index 61d718b618..f06b2bd2f6 100644 --- a/packages/client/connection/src/index.ts +++ b/packages/client/connection/src/index.ts @@ -1,12 +1,6 @@ -/** - * Connection plugin, node half: the host end of the web transport. Registers - * the /api prefix route on the web server and bridges node:http requests to - * the transport-agnostic fetch-shaped api handler. The wire consumer layer - * lives in the client half (src/client/ — contract: api-contracts v3 - * section 3); consumers import the /client subpath. - */ +/** Host HTTP bridge for browser-client RPC. */ import type { Context } from 'cordis' -// Type-only route import; it also carries the httpServer Context merge. +// Activates the httpServer Context merge used below. import type { WebRoute } from '@deepseek-ai/dsh-host-webserver' import { toFetchHandler } from '@deepseek-ai/dsh-host-apiproxy' import { API_PATH } from './api-path.ts' @@ -14,16 +8,15 @@ import { bridge } from './http-bridge.ts' export { API_PATH } from './api-path.ts' -/** Cordis plugin name. */ +/** Stable Cordis plugin name. */ export const name = 'client-connection' -/** Required services: the route registry and the api gateway. */ +/** Services required before mounting the route. */ export const inject = ['httpServer', 'apiProxy'] /** - * Mount the /api transport: wrap the api gateway into a fetch handler and - * serve it under the /api prefix. - * @param ctx - host plugin context carrying httpServer and apiProxy. + * Mounts the API gateway under the browser transport prefix. + * @param ctx - Host plugin context. */ export function apply(ctx: Context): void { const apiHandler = toFetchHandler(ctx.apiProxy) diff --git a/packages/client/i18n/src/client/index.ts b/packages/client/i18n/src/client/index.ts index 37e1c0cdb5..9dea9c4bd4 100644 --- a/packages/client/i18n/src/client/index.ts +++ b/packages/client/i18n/src/client/index.ts @@ -1,15 +1,10 @@ /** - * i18n plugin, browser half: namespace x locale dictionary registry with a - * bound translate function whose reference is stable (safe for inject - * surfaces). Mounts ctx.i18n and seeds the zh/en base dictionaries. - * Contract: api-contracts v3 section 8. + * Browser-side locale registry. Bound translation functions retain stable + * identity for injected consumers. */ import type { Context } from 'cordis' -// The snapshot-store engine lives in runtime (store relocation): framework -// data stores like this locale cell use it directly. The store carries no -// hook — a React consumer binds a selector hook via web-react's -// bindSnapshotSelector at its own seam (none exists today; the current -// consumers are translate() reads and test-side subscribe/set). +// Snapshot stores are framework-neutral; React consumers bind hooks at their +// rendering boundary. import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client' import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client' import { en } from '../locales/en.ts' diff --git a/packages/client/i18n/src/index.ts b/packages/client/i18n/src/index.ts index 1e2de41ace..e759f1edc1 100644 --- a/packages/client/i18n/src/index.ts +++ b/packages/client/i18n/src/index.ts @@ -1,11 +1,4 @@ -/** - * i18n plugin, node half. Pure UI plugin: the empty apply exists so the - * plugin appears in the host cordis.yml / Loader (load and lifecycle follow - * the host; the browser half ships via exports["./client"], discovered - * through the package.json dshClient declaration). Everything else — - * I18nService, Translate, LocaleDict — lives in the client half; consumers - * import the /client subpath. Contract: api-contracts v3 section 8. - */ +/** Host loader entry for the browser implementation exported from `./client`. */ /** Host plugin body — no host-side behavior for the i18n plugin. */ export function apply(): void {} diff --git a/packages/client/runtime/src/client/contract/store.ts b/packages/client/runtime/src/client/contract/store.ts index ce4444cf36..7dc3b584ba 100644 --- a/packages/client/runtime/src/client/contract/store.ts +++ b/packages/client/runtime/src/client/contract/store.ts @@ -161,11 +161,7 @@ function deepFreeze(value: unknown): void { } } -// ---- defineStore shell (slot terminal design §4) ---- -// The type authority is ui-slots' store family (create(scopeKey?) and -// clearPersisted() included); this module houses only the engine-backed -// implementation. The one engine-side widening left: instances expose the -// raw engine store for framework/test surfaces. +// ui-slots owns the contract; this module supplies the engine implementation. /** A live engine instance: the contract instance plus the raw engine store. */ export interface EngineStoreInstance> extends StoreInstance { diff --git a/packages/client/runtime/src/client/index.ts b/packages/client/runtime/src/client/index.ts index 33097d4573..4c0bf3d01f 100644 --- a/packages/client/runtime/src/client/index.ts +++ b/packages/client/runtime/src/client/index.ts @@ -1,12 +1,7 @@ /** - * Browser half: the whole runtime contract surface (api-contracts v3 §4) — - * SlotsService (declaration ledger + renderer seam + store axis, built-in - * 'root'), SessionsService (list store + current selection + scope tree + - * object layer), and the cordis Context/Events merges. apply mounts - * ctx.slots + ctx.sessions and wires the connection stream loop into the - * object layer. A static-arrival entry: the web shell bundles this module - * and mounts it through the host graph (module loading lives in - * @deepseek-ai/dsh-client-modules, entry governance in the vendored Loader). + * Browser runtime services for slots, sessions, and connection-stream + * delivery. The web shell mounts this static client entry through the host + * plugin graph. */ import type { Context } from 'cordis' import type { ConnectionHandle, SessionId } from '@deepseek-ai/dsh-client-connection/client' @@ -17,15 +12,11 @@ import type { SessionListState } from './sessions/service.ts' import type { ConversationSnapshot, RunningToolCall, ToolResultNode } from './sessions/conversation.ts' export { SlotsService } from './slots.ts' -// RootOwnerProps rides the 'root' SlotMap row (both migrated here from -// ui-layout: the framework slot is declared by the framework package). export type { RootOwnerProps } from './slots.ts' export { SessionsService, scopeOf } from './sessions/service.ts' export type { Session } from './sessions/session.ts' export type { SessionBinding, SessionListState, SessionSummary } from './sessions/service.ts' -// The snapshot-store engine lives here since the store migration (the data -// layer owns its substrate; web-react is React glue only). The './client' -// main export is the single serving door — no store subpath. +// Runtime owns the snapshot store; web-react only binds it to React. export { createSnapshotStore, defineStore, shallowEqual } from './contract/store.ts' export type { EngineStoreHandle, EngineStoreInstance, ObservableSnapshot, SnapshotStore, @@ -35,21 +26,11 @@ export type { RunningToolCall, SteeringMessageNode, ToolResultNode, UnknownSurfaceNode, UserMessageNode, } from './sessions/conversation.ts' -// PendingWait is a value export: tests construct fixture waits directly. export { PendingWait } from './sessions/pending.ts' export type { PendingInteraction, PendingKind, PendingPayloads } from './sessions/pending.ts' export type { SessionId } from '@deepseek-ai/dsh-client-connection/client' -// ---- Narrowed aliases (the single narrowing point of the slot type chain: -// ui-slots/web-react stay generic and dependency-inverted; the client-tree -// concrete types live here, where their subjects live) ---- - -/** - * The client cordis context face: the base Context plus the service keys - * this package's declaration merge contributes (slots/sessions/loader) and - * every later plugin's merge. A plain alias — the merges land on Context - * itself inside the client program; the name marks intent at consumer seams. - */ +/** Client-side Cordis context after declaration merging. */ export type ClientContext = Context /** The conversation-snapshot selector hook (ConvViewProps/ToolRowProps take this). */ @@ -69,14 +50,12 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { * every session-scope slot component receives these from the framework. */ interface SessionStandardProps { - /** Selector hook over this session's conversation snapshot. */ useSession: SnapshotSelectorHook /** The framework-resolved session id (owners never pass it). */ sessionId: SessionId } - /** Global standard kit, real members: the session-list hook every slot component receives. */ + /** Props injected into every global slot component. */ interface GlobalStandardProps { - /** Selector hook over the session list snapshot (`current` included — the arbitrated selection seat). */ useSessions: SnapshotSelectorHook } } @@ -99,9 +78,8 @@ declare module 'cordis' { /** Required services: the wire handle mounted by the connection plugin. */ export const inject = ['connection'] -/** - * Client plugin body: mount slots + sessions, start the stream loop. - * @param ctx - client cordis context. +/** Mounts the browser runtime services and connection stream. + * @param ctx - Client Cordis context. */ export function apply(ctx: Context): void { ctx.plugin(SlotsService) diff --git a/packages/client/runtime/src/client/sessions/fold-adapter.ts b/packages/client/runtime/src/client/sessions/fold-adapter.ts index 23b35f86bf..0f40d9bf2a 100644 --- a/packages/client/runtime/src/client/sessions/fold-adapter.ts +++ b/packages/client/runtime/src/client/sessions/fold-adapter.ts @@ -24,10 +24,10 @@ export interface CallIndexEntry { callView: ToolCallView | null } -/** Non-surface-eligible sentinel event (safely skipped by surfaceOpOf's undefined branch). - * 'noop/padding' is not a real event type on purpose: a genuine type with fake data would - * surface as garbage the day anyone adds handling for it (design §D.1; the cast is the one - * place a synthetic event enters the window). */ +/** Non-surface sentinel used to preserve paged-window sequence offsets. + * `noop/padding` is deliberately not a real event type, so it cannot acquire + * surface behavior; this cast is the only synthetic event entry point. + */ function paddingEvent(seq: number): SessionEvent { return { type: 'noop/padding', seq, time: 0, data: {} } as unknown as SessionEvent } diff --git a/packages/client/runtime/src/client/sessions/service.ts b/packages/client/runtime/src/client/sessions/service.ts index d8a6f05762..af07362f08 100644 --- a/packages/client/runtime/src/client/sessions/service.ts +++ b/packages/client/runtime/src/client/sessions/service.ts @@ -291,8 +291,7 @@ export class SessionsService { fiber, ctx, binding: { sessionId: id, session, ctx }, - // Bare source form (store migration): the Session object IS the - // observable; the React side binds the useSession hook per cell. + // Session is the observable; React binds a selector hook at its own seam. cell: { sessionId: id, session }, } this.scopes.set(id, record) diff --git a/packages/client/runtime/src/client/sessions/session.ts b/packages/client/runtime/src/client/sessions/session.ts index 6b773e0903..0681e2bb5f 100644 --- a/packages/client/runtime/src/client/sessions/session.ts +++ b/packages/client/runtime/src/client/sessions/session.ts @@ -1,7 +1,4 @@ -// Session: wraps every contract call that needs a sessionId + all conversation state for this -// session (design §A.2/§A.9/§D.2/§D.3). Instances are resident (ruling 2): never destroyed once -// created, they keep consuming mux frames in the background; React connects directly via -// subscribe/getSnapshot. +// Sessions remain resident after creation so they continue consuming mux frames off-screen. import type { ContentBlock } from '@deepseek-ai/dsh-llm/types' import type { SessionEvent } from '@deepseek-ai/dsh-session/types' @@ -22,14 +19,12 @@ import { FoldAdapter } from './fold-adapter.ts' import { Notifier } from './notifier.ts' import { PartialAccumulator } from './partial.ts' -/** Messages per page (F.4 ledger: promote to Config at graduation; every call site references this constant). */ +/** Messages requested per history page. */ export const PAGE_MESSAGES = 50 /** - * Per-session state owner: event window + fold + partial, snapshot out via - * subscribe/getSnapshot (see the web client architecture RFC). Bare source - * only (store migration): the React machinery binds the per-cell useSession - * hook at its own seam — no selector hook member lives on the data layer. + * Owns a session's event window, derived conversation state, and observable + * snapshot. React bindings remain outside this data layer. */ export class Session implements ObservableSnapshot { // ---- Window and derived state (all private; the snapshot is the only read surface) ---- @@ -54,8 +49,7 @@ export class Session implements ObservableSnapshot { * Derived from window events (turn/end sweep) — rebuilt by rebuildDerivedFromWindow like partial/openCalls. */ private frozenNodes: ConversationNode[] = [] private pending = new Map() - // Revision counters + caches backing the snapshot's reference-stability contract (§A.9.4/§C.2, - // audit S5): buildSnapshot reuses the previous array when the revision is unchanged, so + // Revision counters preserve array identity when derived content is unchanged, so // React.memo children survive unrelated snapshot swaps (chunk storms must not re-render every // tool card and pending card). Mutation sites bump the matching revision. partial needs no // counter — PartialAccumulator.toPartial already returns a cached reference when unchanged. @@ -69,9 +63,9 @@ export class Session implements ObservableSnapshot { private removed = false private promptError: PromptError | null = null private lastAgentError: string | null = null - /** Buffer for live events arriving while open/resync is in flight (stitched by seq once history lands, §D.3). */ + /** Live events buffered during open/resync and stitched by sequence once history lands. */ private liveBuffer: { event: SessionEvent; view: ToolEventView | undefined }[] = [] - /** Gap-repair (resync-lite) in flight: acceptLiveEvent detours to liveBuffer until the tail page lands (audit S3). */ + /** Gap repair in flight; live events detour to the buffer until the tail page lands. */ private stitching = false /** subscribed.lastSeq baseline (gap detection; null when no subscribed frame arrived — degrade to the liveBuffer dedup path). */ private subscribedLastSeq: number | null = null @@ -292,8 +286,7 @@ export class Session implements ObservableSnapshot { this.notifier.markDirty() } - /** Instance-eviction hook, reserved no-op (design §F.6): resident instances are never destroyed - * in v1; an eviction policy lands here (unsubscribe, drop buffers) without touching call sites. */ + /** No-op because session instances remain resident. */ dispose(): void {} // ---- 私有 ---- diff --git a/packages/client/runtime/src/index.ts b/packages/client/runtime/src/index.ts index b0d0f0a7c8..c1ea85d1e5 100644 --- a/packages/client/runtime/src/index.ts +++ b/packages/client/runtime/src/index.ts @@ -1,11 +1,4 @@ -/** - * Runtime plugin, node half. The implementation lives entirely in the client - * half (src/client/ — SlotsService, SessionsService + object layer, and the - * shell-held ClientLoader under ./loader); consumers import the /client or - * /loader subpaths. The empty apply exists so the plugin appears in the host - * Loader (lifecycle governance + dshClient discovery). Contract: - * api-contracts v3 section 4. - */ +/** Host loader entry for the browser runtime exported from `./client` and `./loader`. */ /** Host plugin body — no host-side behavior for the runtime plugin. */ export function apply(_ctx: unknown): void {} diff --git a/packages/client/runtime/tests/sessions-service.spec.ts b/packages/client/runtime/tests/sessions-service.spec.ts index 8c850426bd..2b469ca055 100644 --- a/packages/client/runtime/tests/sessions-service.spec.ts +++ b/packages/client/runtime/tests/sessions-service.spec.ts @@ -187,8 +187,7 @@ describe('cell (render-layer session kit)', () => { const cell = b.svc.cell('s1') expect(cell).toBeDefined() expect(cell?.sessionId).toBe('s1') - // Bare-source form (store migration): the cell carries the Session - // observable itself; hook binding happens in the React machinery. + // Hook binding happens in React; the cell carries the observable itself. expect(cell?.session).toBe(b.svc.manager.get(sid('s1'))) expect(b.svc.cell('s1')).toBe(cell) expect(b.svc.cell('ghost')).toBeUndefined() diff --git a/packages/client/ui-conversation/src/client/apply.ts b/packages/client/ui-conversation/src/client/apply.ts index 372eb36c80..536859b59a 100644 --- a/packages/client/ui-conversation/src/client/apply.ts +++ b/packages/client/ui-conversation/src/client/apply.ts @@ -1,14 +1,4 @@ -/** - * Client plugin body: register the conversation/details slot occupants and - * the no-session empty state, contribute the chat entry into the - * 'conversation.view' ring that the conversation registration declares, then - * mount the conversation service (class plugin) and the bash toolview sample. - * Assembly only — components receive everything through props: the framework - * standard kit and store faces arrive automatically from the declarations - * below; the inject factories contribute the plain-data-and-callbacks - * business face (design §5). Tool rows are ordinary keyed-slot registrations - * into 'conversation.chat.toolview' — no dedicated registry exists. - */ +/** Registers the conversation components, shared store, and service callbacks. */ import type { Context } from 'cordis' import type { BoundActions } from '@deepseek-ai/dsh-client-ui-slots' import type { SessionId, SessionsService } from '@deepseek-ai/dsh-client-runtime/client' @@ -25,7 +15,7 @@ import { ConversationRoot } from './skeleton/ConversationRoot.tsx' import { DetailsPanel } from './skeleton/DetailsPanel.tsx' import { EmptyState } from './skeleton/EmptyState.tsx' -/** Required services (cordis fiber inject — the loader passes the whole export surface as an object plugin). */ +/** Services required by the conversation plugin. */ export const inject = ['slots', 'layout', 'sessions'] /** Resolve the session-scoped conversation service (scope-addressed send/cancel), failing loud. */ @@ -37,24 +27,17 @@ function scopedConversation(sessions: SessionsService, id: SessionId): Conversat return conversation } -/** - * Client plugin body. - * @param ctx - client root context. +/** Mounts the conversation plugin. + * @param ctx - Client root context. */ export function apply(ctx: Context): void { const sessions = ctx.sessions const layout = ctx.layout const slots = ctx.slots - // Shared store handle, constructed here so its identity lives and dies with - // this fiber (a module-level handle would be a de-facto singleton). The - // conversation, chat-view, and details registrations all declare it; same - // scope key = same instance, so chat-view selection writes and details - // reads meet in one store. + // Apply-time construction keeps store identity bound to this fiber. const chatStore = createChatStore() - // Tab projection over the view ring's ledger (list entries carry id/order/ - // label as registration options; the ledger keeps them order-sorted). const viewTabs = (): ViewTab[] => { const tabs: ViewTab[] = [] for (const entry of slots.entries('conversation.view')) { diff --git a/packages/client/ui-conversation/src/client/chat/StatsLine.tsx b/packages/client/ui-conversation/src/client/chat/StatsLine.tsx index d7211f2f91..50dead9529 100644 --- a/packages/client/ui-conversation/src/client/chat/StatsLine.tsx +++ b/packages/client/ui-conversation/src/client/chat/StatsLine.tsx @@ -1,9 +1,4 @@ -// StatsLine: the session stats row (figma 122:11212 "cache hit 92% · 1,284 -// tokens · 45.2s · 5 turns · 32 steps"), rendered by ChatView under the flow -// (part of the chat view body — the chrome attachment mechanism retired with -// the view ring). Duration has no data source in P-I (ledger). Subscribes to -// `nodes` only: chunk batches never swap that reference, so the row renders -// zero times during streaming (the RFC performance model's acceptance row). +// Settled-node identity prevents stream-delta updates from rerendering this row. import { memo, useMemo } from 'react' import type { ConversationSnapshot } from '@deepseek-ai/dsh-client-runtime/client' diff --git a/packages/client/ui-conversation/src/client/contract/slots.ts b/packages/client/ui-conversation/src/client/contract/slots.ts index ffbc13ff59..9745c4518b 100644 --- a/packages/client/ui-conversation/src/client/contract/slots.ts +++ b/packages/client/ui-conversation/src/client/contract/slots.ts @@ -1,15 +1,4 @@ -/** - * Slot-ring contract for the conversation package: the 'conversation.view' - * slot this package declares (the view ring — one list entry per conversation - * view tab), the chat view's per-tool row hole ('conversation.chat.toolview', - * keyed on the wire tool name), and the composed props shapes its registrants - * mount into the layout-owned slots (conversation / details / - * conversation.empty) plus its own slots. Terminal slot design (§3): full - * component props are the automatic shares — PropsRuntime (framework - * standard kit) & PropsRenderSlots (declared children) & PropsStore - * (declared store's read/write faces) & the injected business face declared - * here. - */ +/** Conversation slot declarations and their composed component props. */ import type { PropsRenderSlots, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots' import type { PendingInteraction, SessionId, ToolCallBlock } from '@deepseek-ai/dsh-client-runtime/client' import type { createChatStore } from '../stores.ts' @@ -93,15 +82,9 @@ export type ConvViewProps = PropsRuntime<'conversation.view'> /** The shared chat store handle type (apply constructs one; the conversation, details, and chat-view registrations all declare it). */ export type ChatStore = ReturnType -/** - * Injected share of the conversation slot: plain data and callbacks only - * (design §5 — hooks are framework-made). The store lines that used to ride - * here live in the declared {@link ChatStore}; ancestry derives from the - * standard useSessions hook in-component; views render through the declared - * 'conversation.view' child slot, with this face projecting the tab strip. - */ +/** Business callbacks injected into the conversation slot. */ export interface ConversationInjected { - /** View tab read face (uSES triple over the 'conversation.view' slot ledger). */ + /** Views projected from the `conversation.view` slot ledger. */ views: { list(): readonly ViewTab[] subscribe(fn: () => void): () => void @@ -111,7 +94,6 @@ export interface ConversationInjected { send(text: string, mode: 'queue' | 'steer'): void /** Cancel the in-flight turn (failure surfaces via snapshot.promptError). */ stop(): void - /** Navigate to another session (breadcrumb ancestors). */ open(id: SessionId): void } @@ -123,7 +105,6 @@ export interface ConversationInjected { * with zero owner changes. */ export interface ComposerChainProps { - /** The session's live pending waits, in arrival order (snapshot reference). */ interactions: readonly PendingInteraction[] } @@ -139,7 +120,6 @@ export type ConversationSlotProps = export interface ChatViewInjected { /** Selection write + details panel opening in one gesture (store action + layout orchestration). */ openDetails(target: SelectionTarget): void - /** Pull one older history page. */ loadOlder(): void } diff --git a/packages/client/ui-conversation/src/client/contract/views.ts b/packages/client/ui-conversation/src/client/contract/views.ts index da573f007a..9ef9515f19 100644 --- a/packages/client/ui-conversation/src/client/contract/views.ts +++ b/packages/client/ui-conversation/src/client/contract/views.ts @@ -1,14 +1,4 @@ -/** - * Shared conversation contract primitives: the view tab projection (slot - * entries in 'conversation.view' surface as tabs), the chat store state - * shared through the declared store, and the selection primitives every - * domain consumes. Shared face between the skeleton domain (tab strip + - * view outlet) and the chat domain; domain implementation files import this, - * never each other. The view ring itself IS the 'conversation.view' slot - * (contract in slots.ts) — the package-local view registry is retired, and - * so is the hand-threaded translate channel (framework-level per-slot i18n - * injection is the planned replacement). - */ +/** Shared conversation view, selection, and store-state contracts. */ /** Tool call identity as carried on the wire (branded upstream in connection). */ export type CallId = string @@ -23,11 +13,8 @@ export interface SelectionTarget { turnSeq: number; stepSeq?: number; callId?: C export interface ViewTab { id: string; label: string } /** - * Chat store state (slot terminal design §4): the per-session store shared by - * the conversation, chat-view, and details registrations. `createChatStore` - * implements this shape. `view` may carry a stale persisted id after a view - * plugin unloads — the slot ledger is the runtime validator (unknown ids fall - * back to the first registered view). + * Per-session state shared by conversation, chat-view, and details slots. + * Unknown persisted view ids fall back to the first registered view. */ export interface ChatStoreState { /** Details-linkage channel (conversation writes, details reads). */ diff --git a/packages/client/ui-conversation/src/client/index.ts b/packages/client/ui-conversation/src/client/index.ts index 8cfebac81c..d55ba1bd1e 100644 --- a/packages/client/ui-conversation/src/client/index.ts +++ b/packages/client/ui-conversation/src/client/index.ts @@ -1,12 +1,7 @@ /** - * Conversation domain plugin, browser half: skeleton (header/tabs/composer), - * the 'conversation.view' slot ring (chat entry here; other plugins - * contribute view tabs through ctx.slots), the chat view's keyed - * 'conversation.chat.toolview' row hole, scope-addressed ConversationService, - * minimal details panel. Contract: api-contracts v3 section 7. Thin shell: - * type surfaces live in contract/, assembly in apply.ts; the implementation - * domains (skeleton/chat) never import each other — contract/ is their only - * shared face. + * Browser conversation plugin. `contract/` is the shared type boundary + * between the independently implemented skeleton and chat domains; `apply.ts` + * owns their slot assembly. */ import type { ConversationService } from './service.ts' diff --git a/packages/client/ui-conversation/src/client/service.ts b/packages/client/ui-conversation/src/client/service.ts index 0c6b9632bb..68fb7c4c31 100644 --- a/packages/client/ui-conversation/src/client/service.ts +++ b/packages/client/ui-conversation/src/client/service.ts @@ -1,17 +1,11 @@ /** - * ConversationService implementation: scope-addressed send/cancel and the - * empty-state startSession chain. Contract: api-contracts v3 section 7. - * Selection/draft state moved to the declared chat store (slot terminal - * design §4); the view registry moved to the 'conversation.view' slot (slot - * ledger owns registration, ordering, and disposal) — what remains is the - * send/stop orchestration face. + * Scope-addressed conversation send, cancel, and empty-state session startup. * * Scope addressing rides the cordis Service tracker: property access through * `ctx.conversation` rebinds `this.ctx` to the caller's context, so methods - * read the session tag with scopeOf (same mechanism as the host tool - * registry). Mutable state lives in plain objects reached by one property - * read — field assignment through the tracker's shadow proxy is off-limits, - * as are `#` hard-private fields. + * read the session tag with `scopeOf`. Mutable state must remain reachable + * through one property read; assignment through the tracker proxy and `#` + * private fields bypass that rebinding. */ import { Service } from 'cordis' import type { Context } from 'cordis' diff --git a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx index 04d1dd867d..a27a2662b7 100644 --- a/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx +++ b/packages/client/ui-conversation/src/client/skeleton/InputBar.tsx @@ -1,12 +1,5 @@ -// InputBar: the one composer input (figma Input_Bottom). The same component -// serves the empty state (variant='hero': centered launch card) and the -// resident composer (variant='composer') — the empty→content transition is a -// position move of this component, never a swap (layout ruling). Running -// LOCKS the input: textarea disabled with the draft visible, stop is the only -// action; the turn ending re-enables and refocuses. -// -// Bottom chrome (attach / Plan / Read-only / model) is visual-only for now — -// local native