Merge origin/master into worktree/remove-sdk-project-toolchain

# Conflicts:
#	.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
#	.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
#	.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.i18n.yaml
#	.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.md
#	.agents/notes/implemented/feature/2026-07-21-continuable-background-subagents.zh.md
#	.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.i18n.yaml
#	.agents/notes/proposed/architecture/2026-07-15-sdk-project-editing-architecture.md
#	.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.i18n.yaml
#	.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.md
#	.agents/notes/proposed/feature/2026-07-14-sdk-developer-projects.zh.md
#	packages/README.i18n.yaml
#	packages/README.md
#	packages/scaffold/create-sdk/README.md
#	packages/scaffold/create-sdk/src/args.ts
#	packages/scaffold/scripts/src/args.ts
#	packages/sdk/README.i18n.yaml
#	scripts/verify-package-readme-model-experience.ts
This commit is contained in:
Tianyi Cui
2026-08-11 15:56:06 +08:00
633 changed files with 1799 additions and 1414 deletions

View File

@@ -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 packages/README.md
README.md: a0960bdd6f943659181c865ebb6a49940507571f
README.md: aea083505cf84207a12086361e5d7f41176c0241
README.zh.md: 013806e802f524b34757bb2de073625eb8b0f768

View File

@@ -10,48 +10,48 @@ Groups hold `packages/<group>/<pkg>/`; names stay `@deepseek-ai/dsh-<pkg>`. **Gr
| Group | Role | Release expectation |
|---|---|---|
| [`core/`](core/README.md) | Product API spine: sessions, prompts, tools, agent services, and the concrete loop | Product — stable surface |
| [`api/`](api/README.md) | Remote BFF assembly and TypeRT RPC gateway | Product — stable surface |
| [`typert/`](typert/README.md) | Type graph generation, artifact loading, and runtime registry | Product — stable surface |
| [`goal/`](goal/README.md) | Same-session goal persistence and lifecycle | Product — stable surface |
| [`feedback/`](feedback/README.md) | Human feedback | Product — stable surface |
| [`llm/`](llm/README.md) | LLM capability family: the abstract service + provider adapters | Product — stable surface |
| [`core/`](core/README.md) | Product API spine: sessions, prompts, tools, agent services, and the concrete loop | Product — stable API |
| [`api/`](api/README.md) | Remote BFF assembly and TypeRT RPC gateway | Product — stable API |
| [`typert/`](typert/README.md) | Type graph generation, artifact loading, and runtime registry | Product — stable API |
| [`goal/`](goal/README.md) | Same-session goal persistence and lifecycle | Product — stable API |
| [`feedback/`](feedback/README.md) | Human feedback | Product — stable API |
| [`llm/`](llm/README.md) | LLM capability family: the abstract service + provider adapters | Product — stable API |
| [`e2b/`](e2b/README.md) | E2B providers | POC |
| [`subprocess/`](subprocess/README.md) | Subprocess capability family: Service Definition + local process-tree provider | Product — stable surface |
| [`bash/`](bash/README.md) | Bash capability family: executor seam, local impl, model-facing tool | Product — stable surface |
| [`pty/`](pty/README.md) | Persistent PTY capability family: owner-scoped sessions, local implementation, and model-facing tools | Product — stable surface |
| [`code-runtime/`](code-runtime/README.md) | Code-execution capability family: Service Definition + worker-thread provider + Code Mode Consumer | Product — stable surface |
| [`sandbox/`](sandbox/README.md) | Process-confinement seam; bwrap/Landlock/Seatbelt backends | Product — stable surface |
| [`fs/`](fs/README.md) | Filesystem capability family: seam, local impl, model-facing file tools, bash-backed discovery tools | Product — stable surface |
| [`lsp/`](lsp/README.md) | LSP capability family: seam, generic stdio provider, and the `lsp` tool | Product — stable surface |
| [`skill/`](skill/README.md) | Skill capability family: the provider registry, local provider, and model-facing catalog/loader | Product — stable surface |
| [`compact/`](compact/README.md) | Compaction capability family: Service Definition + basic provider + command Consumer | Product — stable surface |
| [`context/`](context/README.md) | Model-visible request context, including workspace instructions and time context | Product — stable surface |
| [`subagent/`](subagent/README.md) | Subagent capability family: the provider-registry contract and the model-facing delegation tool | Product — stable surface |
| [`tasks/`](tasks/README.md) | Generic background-task runtime and model-facing `task_*` control tools | Product — stable surface |
| [`workflow/`](workflow/README.md) | Workflow seam, worker-thread engine, and model-facing `workflow`/`ralph` tools | Product — stable surface |
| [`web/`](web/README.md) | Web capability family: seam, search/fetch provider impls, and the model-facing web tools | Product — stable surface |
| [`attachment/`](attachment/README.md) | Durable attachment identity, validation, local content-addressed storage | Product — stable surface |
| [`spill/`](spill/README.md) | Spill capability family: storage seam, local impl, tool-result spill policy | Product — stable surface |
| [`todo/`](todo/README.md) | The model-facing `todo_write` tool | Product — stable surface |
| [`plan/`](plan/README.md) | Plan collaboration state with a direct entry command and reviewed exit | Product — stable surface |
| [`preset/`](preset/README.md) | Per-session agent composition from preset `cordis.yml` files | Product — stable surface |
| [`guard/`](guard/README.md) | Loop-hygiene guards: advisory repeat-call reminders + the `tools/execute` deadline enforcer | Product — stable surface |
| [`bundle/`](bundle/README.md) | Installable `dsh --profile` patch layers | Product — stable surface |
| [`self-modification/`](self-modification/README.md) | Agent runtime self-modification: live plugin/service inspection and model-written plugin mount/unmount ([design](../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)) | Product — stable surface |
| [`hooks/`](hooks/README.md) | Hook bridges + the shared Claude Code / Codex wire-protocol library | Product — stable surface |
| [`session/`](session/README.md) | Durable session data plane: persistence seam + JSONL/SQLite backends, projection seam, log-backed titles, session reporting | Product — stable surface |
| [`session-query/`](session-query/README.md) | Session retrieval family: logical corpus, bounded reads, lineage, event relationships, semantic filtering, and SQLite full-text search | Product — stable surface |
| [`settings/`](settings/README.md) | User-settings seam + file-backed provider | Product — stable surface |
| [`credentials/`](credentials/README.md) | Credential-reference seam + env-over-`.env` provider | Product — stable surface |
| [`storage/`](storage/README.md) | Non-session storage hub + backends + domain form | Product — stable surface |
| [`workspace/`](workspace/README.md) | Workspace entity | Product — stable surface |
| [`sdk/`](sdk/README.md) | Out-of-process runtime SDK: JSON-RPC protocol, TypeScript client, and server plugin | Product — stable surface |
| [`acp/`](acp/README.md) | Automation-only Agent Client Protocol server | Product — stable surface |
| [`interaction/`](interaction/README.md) | Human-collaboration plane: approval/interaction seams, permission preset, commands, ask-user tool | Product — stable surface |
| [`boot/`](boot/README.md) | Shared app-bin boot glue | Product — stable surface |
| [`host/`](host/README.md) | Web-GUI host half: API gateway + HTTP route server | Product — stable surface |
| [`client/`](client/README.md) | Web-GUI browser half: shell, wire, object services, slots, `ui-*` plugins | Product — stable surface |
| [`subprocess/`](subprocess/README.md) | Subprocess capability family: Service Definition + local process-tree provider | Product — stable API |
| [`bash/`](bash/README.md) | Bash capability family: executor seam, local impl, model-facing tool | Product — stable API |
| [`pty/`](pty/README.md) | Persistent PTY capability family: owner-scoped sessions, local implementation, and model-facing tools | Product — stable API |
| [`code-runtime/`](code-runtime/README.md) | Code-execution capability family: Service Definition + worker-thread provider + Code Mode Consumer | Product — stable API |
| [`sandbox/`](sandbox/README.md) | Process-confinement seam; bwrap/Landlock/Seatbelt backends | Product — stable API |
| [`fs/`](fs/README.md) | Filesystem capability family: seam, local impl, model-facing file tools, bash-backed discovery tools | Product — stable API |
| [`lsp/`](lsp/README.md) | LSP capability family: seam, generic stdio provider, and the `lsp` tool | Product — stable API |
| [`skill/`](skill/README.md) | Skill capability family: the provider registry, local provider, and model-facing catalog/loader | Product — stable API |
| [`compact/`](compact/README.md) | Compaction capability family: Service Definition + basic provider + command Consumer | Product — stable API |
| [`context/`](context/README.md) | Model-visible request context, including workspace instructions and time context | Product — stable API |
| [`subagent/`](subagent/README.md) | Subagent capability family: the provider-registry contract and the model-facing delegation tool | Product — stable API |
| [`tasks/`](tasks/README.md) | Generic background-task runtime and model-facing `task_*` control tools | Product — stable API |
| [`workflow/`](workflow/README.md) | Workflow seam, worker-thread engine, and model-facing `workflow`/`ralph` tools | Product — stable API |
| [`web/`](web/README.md) | Web capability family: seam, search/fetch provider impls, and the model-facing web tools | Product — stable API |
| [`attachment/`](attachment/README.md) | Durable attachment identity, validation, local content-addressed storage | Product — stable API |
| [`spill/`](spill/README.md) | Spill capability family: storage seam, local impl, tool-result spill policy | Product — stable API |
| [`todo/`](todo/README.md) | The model-facing `todo_write` tool | Product — stable API |
| [`plan/`](plan/README.md) | Plan collaboration state with a direct entry command and reviewed exit | Product — stable API |
| [`preset/`](preset/README.md) | Per-session agent composition from preset `cordis.yml` files | Product — stable API |
| [`guard/`](guard/README.md) | Loop-hygiene guards: advisory repeat-call reminders + the `tools/execute` deadline enforcer | Product — stable API |
| [`bundle/`](bundle/README.md) | Installable `dsh --profile` patch layers | Product — stable API |
| [`self-modification/`](self-modification/README.md) | Agent runtime self-modification: live plugin/service inspection and model-written plugin mount/unmount ([design](../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)) | Product — stable API |
| [`hooks/`](hooks/README.md) | Hook bridges + the shared Claude Code / Codex wire-protocol library | Product — stable API |
| [`session/`](session/README.md) | Durable session data plane: persistence seam + JSONL/SQLite backends, projection seam, log-backed titles, session reporting | Product — stable API |
| [`session-query/`](session-query/README.md) | Session retrieval family: logical corpus, bounded reads, lineage, event relationships, semantic filtering, and SQLite full-text search | Product — stable API |
| [`settings/`](settings/README.md) | User-settings seam + file-backed provider | Product — stable API |
| [`credentials/`](credentials/README.md) | Credential-reference seam + env-over-`.env` provider | Product — stable API |
| [`storage/`](storage/README.md) | Non-session storage hub + backends + domain form | Product — stable API |
| [`workspace/`](workspace/README.md) | Workspace entity | Product — stable API |
| [`sdk/`](sdk/README.md) | Out-of-process runtime SDK: JSON-RPC protocol, TypeScript client, and server plugin | Product — stable API |
| [`acp/`](acp/README.md) | Automation-only Agent Client Protocol server | Product — stable API |
| [`interaction/`](interaction/README.md) | Human-collaboration plane: approval/interaction seams, permission preset, commands, ask-user tool | Product — stable API |
| [`boot/`](boot/README.md) | Shared app-bin boot glue | Product — stable API |
| [`host/`](host/README.md) | Web-GUI host half: API gateway + HTTP route server | Product — stable API |
| [`client/`](client/README.md) | Web-GUI browser half: shell, wire, object services, slots, `ui-*` plugins | Product — stable API |
| [`examples/`](examples/README.md) | Demo bundles (agent-spine + CLI/ACP/JSON-RPC bins) leaves load | Support — example infra |
| [`support/`](support/README.md) | Support infrastructure (testkits, invariants, replay, Loader smokes) | Support — lower compatibility expectations |
| [`util/`](util/README.md) | Low-level zero-dependency utilities shared across groups (`Branded<B>`, Harness home/path helpers, timeout, retention) | Support — small, stable, harness-dep-free |

View File

@@ -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 packages/api/remotes/README.md
README.md: 3d9de0955faefe37c95ff8bb792d57c4fa1f1a3a
README.md: 567ece0fb58d4b9c0b022dd2ce4d8ee87caccc57
README.zh.md: 12add6f8efc5b6af3e9b74b26a1abb2bb3936e0a

View File

@@ -20,7 +20,7 @@ The package-local `clientBundle(..., { hostPhase: true })` makes Host tsdown bun
## Model Experience
None, as this BFF selects Remote application methods and identity policy but registers no model surface.
None, as this BFF selects Remote application methods and identity policy but registers nothing model-facing.
#### KV Cache effect

View File

@@ -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 packages/bash/bash-local/README.md
README.md: cb8e7f0ae766d9b1c5f1678e77d35992085d3d52
README.zh.md: 20af9c18998c6f3f0403c50f3a8ac599607dc094
README.md: 253d1c9efb518ca204956f062b2f529f4e83531b
README.zh.md: 5a0d943a08d1da6c73ad07f469c056180b476ff3

View File

@@ -25,7 +25,7 @@ The package root exports the default and named `LocalBashExecutor` plugin plus i
- **Spawn per call, no shell state** — every call is a fresh non-login `bash -c` with no rc files.
- **Configured budgets over managed groups** — `resolve()` fills `workdir`/`timeoutMs`/`stdoutMaxBytes` from config, and every spawn hands the service explicit byte caps, spill cap, and `graceMs`. The grace must be positive, finite, and no greater than [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md), so Node can represent it with one timer. Process-group kills, post-exit pipe draining, tail retention, and bounded spill files are [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) mechanics. A foreground `BashExecRequest.stdoutMaxBytes` can raise stdout's capture budget for one trusted caller; stderr and background runs still use `maxOutputBytes`.
- **Timeout and cancel classification** — `run()` fuses its config-clamped timeout with the caller's signal through one deadline; only the executor's own timeout reports `timedOut`, an upstream cancel reports `aborted`, and a self-signaled command reports neither ([timeout-library Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md)).
- **Model-friendly terminal env** — `NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` prevents pagers and ANSI color from garbling results. These values merge as ordinary env under the service's credential scrub and `DSH_*` channel rules; an explicit caller entry still wins. See the [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) and [managed environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md).
- **Model-friendly terminal env** — `NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` prevents pagers and ANSI color from garbling results. These values merge as ordinary env under the service's credential scrub and `DSH_*` channel rules; an explicit caller entry still wins. See the [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md) and [managed environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md).
- **Background processes** — `start()` returns a live `BashProcess` handle immediately with no timeout, and `readOutput()` merges offset-based stdout/stderr reads into one consuming delta, placing stderr under a `[stderr]` marker when present. A running process belongs to the subprocess service, survives executor reloads, and is killed and joined on service disposal. Task ids, ownership, polling, and notices belong to the generic [`ctx.tasks` runtime](../../tasks/tasks/README.md), which the tool layer registers the handle with.
## Model Experience

View File

@@ -25,7 +25,7 @@
- **每次调用都 spawn不保留 shell 状态**:每次调用都启动新的非登录 `bash -c`,且不读取 rc 文件。
- **在受管进程组之上应用配置预算**`resolve()` 从配置补全 `workdir``timeoutMs``stdoutMaxBytes`,每次 spawn 都向服务传入显式的字节上限、spill 上限与 `graceMs`。该宽限期须为正有限值,且不得大于 [`MAX_TIMER_DELAY_MS`](../../util/timeout/README.md),这样 Node 就能用一个定时器表示它。进程组终止、退出后管道排空、尾部保留与有界 spill 文件是 [`dsh-subprocess-local`](../../subprocess/subprocess-local/README.md) 的机制。前台 `BashExecRequest.stdoutMaxBytes` 可为某个受信任调用方提高单次 stdout 捕获预算stderr 和后台运行仍使用 `maxOutputBytes`
- **超时与取消分类**`run()` 通过同一个 deadline 把经配置钳位的超时与调用方的信号融合;只有执行器自身的超时报告 `timedOut`,上游取消报告 `aborted`,自身因信号终止的命令两者皆不报告(见[超时库 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md))。
- **适合模型的终端环境**`NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` 防止分页器与 ANSI 颜色破坏结果。这些值作为普通 env 合并,遵循服务的凭据清除与 `DSH_*` 通道规则;调用方的显式条目依旧优先。详见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) 与 [受管环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
- **适合模型的终端环境**`NO_COLOR=1 TERM=dumb PAGER=cat GIT_PAGER=cat` 防止分页器与 ANSI 颜色破坏结果。这些值作为普通 env 合并,遵循服务的凭据清除与 `DSH_*` 通道规则;调用方的显式条目依旧优先。详见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md) 与 [受管环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
- **后台进程**`start()` 会立即返回活动的 `BashProcess` 句柄且不应用超时;`readOutput()` 把基于偏移量的 stdout/stderr 读取合并为一条消费式增量,并在存在 stderr 时将其置于 `[stderr]` 标记下。运行中的进程属于 subprocess 服务,可在执行器重载后存活,并在服务 dispose 时被终止且等待退出。task id、所有权、轮询和通知属于通用 [`ctx.tasks` 运行时](../../tasks/tasks/README.md),工具层会在其中注册该句柄。
## 模型体验

View File

@@ -307,7 +307,7 @@ describe('process lifecycle ownership (the subprocess service, not the executor)
const bash = ctx.bash as LocalBashExecutor
// The child prints its own pid ($$ = the detached bash group leader) so
// the test can probe liveness through the public read surface alone.
// the test can probe liveness through the public read API alone.
const proc = bash.start(bash.resolve({ command: 'echo $$; sleep 60' }))
const pid = Number((await readUntil(proc, '\n')).trim())
expect(Number.isInteger(pid) && pid > 0).toBe(true)

View File

@@ -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 packages/bash/bash/README.md
README.md: 23b0acd096bb835ef57563337c91e5cf63b58677
README.zh.md: 14ba749a0018bd6a63475bc0ab72c6fe6d26893a
README.md: a076c6ef3150de86c251f0501b18de01c4491c67
README.zh.md: 1be9f817a8379b2974fb4cfd63c197c320934f00

View File

@@ -33,7 +33,7 @@ Implementations subclass `BashExecutor` and implement the abstract methods. Disp
The per-session sandbox-mode override vocabulary (the `'sandbox/mode'` event, the `effectiveSandboxMode(events)` fold, and the `setSandboxMode(session, mode)` write path) is NOT here — it is policy state shared by every enforcing family, owned by [`@deepseek-ai/dsh-sandbox-policy`](../../sandbox/sandbox-policy/). `run()` returns `BashRunResult`; `start()` returns `BashProcess`, whose incremental read and kill methods are adapted by `dsh-tool-bash` into a generic task registration. A sandboxing executor stamps `BashSandboxInfo` on foreground results and settled process handles. See `src/types.ts` and [subsystems/bash.md](../../../docs/subsystems/bash.md).
`stdin` and ordinary `env` are set by in-process plugins (the hooks bridges, native plugins) to feed a hook command its JSON payload and `CLAUDE_PROJECT_DIR`/`CLAUDE_PLUGIN_ROOT` values. `dshEnv` is a separate trusted overlay restricted by type to managed keys; the exported `DSH_ENV_PREFIX` is the single source for that namespace, its `DshEnvironmentKey` template type, executor scrubbing, registry validation, derived built-in names, and model guidance. Model bash uses the current snapshot collected by `ctx.bashEnv`. Implementations remove inherited managed keys, then merge `dshEnv` after ordinary `env`, so an omitted current fact cannot fall back to stale ambient state and an `env` entry cannot displace a managed value. The model-facing tool exposes none of these as parameters. All three remain optional on the resolved spec; absent means no input/overlay. See [the bash-stdin-env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) and [the session environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md).
`stdin` and ordinary `env` are set by in-process plugins (the hooks bridges, native plugins) to feed a hook command its JSON payload and `CLAUDE_PROJECT_DIR`/`CLAUDE_PLUGIN_ROOT` values. `dshEnv` is a separate trusted overlay restricted by type to managed keys; the exported `DSH_ENV_PREFIX` is the single source for that namespace, its `DshEnvironmentKey` template type, executor scrubbing, registry validation, derived built-in names, and model guidance. Model bash uses the current snapshot collected by `ctx.bashEnv`. Implementations remove inherited managed keys, then merge `dshEnv` after ordinary `env`, so an omitted current fact cannot fall back to stale ambient state and an `env` entry cannot displace a managed value. The model-facing tool exposes none of these as parameters. All three remain optional on the resolved spec; absent means no input/overlay. See [the bash-stdin-env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md) and [the session environment Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md).
The exported `parseExitStatus` (with `ParsedExitStatus`) is the shared rendering contract half of the shell tools: the inverse of the `[exit code: N]` / `[killed by signal: X]` markers `dsh-tool-bash`'s `renderResult` and `dsh-tool-pwsh`'s `renderPwshResult` append. Both tools' `presentResult` use it to split the rendered text into the terminal card's output body and its exit-status pill; it lives with the Service Definition so the two tools never drift on the marker contract.

View File

@@ -33,7 +33,7 @@
每会话沙箱模式覆盖词汇(`'sandbox/mode'` 事件、`effectiveSandboxMode(events)` fold 以及 `setSandboxMode(session, mode)` 写入路径)不位于此处。它是所有强制执行家族共享的策略状态,属于 [`@deepseek-ai/dsh-sandbox-policy`](../../sandbox/sandbox-policy/)。`run()` 返回 `BashRunResult``start()` 返回 `BashProcess`,其增量读取与终止方法由 `dsh-tool-bash` 适配为通用任务注册。沙箱执行器会在前台结果与已结算进程句柄上标记 `BashSandboxInfo`。详见 `src/types.ts` 与 [subsystems/bash.md](../../../docs/subsystems/bash.md)。
`stdin` 与普通 `env` 由同进程插件hooks 桥接、原生插件)设置,用于向 hook 命令提供其 JSON payload 和 `CLAUDE_PROJECT_DIR``CLAUDE_PLUGIN_ROOT` 值。`dshEnv` 是受类型限制、仅允许受管 key 的独立受信任 overlay导出的 `DSH_ENV_PREFIX` 是该 namespace、其 `DshEnvironmentKey` 模板类型、执行器清理、注册表验证、派生内置名称与模型指引的统一来源。模型 bash 使用 `ctx.bashEnv` 收集的当前快照。实现会移除继承的受管 key再在普通 `env` 之后合并 `dshEnv`,因此省略的当前事实不会回退到陈旧环境状态,`env` 条目也无法顶掉受管值。面向模型的工具不将这三者中的任何一个公开为参数。这三者在已解析 spec 上仍然可选缺失表示没有输入overlay。详见 [bash-stdin-env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md) 与 [会话环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
`stdin` 与普通 `env` 由同进程插件hooks 桥接、原生插件)设置,用于向 hook 命令提供其 JSON payload 和 `CLAUDE_PROJECT_DIR``CLAUDE_PLUGIN_ROOT` 值。`dshEnv` 是受类型限制、仅允许受管 key 的独立受信任 overlay导出的 `DSH_ENV_PREFIX` 是该 namespace、其 `DshEnvironmentKey` 模板类型、执行器清理、注册表验证、派生内置名称与模型指引的统一来源。模型 bash 使用 `ctx.bashEnv` 收集的当前快照。实现会移除继承的受管 key再在普通 `env` 之后合并 `dshEnv`,因此省略的当前事实不会回退到陈旧环境状态,`env` 条目也无法顶掉受管值。面向模型的工具不将这三者中的任何一个公开为参数。这三者在已解析 spec 上仍然可选缺失表示没有输入overlay。详见 [bash-stdin-env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md) 与 [会话环境 Agent Note](../../../.agents/notes/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md)。
导出的 `parseExitStatus`(连同 `ParsedExitStatus`)是 shell 工具共享渲染约定的另一半:`dsh-tool-bash``renderResult``dsh-tool-pwsh``renderPwshResult` 追加的 `[exit code: N]``[killed by signal: X]` marker 的逆解析。两个工具的 `presentResult` 都用它把渲染文本拆成 terminal 卡的输出正文与其退出状态 pill它放在 Service Definition 中,两个工具便永远不会在 marker 约定上漂移。

View File

@@ -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 packages/bash/tool-bash/README.md
README.md: 9e2cd0c2ed999a11dcbffcd99a1d0cb672367905
README.zh.md: 8a1e7ba0af36ffd891b38491075ee75283fdbed5
README.md: d5483e5e266a5362d9a8d324d71046591fe146bc
README.zh.md: 527543ea7130cb89e7e9776cb71adec842527e85

View File

@@ -42,7 +42,7 @@ The tool owns its `presentCall`/`presentResult` render intent. A foreground call
## The tool builds its request from named args only
`BashExecRequest` carries optional `stdoutMaxBytes`, `stdin`, ordinary `env`, and managed `dshEnv`, used by trusted in-process plugins and this tool's environment registry. The model-facing tool exposes none of `stdoutMaxBytes`, `stdin`, or `env`: it builds requests from named command/workdir/timeout/signal/sandbox fields plus the registry-collected `dshEnv`. Extra model keys are ignored and cannot replace managed values. Shell syntax provides equivalent command-level behavior, while the local executor scrubs ambient credentials and stale `DSH_*` values. See the [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md).
`BashExecRequest` carries optional `stdoutMaxBytes`, `stdin`, ordinary `env`, and managed `dshEnv`, used by trusted in-process plugins and this tool's environment registry. The model-facing tool exposes none of `stdoutMaxBytes`, `stdin`, or `env`: it builds requests from named command/workdir/timeout/signal/sandbox fields plus the registry-collected `dshEnv`. Extra model keys are ignored and cannot replace managed values. Shell syntax provides equivalent command-level behavior, while the local executor scrubs ambient credentials and stale `DSH_*` values. See the [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md).
## Permissions and escalation

View File

@@ -42,7 +42,7 @@
## 工具仅使用具名参数构建请求
`BashExecRequest` 携带可选的 `stdoutMaxBytes``stdin`、普通 `env` 和托管 `dshEnv`,供可信进程内插件及此工具的环境注册表使用。模型侧工具不公开 `stdoutMaxBytes``stdin``env`:它使用具名的命令/工作目录/超时/信号/沙箱字段,加上从注册表收集的 `dshEnv` 来构建请求。额外模型键会被忽略无法替换托管值。Shell 语法可以提供等价的命令级行为,而本地执行器会清除环境中的凭据和陈旧 `DSH_*` 值。参见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-surface.md)。
`BashExecRequest` 携带可选的 `stdoutMaxBytes``stdin`、普通 `env` 和托管 `dshEnv`,供可信进程内插件及此工具的环境注册表使用。模型侧工具不公开 `stdoutMaxBytes``stdin``env`:它使用具名的命令/工作目录/超时/信号/沙箱字段,加上从注册表收集的 `dshEnv` 来构建请求。额外模型键会被忽略无法替换托管值。Shell 语法可以提供等价的命令级行为,而本地执行器会清除环境中的凭据和陈旧 `DSH_*` 值。参见 [stdin/env Agent Note](../../../.agents/notes/implemented/architecture/2026-06-30-bash-stdin-env-trusted-plugin-api.md)。
## 权限与升权

View File

@@ -66,7 +66,7 @@ export function renderResult(
* Shape one background-process read into the `task_output` delta the model
* sees: the incremental delta, plus the lossy-read notice (with full-stream
* spill paths) when in-memory truncation dropped unread bytes. Empty-delta
* rendering (`(no new output)`) is the generic control surface's job.
* rendering (`(no new output)`) is the generic task controller's job.
* @param read - one incremental read from the process handle.
* @param sandbox - settled sandbox facts, when this was a confined process.
* @param escalationModes - escalation targets advertised by this composition.

View File

@@ -42,7 +42,7 @@ async function setup() {
return ctx
}
/** Full harness: the generic task runtime + its control surface, then the bash tool. */
/** Full harness: the generic task runtime + its controller, then the bash tool. */
async function setupWithTasks() {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
@@ -524,7 +524,7 @@ describe('background execution through the task runtime', () => {
})
it('never spawns the process when tasks.start preflight throws (no orphan, by construction)', async () => {
// With no control surface, task preflight fails before the executor can spawn.
// With no task controller, preflight fails before the executor can spawn.
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
@@ -536,7 +536,7 @@ describe('background execution through the task runtime', () => {
const result = await call(ctx, 'bash', { command: 'sleep 60', description: 'test command', run_in_background: true })
expect(result.isError).toBe(true)
expect(text(result)).toContain('no control surface serves this agent')
expect(text(result)).toContain('no task controller serves this agent')
// Declare-then-execute: the failed preflight means no process ever ran.
expect((ctx.bash as CountingStartExecutor).starts).toBe(0)
})

View File

@@ -139,7 +139,7 @@ async function setup(toolConfig: Partial<ToolPwsh.Config> = {}, dshHome?: string
return { ctx, bash }
}
/** Full harness: the generic task runtime + its control surface, then the pwsh tool. */
/** Full harness: the generic task runtime + its controller, then the pwsh tool. */
async function setupWithTasks(toolConfig: Partial<ToolPwsh.Config> = {}, dshHome?: string) {
const ctx = new Context()
await ctx.plugin(SystemPrompt)
@@ -766,7 +766,7 @@ describe('background execution through the task runtime', () => {
})
it('never spawns the process when tasks.start preflight throws (no orphan, by construction)', async () => {
// With no control surface, task preflight fails before the executor can spawn.
// With no task controller, preflight fails before the executor can spawn.
const ctx = new Context()
await ctx.plugin(SystemPrompt)
await ctx.plugin(ToolRegistry)
@@ -778,7 +778,7 @@ describe('background execution through the task runtime', () => {
const result = await call(ctx, 'pwsh', { command: 'Start-Sleep -Seconds 60', description: 'test command', run_in_background: true })
expect(result.isError).toBe(true)
expect(text(result)).toContain('no control surface serves this agent')
expect(text(result)).toContain('no task controller serves this agent')
// Declare-then-execute: the failed preflight means no process ever ran.
expect(bash.startCalls).toBe(0)
})

View File

@@ -190,7 +190,7 @@ function ensureSymlink(link: string, target: string): void {
// Concurrent launches heal the same fallback; losing the race to a
// process writing the identical link is success, anything else is not.
// The window between the lstat miss above and this write cannot be
// staged deterministically from the public surface.
// staged deterministically from the public API.
/* v8 ignore next 4 */
if ((error as NodeJS.ErrnoException).code !== 'EEXIST'
|| !lstatSync(link).isSymbolicLink() || readlinkSync(link) !== target) {

View File

@@ -387,7 +387,7 @@
thresholds: [3, 5, 8]
argumentsPreviewChars: 500
# Every mode enables the stable web_search model surface. DeepSeek search
# Every mode enables the stable model-facing web_search tool. DeepSeek search
# resolves the same DEEPSEEK_API_KEY credential the Models page manages for
# chat, at each search; its Messages endpoint is separate from the
# chat-completions endpoint, so it takes its own base-URL override. Fetch stays

View File

@@ -13,7 +13,7 @@ The [slot system standard](../../.agents/notes/implemented/architecture/2026-07-
3. **Component props are the four shares, all derived**: `PropsRuntime<K>` (SlotMap: owner params + `useSession`/`sessionId` on session scope + global `useSessions`/`useWorkspaces`) & `PropsRenderSlots<S>` (children keys) & `PropsStore<H>` (store factory) & the inject face. Never hand-write a member a share already derives; never re-type a share locally.
4. **Hooks are framework-made only**: `useSession`, `useSessions`, `useWorkspaces`, `useStore`, `renderSlot` are the five standing seats, plus the `use<Name>` hooks the renderer binds from provide contributions and inject `hooks` compartments. Business code never creates a hook or selector as a prop value — pass plain data and callbacks. (Component-internal behavioral hooks that subscribe to nothing external are fine.)
5. **Live data has exactly three channels**: parent knows it → owner props at the renderSlot site; only the component knows it → local state; shared across entries or survives remounts → a store declared at register. Derived data is a pure function over framework-hook data (`useMemo`), never its own subscription.
6. **Stores: read `props.useStore`, write `props.actions.*`** — the declared actions are the complete mutation surface. Write the store as an exported `createXXXStore()` factory (module-level handles are forbidden — de-facto singletons); share by passing one handle to several registers inside `apply`. Production code never calls the factory or `.create()` outside `apply`; tests do (that is the sanctioned zero-machinery path).
6. **Stores: read `props.useStore`, write `props.actions.*`** — the declared actions are the complete mutation API. Write the store as an exported `createXXXStore()` factory (module-level handles are forbidden — de-facto singletons); share by passing one handle to several registers inside `apply`. Production code never calls the factory or `.create()` outside `apply`; tests do (that is the sanctioned zero-machinery path).
7. **inject returns plain data and callbacks** from the apply closure's own ctx — no hand-made hooks, no ReactNode producers, no whole-service objects. A registrant-private reactive fact uses the reserved `hooks` compartment (bare observables the renderer binds to `use<Name>`; components never see the sources). The plugin may use only the dependencies named by its `inject` declaration; there is no wider ctx to reach for.
## Reactive read and contract-currency discipline
@@ -32,7 +32,7 @@ How live data reaches render code, and what UI domains may share:
The `/client` entrypoint of a UI plugin package is its public browser API, not a convenience barrel. Three rules apply package-wide (do not restate them as per-file comments):
1. **A UI plugin exports no values beyond what cordis loading needs**`apply` / `inject` (and `Config` where present), plus store factories consumed type-only by components (`ReturnType<typeof createXXXStore>`). Shared types (owner data, injected values, composed prop aliases) may also be exported. Implementation components, pure helpers, constants, and store handles stay internal. Adding any new value export requires user sign-off, not a matching consumer.
2. **Same-package tests import internals directly** — relative `../src/client/xxx.ts` from package tests, or the `./src/*` subpath where a spec lives outside the package. Never widen the public surface to make a test compile.
2. **Same-package tests import internals directly** — relative `../src/client/xxx.ts` from package tests, or the `./src/*` subpath where a spec lives outside the package. Never widen the public API to make a test compile.
3. **Cross-package imports of another plugin's symbols are in principle forbidden.** The sanctioned routes are the slot system (register/renderSlot) and ctx services. If neither fits, stop and escalate — do not add an export to unblock yourself.
## ctx discipline (components never see ctx)

View File

@@ -71,7 +71,7 @@ const MARKDOWN_FIXTURE = [
'- first item',
' - nested item',
'',
'| Surface | State |',
'| Area | State |',
'| --- | --- |',
'| history | rendered |',
'| streaming | stable |',

View File

@@ -46,7 +46,7 @@ export type { ClientConnectionRpc } from '../rpc.ts'
export const inject: string[] = []
/**
* The ctx.connection service surface: the api client plus a one-shot
* The ctx.connection service API: the API client plus a one-shot
* controller starter (the runtime plugin supplies sinks when its object layer
* is ready — connection stays consumer-agnostic).
*/

View File

@@ -87,7 +87,7 @@ describe('connection client apply', () => {
it('start() hands out one loop, rejects a second consumer, and stop() aborts the streams', async () => {
;(globalThis as Win).location = { hostname: 'localhost', search: '?fixture' }
const handle = await mount()
// config omitted: the `config ?? {}` default arm is part of the surface.
// config omitted: the `config ?? {}` default arm is part of the API.
const loop = handle.start({})
expect(() => handle.start({})).toThrow(/already owned by another consumer/)
loop.stop() // teardown must not throw; the fixture streams abort quietly

View File

@@ -16,7 +16,7 @@ export const inject = ['invariants']
/**
* No runtime invariant: ns-by-locale dictionary registry with a stable
* bind(ns) surface — it emits no cordis events and owns no cross-plugin
* bind(ns) API — it emits no cordis events and owns no cross-plugin
* mutable relation; fallback-chain resolution and locale-store behavior are
* asserted directly by this package's behavior specs.
*/

View File

@@ -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 packages/client/modules/README.md
README.md: a1d578850c2518a85dc32f048768b78caf5ffec4
README.md: efaff699839b977cc45f89f3c164402241b90dc2
README.zh.md: 772a4870f7ef6730d9d3d4db434ed771d97984f0

View File

@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
Client module system: the browser peer of Node's internal ESM loader, built as a lazy CJS table. The web shell mounts the vendored cordis Loader for entry governance (fiber lifecycle, inject waiting, update/refresh) and injects this package's `ClientModuleLoader` through its `internal` contract — the vendored side's only consumption point is `EntryTree.import`, so replacing `internal` replaces exactly "how plugin code arrives" and nothing else.
Lazy CJS model (web2): executing a plugin bundle only REGISTERS its factory (`window.__ModuleLoader__.load({id, factory})`); every module body side effect — CSS injection included — lives in the factory closure and runs at materialization (`factory(require)` → export surface, memoized in `loadCache`), not at script execution. A factory that requires another registered-but-unmaterialized module materializes it recursively, so load order needs no external sequencing; require cycles throw (factory-form CJS cannot deliver partial exports). `<id>/client` and the bare id name the same surface (a plugin bundle IS its package's client half).
Lazy CJS model (web2): executing a plugin bundle only REGISTERS its factory (`window.__ModuleLoader__.load({id, factory})`); every module body side effect — CSS injection included — lives in the factory closure and runs at materialization (`factory(require)` → exports, memoized in `loadCache`), not at script execution. A factory that requires another registered-but-unmaterialized module materializes it recursively, so load order needs no external sequencing; require cycles throw (factory-form CJS cannot deliver partial exports). `<id>/client` and the bare id resolve to the same exports (a plugin bundle IS its package's client half).
Resolution branch order (`import(specifier)`): platform seed word → shell instance; memoized record → surface; shell-own static registry (`registerStatic`, app-shell) → module; registered factory → materialize; graph row (`window.__DSH_BOOT__`) → load its external classic script + materialize; anything else throws — the runtime mirror of the build-time bundle purity gate. The synchronous `require` handed to factories walks the same order minus the asynchronous load branch and records observed edges into the module record. `prefetch` is the stage-one arrival hook (script load and factory registration only; concurrent calls share one in-flight task); `invalidate` drops the factory and materialized record so the next prefetch/import reloads the script (the HMR hook).

View File

@@ -10,13 +10,13 @@
* factory (`window.__ModuleLoader__.load({id, factory})`); every module body
* side effect — including CSS injection — lives inside the factory closure
* and runs at materialization, not at script execution. Materialization
* (factory(require) → export surface) happens on first import/require and is
* (factory(require) → exports) happens on first import/require and is
* memoized in {@link ClientModuleLoader.loadCache}; a factory that requires
* another registered-but-unmaterialized module materializes it recursively,
* so load order needs no external sequencing.
*
* Resolution branch order (import): seed word → shell instance; memoized
* record → surface; static registry (shell-own modules, e.g. app-shell) →
* record → exports; static registry (shell-own modules, e.g. app-shell) →
* module; registered factory → materialize; graph row → load + materialize;
* anything else → throw (loud — the runtime mirror of the
* build-time bundle purity gate). The synchronous `require` handed to
@@ -149,13 +149,13 @@ export interface ClientPluginHandoff {
id: string
/**
* Closure factory holding the whole bundle body: receives the synchronous
* require bound to the module table and returns the bundle's export
* surface. Runs once, at materialization.
* require bound to the module table and returns the bundle's exports. Runs
* once, at materialization.
*/
factory: (require: (spec: string) => unknown) => Record<string, unknown>
}
/** Window surface of the web boot protocol: the host-injected graph, the registration sink, and the kernel handoff slot. */
/** Window API of the web boot protocol: the host-injected graph, registration sink, and kernel handoff slot. */
export interface DshWindow {
/** Host-composed entry graph, injected before the shell bundle runs; wire-boundary raw until {@link parseBootManifest}. */
__DSH_BOOT__?: unknown
@@ -174,8 +174,8 @@ export interface DshWindow {
export interface ClientModuleRecord {
/** Module id (entry name / package name). */
id: string
/** The materialized export surface (factory `module.exports`, or the shell module for static registrations). */
surface: unknown
/** Materialized exports (`module.exports` from a factory, or a statically registered shell module). */
exports: unknown
/** Owned `<style data-plugin>` tag ids (`data-plugin-css` values) injected during materialization. */
styles: string[]
/** Observed `require()` edges (module-graph boundary; only table words can appear today). */
@@ -190,7 +190,7 @@ export interface ClientModuleRecord {
export interface ClientModuleLoader {
/** Discriminant against Node's internal loader shapes ('v1'/'v2'). */
version: 'client'
/** Materialized-module registry: id → record. The governance-side read face for entry export surfaces. */
/** Materialized-module registry: id → record. The governance-side read API for entry exports. */
loadCache: Map<string, ClientModuleRecord>
/**
* Internal contract consumed by the vendored Loader's `tree.import`. Resolves
@@ -199,7 +199,7 @@ export interface ClientModuleLoader {
* @param specifier - module specifier (entry name or table word).
* @param parentURL - importer URL (unused — the client module graph is flat).
* @param attrs - Import attributes (unused; interface parity with Node's loader contract).
* @returns the module's export surface.
* @returns the module's exports.
*/
import(specifier: string, parentURL: string, attrs: Record<string, unknown>): Promise<unknown>
/**

View File

@@ -28,7 +28,7 @@ const defaultLoadBundle = (url: string): Promise<void> => new Promise((resolve,
/**
* A plugin bundle IS its package's client half: `<id>/client` (the exports
* subpath external bundles emit) and the bare graph id name the same
* surface, so table lookups normalize the suffix away.
* exports, so table lookups normalize the suffix away.
*/
const stripClientSuffix = (spec: string): string =>
spec.endsWith('/client') ? spec.slice(0, -'/client'.length) : spec
@@ -123,8 +123,8 @@ export class ClientModuleSystem implements ClientModuleLoader {
this.materializing.add(id)
try {
const edges = new Set<string>()
const surface = registered(this.makeRequire(edges))
const record: ClientModuleRecord = { id, surface, styles: claimStyles(id), edges }
const exports = registered(this.makeRequire(edges))
const record: ClientModuleRecord = { id, exports, styles: claimStyles(id), edges }
this.loadCache.set(id, record)
return record
} finally {
@@ -146,8 +146,8 @@ export class ClientModuleSystem implements ClientModuleLoader {
if (this.statics.has(spec)) return this.statics.get(spec)
const id = stripClientSuffix(spec)
const record = this.loadCache.get(id)
if (record !== undefined) return record.surface
if (this.factories.has(id)) return this.materialize(id).surface
if (record !== undefined) return record.exports
if (this.factories.has(id)) return this.materialize(id).exports
throw new Error(
`client-modules: require("${spec}") missed the module table — not a platform seed word, not a shell-own module, `
+ 'and no registered factory (a build-time externals drift, or a forbidden cross-plugin value import)',
@@ -158,11 +158,11 @@ export class ClientModuleSystem implements ClientModuleLoader {
async import(specifier: string): Promise<unknown> {
if (this.seed.has(specifier)) return this.seed.get(specifier)
const existing = this.loadCache.get(specifier)
if (existing !== undefined) return existing.surface
if (existing !== undefined) return existing.exports
if (this.statics.has(specifier)) {
const surface = this.statics.get(specifier)
this.loadCache.set(specifier, { id: specifier, surface, styles: [], edges: new Set() })
return surface
const exports = this.statics.get(specifier)
this.loadCache.set(specifier, { id: specifier, exports, styles: [], edges: new Set() })
return exports
}
if (!this.factories.has(specifier)) {
const row = this.graphRows.get(specifier)
@@ -174,7 +174,7 @@ export class ClientModuleSystem implements ClientModuleLoader {
}
await this.arrive(row)
}
return this.materialize(specifier).surface
return this.materialize(specifier).exports
}
registerStatic(id: string, module: unknown): void {

View File

@@ -70,7 +70,7 @@ describe('lazy CJS arrival', () => {
expect(b.loader.loadCache.size).toBe(0)
})
it('import materializes once and memoizes the export surface', async () => {
it('import materializes once and memoizes the exports', async () => {
const ran: string[] = []
const b = bench([row('a')], { a: () => { ran.push('a'); return { marker: 'a' } } })
const first = await b.loader.import('a', '', {})
@@ -83,8 +83,8 @@ describe('lazy CJS arrival', () => {
it('import without prefetch loads, registers, and materializes in one call', async () => {
const b = bench([row('a')], { a: () => ({ marker: 'direct' }) })
const surface = await b.loader.import('a', '', {})
expect((surface as { marker: string }).marker).toBe('direct')
const exports = await b.loader.import('a', '', {})
expect((exports as { marker: string }).marker).toBe('direct')
expect(b.fetched).toHaveLength(1)
})
@@ -123,8 +123,8 @@ describe('require resolution', () => {
})
await b.loader.prefetch('a')
await b.loader.prefetch('b')
const surface = await b.loader.import('a', '', {})
expect((surface as { got: string }).got).toBe('from-b')
const exports = await b.loader.import('a', '', {})
expect((exports as { got: string }).got).toBe('from-b')
expect(order).toEqual(['a', 'b'])
expect(b.loader.loadCache.get('a')?.edges.has('b/client')).toBe(true)
expect(b.loader.loadCache.has('b')).toBe(true)
@@ -135,8 +135,8 @@ describe('require resolution', () => {
const b = bench([row('a')], {
a: req => ({ dep: req('react') }),
}, { seed: { react } })
const surface = await b.loader.import('a', '', {})
expect((surface as { dep: unknown }).dep).toBe(react)
const exports = await b.loader.import('a', '', {})
expect((exports as { dep: unknown }).dep).toBe(react)
expect(await b.loader.import('react', '', {})).toBe(react)
expect(b.loader.loadCache.has('react')).toBe(false)
})
@@ -284,8 +284,8 @@ describe('default transport seam', () => {
})
})
const loader: ClientModuleLoader = new ClientModuleSystem({ modules: [row('dee')], staticModules: {} })
const surface = await loader.import('dee', '', {})
expect((surface as { marker: string }).marker).toBe('via-script')
const exports = await loader.import('dee', '', {})
expect((exports as { marker: string }).marker).toBe('via-script')
expect(append).toHaveBeenCalledOnce()
expect([...document.querySelectorAll('script')]).toEqual([])
})

View File

@@ -18,7 +18,7 @@ import type {
} from '@deepseek-ai/dsh-client-ui-slots'
// Store contract types are ui-slots authority; re-exported beside the engine
// so store consumers get one import surface.
// so store consumers get one import path.
export type {
ActionsDecl, BakedActions, BoundActions, StoreFactory, StoreHandle, StoreInstance, StoreSpec,
} from '@deepseek-ai/dsh-client-ui-slots'
@@ -165,7 +165,7 @@ function deepFreeze(value: unknown): void {
/** A live engine instance: the contract instance plus the raw engine store. */
export interface EngineStoreInstance<T, A extends ActionsDecl<T>> extends StoreInstance<T, A> {
/** The underlying engine store (framework/test surface; components never see it). */
/** The underlying engine store (framework/test API; components never see it). */
readonly store: SnapshotStore<T>
}

View File

@@ -243,7 +243,7 @@ export function apply(ctx: Context): void {
workspaces.handleHostEnvelope(envelope)
// Typed-event bridge: the session layer ignores registry frames (no
// session routing); consumers (command directory caches, the settings
// and model surfaces) subscribe on ctx.
// and model services) subscribe on ctx.
const frame = envelope.payload
if (frame.type === 'host/commands-changed') ctx.emit('commands/changed')
else if (frame.type === 'host/session-preset-changed') {

View File

@@ -430,7 +430,7 @@ export class SessionManager {
}
}
// ---- List surface ----
// ---- List API ----
/** Full refresh via session.list (single-flight: an in-flight call is reused). */
refreshList(): Promise<void> {
@@ -629,7 +629,7 @@ export class SessionManager {
this.notifier.markDirty()
}
// ---- Subscription surface (for useSessionList) ----
// ---- Subscription API (for useSessionList) ----
/**
* uSES subscription entry for useSessionList.

View File

@@ -60,7 +60,7 @@ export interface SessionOptions {
* remaining public members are manager/runtime entry points.
*/
export class Session implements SessionFace {
// ---- Window and derived state (all private; the snapshot is the only read surface) ----
// ---- Window and derived state (all private; the snapshot is the only read API) ----
private events: SessionEvent[] = []
/** Wire views aligned with `events` by index (envelope-level annotations; undefined = no view).
* Kept parallel rather than merged so `events` stays the raw log slice (model-visible ⟺ logged). */
@@ -428,7 +428,7 @@ export class Session implements SessionFace {
await this.open()
}
// ---- Subscription surface (useSyncExternalStore direct wiring) ----
// ---- Subscription API (useSyncExternalStore direct wiring) ----
/**
* uSES subscription entry.

View File

@@ -25,7 +25,7 @@ const CSS_VIRTUAL_PREFIX = '\0dsh-css:'
const CSS_VIRTUAL_SUFFIX = '.mjs'
/**
* Wire/type layers a client bundle may inline: browser-safe contract surfaces
* Wire/type layers a client bundle may inline: browser-safe contracts
* with no runtime identity to share (no Symbol/instanceof/singleton state).
* Everything else under @deepseek-ai/* is either a module-table entry
* (external) or a leak the purity gate rejects.

View File

@@ -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 packages/client/ui-conversation/README.md
README.md: 605bba15d704c0c6e9f28abb3cddeb68bdd7e0d8
README.md: ed8f888d35693ecaa2667ea432b462f4bb3369cf
README.zh.md: e6a2dd0b545b66ab01b213b5ebc937e22af8ac1a

View File

@@ -40,7 +40,7 @@ The composer bar declares session-scoped single seats for `'conversation.input.p
The chat stats line takes its token accounting from the generic token-meter `tokenUsage` projection read through the standard-kit `useProjection`: billed input is uncached input plus cache reads and writes; cache hit divides cache reads by that total. Visible nodes supply only the turn and step counts plus the LLM and tool wall times, which are window-scoped facts about what is on screen rather than accounting; durable token and context groups remain visible when compaction leaves no assistant node in the loaded window. The same window fold averages each recorded step's TTFT and divides sampled output tokens by their summed decode spans into a latency/throughput group localized through the `conversation` locale namespace (`TTFT avg … · … tok/s` in English); a step missing a timing boundary or a usage sample drops out of those figures instead of skewing them. The turn-count, step-count, duration, cache, and token labels use the same namespace. Each settled turn additionally appends hover-revealed `TTFT {s}s · {tps} tok/s` labels to its assistant footer after the `Ran for` duration — the turn's first-step TTFT and its turn-aggregate decode throughput — gated on the turn's timing being in the loaded window (a contiguous log suffix, so an in-window turn carries every one of its steps) and omitting whichever figure is unrecorded. A deployment without token-meter drops the token groups; when the line overflows, it elides with an ellipsis and a delayed hover tooltip carries the full text only while actually clipped. Context occupancy renders as the composer's trailing ContextMeter: a 14px occupancy ring after the model seat, fed by `contextPressure` and rendered only once both a numerator and a route capacity are known, that click-opens a panel pairing the `percent used` header and `~used / capacity` figures with a color-segmented bar and `~`-prefixed heuristic composition rows (system prompt, tools, messages) from the `contextBreakdown` projection. The ring and header read `projectedTokens` — the provider sample carried forward over the surface's movement since — so a compaction registers immediately instead of after a further turn; the composition rows stay wholly heuristic and therefore still do not sum to the header ([rationale](../../llm/token-meter/README.md)). Occupancy is deliberately an approximation: numerator and capacity are independent last-wins projection fields, not one atomic request observation.
`src/client/` is organized by domain. `contract/` is the shared face for slot declarations, composed props, and cross-domain types; `skeleton/`, `chat/`, `input/`, `queue/`, and `settings/` keep their implementations internal, while `apply.ts` is their assembly point. The `/client` export surface contains only loader entries, service classes, and contract types; components and store factories reach the page through slot registrations.
`src/client/` is organized by domain. `contract/` is the shared face for slot declarations, composed props, and cross-domain types; `skeleton/`, `chat/`, `input/`, `queue/`, and `settings/` keep their implementations internal, while `apply.ts` is their assembly point. The `/client` exports contain only loader entries, service classes, and contract types; components and store factories reach the page through slot registrations.
A finished turn materializes one ordered `turn-tail` Conversation Node. Its engine-owned `TurnLocation` supplies the closing Assistant and Turn data; the renderer places the `conversation.chat.turnTail` chain before that node's IconActions and dispatches `TurnTailOwnerProps` containing the Turn, closing seq, and `openFile`. This package owns only the hole; `@deepseek-ai/dsh-client-ui-deliverables` accumulates mutation-tool `locations` into Turn data and owns the produced-files row, chip cap, and copy, so composing that plugin out of cordis.yml turns the surface off while the hole renders empty at zero cost. The closing prose participates through the same off switch: the chat view asks the optional `chatFileMentions` service (ctx.get; provided by the same plugin) for a closing message's inline-code vocabulary and threads the result into MarkdownText's `fileMentions` seam — an absent service leaves the prose inert.

View File

@@ -122,7 +122,7 @@ export class ConversationService extends Service implements IConversation {
/**
* Send a prompt into the scoped session. Business failures also land in the
* session snapshot's promptError (object-layer surface); the rejection here
* session snapshot's promptError (object-layer state); the rejection here
* exists for caller choreography (the composer restores the draft on it).
* @param text - prompt text, sent verbatim as one text block.
*/

View File

@@ -1,15 +1,15 @@
// @vitest-environment jsdom
// apply inject factories exercised end to end against the terminal thin
// shape: the strict session surface (views triple, draft mirror), the
// API: the strict session API (views triple, draft mirror), the
// provide-channel input face (machine-sink submit choreography incl.
// optimistic clear + failure restore), the resident surface (selectWorkspace
// optimistic clear + failure restore), the resident API (selectWorkspace
// draft carrying), the composer-bar stop face, openDetails = select action +
// layout orchestration, and the closeDetails details surface. Complements
// layout orchestration, and the closeDetails details API. Complements
// chat-apply.spec.tsx (registration) and selection-survival.spec.tsx (store
// axis). History opening is NOT an inject concern — the runtime sessions
// service opens on watch (sessions-service.spec.ts owns that behavior).
//
// The inject surfaces are read off the ledger entries deliberately (typed at
// The inject APIs are read off the ledger entries deliberately (typed at
// this spec's own contract): these cases pin factory choreography the UI
// guards would mask. Rendering-path acceptance lives in
// chat-toolview-slot.spec.tsx.
@@ -75,30 +75,30 @@ async function bench() {
const entryOf = (key: 'conversation' | 'conversation.session' | 'conversation.session.header' | 'conversation.composer.bar' | 'conversation.view' | 'details') =>
runtime.slots.entries(key)[0]!
/** Resolve store instance + call the inject the way the outlet would. */
const conversationSurface = (id: SessionId) => {
const conversationApi = (id: SessionId) => {
const entry = entryOf('conversation.session')
const instance = runtime.storeOf('conversation.session', id) as ChatInstance
const injected = (entry.inject as unknown as (sessionId: SessionId, actions: ChatActions) => ConversationSessionInjected)(
id, instance.actions)
return { instance, injected }
}
const conversationHeaderSurface = (id: SessionId) => {
const conversationHeaderApi = (id: SessionId) => {
const entry = entryOf('conversation.session.header')
const instance = runtime.storeOf('conversation.session.header', id) as ChatInstance
const injected = (entry.inject as unknown as (sessionId: SessionId, actions: ChatActions) => ConversationSessionHeaderInjected)(
id, instance.actions)
return { instance, injected }
}
const residentSurface = (id: SessionId | undefined) => {
const residentApi = (id: SessionId | undefined) => {
const entry = entryOf('conversation')
return (entry.inject as unknown as (sessionId: SessionId | undefined) => ConversationInjected)(id)
}
const composerSurface = (id: SessionId | undefined) => {
const composerApi = (id: SessionId | undefined) => {
const entry = entryOf('conversation.composer.bar')
return (entry.inject as unknown as (sessionId: SessionId | undefined) => ComposerBarInjected)(id)
}
/** Same resolution for the chat entry riding the view ring. */
const chatViewSurface = (id: SessionId) => {
const chatViewApi = (id: SessionId) => {
const entry = entryOf('conversation.view')
const instance = runtime.storeOf('conversation.view', id) as ChatInstance
const injected = (entry.inject as unknown as (sessionId: SessionId, actions: ChatActions) => ChatViewInjected)(
@@ -106,7 +106,7 @@ async function bench() {
return { instance, injected }
}
/** Materialize the input provide contribution the way the runtime does. */
const inputSurface = (id: SessionId) => {
const inputApi = (id: SessionId) => {
const info = runtime.sessions.provideInfo(id)!
const state = info.hooks['input'] as {
getSnapshot: () => { draft: string }
@@ -120,21 +120,21 @@ async function bench() {
}
return {
runtime, feature, slots: runtime.slots, entryOf,
conversationSurface, conversationHeaderSurface, residentSurface, composerSurface, chatViewSurface, inputSurface,
conversationApi, conversationHeaderApi, residentApi, composerApi, chatViewApi, inputApi,
sessionFake, layoutFake,
}
}
describe('conversation slot inject surface', () => {
it('assembles the thin surface side-effect-free', async () => {
describe('conversation slot inject API', () => {
it('assembles the thin API side-effect-free', async () => {
const b = await bench()
const { injected } = b.conversationSurface(ROOT)
const { injected } = b.conversationApi(ROOT)
// Assembly has no session side effects: opening the event window belongs
// to the runtime watch path, not the inject factory.
expect(b.sessionFake.open).not.toHaveBeenCalled()
expect(injected.views.list().map(v => v.id)).toEqual(['chat'])
const chatView = b.chatViewSurface(ROOT)
const chatView = b.chatViewApi(ROOT)
chatView.injected.loadOlder()
expect(b.sessionFake.loadOlder).toHaveBeenCalledTimes(1)
chatView.injected.forkAt(17)
@@ -149,8 +149,8 @@ describe('conversation slot inject surface', () => {
it('the provide-channel input face submits through the machine sink: trim, optimistic clear, failure restore without clobber', async () => {
const b = await bench()
const { injected } = b.conversationSurface(ROOT)
const { state, actions } = b.inputSurface(ROOT)
const { injected } = b.conversationApi(ROOT)
const { state, actions } = b.inputApi(ROOT)
// Whitespace-only: the machine treats it as empty — no prompt, draft kept.
actions.setDraft(' ')
actions.submit()
@@ -176,16 +176,16 @@ describe('conversation slot inject surface', () => {
await new Promise(r => setTimeout(r, 0))
expect(state.getSnapshot().draft).toBe('typed during flight')
// The provide contribution is idempotent per session: one shell identity.
expect(b.inputSurface(ROOT).state).toBe(state)
expect(b.inputApi(ROOT).state).toBe(state)
// The draft mirror rides the conversation inject face.
const mirrored: string[] = []
const unbind = injected.bindDraftMirror(text => mirrored.push(text))
actions.setDraft('mirrored text')
expect(mirrored).toEqual(['mirrored text'])
unbind()
// Stop failure is swallowed (promptError owns the surface).
// Stop failure is swallowed (promptError owns the display).
b.sessionFake.cancel.mockResolvedValueOnce({ ok: false, error: { code: 'internal', message: 'x', details: {} } })
b.composerSurface(ROOT).stop!()
b.composerApi(ROOT).stop!()
await new Promise(r => setTimeout(r, 0))
expect(b.sessionFake.cancel).toHaveBeenCalledTimes(1)
await b.runtime.dispose()
@@ -216,20 +216,20 @@ describe('conversation slot inject surface', () => {
it('openDetails (chat view face) writes the selection through the store actions and opens the panel', async () => {
const b = await bench()
const { instance, injected } = b.chatViewSurface(ROOT)
const { instance, injected } = b.chatViewApi(ROOT)
injected.openDetails({ turnSeq: 2, callId: 'c1' })
expect(instance.store.getSnapshot().selection).toEqual({ turnSeq: 2, callId: 'c1' })
expect(b.layoutFake.openDetails).toHaveBeenCalledTimes(1)
// The chat view shares the conversation entry's store instance: selection
// writes land where the skeleton and details read.
const conv = b.conversationSurface(ROOT)
const conv = b.conversationApi(ROOT)
expect(conv.instance).toBe(instance)
await b.runtime.dispose()
})
it('openFile (chat view face) resolves against session cwd and calls workspaces.openPath', async () => {
const b = await bench()
const { injected } = b.chatViewSurface(ROOT)
const { injected } = b.chatViewApi(ROOT)
injected.openFile('src/a.ts')
await vi.waitFor(() => {
expect(b.runtime.workspaces.calls).toContainEqual({ method: 'openPath', args: ['/proj/src/a.ts'] })
@@ -239,11 +239,11 @@ describe('conversation slot inject surface', () => {
it('routes workspace switching through the runtime owner, carrying the draft', async () => {
const b = await bench()
const resident = b.residentSurface(ROOT)
const resident = b.residentApi(ROOT)
// Same-session connect (the picked workspace resolves to this session):
// no draft movement, plain re-open.
b.runtime.workspaces.stub('connectWorkspace', () => Promise.resolve(ROOT))
const { state, actions } = b.inputSurface(ROOT)
const { state, actions } = b.inputApi(ROOT)
actions.setDraft('carry me')
void resident.selectWorkspace('workspace-1' as never)
await vi.waitFor(() => {
@@ -261,7 +261,7 @@ describe('conversation slot inject surface', () => {
expect(b.runtime.sessions.calls).toContainEqual({ method: 'open', args: [OTHER] })
})
expect(state.getSnapshot().draft).toBe('')
expect(b.inputSurface(OTHER).state.getSnapshot().draft).toBe('carry me')
expect(b.inputApi(OTHER).state.getSnapshot().draft).toBe('carry me')
await b.runtime.dispose()
})
@@ -269,7 +269,7 @@ describe('conversation slot inject surface', () => {
const b = await bench()
// No-session resident (hero before any session): connect resolves and
// navigation proceeds without any draft choreography.
const noSession = b.residentSurface(undefined)
const noSession = b.residentApi(undefined)
b.runtime.workspaces.stub('connectWorkspace', () => Promise.resolve(ROOT))
void noSession.selectWorkspace('workspace-0' as never)
await vi.waitFor(() => {
@@ -279,15 +279,15 @@ describe('conversation slot inject surface', () => {
// Cross-session connect with an EMPTY draft: no move, no clearing.
const OTHER = 'b9-other' as SessionId
await b.runtime.sessions.add({ id: OTHER }, { current: false })
const resident = b.residentSurface(ROOT)
const { state } = b.inputSurface(ROOT)
const resident = b.residentApi(ROOT)
const { state } = b.inputApi(ROOT)
expect(state.getSnapshot().draft).toBe('')
b.runtime.workspaces.stub('connectWorkspace', () => Promise.resolve(OTHER))
void resident.selectWorkspace('workspace-3' as never)
await vi.waitFor(() => {
expect(b.runtime.sessions.calls).toContainEqual({ method: 'open', args: [OTHER] })
})
expect(b.inputSurface(OTHER).state.getSnapshot().draft).toBe('')
expect(b.inputApi(OTHER).state.getSnapshot().draft).toBe('')
// Connect failure: the rejection propagates to the caller (the view owns
// the rollback) and no further navigation happens.
@@ -310,7 +310,7 @@ describe('conversation slot inject surface', () => {
it('views read face projects the ring ledger (subscribe/version through ctx.slots)', async () => {
const b = await bench()
const { injected } = b.conversationSurface(ROOT)
const { injected } = b.conversationApi(ROOT)
const before = injected.views.version()
const listener = vi.fn()
const unsub = injected.views.subscribe(listener)
@@ -332,7 +332,7 @@ describe('conversation slot inject surface', () => {
})
})
describe('details inject surface', () => {
describe('details inject API', () => {
it('details injects the one layout callback; selection rides the shared store instead', async () => {
const b = await bench()
const entry = b.entryOf('details')

View File

@@ -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 packages/client/ui-goal/README.md
README.md: a53fb3a89eaee364cb025ca728ca42ce934887b0
README.md: f0446aa0637bc181f7fdc22e5d0d3192e0ac20cf
README.zh.md: 1ad9f50aee5b103f6455e4d4b7d29fa9eb29a108

View File

@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
Goal surface plugin, browser half: the `GoalBar` strip is the second standalone card in the `conversation.input.dock` composer-context stack (order 10, after Todo and before Queue). The live goal arrives through `useProjection('goal')` — the host-computed whole value seeded by the history tail page and updated by `session/projection` frames — so the plugin owns no domain store, refresh chain, or event listener. The slot inject face carries only the four mutation verbs (edit / pause / resume / clear through `ctx.remote.goals` — an active goal offers the pause action, a paused one resume); each reads the CAS ref from the session's current projected value at call time and surfaces the rejected Remote error inline. The strip single-flights mutations synchronously because React's pending render cannot fence same-frame clicks; after a successful clear it immediately suppresses that exact goal id while the authoritative null projection catches up. Goal creation stays on the `/goal` host command; loading, absent, completed, and successfully cleared goals render nothing.
The `/client` export surface is the plugin body (`apply`/`inject`), the `GoalBar`/`GoalDock` components, and the injected verb face types.
The `/client` exports are the plugin body (`apply`/`inject`), the `GoalBar`/`GoalDock` components, and the injected verb face types.
## Model Experience

View File

@@ -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 packages/client/ui-layout/README.md
README.md: fa60520a20ac8a7f25d494879c68efb06a28998f
README.md: c58b9a26ac794131aaa197722d14bd04de9f0cae
README.zh.md: 2ce3d1972d39013d84216596dc57d48ab2d245d9

View File

@@ -6,7 +6,7 @@ Shell plugin: three-column AppFrame (drag handles and concession chain) plus the
AppFrame always mounts the conversation and details columns; a connected Session renders through `SessionProvider`. The transient layout store starts the sidebar at its default width and details closed, and it never reads or writes `localStorage`. Hero and other unselected states also derive a zero rendered details width without changing that stored preference. AppFrame retains the last non-blank Session id across those states: the first Session remains closed, an explicit details action opens the contract default width, returning to the same Session restores its unchanged width, and selecting a different Session closes details before paint. The conversation owner share is empty, while the sidebar owner share contains only `collapsed` and `width`; registrants obtain business data from standard hooks and actions from their own inject faces.
The `/client` export surface is the plugin body (`apply`/`inject`), `LayoutService`, and the four owner-share interfaces. AppFrame, the panel store, and the concession solver remain package-internal.
The `/client` exports are the plugin body (`apply`/`inject`), `LayoutService`, and the four owner-share interfaces. AppFrame, the panel store, and the concession solver remain package-internal.
## Model Experience

View File

@@ -15,7 +15,7 @@ import { createLayoutStore } from './stores.ts'
import { LayoutService } from './service.ts'
import { ThemePresenter } from './theme-presenter.ts'
// Contract surface only (export-convergence rule: cross-package consumers
// Contract exports only (export-convergence rule: cross-package consumers
// keep a symbol exported; test-only/package-internal symbols live off /src).
// ILayout: the ctx.layout face consumers and test fakes type against.
// OwnerShare contracts below are the render-side halves registrants compose
@@ -64,7 +64,7 @@ export interface ConvOwnerProps {}
/** Details owner share: empty — sessionId arrives as a framework-standard prop. */
export interface DetailsOwnerProps {}
/** Required services (cordis fiber inject — the loader passes the whole export surface as an object plugin). */
/** Required services (cordis fiber inject — the loader passes all module exports as an object plugin). */
export const inject = ['slots', 'theme']
/**

View File

@@ -3,7 +3,7 @@
// ONE register() call declares the three child slots + seats the store factory
// + wires the panel actions through the inject hook; teardown cascades
// (service unprovided + declarations gone + registration cleared). Node half
// and the invariant companion ride along — one-line surfaces the aggregate
// and the invariant companion ride along — one line exposes the aggregate
// coverage gate still requires exercised.
import { Context } from '@deepseek-ai/cordis'
@@ -111,7 +111,7 @@ describe('node half + invariant companion', () => {
const register = vi.fn().mockReturnValue(() => {})
const ctx = { invariants: { register } } as never
// The /invariant subpath types live in lib/types (build product); assert
// the surface so the call stays typed where lint runs without a build.
// the API so the call stays typed where lint runs without a build.
const dispose = await (invariant as { apply: (ctx: never) => Promise<() => void> }).apply(ctx)
expect(register).toHaveBeenCalledWith('@deepseek-ai/dsh-client-ui-layout', expect.any(Function))
// The installer is the declared no-op — calling it must not throw.

View File

@@ -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 packages/client/ui-model/README.md
README.md: 519429834f214fcb82eeb692378fb79770fa30be
README.md: fdc3258eb37b45f4773648d2d796c275f8657d47
README.zh.md: 116e151d1afeaaa22618c408eed2c7542d1357e7

View File

@@ -10,7 +10,7 @@ When the Host reports that no adapter serves the session's route (`session.model
Directories are per-session, resolved lazily through `ctx.models.directoryFor(sessionId)`, and disposed with the session scope. Addressed subagent sessions expose neither entry, and their directory rejects loads, selections, and reconnect refreshes, because ordinary Agent-bound model RPCs would activate persisted child history outside the direct-parent continuation path.
The `/client` export surface is the plugin body (`apply`/`inject`), `ModelService`, `ModelDirectory` with its state shape, and the seat's injected face type.
The `/client` exports are the plugin body (`apply`/`inject`), `ModelService`, `ModelDirectory` with its state fields, and the seat's injected face type.
## Model Experience

View File

@@ -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 packages/client/ui-models/README.md
README.md: e0c5728d47e053df1934ef9eb69df3f8d985a4ec
README.zh.md: fe11e6cdd190e19d5b5dac6dc95950ba59a3172b
README.md: 89a253fca9f16ea5655cdd7536a441e98dcc4d3c
README.zh.md: 350be495f1491738e6e861ba127b3b0070a5f073

View File

@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
Models settings plugin: the provider configuration page and official-DeepSeek conditional onboarding step. It joins three wire domains into one shared snapshot — `llm.providers` (the configurable-provider directory with each route's live/dormant state), `settings.describe` (serialized schemas, layered redacted values, secret slots), and `credentials.describe` (value-free configured/source/writable badges) — and renders provider rows with one editor card at a time, without presenting route liveness as provider status.
Rows are the *configured* providers (their profile resolves in the owning namespace); a whole-section provider whose key is not configured anywhere (the first-run DeepSeek posture) renders as its open setup card instead of a row, and the add flow is a card carrying the dormant-directory provider select — a bare-mounted `llm-pi-ai` offers its whole installed catalog before any route exists. The pi-ai card additionally edits that route's **model list** and can ask the provider what it serves. A row labels API-key state with a green solid dot only when a referenced credential is confirmed configured, and with a red solid dot only when a named reference is confirmed missing; reference-free provider-native authentication and unavailable credential enrichment remain unmarked. The editor is a hand-written card per adapter family: the primary field is a single **API key** input — the page never asks for an environment-variable name; a typed key stores **write-only** through `credentials.set` under the profile's reference, deriving `<ROUTE>_API_KEY` when the profile has none, and the pi-ai profile records that derivation as `apiKeyEnv`, so `settings.yaml` never carries a key value. Leaving a new pi-ai provider's key blank saves a reference-free profile and therefore preserves provider-native authentication such as the Bedrock credential chain or Vertex ADC. A successful Apply emits a local accessible status message without echoing secret material. The collapsed 自定义设置 fold carries the curated extras — `baseURL` for both families (the deepseek placeholder shows the public endpoint) and each adapter's model catalog. Reasoning effort is deliberately NOT among them: it is a per-model capability and the models under one provider disagree about which levels they accept, so a provider-scoped control could only be set to a value some of them reject — which would hide even the models that support the level. The composer's model picker offers each model its own levels, and a switch there records provider, model, and effort together as the default for the next session. The profile field stays in `settings.yaml` for a deployment that knows its route. Each DeepSeek row edits `id`, optional display `name`, and optional `contextWindow`; existing fields outside that curated set survive edits, while every other profile field stays owned by `settings.yaml`. A row is deletable only when the user layer alone carries it (removal restores the composition base), and its localized confirmation dialog names the provider in the title, description, and final action. A row is tagged **Custom** when the directory entry says the owning adapter ships nothing under that key. The tag follows that answer alone: having a stored profile does not make a route custom — narrowing a shipped provider's models stores one too — and an adapter that reports nothing leaves its rows untagged rather than being read as shipped.
Rows are the *configured* providers (their profile resolves in the owning namespace); a whole-section provider whose key is not configured anywhere (the first-run DeepSeek posture) renders as its open setup card instead of a row, and the add flow is a card carrying the dormant-directory provider select — a bare-mounted `llm-pi-ai` offers its whole installed catalog before any route exists. The pi-ai card additionally edits that route's **model list** and can ask the provider what it serves. A row labels API-key state with a green solid dot only when a referenced credential is confirmed configured, and with a red solid dot only when a named reference is confirmed missing; reference-free provider-native authentication and unavailable credential enrichment remain unmarked. The editor is a hand-written card per adapter family: the primary field is a single **API key** input — the page never asks for an environment-variable name; a typed key stores **write-only** through `credentials.set` under the profile's reference, deriving `<ROUTE>_API_KEY` when the profile has none, and the pi-ai profile records that derivation as `apiKeyEnv`, so `settings.yaml` never carries a key value. Leaving a new pi-ai provider's key blank saves a reference-free profile and therefore preserves provider-native authentication such as the Bedrock credential chain or Vertex ADC. A successful Apply emits a local accessible status message without echoing secret material. The collapsed 自定义设置 fold carries the curated extras — `baseURL` for both families (the deepseek placeholder shows the public endpoint), each adapter's model catalog, and the **display name** and **API protocol** of a pi-ai route the adapter does not ship. Those two are what a hand-declared route names for itself: the create card asks for both because nothing can default them, so the editor reaches both rather than leaving them to `settings.yaml`. Clearing the name unsets it and the route falls back to its id, which is what the placeholder shows; the protocol has no such fallback. A catalog route gets neither — it defaults its name from its catalog entry, and its models each carry their own protocol, so a route-level one could only override every one of them. The Provider ID stays fixed: it is the settings key, the name every other namespace and every logged session references, and the stem of a credential reference the page cannot read back to move. Reasoning effort is deliberately NOT among them: it is a per-model capability and the models under one provider disagree about which levels they accept, so a provider-scoped control could only be set to a value some of them reject — which would hide even the models that support the level. The composer's model picker offers each model its own levels, and a switch there records provider, model, and effort together as the default for the next session. The profile field stays in `settings.yaml` for a deployment that knows its route. Each DeepSeek row edits `id`, optional display `name`, and optional `contextWindow`/`maxTokens`; existing fields outside that curated set survive edits, while every other profile field stays owned by `settings.yaml`. A row is deletable only when the user layer alone carries it (removal restores the composition base), and its localized confirmation dialog names the provider in the title, description, and final action. A row is tagged **Custom** when the directory entry says the owning adapter ships nothing under that key. The tag follows that answer alone: having a stored profile does not make a route custom — narrowing a shipped provider's models stores one too — and an adapter that reports nothing leaves its rows untagged rather than being read as shipped.
The DeepSeek step projects `deepseek-official` readiness from that same joined snapshot after earlier onboarding pages complete. It recognizes the official adapter through its `llm-deepseek` configurable-provider declaration, so an undeclared live route with the same provider id is not treated as repairable configuration. A configured credential reference completes the step without rendering, including a read-only launch-environment credential. Only a mounted, active adapter with a missing writable reference shows the page that opens Settings on Models, whose existing setup card exclusively owns key input and `credentials.set`; the step never holds a secret. An absent adapter, inactive route, failed join, read-only deployment, or unusable settings or credential capability completes the step without rendering so onboarding cannot block the product; Models remains the diagnostic surface.
@@ -28,7 +28,7 @@ None; this package neither assembles nor sends a provider request.
## Known Limitations and Deferred Work
- **Only the API key and curated fold fields are editable on the card** — the hand-written editor traded schema-generic field coverage for the mockup layout ([Agent Note](../../../.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md)). DeepSeek exposes `baseURL`, `reasoningEffort`, and model `id`/`name`/`contextWindow`/`maxTokens`; pi-ai exposes `baseURL` and `reasoning`. Retry policy, timeouts, DeepSeek model descriptions, and other advanced fields remain in `settings.yaml`; existing model fields the editor does not show are preserved. A profile schema without the conventional fields renders the hint alone, and the two curated layouts key on the `llm-deepseek`/`llm-pi-ai` namespaces by name.
- **Only the API key and curated fold fields are editable on the card** — the hand-written editor traded schema-generic field coverage for the mockup layout ([Agent Note](../../../.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md)). Both families expose `baseURL` and model `id`/`name`/`contextWindow`/`maxTokens`; a hand-declared pi-ai route also exposes `displayName` and `api`. Retry policy, timeouts, DeepSeek model descriptions, and other advanced fields remain in `settings.yaml`; existing model fields the editor does not show are preserved. A profile schema without the conventional fields renders the hint alone, and the two curated layouts key on the `llm-deepseek`/`llm-pi-ai` namespaces by name.
- **Credential cleanup is intentionally narrow** — deleting a row removes the configured, writable credential only when its reference is the exact `<ROUTE>_API_KEY` target this page derives. Custom references, environment credentials, and unidentifiable targets are retained because the row cannot prove ownership of them.
- **Only pi-ai routes can be hand-declared** — the custom-provider card writes into `llm-pi-ai`, the one namespace whose profiles describe a whole provider. A `llm-deepseek` route is a composition fact, not something this page can create.
- **Interrogation covers OpenAI-compatible endpoints** — the adapter reads only that model-list response format, so a gateway speaking another protocol reports that it cannot be asked and its models are entered by hand.

View File

@@ -4,7 +4,7 @@
模型设置插件:提供方配置页和按条件显示的 DeepSeek 官方首次使用引导步骤。它把三个协议领域汇聚为一个共享快照:`llm.providers`(可配置提供方目录,含每条路由的存活/休眠状态)、`settings.describe`(序列化 schema、分层脱敏值、secret 槽位)与 `credentials.describe`(不含值的 configured/source/writable 徽标);页面据此渲染提供方行,一次只展开一张编辑卡片,且不把路由存活状态呈现为提供方状态。
行是*已配置*的提供方(其 profile 在所属 namespace 中解析得出密钥未在任何地方配置的整分节提供方DeepSeek 的首次运行姿态)会渲染为其展开的设置卡片而非一行,「新增」流程则是一张承载休眠目录提供方选择框的卡片——裸挂载的 `llm-pi-ai` 在任何路由存在之前就能提供其完整的已安装 catalog。pi-ai 卡片还会编辑该路由的**模型列表**,并可查询提供方所提供的模型。只有确认引用的凭据已配置时,行才会以绿色实心点标示 API 密钥状态;只有确认具名引用缺失时,才会以红色实心点标示。无引用的提供方原生认证以及无法取得凭据补充信息时都不显示状态点。编辑器是每个适配器家族各一张的手写卡片:主字段是单独一个 **API 密钥**输入框——页面从不询问环境变量名;键入的密钥经 `credentials.set` 以**只写**方式存入 profile 的引用之下profile 没有引用时便派生 `<ROUTE>_API_KEY`pi-ai profile 会把这次派生记录为 `apiKeyEnv`,因此 `settings.yaml` 从不携带密钥值。为新的 pi-ai 提供方留空密钥会保存一个不带引用的 profile因此能保留提供方原生认证例如 Bedrock 凭据链或 Vertex ADC。「应用」成功后会发出本地无障碍状态消息且绝不回显任何机密内容。收起的「自定义设置」折叠区承载精选的额外字段——两个家族都有 `baseURL`deepseek 的占位符显示公共端点),以及适配器自己的模型目录。推理等级刻意**不在**其中它是按模型的能力而同一提供方下各模型接受的档位并不一致因此提供方级的控件只可能被设成其中一些模型会拒绝的值——那会连支持该档位的模型也一并隐藏。输入框的模型选择器为每个模型提供它自己的档位在那里切换会把提供方、模型、推理等级一并记为下一个会话的默认值。profile 字段仍留在 `settings.yaml`,供清楚自己路由的部署使用。每条 DeepSeek 模型行可编辑 `id`、可选的显示名称 `name` 与可选的 `contextWindow`;精选集合以外的现有字段会在编辑后保留,其余每个 profile 字段仍归 `settings.yaml` 所有。只有当某行仅由用户层承载时它才可删除(删除会还原组合 base其本地化确认对话框会在标题、说明和最终操作中点名该提供方。当目录条目表明拥有该路由的适配器在这个键下什么都没有时该行会带上 **自定义** 标签。标签只跟随这个答案:存了 profile 并不使一条路由成为自定义——收窄一个内置提供方的模型同样会存下 profile——而什么都不回答的适配器其路由保持无标签不会被当成内置。
行是*已配置*的提供方(其 profile 在所属 namespace 中解析得出密钥未在任何地方配置的整分节提供方DeepSeek 的首次运行姿态)会渲染为其展开的设置卡片而非一行,「新增」流程则是一张承载休眠目录提供方选择框的卡片——裸挂载的 `llm-pi-ai` 在任何路由存在之前就能提供其完整的已安装 catalog。pi-ai 卡片还会编辑该路由的**模型列表**,并可查询提供方所提供的模型。只有确认引用的凭据已配置时,行才会以绿色实心点标示 API 密钥状态;只有确认具名引用缺失时,才会以红色实心点标示。无引用的提供方原生认证以及无法取得凭据补充信息时都不显示状态点。编辑器是每个适配器家族各一张的手写卡片:主字段是单独一个 **API 密钥**输入框——页面从不询问环境变量名;键入的密钥经 `credentials.set` 以**只写**方式存入 profile 的引用之下profile 没有引用时便派生 `<ROUTE>_API_KEY`pi-ai profile 会把这次派生记录为 `apiKeyEnv`,因此 `settings.yaml` 从不携带密钥值。为新的 pi-ai 提供方留空密钥会保存一个不带引用的 profile因此能保留提供方原生认证例如 Bedrock 凭据链或 Vertex ADC。「应用」成功后会发出本地无障碍状态消息且绝不回显任何机密内容。收起的「自定义设置」折叠区承载精选的额外字段——两个家族都有 `baseURL`deepseek 的占位符显示公共端点)、各适配器自己的模型目录,以及适配器未提供的那类 pi-ai 路由的**显示名称**与 **API 协议**。这两个字段是手工声明路由为自己命名的东西:创建卡片之所以索要它们,正因为没有东西能为它们兜底,因此编辑器也够得着这两个,而不是把它们留给 `settings.yaml`。清空名称即取消设置,路由退回自己的 id——占位符显示的就是它协议没有这样的兜底。内置目录路由两个都不给它的名称由目录条目兜底它的每个模型各自带着自己的协议路由级协议只可能把它们全部覆盖掉。Provider ID 保持固定:它是 settings 的键、是其他每个 namespace 与每一条已记录会话引用的名字,也是页面读不回、因而搬不走的凭据引用词干。推理等级刻意**不在**其中它是按模型的能力而同一提供方下各模型接受的档位并不一致因此提供方级的控件只可能被设成其中一些模型会拒绝的值——那会连支持该档位的模型也一并隐藏。输入框的模型选择器为每个模型提供它自己的档位在那里切换会把提供方、模型、推理等级一并记为下一个会话的默认值。profile 字段仍留在 `settings.yaml`,供清楚自己路由的部署使用。每条 DeepSeek 模型行可编辑 `id`、可选的显示名称 `name` 与可选的 `contextWindow`/`maxTokens`;精选集合以外的现有字段会在编辑后保留,其余每个 profile 字段仍归 `settings.yaml` 所有。只有当某行仅由用户层承载时它才可删除(删除会还原组合 base其本地化确认对话框会在标题、说明和最终操作中点名该提供方。当目录条目表明拥有该路由的适配器在这个键下什么都没有时该行会带上 **自定义** 标签。标签只跟随这个答案:存了 profile 并不使一条路由成为自定义——收窄一个内置提供方的模型同样会存下 profile——而什么都不回答的适配器其路由保持无标签不会被当成内置。
前序首次使用引导页面完成后DeepSeek 步骤会从同一个联接快照得出 `deepseek-official` 的就绪状态。它通过 `llm-deepseek` 的可配置提供方声明识别官方适配器,因此同 id 但未声明的存活路由不属于可修复配置。凭据引用已配置时该步骤会直接完成而不渲染其中包括来自启动环境且只读的凭据。只有已挂载且活跃、引用可写但尚未配置的适配器才会显示前往「设置」Models 分区的页面;密钥输入和 `credentials.set` 仅由该分区已有的设置卡片负责,该步骤绝不持有 secret。适配器缺失、路由不活跃、联接失败、部署只读或设置凭据能力不可用时该步骤均不渲染并直接完成以免首次使用引导阻塞产品Models 页仍是诊断界面。
@@ -28,7 +28,7 @@ pi-ai profile 的 `models` 列表就在卡片上编辑:一行一个模型,
## 已知限制与暂缓事项
- **卡片上可编辑的只有 API 密钥与精选折叠区字段**:手写编辑器用 schema 通用的字段覆盖面换来了设计稿上的布局([Agent Note](../../../.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md))。DeepSeek 公开 `baseURL``reasoningEffort` 与模型的 `id`/`name`/`contextWindow`/`maxTokens`pi-ai 公开 `baseURL``reasoning`。重试策略、超时、DeepSeek 模型说明及其他进阶字段仍留在 `settings.yaml` 中;编辑器未展示的现有模型字段会予以保留。不带这些约定字段的 profile schema 只渲染该提示,两套精选布局则以 `llm-deepseek`/`llm-pi-ai` 这两个 namespace 的名字为键。
- **卡片上可编辑的只有 API 密钥与精选折叠区字段**:手写编辑器用 schema 通用的字段覆盖面换来了设计稿上的布局([Agent Note](../../../.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md))。两个家族都公开 `baseURL` 与模型的 `id`/`name`/`contextWindow`/`maxTokens`手工声明的 pi-ai 路由还公开 `displayName``api`。重试策略、超时、DeepSeek 模型说明及其他进阶字段仍留在 `settings.yaml` 中;编辑器未展示的现有模型字段会予以保留。不带这些约定字段的 profile schema 只渲染该提示,两套精选布局则以 `llm-deepseek`/`llm-pi-ai` 这两个 namespace 的名字为键。
- **凭据清理范围刻意保持狭窄**:删除一行时,仅当其引用与页面派生的 `<ROUTE>_API_KEY` 目标完全一致,才会清除已配置且可写的凭据。自定义引用、环境凭据和无法识别的目标会保留,因为该行无法证明自己拥有它们。
- **只有 pi-ai 路由可以手工声明**:自定义提供方卡片写入 `llm-pi-ai`——唯一一个其 profile 描述整个提供方的 namespace。`llm-deepseek` 路由是组合面的事实,不是本页能创建的东西。
- **询问只覆盖 OpenAI 兼容端点**:适配器只读这种模型列表响应格式,因此讲其他协议的网关会报告自己无法被询问,其模型需手工填写。

View File

@@ -236,7 +236,7 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
<div className={styles['field']}>
<span className={styles['fieldLabel']}>{t('customApi')}</span>
<select
className={styles['input']}
className={`${styles['input']} ${styles['selectInput']}`}
value={protocol}
aria-label={t('customApi')}
disabled={profileDisabled}

View File

@@ -54,6 +54,8 @@ interface EditorTarget extends ProviderIdentity {
settingsPath: readonly string[]
/** Writable credential identified under this page's conventional reference. */
credentialRef?: string
/** The adapter reports this route as one it does not ship (see {@link ProviderEditorProps.declared}). */
declared?: boolean
}
/** Values that vary around the shared provider-editor rendering. */
@@ -71,6 +73,7 @@ function renderProviderEditor({ target, ...props }: ProviderEditorRenderProps):
provider={target.provider}
displayName={target.displayName}
settingsPath={target.settingsPath}
{...target.declared === true ? { declared: true } : {}}
{...props}
/>
)
@@ -135,6 +138,10 @@ function targetOf(row: ProviderRow): EditorTarget {
settingsNs: row.entry.settingsNs,
settingsPath: row.entry.settingsPath,
...credentialRef === undefined ? {} : { credentialRef },
// Absent is not "shipped": an adapter that answers nothing leaves the
// route-level fields only a declared route owns off the card, exactly as
// it leaves the custom tag off the row.
...row.entry.declared === true ? { declared: true } : {},
}
}
@@ -177,8 +184,10 @@ function Loaded({ injected }: { injected: ModelsSectionInjected }): ReactNode {
setAdding(false)
setDeclaring(false)
if (changed) {
setSavedTarget(target)
void controller.load()
// Announced only once the refreshed directory is in the snapshot the
// notice reads its name from: an apply can rename the route, and the
// target captured when the card opened still carries the old name.
void controller.load().then(() => { setSavedTarget(target) })
}
}
@@ -218,6 +227,17 @@ function Loaded({ injected }: { injected: ModelsSectionInjected }): ReactNode {
)
}
// The saved provider as the directory currently names it. The route id is
// what the apply cannot change, so it is what the notice is keyed by; a row
// the same apply removed keeps the captured identity, since nothing newer
// exists to name it with.
const savedRow = savedTarget === undefined
? undefined
: state.rows.find(row => row.entry.provider === savedTarget.provider)
const savedIdentity = savedRow === undefined
? savedTarget
: { provider: savedRow.entry.provider, displayName: savedRow.entry.displayName }
const configured = state.rows.filter(row => row.configured)
const addable = state.rows.filter(row => !row.configured && row.entry.settingsNs !== '')
const addTarget = adding ? editing : undefined
@@ -232,11 +252,11 @@ function Loaded({ injected }: { injected: ModelsSectionInjected }): ReactNode {
<h2 className={styles['title']}>{t('title')}</h2>
<p className={styles['intro']}>{t('intro')}</p>
{!state.writable && state.status === 'ready' ? <p className={styles['notice']}>{t('readOnly')}</p> : null}
{savedTarget === undefined
{savedIdentity === undefined
? null
: (
<p className={styles['savedNotice']} role="status" aria-live="polite">
{providerCopy(t('savedProvider'), savedTarget)}
{providerCopy(t('savedProvider'), savedIdentity)}
</p>
)}
<ul className={styles['rows']}>

View File

@@ -7,7 +7,10 @@
* a key is entered; a blank key materializes a reference-free profile for
* provider-native authentication);
* the collapsed 自定义设置 area carries the per-family extras (`baseURL` for
* both families and DeepSeek's id/name/context-window model catalog).
* both families, DeepSeek's id/name/context-window model catalog, and the
* display name and wire protocol of a pi-ai route the adapter does not ship —
* the two fields the create card asked that route for, editable here for the
* same reason).
* Reasoning effort is deliberately absent: it is a per-MODEL capability, and
* the models under one provider disagree about it, so a provider-scoped
* control can only be set to a value some of them reject. The composer's
@@ -30,7 +33,7 @@ import {
import { apiKeyFailure } from './apiKey.ts'
import { EditorFooter } from './EditorFooter.tsx'
import { ModelListEditor } from './ModelListEditor.tsx'
import { deriveKeyRef, messageOf } from './store.ts'
import { deriveKeyRef, messageOf, protocolChoices } from './store.ts'
import type { en } from './locales.ts'
import styles from './ModelsSection.module.css'
@@ -48,6 +51,14 @@ export interface ProviderEditorProps {
displayName: string
/** Hide the title row (the add card renders its own provider select). */
hideTitle?: boolean
/**
* Whether the adapter reports this route as hand-declared — absent from its
* installed catalog. Such a route carries its own wire protocol, chosen when
* it was created and editable here for the same reason; a catalog route's
* models each carry theirs, so a route-level protocol there could only
* override every one of them and the card does not offer it.
*/
declared?: boolean
/** The owning namespace view (schema, layers, secrets). */
namespace: SettingsNamespaceView
/** Path from the section root to this provider's profile. */
@@ -139,6 +150,14 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
const disabled = props.readOnly || busy
const layout = layoutOf(namespace.ns)
const keyRef = refFor(namespace, settingsPath, props.provider)
// The same schema read the create card makes, so the choices offered here
// and there cannot drift apart: both come from the adapter's own `Config`.
// Only the pi-ai layout has a per-route protocol for the read to find, and
// it rehydrates the whole section schema, so the other layouts skip it.
const protocols = useMemo(
() => layout === 'pi-ai' ? protocolChoices(namespace) : [],
[layout, namespace],
)
useEffect(() => {
let stale = false
@@ -289,11 +308,15 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
}
/**
* The curated fields of one known adapter family. Taking the narrowed
* family as a parameter is what makes `EFFORT_FIELD` total here: an
* unknown namespace never reaches this body.
* The curated fields of one known adapter family. The family arrives
* narrowed so the per-family branches below are total: an unknown namespace
* renders the hint instead and never reaches this body.
*/
const curatedFields = (family: 'deepseek' | 'pi-ai'): ReactNode => {
// What a hand-declared route names for itself and nothing else can supply.
// A whole-section `llm-deepseek` profile is a composition fact with no
// per-route identity for its schema to carry, hence the family test.
const ownsIdentity = family === 'pi-ai' && props.declared === true
const customModels = getPath(draft, ['models'])
const modelsOverridden = hasPath(draft, ['models'])
const models = modelDrafts(modelsOverridden ? customModels : inheritedModels())
@@ -334,6 +357,33 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
<details className={styles['customized']}>
<summary className={styles['customizedSummary']}>{t('customized')}</summary>
<div className={styles['customizedBody']}>
{/* The name and the protocol are the create card's two remaining
profile fields; a route the adapter ships defaults both from
its catalog entry and neither belongs on its card. */}
{ownsIdentity
? (
<div className={styles['field']}>
<span className={styles['fieldLabel']}>{t('customDisplayName')}</span>
<input
className={styles['input']}
type="text"
value={stringAt(draft, 'displayName') ?? ''}
// What this route is called the moment the field is
// cleared, which is the layer beneath the one this field
// edits: a `cordis.yml` may pin a name for a route the
// catalog does not ship, and only when nothing does is
// the answer the route id. Reading the effective value
// instead would echo the stored override back as the
// thing clearing restores.
placeholder={stringAt(getPath(namespace.base, settingsPath), 'displayName')
?? props.provider}
aria-label={t('customDisplayName')}
disabled={disabled}
onChange={(event) => { setField('displayName', event.target.value) }}
/>
</div>
)
: null}
<div className={styles['field']}>
<span className={styles['fieldLabel']}>{t('baseUrl')}</span>
<input
@@ -350,6 +400,31 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
}}
/>
</div>
{/* The protocol sits beside the endpoint it describes, as it does
on the create card. */}
{ownsIdentity
? (
<div className={styles['field']}>
<span className={styles['fieldLabel']}>{t('customApi')}</span>
<select
className={`${styles['input']} ${styles['selectInput']}`}
value={probeApi ?? ''}
aria-label={t('customApi')}
disabled={disabled}
onChange={(event) => { setField('api', event.target.value) }}
>
{/* A profile naming no protocol — hand-written into
settings.yaml with no model to need one — selects
nothing rather than reading as if it had picked the
first choice. The option is named because a screen
reader announces it either way, and an empty one is
announced as a choice with no identity. */}
{probeApi === undefined ? <option value="">{t('customApiUnset')}</option> : null}
{protocols.map(choice => <option key={choice} value={choice}>{choice}</option>)}
</select>
</div>
)
: null}
{/* Both families edit the same rows through the same contract; only
the extras differ — DeepSeek's inherited capacities, pi-ai's
endpoint interrogation. */}

View File

@@ -80,6 +80,7 @@ export const en = {
customRouteTaken: 'A provider already uses this ID.',
customDisplayName: 'Display name',
customApi: 'API protocol',
customApiUnset: 'Not selected',
customNeedsBaseUrl: 'A custom provider needs a base URL.',
customNeedsModels: 'A custom provider needs at least one model.',
create: 'Create provider',
@@ -173,6 +174,7 @@ export const zh: typeof en = {
customRouteTaken: '已有提供方使用了这个 ID。',
customDisplayName: '显示名称',
customApi: 'API 协议',
customApiUnset: '未选择',
customNeedsBaseUrl: '自定义提供方需要填写 API 地址。',
customNeedsModels: '自定义提供方至少需要一个模型。',
create: '创建提供方',

View File

@@ -5,7 +5,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import Schema from '@deepseek-ai/schemastery'
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
import type { RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-client-connection/client'
import { ModelsSection } from '../src/client/ModelsSection.tsx'
import { ModelsSection, providerCopy } from '../src/client/ModelsSection.tsx'
import type { ModelsSectionInjected } from '../src/client/ModelsSection.tsx'
import { CustomProviderCard } from '../src/client/CustomProviderCard.tsx'
import { formatCapacity, parseCapacity } from '../src/client/DeepSeekModelsEditor.tsx'
@@ -47,6 +47,7 @@ function fail<T>(message: string, code: string): RpcResponse<T> {
function piAiNamespace(
providers: Record<string, unknown>,
userProviders: Record<string, unknown> = providers,
baseProviders: Record<string, unknown> = {},
): SettingsNamespaceView {
return {
ns: 'llm-pi-ai',
@@ -54,7 +55,7 @@ function piAiNamespace(
// `value` is the effective section; `user` is only the layer this page
// writes. They differ whenever a composition `base` supplies something.
value: { providers },
base: {},
base: { providers: baseProviders },
user: { providers: userProviders },
applies: 'live',
secrets: [],
@@ -66,6 +67,8 @@ function scriptedFace(options: {
providers?: Record<string, unknown>
/** User layer, when it differs from the effective section. */
userProviders?: Record<string, unknown>
/** Composition layer, for a route a `cordis.yml` pins rather than the page. */
baseProviders?: Record<string, unknown>
/** Routes the adapter reports as hand-declared; the rest come back as shipped. */
declaredRoutes?: readonly string[]
discover?: ReturnType<typeof vi.fn>
@@ -75,7 +78,7 @@ function scriptedFace(options: {
const providers = options.providers ?? {
openai: { apiKeyEnv: 'OPENAI_API_KEY', baseURL: 'https://proxy.example/v1' },
}
const namespace = piAiNamespace(providers, options.userProviders ?? providers)
const namespace = piAiNamespace(providers, options.userProviders ?? providers, options.baseProviders ?? {})
const discover = options.discover ?? vi.fn(() => Promise.resolve(ok({ models: [] })))
const mutate = options.mutate ?? vi.fn(() => Promise.resolve(ok(namespace)))
const set = options.set ?? vi.fn(() => Promise.resolve(ok({})))
@@ -719,10 +722,152 @@ describe('hand-declared providers', () => {
expect(fields()).toEqual([en.customRoute, en.customDisplayName, en.baseUrl, en.customApi, en.keyInput])
cleanup()
// A shipped route's models each carry their own protocol, so its editor
// offers no route-level protocol to override them with.
await mountSection({ providers: { openai: { apiKeyEnv: 'OPENAI_API_KEY' } } })
openEditor('openai')
fireEvent.click(screen.getByText(en.customized))
expect(fields()).toEqual([en.keyInput, en.baseUrl])
cleanup()
// A hand-declared route named its own protocol at creation, so editing it
// reaches the same field the create card asked for.
await mountSection({
providers: { 'acme-gateway': { api: 'openai-completions', baseURL: 'https://gateway.acme.example/v1' } },
declaredRoutes: ['acme-gateway'],
})
openEditor('acme-gateway')
expect(fields()).toEqual([en.keyInput, en.customDisplayName, en.baseUrl, en.customApi])
})
it('renames a declared route and falls back to its id when the name is cleared', async () => {
const { mutate } = await mountSection({
providers: {
'acme-gateway': { displayName: 'Acme Gateway', api: 'openai-completions', baseURL: 'https://acme.test/v1' },
},
declaredRoutes: ['acme-gateway'],
})
openEditor('acme-gateway')
const name = screen.getByLabelText<HTMLInputElement>(en.customDisplayName)
expect(name.value).toBe('Acme Gateway')
// The route id, not the stored name: it is what the route will be called
// the moment the field is cleared.
expect(name.placeholder).toBe('acme-gateway')
fireEvent.change(name, { target: { value: 'Acme 网关' } })
fireEvent.click(screen.getByText(en.apply))
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
expect(firstMutate(mutate).ops)
.toEqual([{ op: 'set', path: ['providers', 'acme-gateway', 'displayName'], value: 'Acme 网关' }])
})
it('offers the composition name as what a cleared field falls back to', async () => {
// A `cordis.yml` can pin a route the catalog does not ship, so a declared
// route's profile is not always the page's own. The field edits the user
// layer alone, and clearing it restores the layer beneath — the
// composition name here, not the route id — so that is what it offers.
await mountSection({
providers: { 'acme-gateway': { displayName: 'Acme (pinned)', api: 'openai-completions' } },
baseProviders: { 'acme-gateway': { displayName: 'Acme (pinned)', api: 'openai-completions' } },
userProviders: {},
declaredRoutes: ['acme-gateway'],
})
openEditor('acme-gateway')
const name = screen.getByLabelText<HTMLInputElement>(en.customDisplayName)
expect(name.value).toBe('')
expect(name.placeholder).toBe('Acme (pinned)')
})
it('names the provider as the refreshed directory reports it after a rename', async () => {
// The status line used to echo the target captured when the card opened,
// which never lied while the name could not change. It can now.
const { face } = await mountSection({
providers: { 'acme-gateway': { displayName: 'Acme Gateway', api: 'openai-completions' } },
declaredRoutes: ['acme-gateway'],
})
// The reload after the write answers with the renamed route, exactly as
// the adapter re-registers it.
face.llm.providers = vi.fn(() => Promise.resolve(ok({
providers: [{
provider: 'acme-gateway',
displayName: 'Acme 网关',
settingsNs: 'llm-pi-ai',
settingsPath: ['providers', 'acme-gateway'],
active: true,
declared: true,
}],
})))
openEditor('acme-gateway')
fireEvent.change(screen.getByLabelText(en.customDisplayName), { target: { value: 'Acme 网关' } })
fireEvent.click(screen.getByText(en.apply))
const notice = await screen.findByRole('status')
expect(notice.textContent).toBe(providerCopy(en.savedProvider, {
provider: 'acme-gateway',
displayName: 'Acme 网关',
}))
})
it('drops the stored name rather than storing an empty one the adapter refuses', async () => {
// `llm-pi-ai` rejects an empty displayName outright, so clearing the field
// must unset it — which is also what the user means: use the route id.
const { mutate } = await mountSection({
providers: { 'acme-gateway': { displayName: 'Acme Gateway', api: 'openai-completions' } },
declaredRoutes: ['acme-gateway'],
})
openEditor('acme-gateway')
fireEvent.change(screen.getByLabelText(en.customDisplayName), { target: { value: ' ' } })
fireEvent.click(screen.getByText(en.apply))
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
expect(firstMutate(mutate).ops)
.toEqual([{ op: 'unset', path: ['providers', 'acme-gateway', 'displayName'] }])
})
it('edits the protocol a declared route was created with', async () => {
const { mutate } = await mountSection({
providers: {
'acme-gateway': {
apiKeyEnv: 'ACME_GATEWAY_API_KEY',
api: 'openai-completions',
baseURL: 'https://gateway.acme.example/v1',
models: [{ id: 'acme-large' }],
},
},
declaredRoutes: ['acme-gateway'],
})
openEditor('acme-gateway')
const protocol = screen.getByLabelText<HTMLSelectElement>(en.customApi)
expect(protocol.value).toBe('openai-completions')
fireEvent.change(protocol, { target: { value: 'anthropic-messages' } })
fireEvent.click(screen.getByText(en.apply))
await waitFor(() => { expect(mutate).toHaveBeenCalledTimes(1) })
// Only the protocol travels: every other stored field is unchanged, so no
// op restates it.
expect(firstMutate(mutate)).toEqual({
ns: 'llm-pi-ai',
ops: [{ op: 'set', path: ['providers', 'acme-gateway', 'api'], value: 'anthropic-messages' }],
expectedRevision: 3,
})
})
it('selects nothing for a declared route whose profile names no protocol', async () => {
// A route hand-written into settings.yaml with no model needs no protocol
// to resolve, so the card can be opened over one. The select must not read
// as if that route had picked its first choice.
await mountSection({
providers: { 'acme-gateway': { baseURL: 'https://gateway.acme.example/v1' } },
declaredRoutes: ['acme-gateway'],
})
openEditor('acme-gateway')
expect(screen.getByLabelText<HTMLSelectElement>(en.customApi).value).toBe('')
})
it('retries only the key after the profile landed, and reports the provider on cancel', async () => {

View File

@@ -61,6 +61,29 @@ describe('ModelsSection theme styles', () => {
expect(block('.rowCard')).not.toMatch(/\bbackground\s*:/)
})
it('gives every dropdown the shared chevron instead of the OS arrow', () => {
// `select.input` caps the control at 240px, and the OS arrow is painted
// flush inside that shrunk right edge — visibly tighter than every other
// control on the page. `.selectInput` is what removes it, reserves the
// right pad, and paints the shared chevron; a `<select>` that takes
// `.input` alone silently keeps the OS one.
const sources = readdirSync(fileURLToPath(new URL('../src/client/', import.meta.url)))
.filter(name => name.endsWith('.tsx'))
.map(name => ({
name,
text: readFileSync(fileURLToPath(new URL(`../src/client/${name}`, import.meta.url)), 'utf8'),
}))
const bare = sources.flatMap(({ name, text }) => text
.split('<select')
.slice(1)
// The element's own attributes end at the first `>`; a child `<option>`
// carries no className of its own and must not answer for the select.
.map(rest => rest.slice(0, rest.indexOf('>')))
.filter(attributes => !attributes.includes('selectInput'))
.map(() => name))
expect(bare).toEqual([])
})
it('never falls back to a literal colour', () => {
// A token that resolves is never the problem; an undeclared one takes this
// branch, and a literal here is a single colour for both themes.

View File

@@ -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 packages/client/ui-permission/README.md
README.md: 742e82d767152073ab963dc74c0565d6e8f8e5c4
README.md: cf3981d66745bd5a65c83e09daeb8463023f447b
README.zh.md: 70bbbb2d14358cbe52a6fc27deb7ce01d5f3679b

View File

@@ -6,7 +6,7 @@ Permission browser surfaces for two different lifetimes. The General-settings ro
The current-session surface remains a popupSelect DECORATION hung on the host `/permission` command (`ctx.command.decorate`). A decoration is not a second command — the host command keeps its slash-menu row, the argued path (`/permission <preset>` switches directly), and the durable lifecycle logging; the decoration replaces only the bare invocation with the picker: one flat preset list with the current value marked active and kebab-case preset names rendered as title-case labels (`workspace-write``Workspace Write`, the composer chip's display transform twin), where a pick submits the `/permission <preset>` command line. Options and the active mark read the session's `permissions` projection (the same host-computed select the composer chip renders), so both current-session surfaces share one read source and one write path, and the pushed projection frame is the single confirmation both follow. The decoration is available exactly while the projection key is present; a permission-less composition shows neither picker nor Settings row.
The `/client` export surface is the plugin body (`apply`/`inject`).
The `/client` exports are the plugin body (`apply`/`inject`).
## Model Experience

View File

@@ -15,7 +15,7 @@ export const name = 'client-ui-primitives-invariant'
export const inject = ['invariants']
/**
* No runtime invariant: pure props-in React atoms with zero cordis surface
* No runtime invariant: pure props-in React atoms with no Cordis API
* no events, no services, no mutable cross-plugin state; rendering contracts
* are asserted directly by this package's component specs.
*/

View File

@@ -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 packages/client/ui-sidebar/README.md
README.md: 45ae267d98b17bbc612cf932f5b95b42ba6ff4bf
README.md: 4eb9eeb73f1f8398eb9d16434996840182ba79a9
README.zh.md: a9fb927305d0bab5fb4d27adbfdbec90dfa1dd6d

View File

@@ -12,7 +12,7 @@ Scrollbars in the column are a pointer affordance: the shell rebinds ui-theme's
The foot is the `sidebar.settings` seat: the sidebar renders only the bottom-pinned layout slot and shares its column state (`wide`); ui-settings registers the trigger row and settings panel there.
The `/client` export surface is the plugin body (`apply`/`inject`) plus the contract types only; SidebarRoot, the row components, and the tree derivation remain package-internal behind the slot registration.
The `/client` exports are the plugin body (`apply`/`inject`) plus the contract types only; SidebarRoot, the row components, and the tree derivation remain package-internal behind the slot registration.
## Model Experience

View File

@@ -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 packages/client/ui-skill/README.md
README.md: 36b4cf4181d74ca1ea05fd8ed2db5e42fa36c7f2
README.md: 0456db4de9453e5060e39b5f061422486e44dfc9
README.zh.md: 336f43117e7bc4de41a31e636ee0966e5d1a2cd6

View File

@@ -8,7 +8,7 @@ A pick lands the literal `/name ` text and the prompt ships the same literal ([s
A failed `skill.list` throws from `candidates`, which the slash shell logs and folds into a silent menu-group drop — the menu shows only pending/ready states.
The `/client` export surface is the plugin body (`apply`/`inject`) only; the source object is internal to the registration effect.
The `/client` exports are the plugin body (`apply`/`inject`) only; the source object is internal to the registration effect.
## Skill tool row

View File

@@ -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 packages/client/ui-slash/README.md
README.md: 3f97351bea04dbc268e6acc105be10e1e4e0a4b5
README.md: e6c9c3240d03a0cde57eda0bce2a995f91786f4e
README.zh.md: cf9e42d419cb472509a74ba227990b1502b38a75

View File

@@ -8,7 +8,7 @@ Layering: `src/core/` is the pure core — `detectTrigger`, `menuReduce`/`seedGr
MenuView renders the menu store into the `conversation.input.overlay` slot (list kind, session scope) and renders null while closed. Typed triggers seed every source registered for that trigger; a programmatic launcher seeds only its requested source and publishes the source name through the controller's `launcher` snapshot store until the menu closes or typed tracking resumes. Groups sort by the optional `SlashSource.order` (lower first, default 0, ties keep registration order) under title rows localized through the `slash.menu` locale namespace (an unknown source shows its raw name); the list height clamps to the space above the composer, and a pointer down outside both the menu and the surrounding composer card dismisses it. The slot is owned by ui-conversation's composer entry (anchor, children declaration, lifecycle); its SlotMap type merge lives in this package's `src/client/slots.ts` because the dependency direction (ui-conversation → ui-slash) admits no reverse type import. Combobox pattern: focus stays in the textarea, rows pick on mousedown, the highlight rides `aria-activedescendant`.
The `/client` export surface is the plugin body (`apply`/`inject`), `SlashService`, `MenuViewInjected`, and the contract types. MenuView itself is internal — the slot registration closes over it.
The `/client` exports are the plugin body (`apply`/`inject`), `SlashService`, `MenuViewInjected`, and the contract types. MenuView itself is internal — the slot registration closes over it.
## Model Experience

View File

@@ -27,7 +27,7 @@ export interface LocaleFace extends HostObservable<{ revision: number }> {
bind(ns: string): Translate
}
/** Minimal observable surface for host-provided standard-kit data sources. */
/** Minimal observable API for host-provided standard-kit data sources. */
export interface HostObservable<T> {
getSnapshot(): T
subscribe(fn: () => void): () => void
@@ -97,7 +97,7 @@ export interface RenderOpts {
hookContext?: unknown
}
/** Host surface the runtime SlotsService presents to the installed renderer. */
/** Host API the runtime SlotsService presents to the installed renderer. */
export interface SlotRendererHost {
/**
* Subscribe to a key's registration changes (microtask-batched).
@@ -167,8 +167,8 @@ export interface SlotRendererHost {
/** The installation contract: runtime owns install()/renderSlot(); web-react implements rendering. */
export interface SlotRenderer {
/**
* Render the root slot tree over the host surface (the only ctx-level entry).
* @param host - the installing service's host surface.
* Render the root slot tree over the host API (the only ctx-level entry).
* @param host - the installing service's host API.
* @param ownerProps - owner props from the shell's renderSlot('root', ...) call.
* @returns the rendered tree.
*/

View File

@@ -1,6 +1,6 @@
// SlotCore terminal-design behavior: the single register composition API —
// a-priori 'root', children declaration/authorization, load-time validation,
// one-axis lifecycle cascade, store scope pinning, subscription surface.
// one-axis lifecycle cascade, store scope pinning, subscription API.
import { describe, expect, it, vi } from 'vitest'
import type { SlotComponent, StoreHandle } from '@deepseek-ai/dsh-client-ui-slots'
import { SlotCore } from '@deepseek-ai/dsh-client-ui-slots'
@@ -230,7 +230,7 @@ describe('store scope pinning', () => {
})
})
describe('subscription surface', () => {
describe('subscription API', () => {
it('tracks declaration epochs separately from ordinary entry mutations', () => {
const core = new SlotCore()
expect(core.declarationEpoch('root')).toBe(1)

View File

@@ -5,8 +5,8 @@ import { SlotCore } from '@deepseek-ai/dsh-client-ui-slots'
declare module '@deepseek-ai/dsh-client-ui-slots' {
interface SlotMap {
'surface.a': { kind: 'single'; scope: 'root' }
'surface.b': { kind: 'single'; scope: 'root' }
'dynamic.a': { kind: 'single'; scope: 'root' }
'dynamic.b': { kind: 'single'; scope: 'root' }
'surface.injected': { kind: 'single'; scope: 'root'; inject: { token: string } }
}
}
@@ -16,17 +16,17 @@ const Comp: SlotComponent<object> = () => null
describe('dynamic-key escape hatch', () => {
it('specDynamic reads wide-typed specs for string keys; undefined while undeclared', () => {
const core = new SlotCore()
expect(core.specDynamic('surface.a')).toBeUndefined()
core.register({ name: 'root', children: { 'surface.a': { kind: 'single', scope: 'root' } } }, Comp as never)
expect(core.specDynamic('surface.a')).toEqual({ kind: 'single', scope: 'root' })
expect(core.specDynamic('dynamic.a')).toBeUndefined()
core.register({ name: 'root', children: { 'dynamic.a': { kind: 'single', scope: 'root' } } }, Comp as never)
expect(core.specDynamic('dynamic.a')).toEqual({ kind: 'single', scope: 'root' })
expect(core.specDynamic('never.declared')).toBeUndefined()
})
it('spec() narrows by SlotMap key', () => {
const core = new SlotCore()
core.register({ name: 'root', children: { 'surface.a': { kind: 'single', scope: 'root' } } }, Comp as never)
expect(core.spec('surface.a')).toEqual({ kind: 'single', scope: 'root' })
expect(core.spec('surface.b')).toBeUndefined()
core.register({ name: 'root', children: { 'dynamic.a': { kind: 'single', scope: 'root' } } }, Comp as never)
expect(core.spec('dynamic.a')).toEqual({ kind: 'single', scope: 'root' })
expect(core.spec('dynamic.b')).toBeUndefined()
})
it('records the parent-declared Slot inject on the runtime spec', () => {
@@ -41,9 +41,9 @@ describe('dynamic-key escape hatch', () => {
it('entries/getVersion on an untouched key return the frozen empty array and 0', () => {
const core = new SlotCore()
expect(core.entries('surface.b')).toHaveLength(0)
expect(core.entries('surface.b')).toBe(core.entries('surface.b'))
expect(core.getVersion('surface.b')).toBe(0)
expect(core.entries('dynamic.b')).toHaveLength(0)
expect(core.entries('dynamic.b')).toBe(core.entries('dynamic.b'))
expect(core.getVersion('dynamic.b')).toBe(0)
})
it('isLive is false for entries the core never held', () => {

View File

@@ -103,7 +103,7 @@ declare function NarrowTakeover(props: PropsRuntime<'chain.takeover'> & { matche
describe('terminal-design type chain', () => {
it('holds the positive chain and the compile-time negatives', () => {
// Everything below is compile-surface only.
// Everything below is compile-time only.
const samples = (core: SlotCore, chat: ChatHandle, fp: FrameProps, cp: ConvProps, acts: BoundActions<ChatHandle>) => {
// ── positive chain ─────────────────────────────────────────────
// Frame: children + factory store + inject; actions arrive baked.

View File

@@ -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 packages/client/ui-theme/README.md
README.md: 7b81728b02c0dadcbd134a4bca20c49721a3dd52
README.md: cab9961a6d703d600a856e71339cda7062d20d62
README.zh.md: 81b64c356749b6ffe12694b218e92eaa483ff739

View File

@@ -20,5 +20,5 @@ None; this package neither assembles nor sends a provider request.
## Known Limitations and Deferred Work
- **Third-party themes are a surface, not a product** — registering one means overriding same-named alias variables; no validation exists that an override set is complete.
- **Third-party themes are an extension point, not a product** — registering one means overriding same-named alias variables; no validation exists that an override set is complete.
- **The token sheets are the sole color authority** — values absent from cssdesign (for example the design's #4176E6 tab blue) are deliberately not appended; the nearest semantic token wins. Design-owner-approved additions are the exception and enter as a static step plus a semantic alias in the same change (`--dsw-static-blue-900` / `--dsw-alias-label-primary-bluish`).

View File

@@ -2,7 +2,7 @@
/**
* Real tsdown artifact shape: lib/client.js hands off through
* window.__ModuleLoader__.load, resolves externals through the injected
* require, returns the export surface (apply + inject), and a mounted apply
* require, returns the exports (apply + inject), and a mounted apply
* registers the view tab into a real SlotsService ring. Skips when dist/ is
* not built (`pnpm --filter @deepseek-ai/dsh-client-ui-trajectory bundle`).
*/
@@ -52,24 +52,24 @@ describe('tsdown client artifact', () => {
['@deepseek-ai/dsh-client-runtime/client', await import('@deepseek-ai/dsh-client-runtime/client')],
['@deepseek-ai/dsh-client-ui-primitives', await import('@deepseek-ai/dsh-client-ui-primitives')],
])
const surface = handoff!.factory((spec) => {
const exports = handoff!.factory((spec) => {
if (!modules.has(spec)) throw new Error(`unexpected require: ${spec}`)
return modules.get(spec)
})
return { handoff: handoff!, surface }
return { handoff: handoff!, exports }
}
it.skipIf(code === undefined)('hands off with the manifest id and a DI-require factory', async () => {
const { handoff, surface } = await loadArtifact()
const { handoff, exports } = await loadArtifact()
expect(handoff.id).toBe(PLUGIN_ID)
expect(surface.apply).toBeTypeOf('function')
expect(surface.inject).toEqual([
expect(exports.apply).toBeTypeOf('function')
expect(exports.inject).toEqual([
'slots', 'conversationEvents', 'conversationViews', 'sessions', 'locale',
])
})
it.skipIf(code === undefined)('mounted as an object plugin, apply registers the view tab on the real ring', async () => {
const { surface } = await loadArtifact()
const { exports } = await loadArtifact()
const ctx = new Context()
const slots = new SlotsService(ctx)
await ctx.plugin(ConversationEventRegistry).await()
@@ -87,7 +87,7 @@ describe('tsdown client artifact', () => {
ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never)
const locale = await import('@deepseek-ai/dsh-client-locale/client')
ctx.plugin({ inject: [...locale.inject], apply: locale.apply })
const fiber = ctx.plugin(surface as { apply: (ctx: Context) => void })
const fiber = ctx.plugin(exports as { apply: (ctx: Context) => void })
await fiber.await()
const events = ctx.get('conversationEvents') as ConversationEventRegistry
const views = ctx.get('conversationViews') as ConversationViewRegistry

View File

@@ -6,7 +6,7 @@ export { bindSnapshotSelector } from './bind.ts'
/**
* Selector hook over a session's conversation snapshot. Wide (`object`) by
* default inside this dependency-inverted package; runtime narrows it once at
* its export surface (`UseSession<ConversationSnapshot>`) — the snapshot type
* its exports (`UseSession<ConversationSnapshot>`) — the snapshot type
* never flows back into web-react.
*/
export type UseSession<Snap extends object = object> = SnapshotSelectorHook<Snap>

View File

@@ -20,7 +20,7 @@ export const HostContext = createContext<SlotRendererHost | null>(null)
/**
* Read the installed renderer host; throws outside the rendered root tree
* (framework components must not render detached from the renderer).
* @returns the host surface.
* @returns the host API.
*/
export function useHost(): SlotRendererHost {
const host = useContext(HostContext)
@@ -132,7 +132,7 @@ export function SessionMaybeProvider({ children }: { children: ReactNode }) {
)
}
/** SessionProvider surface: render-prop body plus the no-session branch. */
/** SessionProvider API: render-prop body plus the no-session branch. */
export interface SessionProviderProps {
/** No-session body (also covers a current id whose session cannot be resolved). */
empty?: (() => ReactNode) | undefined

View File

@@ -3,7 +3,7 @@
* Integration against the real ui-slots SlotCore through a passthrough host:
* registrations go through the real register() (options form, children
* declaration), and the outlets ride the real subscribe/getVersion/entries/
* isLive surfaces — microtask-batched notifications, mutation-stable entry
* isLive APIs — microtask-batched notifications, mutation-stable entry
* references (the cache axis), and ledger-fed stale bindings are the
* real-core semantics the fake-host suite cannot vouch for.
*/

View File

@@ -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 packages/client/web/README.md
README.md: 48355a046910bd5b78249af7b7781dab6a7c3a60
README.md: 4b44e0e2c2e192363ce32ac2decc218f5ea17bd2
README.zh.md: f0b567569849c698e8a1a29323e7088b49996522

View File

@@ -6,7 +6,7 @@ Web shell kernel: `new AppWebEntry(el, seams?).run()` mounts the whole client th
Shell self-sufficiency (web2 hard rule): the kernel value-imports no plugin package — the boot status store and signals are hand-rolled here (`loader-status.ts`), so the loading page works while (and especially when) plugins fail. The app-shell assembly (`@deepseek-ai/dsh-client-app-shell`, a shell-owned pseudo entry with no npm package behind it) is the only module registered through `registerStatic`; it inject-waits on slots/sessions/layout like any plugin.
`PLATFORM_MODULES` (src/platform.ts) is the single source of truth for the shared module surface: seed-table keys, tsdown client externals, and the vite alias set are its projections.
`PLATFORM_MODULES` (src/platform.ts) is the single source of truth for shared modules: seed-table keys, tsdown client externals, and the Vite alias set are its projections.
The optional override parameter `seams` forwards the module system's `loadBundle` transport override (`BootSeams`) for environments where external `<script>` execution cannot reach the page context; ordinary browser callers omit it.

View File

@@ -19,7 +19,7 @@ import type { PlatformModule } from './platform.ts'
/**
* Build the static table handed to the module loader at boot.
* @returns module specifier → export-surface entity (one entry per platform word).
* @returns module specifier → exported entity (one entry per platform word).
*/
export function getStaticModules(): Record<string, unknown> {
// The satisfies pin is the projection contract: a word added to

View File

@@ -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 packages/code-runtime/code-runtime-worker/README.md
README.md: 590b79dcd1bc322350060b55767b09c6305edacc
README.md: 44ba6a98169057f24d55f41cf3ca7b9f68bb329c
README.zh.md: e45aa484c0935d5b4b1626f5c4c7c72482cd5b5d

View File

@@ -34,7 +34,7 @@ Every field is validated and defaulted; `maxOutputBytes` is a safe integer of at
Source mode loads erasable-only `src/worker.ts` through Node's native type stripping. Its transitive runtime closure contains only Node built-ins and relative source modules, so a fresh checkout never requires a sibling workspace package's unbuilt `lib/` export. The worker-local and session-owned JSON boundaries both flatten and rebuild validated values around the message port so application nesting never reaches structured clone. Built mode passes the sibling `lib/worker.cjs` as a filesystem path because pkg's VFS Worker hook expects CommonJS; the same path works under ordinary Node. The repository-wide requirement to exercise this published entry path belongs to the [testing policy](../../../docs/testing.md).
The SDK surface is the default/named `WorkerCodeRuntime` class plus `Config`. The operational `./worker` subpath exists only as the packaged spawn entry; the wire protocol and bootstrap helpers are source-private implementation details.
The SDK API is the default/named `WorkerCodeRuntime` class plus `Config`. The operational `./worker` subpath exists only as the packaged spawn entry; the wire protocol and bootstrap helpers are source-private implementation details.
## Model Experience
@@ -49,6 +49,6 @@ No direct invalidation; the named consumer owns any request-prefix changes.
- **OS processes a program spawns survive termination** — `worker.terminate()` ends the thread only, weaker than bash-local's process-group kill; orphan cleanup is a deployment concern until a container backend exists.
- **Type-strip rides Node's experimental `stripTypeScriptTypes` API** — amaro or sucrase are the named drop-in replacements if the relied-on behavior shifts.
- **`computeMs` expiry can overshoot by up to one poll interval** — busy time is sampled every 25 ms (an internal constant, deliberately not config).
- **Programs get a five-method `console` shim** (`log`/`info`/`warn`/`error`/`debug`) — deliberately not Node's full console surface.
- **Programs get a five-method `console` shim** (`log`/`info`/`warn`/`error`/`debug`) — deliberately not Node's full console API.
- **Intermediate binding values have no byte cap** — a program can exhaust process or worker memory with a value that never becomes outer output.
- **The 64 MiB default is a rejection boundary, not recoverable storage** — outer spill can save only the bounded logs and diagnostic returned after `output-limit`; bytes rejected beyond the runtime cap never reach the spill layer.

View File

@@ -22,7 +22,7 @@ function defineBindingErrorField(error: Error, key: string, value: string): void
capturedObjectDefineProperty(error, key, attributes)
}
/** The port surface the bootstrap needs — satisfied by `parentPort` and by the tests' fake. */
/** The port API the bootstrap needs — satisfied by `parentPort` and by the tests' fake. */
export interface BootstrapPort {
postMessage(message: WorkerToHost): void
on(event: 'message', listener: (message: ReplyMessage) => void): void
@@ -105,7 +105,7 @@ const CONSOLE_LEVELS = ['log', 'info', 'warn', 'error', 'debug'] as const
* `util.inspect`-style (matching real console formatting closely enough for
* a model to recognize its own output) into the buffer. Only these five
* exist — the program gets a deliberately small console, not Node's full
* surface.
* console API.
* @param logs - the buffer every rendered line is pushed into.
* @returns the five-method console object handed to the program.
*/

View File

@@ -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 packages/code-runtime/code-runtime/README.md
README.md: 1d3cee958c186b7d0b2be1f69400977d009abc40
README.md: ee64f7a16295bcf31e40eb305ca35aa774dac8d2
README.zh.md: 9aac1196085d12dfe240af7667e98787f445c534

View File

@@ -32,7 +32,7 @@ No direct invalidation; the named consumer owns any request-prefix changes.
## Known Limitations and Deferred Work
- **`run()` is one-shot** — `logs` arrive only on the resolved `CodeRunResult`; the seam exposes no streaming-log or progress surface for a live program's output.
- **`run()` is one-shot** — `logs` arrive only on the resolved `CodeRunResult`; the seam exposes no streaming-log or progress API for a live program's output.
- **A persistent REPL-style kernel is recorded future work** — the no-state-between-runs contract stands until a persistent-kernel backend brings its own logging story ([Code Mode Agent Note](../../../.agents/notes/implemented/feature/2026-06-15-code-mode.md)).
- **Only the worker-thread backend ships** — `'process'`/`'container'` are declared well-known `isolation` values with no implementation; a hard security boundary awaits a container backend.
- **Intermediate binding values have no byte cap** — implementations remain subject to structured-clone cost and process memory, while a provider or executor may already have imposed its own acquisition bound.

View File

@@ -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 packages/core/agent-loop/README.md
README.md: 6092363fae2853d6c5d92aaf8cd01e41e18e0b52
README.md: 889fd74671eddc202b814cdf2749069ec3cea02c
README.zh.md: b65b5334d735a1e0b51fa517ce41c0c953f87cf7

View File

@@ -12,7 +12,7 @@ This is the only package in the harness that contains concrete loop logic. Every
Creation and resume are one rollback-covered transaction: construct a private session, concrete agent, and scoped context; await optional setup; enter both registries; announce `session/created` then `agent/created`; emit `agent/session-start`; and only then start the driver. Setup receives the full scoped `Context` as trusted same-process composition code and must not drive the unpublished agent. Ordinary typed identity and option inputs are borrowed under their readonly contract, while seed events and session metadata are validated and snapshotted because they cross the durable session boundary. An optional `AbortSignal` cancels only load/setup/publication and is detached before the returned handle becomes visible.
The caller fiber and the AgentLoop provider are co-owners. `AgentFactory.createAgent(ownerCtx, options)` and `resume(ownerCtx, options)` receive caller ownership explicitly, while the factory keeps its own dependency context for `sessions`/`llm`/`tools`/`systemPrompt`; this lets a caller inject only `agents` without shrinking the new agent's service surface. Caller unload, handle disposal, or provider unload converge on one memoized quiescence boundary. Provider shutdown waits both resource teardown and the public create/resume wrapper that observed deactivation, so no continuation can publish after dependencies disappear.
The caller fiber and the AgentLoop provider are co-owners. `AgentFactory.createAgent(ownerCtx, options)` and `resume(ownerCtx, options)` receive caller ownership explicitly, while the factory keeps its own dependency context for `sessions`/`llm`/`tools`/`systemPrompt`; this lets a caller inject only `agents` without shrinking the new agent's service set. Caller unload, handle disposal, or provider unload converge on one memoized quiescence boundary. Provider shutdown waits both resource teardown and the public create/resume wrapper that observed deactivation, so no continuation can publish after dependencies disappear.
Each agent and its session share one caller-chosen `SessionId`, assumed globally unique; accidental UUID collisions are outside the supported model. Two concurrent operations with the same id may both prepare, but the final `enter()` calls arbitrate publication and every loser rolls its private resources back. Each detach is bound to the exact entered object, so a stale disposer cannot remove a later same-id replacement. A detach requested during a synchronous creation notification waits for that dispatch to unwind, preserving created/disposed pairing. Teardown runs stop and drain → unwind scope → detach agent → detach session; the id becomes reusable after private scope cleanup. Ordinary non-vetoing `agent/*` notifications go through `agentEvents(ctx, agent)`, and per-step assembly goes through `assembleContextFor(agent)`.

View File

@@ -23,7 +23,7 @@ import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
* `agent/session-start`, `agent/turn-stopping`, and the
* `tools/pre-execute` / `tools/post-execute`
* split with `additionalContexts` buffering. These verify the canonical event
* surface a hook bridge (or a native plugin) programs against, WITHOUT any
* API a hook bridge (or a native plugin) programs against, WITHOUT any
* external protocol — a native plugin uses the typed decisions directly.
*/

View File

@@ -605,7 +605,7 @@ describe('agent scope lifecycle', () => {
agentCtx.systemPrompt.section({
name: 'dependency-origin-section',
order: 1,
text: 'factory dependency surface',
text: 'factory dependency API',
})
},
})

View File

@@ -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 packages/core/agent/README.md
README.md: 61294df5cca1b03f9f158678f2992a6e4fbaaffd
README.zh.md: 5576a07330926cfd8fd5bd42c8a0351a375f035c
README.md: f6b698e93b254c97786155e7d2c7e81f07c0d981
README.zh.md: 8b108ca93b11ce5e4af57f9d9c823a4b10eac5ab

View File

@@ -116,6 +116,6 @@ Prefix-stable while an agent's scoped registrations are unchanged. Setup or relo
- **Ambient identity may outlive liveness** — consumers still check `agent.status`, cancellation, and the owning capability contract before lifecycle-sensitive work.
- **Inter-agent channels beyond delegation** — shared state, streaming child output, and background/poll semantics remain outside the current synchronous `ctx.subagents` seam.
- **`agent/session-start` cannot gate startup** — it remains a synchronous, veto-less notification; async composition that must finish before publication belongs in the factory's `setup(agentCtx)` transaction instead.
- **`cancel()` clears the inbox by default** — it aborts the in-flight turn plus queued and steering work; `cancel(cause, { keepInbox: true })` aborts only the turn and preserves pending items. There is still no step-only abort that keeps the in-flight turn running ([stop-surface Agent Note](../../../.agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.md)).
- **`cancel()` clears the inbox by default** — it aborts the in-flight turn plus queued and steering work; `cancel(cause, { keepInbox: true })` aborts only the turn and preserves pending items. There is still no step-only abort that keeps the in-flight turn running ([stop API Agent Note](../../../.agents/notes/implemented/simplification/2026-06-20-public-agent-stop-api.md)).
- **Each additional `UserMessage` carries exactly one `MessageSource`** — contributions from several plugins merged onto one tool call collapse under one source, so the message cannot name several producers.
- **`SessionStartSource` reserves `'clear'`/`'compact'` with no emitter yet** — only `'startup'`/`'resume'` occur until the driving subsystems land (`TODO(compaction)`).

View File

@@ -116,6 +116,6 @@ inbox 的实时通知刻意采用逐消息的最小载荷:`agent/inbox/inserte
- **环境身份可能比存活状态更久**:消费方在生命周期敏感工作前,仍要检查 `agent.status`、取消状态和所属能力约定。
- **委派以外的 agent 间通道**:共享状态、流式子输出和后台/轮询语义仍在当前同步 `ctx.subagents` seam 之外。
- **`agent/session-start` 不能为启动设置门禁**:它仍是同步且不可 veto 的通知;必须在发布前完成的异步组合属于工厂的 `setup(agentCtx)` 事务。
- **`cancel()` 默认清空 inbox**:它会中止正在处理的轮次以及排队和 steering 工作;`cancel(cause, { keepInbox: true })` 只中止轮次并保留待处理项。仍不存在只中止步骤、同时让正在处理的轮次继续运行的操作([关于停止操作接口的 Agent Note](../../../.agents/notes/implemented/simplification/2026-06-20-public-agent-stop-surface.md))。
- **`cancel()` 默认清空 inbox**:它会中止正在处理的轮次以及排队和 steering 工作;`cancel(cause, { keepInbox: true })` 只中止轮次并保留待处理项。仍不存在只中止步骤、同时让正在处理的轮次继续运行的操作([关于停止操作接口的 Agent Note](../../../.agents/notes/implemented/simplification/2026-06-20-public-agent-stop-api.md))。
- **每条附加 `UserMessage` 恰好携带一个 `MessageSource`**:多个插件合并到一次工具调用上的贡献会归入同一来源,因此该消息无法列出多个生产者。
- **`SessionStartSource` 预留 `'clear'`/`'compact'`,但还没有发出方**:在驱动子系统落地前,只会出现 `'startup'`/`'resume'``TODO(compaction)`)。

View File

@@ -160,7 +160,7 @@ export interface ResumeAgentOptions {
* {@link AgentRegistry.resume}. The disposer is a CAPABILITY: among consumers,
* only the holder can tear this agent down. The registered factory provider is
* also a structural owner because the scoped agent depends on that provider's
* service surface; provider unload stops and drains every live handle it made.
* service API; provider unload stops and drains every live handle it made.
* `dispose()` stops the loop, awaits its exit, unregisters the agent, removes
* its session from the store, and finally unwinds its scoped world.
*

View File

@@ -1,5 +1,5 @@
/**
* Negative-path tests for the export-surface JSDoc gate (`scripts/verify-export-jsdoc.ts`).
* Negative-path tests for the exported-API JSDoc gate (`scripts/verify-export-jsdoc.ts`).
*/
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
@@ -46,7 +46,7 @@ export function hiddenFn(value: string): string { return value }
expect(violations.every(violation => violation.includes('publicFn'))).toBe(true)
})
it('accepts a fully documented surface', () => {
it('accepts a fully documented API', () => {
expect(collectExportJsdocViolations(make(`
/**
* Add one to a count.
@@ -106,7 +106,7 @@ export const halve = (n: number): number => n / 2
'/**\n * Docs.\n * @param ghost - not real.\n */\nexport function f({ a }: { a: number }): void {}\n',
))
expect(violations).toEqual([
expect.stringMatching(/parameter '\{ a \}' is a binding pattern; the export surface needs simple identifier parameters/),
expect.stringMatching(/parameter '\{ a \}' is a binding pattern; the exported API needs simple identifier parameters/),
expect.stringMatching(/@param ghost does not match any parameter \(stale tag\?\)/),
])
})
@@ -166,9 +166,9 @@ describe('verify-export-jsdoc export forms', () => {
))).toEqual([expect.stringMatching(/exported function 'f' .* has no JSDoc\./)])
})
it('does not treat a never-exported sibling declarator as surface', () => {
it('does not treat a never-exported sibling declarator as API', () => {
// `export { publicValue }` resolves to the whole variable statement; only
// the named declarator is surface — the gate must not demand JSDoc for
// the named declarator is exported — the gate must not demand JSDoc for
// the private sibling sharing the statement.
expect(collectExportJsdocViolations(make(
'/** The public knob. */\nconst publicValue = 1, privateHelper = 2\nexport { publicValue }\nvoid privateHelper\n',
@@ -177,7 +177,7 @@ describe('verify-export-jsdoc export forms', () => {
it('unions declarators across multiple export lists over one statement', () => {
// Two lists each name one declarator of the same undocumented statement:
// both are surface (deduplicating on first resolution would drop `b`),
// both are exported (deduplicating on first resolution would drop `b`),
// while the never-exported `c` stays out.
const violations = collectExportJsdocViolations(make(
'const a = 1, b = 2, c = 3\nexport { a }\nexport { b }\nvoid c\n',
@@ -344,7 +344,7 @@ describe('verify-export-jsdoc fail-closed forms', () => {
))).toEqual([])
})
it('treats an inline function-type annotation as the surface signature', () => {
it('treats an inline function-type annotation as the API signature', () => {
expect(collectExportJsdocViolations(make(
'/** Maps a number. */\nexport declare const f: (x: number) => number\n',
))).toEqual([
@@ -402,7 +402,7 @@ describe('verify-export-jsdoc fail-closed forms', () => {
])
})
it('treats a single-call-signature type literal as the surface signature', () => {
it('treats a single-call-signature type literal as the API signature', () => {
expect(collectExportJsdocViolations(make(
'/** Maps. */\nexport declare const f: { (x: number): number }\n',
))).toEqual([

View File

@@ -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 packages/core/scope/README.md
README.md: a8fbe97ae3b59f223bb52e44860439803fda420c
README.md: 1ed09acd7ccb864b16fb5d2ac4390264085d1c9e
README.zh.md: af238232987c74e89cdc4e009d3d0c40f71b02d8

View File

@@ -28,10 +28,10 @@ The registration context determines both visibility and ownership, preventing a
Scope-aware services define a concrete `ScopeLayer` that aggregates their heterogeneous tables and domain helpers. `ScopedLayers.effect()` accepts one synchronous action returning one synchronous undo, installs that undo before optional notification, and reclaims an exact-scope layer only when the complete aggregate is empty. `notify` defaults to `true`; the supplied callback owns whether observer failures throw or are contained. `EntryValues` remains internal, the storage classes are imported from the package root rather than a `/store` subpath, and the shared storage does not define registry-specific filtering or iteration policy. See the [shared scoped-layer storage Agent Note](../../../.agents/notes/implemented/architecture/2026-07-12-scoped-layers-store.md).
Handing out a scoped context hands out the minting plugin's service-resolution surface (resolution walks the minting fiber's dependency chain, not the holder's) — mint it from the plugin whose dependencies the scoped registrations need to resolve.
Handing out a scoped context hands out the minting plugin's service-resolution API (resolution walks the minting fiber's dependency chain, not the holder's) — mint it from the plugin whose dependencies the scoped registrations need to resolve.
## Known Limitations and Deferred Work
- **Only scope-aware surfaces isolate state** — registries must file by `scopeOf()` and events must dispatch through `scopeTarget()`; an arbitrary Cordis service remains context-global merely because it is called through a scoped context.
- **Only scope-aware APIs isolate state** — registries must file by `scopeOf()` and events must dispatch through `scopeTarget()`; an arbitrary Cordis service remains context-global merely because it is called through a scoped context.
- **A context carries one nearest scope key** — the hierarchy lives in the key-level parent relation, not in context tags; nested scope CONTEXTS still shadow to a single tag, and multi-membership policy sets remain unsupported.
- **Service reachability comes from the scope minter** — handing out `Scope.ctx` also hands out the minting plugin's injected service surface, so a broader minter cannot later be narrowed by the holder.
- **Service reachability comes from the scope minter** — handing out `Scope.ctx` also hands out the minting plugin's injected services, so a broader minter cannot later be narrowed by the holder.

View File

@@ -128,8 +128,8 @@ export interface CreateScopeOptions {
/**
* Mint a scope under `ctx`. The scoped context inherits the minting plugin's
* dependency surface and owns every registration made through it.
* @param ctx - active context whose dependency surface the scope inherits.
* dependency API and owns every registration made through it.
* @param ctx - active context whose dependency API the scope inherits.
* @param key - opaque identity used for listener routing.
* @param options - optional scope-chain placement.
* @returns the scoped context and exact/shared disposal boundaries.

View File

@@ -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 packages/core/tools/README.md
README.md: cc7b323ae1de917e93e243e97bd5cf5937ecdca4
README.md: 75d18712a02ecc72c2ea3a7203d2a7377cef87c7
README.zh.md: 8d4ae42596483f77aa82b23a0b41168465e2b165

View File

@@ -111,7 +111,7 @@ Tools optionally own pure `presentCall()` and `presentResult()` render intents,
- Call views are `{ card: 'generic', title, kind?, rawInput?, content?, locations? }`, `{ card: 'terminal', title, description?, cwd? }`, or `{ card: 'diff', title, diffs, locations? }`.
- Result views are `{ card: 'generic', title?, content? }`, `{ card: 'terminal', title?, output?, exitCode?, signal? }`, `{ card: 'diff', title?, diffs }`, `{ card: 'search', shape, title?, truncated, total, … }` (a completed discovery search — grouped-by-file matches for `shape: 'matches'` (grep) or a flat path list for `shape: 'paths'` (glob), with `truncated`/`total` so a UI never presents a capped result as complete; the view carries no result text and a search has no `card: 'search'` call-time analogue), `{ card: 'read', title?, path, offset, lines, totalLines, lang?, content? }` (a completed file read → a line-numbered, optionally syntax-highlighted code view; `offset` is the 1-based first line the window requested, kept even when `lines` is empty; `lines` is `{ number, text }[]` keeping each file line number, and `content` is the envelope-stripped text a UI without read support falls back to), or `{ card: 'web', kind: 'search' | 'fetch', title?, … }` (a completed web retrieval; the `kind` arms carry the structured search sources or the fetch summary, and a UI without the `web` capability falls back to the raw result content).
Returning `undefined` selects generic fallback. Presenters depend only on their arguments and the durable result because UIs call them during live streaming and log replay. `output.presentationMeta(args, value)` derives JSON metadata for direct surface calls; that metadata persists with `tool/result` and returns to `presentResult`, while the canonical value itself remains execution-local and is never replayed. Nested Code dispatches do not compute metadata. `defineTool` soft-validates older logged arguments and falls back instead of crashing replay. `dsh-tool-bash` and `dsh-tool-fs` are the reference implementations; the [canonical-output Agent Note](../../../.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md) owns the value/presentation split and the [render-intent Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md) owns card vocabulary.
Returning `undefined` selects generic fallback. Presenters depend only on their arguments and the durable result because UIs call them during live streaming and log replay. `output.presentationMeta(args, value)` derives JSON metadata for direct top-level calls; that metadata persists with `tool/result` and returns to `presentResult`, while the canonical value itself remains execution-local and is never replayed. Nested Code dispatches do not compute metadata. `defineTool` soft-validates older logged arguments and falls back instead of crashing replay. `dsh-tool-bash` and `dsh-tool-fs` are the reference implementations; the [canonical-output Agent Note](../../../.agents/notes/implemented/architecture/2026-07-20-canonical-tool-output-contract.md) owns the value/presentation split and the [render-intent Agent Note](../../../.agents/notes/implemented/architecture/2026-07-02-tool-render-intent-union.md) owns card vocabulary.
### Code Mode

View File

@@ -260,7 +260,7 @@ type RunCodeOutput = { logs: string[]; result?: JsonValue }
/**
* Registry-private capabilities the bridge receives at construction — the
* `requireRuntime` idiom: operations only the owning registry can mint stay
* off its public service surface and flow here as closures instead.
* off its public service API and flow here as closures instead.
*/
export interface RunCodeBridgeOptions {
/** Resolves `ctx.codeRuntime` or throws the loud misconfiguration error (shared with the registry's assembly-time checks). */
@@ -646,7 +646,7 @@ export function createRunCodeTool(registry: ToolRegistry, options: RunCodeBridge
kind: 'execute',
rawInput: args.code,
}),
// Deliberately no presentResult: the generic surface fallback keeps this
// Deliberately no presentResult: the generic card fallback keeps this
// title and reads durable result content without duplicating a large raw
// result into the host view payload.
})

View File

@@ -94,7 +94,7 @@ export { defineContentToolFixture, type ContentToolFixtureOptions } from './test
// The render-intent vocabulary a tool declares via `presentCall`/`presentResult`
// lives in its own UI-facing module; re-export it so `@deepseek-ai/dsh-tools`
// stays the single public surface for tool producers and UI adapters.
// stays the single public API for tool producers and UI adapters.
export type {
ToolCallKind,
FileLocation,
@@ -200,7 +200,7 @@ export interface ToolOutputDefinition {
readonly schema: JsonSchemaNode
/** Pure projection from validated arguments and value to Native/model content. */
render(args: unknown, value: JsonValue): ContentBlock[]
/** Pure replayable presentation projection, computed only for surface calls. */
/** Pure replayable presentation projection, computed only for top-level calls. */
presentationMeta?(args: unknown, value: JsonValue): JsonValue
}
@@ -1009,7 +1009,7 @@ export class ToolRegistry extends Service {
* Restrict global tools for the calling agent scope. Empty filters, unknown
* names, scope-local names, and reserved transport names fail. Restrictions
* intersect; scoped registrations remain visible.
* @param filter - global-surface mask: `allow` (keep only) and/or `deny` (remove).
* @param filter - global-tool mask: `allow` (keep only) and/or `deny` (remove).
* @returns the exact disposer that lifts this restriction.
*/
restrict(filter: ToolRestriction): () => void {

View File

@@ -60,7 +60,7 @@ export interface GenericCallView {
/** Category for icon/treatment; defaults to `other` when omitted. */
kind?: ToolCallKind
/**
* The salient input to surface in a detail/expanded view (e.g. a background
* The salient input to show in a detail/expanded view (e.g. a background
* task id). Omit to show nothing; a string renders as-is, an object as pretty
* JSON. NOT the full raw args object unless that is genuinely what a reader wants.
*/

View File

@@ -493,7 +493,7 @@ export interface DefineToolOptions<S extends ParameterSchemaSpec, O extends Valu
readonly schema: O
/** Pure Native/model rendering of one validated canonical value. */
render(args: InferArgs<S>, value: InferValue<NoInfer<O>>): ContentBlock[]
/** Pure replayable presentation metadata for direct surface calls. */
/** Pure replayable presentation metadata for direct top-level calls. */
presentationMeta?(args: InferArgs<S>, value: InferValue<NoInfer<O>>): JsonValue
}
/** Optional positive cooperative timeout budget in milliseconds. */

Some files were not shown because too many files have changed in this diff Show More