diff --git a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml index 03688e1ab2..2e396878a8 100644 --- a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md -2026-07-25-web-client-session-scope-and-provide-channel.md: 561f5792e40ef740cdac56af5efb2d08fa15333c -2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 78440131c4e09c9a458009fbe5a2a34a707a481c +2026-07-25-web-client-session-scope-and-provide-channel.md: cca090096dfff8dee1f221a87cdf406252a52a34 +2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 701a4f585c41a23be12883417c5ae0074431096c diff --git a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md index 561f5792e4..cca090096d 100644 --- a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md +++ b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md @@ -106,7 +106,7 @@ Slot scope is the closed set `root | session-maybe | session`: ### Host wire smalls - The summary `blank` column and the `host/session-added` frame's `blank` field (see the blank bit above). -- The SSE frame `host/commands-changed` (a pure invalidation signal); the client routes it into the typed events `commands/changed` and `connection/reset` (broadcast after each connection generation is established; wire-derived caches uniformly treat prior state as stale). +- The SSE frame `host/commands-changed` (a pure invalidation signal); the client routes it into the typed events `commands/changed` and `connection/reset` (broadcast after each connection generation is established; wire-derived caches uniformly treat prior state as stale). The commands frame and its typed client event were later replaced by verbatim forwarding of `commands/change` through `ctx.remote.$on` ([forwarded Remote events](2026-08-10-remote-event-delivery.md)); `connection/reset` is unchanged, and the invalidation-not-diffing contract this bullet states still holds. - `command.list/execute` and `skill.list` are uniformly single-addressed by `sessionId` (a session always has an Agent; `agentFor`'s resume semantics come ready-made); the command-surface narrative lives in the [command surfaces note](2026-07-25-web-command-surfaces-and-assembly.md). - The `session.create` request shape: workspaceId/cwd as either-or, plus an optional caller-preallocated sessionId (a same-id same-cwd retry is idempotent; a different cwd reports `session-conflict`). diff --git a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md index 78440131c4..701a4f585c 100644 --- a/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md @@ -106,7 +106,7 @@ slot scope 是闭集 `root | session-maybe | session`: ### host wire 小件 - summary `blank` 列与 `host/session-added` 帧 `blank` 字段(见上文 blank 位)。 -- SSE(Server-Sent Events)帧 `host/commands-changed`(纯失效信号);client 路由为类型事件 `commands/changed` 与 `connection/reset`(连接代建立后广播,wire 派生缓存一律视旧态为陈旧)。 +- SSE(Server-Sent Events)帧 `host/commands-changed`(纯失效信号);client 路由为类型事件 `commands/changed` 与 `connection/reset`(连接代建立后广播,wire 派生缓存一律视旧态为陈旧)。 该 commands 帧及其类型化 client 事件后来被「`commands/change` 经 `ctx.remote.$on` 原样转发」取代([转发的 Remote 事件](2026-08-10-remote-event-delivery.md));`connection/reset` 不变,本条陈述的「失效而非差分」契约依然成立。 - `command.list/execute`、`skill.list` 一律 `sessionId` 单址(会话恒有 Agent,`agentFor` 的恢复语义现成);命令面叙述见[命令业务面 note](2026-07-25-web-command-surfaces-and-assembly.md)。 - `session.create` 请求形状:workspaceId/cwd 二选一 + 可选调用方预分配 sessionId(同 id 同 cwd 重试幂等,异 cwd 报 `session-conflict`)。 diff --git a/.agents/notes/implemented/architecture/2026-07-30-config-plane-boundaries.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-30-config-plane-boundaries.i18n.yaml index ffa3375b74..db54057b3c 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-config-plane-boundaries.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-30-config-plane-boundaries.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-config-plane-boundaries.md -2026-07-30-config-plane-boundaries.md: 383163d5315ce0fcb39d06ac8b5f7762758fa9b2 -2026-07-30-config-plane-boundaries.zh.md: 0a412d5b8517c98accd056b690129f4e485f0e13 +2026-07-30-config-plane-boundaries.md: f2c4981d6ddf772ee050db0be67dad0b063e2f50 +2026-07-30-config-plane-boundaries.zh.md: deec710e3c54da58333145a902f22154d15bafbb diff --git a/.agents/notes/implemented/architecture/2026-07-30-config-plane-boundaries.md b/.agents/notes/implemented/architecture/2026-07-30-config-plane-boundaries.md index 383163d531..f2c4981d6d 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-config-plane-boundaries.md +++ b/.agents/notes/implemented/architecture/2026-07-30-config-plane-boundaries.md @@ -26,7 +26,7 @@ Three smaller defects sat beside them. `llm/adapters-updated` documented contain **Staleness is detected, not ordered away.** Each namespace carries a monotonic `revision` over its RAW section; writes may carry `expectedRevision`, and a mismatch rejects with `SettingsConflictError` → `settings-conflict` on the wire, both revisions attached. The editor captures the revision it opened at and, on conflict, tells the user to reopen rather than replaying its snapshot. -**The raw layer gets its own event.** `settings/updated` stays gated on the resolved value — that is what a consumer means by change. `settings/document-updated (ns, revision)` fires on any raw-section change, because a configuration surface must learn that a field went from inherited to overridden (same resolved value, different meaning) and that its held revision is stale. The host frame `host/settings-changed` now rides this event, and a change to an exposed provider namespace also emits `host/models-changed`: that namespace holds the provider's catalog, which no route change announces. +**The raw layer gets its own event.** `settings/updated` stays gated on the resolved value — that is what a consumer means by change. `settings/document-updated (ns, revision)` fires on any raw-section change, because a configuration surface must learn that a field went from inherited to overridden (same resolved value, different meaning) and that its held revision is stale. The host frame `host/settings-changed` rode this event, and a change to an exposed provider namespace also emits `host/models-changed`: that namespace holds the provider's catalog, which no route change announces. (`settings/document-updated` is now forwarded to consumers verbatim instead of being reframed — see [forwarded Remote events](2026-08-10-remote-event-delivery.md) — which does not change why the raw layer needs its own event; the derived `host/models-changed` still rides it.) ## Alternatives considered diff --git a/.agents/notes/implemented/architecture/2026-07-30-config-plane-boundaries.zh.md b/.agents/notes/implemented/architecture/2026-07-30-config-plane-boundaries.zh.md index 0a412d5b85..deec710e3c 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-config-plane-boundaries.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-30-config-plane-boundaries.zh.md @@ -26,7 +26,7 @@ Status: implemented **过期是被检测出来的,而不是靠排序绕过去的。**每个 namespace 都带有一个针对其**原始**分节的单调 `revision`;写入可携带 `expectedRevision`,不匹配即以 `SettingsConflictError` 拒绝——在协议上是 `settings-conflict`,并附上两个 revision。编辑器记住自己打开时的 revision,冲突时请用户重新打开,而不是把自己的快照重放上去。 -**原始层拥有自己的事件。**`settings/updated` 仍以解析值为门槛——那才是消费方所说的"变化"。`settings/document-updated (ns, revision)` 则在任何原始分节变化时触发,因为配置界面必须知道某个字段从继承变成了覆盖(解析值相同,含义不同),也必须知道自己持有的 revision 已经过期。host 帧 `host/settings-changed` 现在搭乘这个事件;而已暴露提供方 namespace 的变更还会额外发出 `host/models-changed`:该 namespace 正持有这个提供方的目录,而没有任何路由变更会宣告它。 +**原始层拥有自己的事件。**`settings/updated` 仍以解析值为门槛——那才是消费方所说的"变化"。`settings/document-updated (ns, revision)` 则在任何原始分节变化时触发,因为配置界面必须知道某个字段从继承变成了覆盖(解析值相同,含义不同),也必须知道自己持有的 revision 已经过期。host 帧 `host/settings-changed` 曾搭乘这个事件(现在 `settings/document-updated` 改为原样转发给消费端,见[转发的 Remote 事件](2026-08-10-remote-event-delivery.md)——这不改变原始层为何需要自己的事件,派生的 `host/models-changed` 仍搭乘它);而已暴露提供方 namespace 的变更还会额外发出 `host/models-changed`:该 namespace 正持有这个提供方的目录,而没有任何路由变更会宣告它。 ## 曾考虑的替代方案 diff --git a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml index eec7c013a4..c7b36ebf1a 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-30-web-config-plane.md -2026-07-30-web-config-plane.md: 1af86fc7917cc40b0544bb5c6254994a87ad7a55 -2026-07-30-web-config-plane.zh.md: 15916fe86e2f5e19e9752721917317aa8c39442d +2026-07-30-web-config-plane.md: 4398ab8c23001d1d75ece439de5d03a57005e3b5 +2026-07-30-web-config-plane.zh.md: b06b45788525d2cd77bf8950251ff84928a27c0a diff --git a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md index 1af86fc791..4398ab8c23 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md +++ b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md @@ -12,7 +12,7 @@ The request-level configuration seam made LLM adapter configuration restart-free ## Decision -**Wire domains on the compiled RPC map, rejections as codes, invalidations as frames.** `settings.describe/openDocument/update/replace/mutate`, `credentials.describe/set/unset`, `llm.providers`, and `llm.models` (claiming the reserved `host.listModels` surface) join `RpcMethodMap`, so the seven compiler-locked wiring sites keep contract, schema, handler, and client in lockstep. Seam rejections fold into `settings-rejected {ns}` / `credential-rejected {ref}` business errors (HTTP stays a carrier), and three `HostFrame`s — `host/settings-changed {ns}`, `host/credentials-changed {ref}`, `host/models-changed` — follow the `host/commands-changed` shape so every client converges without polling. Settings reads, native actions, and writes join `pickDirectory`/`openPath` in the connection guard's privileged set: loopback + same-origin or 403, because a LAN-exposed dsh web must not accept configuration access from another origin. +**Wire domains on the compiled RPC map, rejections as codes, invalidations as frames.** `settings.describe/openDocument/update/replace/mutate`, `credentials.describe/set/unset`, `llm.providers`, and `llm.models` (claiming the reserved `host.listModels` surface) join `RpcMethodMap`, so the seven compiler-locked wiring sites keep contract, schema, handler, and client in lockstep. Seam rejections fold into `settings-rejected {ns}` / `credential-rejected {ref}` business errors (HTTP stays a carrier), and three `HostFrame`s — `host/settings-changed {ns}`, `host/credentials-changed {ref}`, `host/models-changed` — follow the `host/commands-changed` shape so every client converges without polling. (The settings, credentials, and commands frames were later replaced by verbatim forwarding of the owning Host events; see [forwarded Remote events](2026-08-10-remote-event-delivery.md). `host/models-changed` remains this package's own derived frame, and the convergence-without-polling decision stands.) Settings reads, native actions, and writes join `pickDirectory`/`openPath` in the connection guard's privileged set: loopback + same-origin or 403, because a LAN-exposed dsh web must not accept configuration access from another origin. **`describe()` grows layers and structural secret redaction.** `SettingsDescriptor` carries `base`/`user` beside the effective value, so the form marks "overridden" by presence in the user layer, not value inequality (an override *equal* to the base is still an override). `describe({ redactSecrets: true })` — mandatory at every wire face — strips `role('secret')` subtrees from all three layers via a pure structural walk of the schema (object/dict/array containers; a secret-role subtree is one opaque leaf) and enumerates the stripped slots as `{path, set}`, so a page can render write-only inputs without ever receiving a value. diff --git a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md index 15916fe86e..b06b457885 100644 --- a/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md @@ -12,7 +12,7 @@ Status: implemented ## 决策 -**wire 领域挂上编译期 RPC 映射,拒绝落为错误码,失效落为帧。**`settings.describe/openDocument/update/replace/mutate`、`credentials.describe/set/unset`、`llm.providers` 与 `llm.models`(认领预留的 `host.listModels` 面)一同加入 `RpcMethodMap`,七处由编译器锁定的接线位点因此让约定、schema、处理器与客户端保持步调一致。seam 侧的拒绝折叠为 `settings-rejected {ns}`/`credential-rejected {ref}` 业务错误(HTTP 仍只是载体),三个 `HostFrame`——`host/settings-changed {ns}`、`host/credentials-changed {ref}`、`host/models-changed`——沿用 `host/commands-changed` 的形状,因此每个客户端都无需轮询即可收敛。settings 读取、原生操作与写入和 `pickDirectory`/`openPath` 一起进入连接守卫的特权集合:回环 + 同源,否则 403,因为暴露在局域网上的 dsh web 绝不能接受来自其他源的配置访问。 +**wire 领域挂上编译期 RPC 映射,拒绝落为错误码,失效落为帧。**`settings.describe/openDocument/update/replace/mutate`、`credentials.describe/set/unset`、`llm.providers` 与 `llm.models`(认领预留的 `host.listModels` 面)一同加入 `RpcMethodMap`,七处由编译器锁定的接线位点因此让约定、schema、处理器与客户端保持步调一致。seam 侧的拒绝折叠为 `settings-rejected {ns}`/`credential-rejected {ref}` 业务错误(HTTP 仍只是载体),三个 `HostFrame`——`host/settings-changed {ns}`、`host/credentials-changed {ref}`、`host/models-changed`——沿用 `host/commands-changed` 的形状,因此每个客户端都无需轮询即可收敛。(settings、credentials 与 commands 三个帧后来被「原样转发所属 host 事件」取代,见[转发的 Remote 事件](2026-08-10-remote-event-delivery.md);`host/models-changed` 仍是本包自有的派生帧,此处「无需轮询即收敛」的决策依然成立。)settings 读取、原生操作与写入和 `pickDirectory`/`openPath` 一起进入连接守卫的特权集合:回环 + 同源,否则 403,因为暴露在局域网上的 dsh web 绝不能接受来自其他源的配置访问。 **`describe()` 增加分层与结构化 secret 脱敏。**`SettingsDescriptor` 在生效值之外携带 `base`/`user`,表单据此按「字段是否出现在用户层」来标记「已覆盖」,而非按值是否不等(与 base *相等*的覆盖仍然是覆盖)。`describe({ redactSecrets: true })`——在每个 wire 面都强制启用——经由对 schema 的纯结构遍历(object/dict/array 容器;secret 角色子树整体是一个不透明叶节点)从全部三层剥除 `role('secret')` 子树,并把剥除的槽位枚举为 `{path, set}`,页面因此不必收到任何值就能渲染只写输入框。 diff --git a/.agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.i18n.yaml new file mode 100644 index 0000000000..2cc3f97d27 --- /dev/null +++ b/.agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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 .agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.md +2026-08-10-remote-event-delivery.md: 58af845e66e24f49a484ae2cb216e995417a5df5 +2026-08-10-remote-event-delivery.zh.md: dd96e6082d0eedd541e128b059abaa195a6035a2 diff --git a/.agents/notes/proposed/architecture/2026-08-10-remote-event-delivery.md b/.agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.md similarity index 60% rename from .agents/notes/proposed/architecture/2026-08-10-remote-event-delivery.md rename to .agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.md index d6e6555263..58af845e66 100644 --- a/.agents/notes/proposed/architecture/2026-08-10-remote-event-delivery.md +++ b/.agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.md @@ -1,22 +1,22 @@ # Agent Note: Remote event delivery (ctx.remote.$on) -Status: proposed +Status: implemented English | [中文](2026-08-10-remote-event-delivery.zh.md) ## Problem -[TypeRT Gateway targeted method calls](2026-08-02-typert-remote-method-calls.md) cover only the request/response shape and deliberately leave Session event streams and stateful interactions to separate designs. Every **one-way Host-to-consumer push** therefore still rides the legacy API Proxy. +[TypeRT Gateway targeted method calls](../../implemented/architecture/2026-08-02-typert-remote-method-calls.md) cover only the request/response shape and deliberately leave Session event streams and stateful interactions to separate designs. Every **one-way Host-to-consumer push** therefore still rides the legacy API Proxy. The Host owns a family of pure invalidation events — "a registry changed, refetch it" — whose payloads are already JSON and whose emission never binds an AgentScope: `commands/change`, `credentials/updated`, `settings/document-updated`. Reaching one UI subscriber takes four hops: the Host cordis event, a hand-written `HostFrame` variant plus its zod branch in apiproxy, a hand-written bridge in client/runtime that re-emits it as a Client cordis event, and finally the consumer's `ctx.on(...)`. Adding one such event edits five places (frame union, zod union, host-stream listener, client bridge, a duplicated Client-side `Events` declaration), and not one of them states a new fact: the name, the payload type, and the emission point were all declared by the owner package's cordis `Events` merge. That duplicated declaration is also **lossy**: the Client side restates it as `settings/changed(ns: string)`, flattening a branded type into bare `string` — the opposite of the Remote method contract, where a consumer type points at the business package's one canonical symbol. -## Proposal +## Decision -Add one one-way subscription verb to the consumer Remote surface, `ctx.remote.$on(event, listener)`, driven by an allowlist and forwarding verbatim: +The consumer Remote surface carries one one-way subscription verb, `ctx.remote.$on(event, listener)`, driven by an allowlist and forwarding verbatim: -- `packages/api/remotes/src/types.ts` holds the allowlist of forwardable Host events, and it is the single control point over what a consumer may subscribe to. That file is listed in the `files` of **both** of this package's faces, so the Host forwarding loop and the consumer key surface read one declaration. +- `packages/api/remotes/src/remote-events.ts` holds the allowlist of forwardable Host events, and it is the single control point over what a consumer may subscribe to. `src/types.ts` beside it derives the type projection and fills the selection seat, staying type-only per the package convention. Both files are listed in the `files` of **both** of this package's faces, so the Host forwarding loop and the consumer key surface read one declaration. - The wire event name **is** the Host cordis event name (`settings/document-updated`) with no `host/` prefix, and the payload **is** the Host argument list, element for element, with no projection, redaction, or renaming. - The carrier reuses the existing host stream: `HostFrame` gains one wrapper variant, `host/remote-event`. No new downlink. - Event **signatures** get no second table. Each owner package moves its cordis `Events` declaration into its client-safe, type-only `./types` export, so both faces read the same declaration and `$on`'s listener type is `Events[Event]` itself. "Verbatim" then holds by construction rather than by proof. @@ -24,7 +24,7 @@ Add one one-way subscription verb to the consumer Remote surface, `ctx.remote.$o When an `Events` entry's signature reaches a Host-only symbol (a Service, `Agent`, a Context), the answer is to **split the code until the entry lands cleanly in `./types`** — never a declaration half-left in `index.ts`, and never a structurally equivalent shadow type in `./types`. None of the three packages needed that here: their entries reach only `SettingsNamespace`, `SettingsUpdateSource`, and `CredentialRef`, all pure types. -This change migrates the three **pure passthrough** events and deletes their `HostFrame` variants. Everything with derivation stays untouched: `host/models-changed` (a fan-in of `llm/adapters-updated` with provider/agent-default namespace filtering), `host/workspace-changed`/`-removed`/`host/archived-sessions-changed` (view derivation plus per-connection dedup state), and `host/session-added`/`-removed`/`host/session-status`/`host/agent-error` (live-object projection or frame-time derived fields). +The three **pure passthrough** events ride this path, and their `HostFrame` variants are gone. Everything with derivation stays untouched: `host/models-changed` (a fan-in of `llm/adapters-updated` with provider/agent-default namespace filtering), `host/workspace-changed`/`-removed`/`host/archived-sessions-changed` (view derivation plus per-connection dedup state), and `host/session-added`/`-removed`/`host/session-status`/`host/agent-error` (live-object projection or frame-time derived fields). `skills/change`, `tools/change`, and `system-prompt/change` have the same shape but **no consumer today**; under "require a current owner and need" they stay out of the allowlist and are recorded here only as the extension seat. @@ -32,7 +32,7 @@ This change migrates the three **pure passthrough** events and deletes their `Ho type-meta gains one **shape predicate**, one **selection seat**, and **one** member on `TypeRTClientRemote`. No runtime code: -```ts +```ts ignore-check /** Cordis events shaped for one-way remote delivery: no Scope binding, void return. */ export type TypeRTForwardableEvent = { [Event in keyof Events]: unknown extends ThisParameterType @@ -47,36 +47,36 @@ export interface TypeRTRemoteEventSelection {} export type TypeRTRemoteEvent = Extract ``` -```ts +```ts ignore-check /** Subscribe to one forwarded Host event; the returned disposer belongs to the calling fiber. */ $on(event: Event, listener: Events[Event]): () => void ``` `Events` resolves per program: the full Host vocabulary in the Host program, whatever the Client face can see in the Client program. The same predicate therefore holds on both sides without dragging Host declarations into the Client. -**The consumer surface has `$on` and no `$dispatch`.** The port that hands a decoded frame to the subscription table stays out of the developer-visible contract, and it cannot be a module-level function reaching across Client plugins: the client bundle purity gate (`packages/client/tsdown.client.ts`) admits value imports only from `CLIENT_EXTERNALS`, the `INLINE_SAFE` wire layer, and generated `/remote` contributions — and inlining around it would copy `ClientRemoteService` into the runtime bundle, making `instanceof` permanently false. +**The surface separates the consumer verb from the carrier handoff**: consumers subscribe with `$on`, and whoever owns the Host frame sink hands each decoded frame over with `$dispatch`. It cannot be a module-level function reaching across Client plugins — the client bundle purity gate (`packages/client/tsdown.client.ts`) admits value imports only from `CLIENT_EXTERNALS`, the `INLINE_SAFE` wire layer, and generated `/remote` contributions, and inlining around it would copy `ClientRemoteService` into the runtime bundle, making `instanceof` permanently false. A cordis service method is the collaboration shape that gate prescribes: -The port is therefore **one internal Client cordis event**, declared in `dsh-type-meta` (a single-face package both sides already depend on, so no new dependency): - -```ts -'remote/host-event'(event: string, args: readonly unknown[]): void +```ts ignore-check +$dispatch(event: string, args: readonly unknown[]): void ``` -client/runtime — the owner of the host frame sink — emits it; `ClientRemoteService` is the only subscriber and turns it into `$on` callbacks through a private `dispatch`. This is the repository's existing cross-plugin plumbing shape: `connection/reset` is declared and emitted by runtime, subscribed by `ui-command`, and pinned by `runtime/tests/wire-events.spec.ts`. The `event` parameter is `string`, not `TypeRTRemoteEvent`: this is a wire boundary, and a name nobody subscribed to is dropped silently. +client/runtime — the owner of the host frame sink — calls it directly, so the frame reaches the subscription table without an intermediate event to relay it. The `event` parameter is `string`, not `TypeRTRemoteEvent`: this is a wire boundary, and a name nobody subscribed to is dropped silently. Delivery shares no implementation with the cordis event system: one-way only, no waterfall/bail/parallel/serial modes and no `@mode` concept (`ReturnType extends void` is the static expression of that rule), no `this` binding, no `EventOptions`, `prepend`, or priority. Listeners run in registration order, and one that throws is contained and logged — it must never take down the frame pump (the same posture `ConnectionController` already applies to its sinks). -### The allowlist: one file both faces read +### The allowlist: one declaration both faces read -`packages/api/remotes/src/types.ts` is listed in the `files` of both `tsconfig.host.json` and `tsconfig.client.json`, and is the allowlist's single home: +`packages/api/remotes/src/remote-events.ts` is listed in the `files` of both `tsconfig.host.json` and `tsconfig.client.json`, and is the allowlist's single home; `src/types.ts` derives its type face: -```ts +```ts ignore-check +// remote-events.ts — the value export const API_REMOTE_FORWARDED_EVENTS = [ 'commands/change', 'credentials/updated', 'settings/document-updated', ] as const +// types.ts — the type face, derived export type ApiRemoteForwardedEvent = typeof API_REMOTE_FORWARDED_EVENTS[number] declare module '@deepseek-ai/dsh-type-meta' { @@ -88,7 +88,7 @@ Forwarding one more event is therefore **one line in that array**: the type proj The Host face adds one shape assertion, binding the Host event vocabulary to that same array: -```ts +```ts ignore-check API_REMOTE_FORWARDED_EVENTS satisfies readonly TypeRTForwardableEvent[] ``` @@ -100,7 +100,7 @@ JSON-safety is a runtime concern: before forwarding, apiproxy validates each arg ### Wire contract (apiproxy) -```ts +```ts ignore-check | { type: 'host/remote-event'; event: string; args: JsonValue[] } ``` @@ -108,8 +108,6 @@ The zod branch keeps `args: z.array(z.unknown())`: the frame arrives from `JSON. `events.host()` subscribes by allowlist when the stream opens (each stream owns its disposers, so no broadcast set is needed). **The registration position is part of the contract**: this block must sit *before* the `settings/document-updated` listener. Cordis fires in registration order, and `host/models-changed` is an invalidation frame *derived* from that same Host event; placing the forwarded frame after the derived one flips the relative order of two frames from one emit compared with the previous behavior (two config cases observe it). -Cordis keys `on` by literal event name, so subscribing from a runtime list erases the handler type once. That is the only type assertion this change introduces; its safety rests on the allowlist predicate and the `isJsonValue` check. - `api/events.ts` is a wire contract file the browser side also compiles, so every type it references must come from an owner package's **client-safe, type-only subpath**, never the package root. Evidence: importing one type from `@deepseek-ai/dsh-session` root drags the root's `declare module 'cordis' { interface Context { sessions: SessionStore } }` into the Client compilation face and overrides the Client's `ctx.sessions: ISessions`, producing 18 errors in the unrelated `ui-slash` and `ui-conversation`. `JsonValue` therefore needs a re-export from `dsh-session/src/types.ts`. ### The apps/web browser e2e belong to the Host face @@ -118,20 +116,20 @@ The `apps/web/tests/**` e2e type-check in the root **`tsconfig.host.json`**: the That yields a discipline this design depends on: **when those tests import a value or a type from a Client package, they pull that package's whole project — and every project it references — into the Host build graph**. Four consumers (`ui-settings-general`, `ui-models`, `ui-permission`, `ui-command`) reference `api/remotes`' Client face, and that face cannot compile until Host tsdown has generated `@deepseek-ai/dsh-goal/remote`. The result is a build-order deadlock: Host tsc needs the Client face, which needs the generated artifact, which Host tsdown produces after Host tsc. -This change therefore **mirrors** the few Client-owned symbols on the test side (`scaffold.ts` exports the mirrored welcome-notice constants; the two chat e2e keep importing `dsh-client-runtime/client` because the `runtime` project is already in the Host graph), which lets those four consumers leave the Host graph. The 15 Client project references in `apps/cli/tsconfig.json` then lose their owner-map role and are deleted as a group. Each mirrored value matches its source verbatim; a drift shows up as a missed selector or an unsuppressed notice, both loud failures. +The few Client-owned symbols are therefore **mirrored** on the test side (`scaffold.ts` exports the mirrored welcome-notice constants; the two chat e2e keep importing `dsh-client-runtime/client` because the `runtime` project is already in the Host graph), which lets those four consumers leave the Host graph. The 15 Client project references in `apps/cli/tsconfig.json` lost their owner-map role and are gone. Each mirrored value matches its source verbatim; a drift shows up as a missed selector or an unsuppressed notice, both loud failures. ### Change inventory | Location | Change | |---|---| -| `dsh-type-meta` | `src/types.ts` gains `TypeRTForwardableEvent`, `TypeRTRemoteEventSelection`, `TypeRTRemoteEvent`, and the `'remote/host-event'` declaration; `TypeRTClientRemote` gains `$on`. Types only, no runtime | -| `api/gateway` Client half | `ClientRemoteService` implements `$on` (subscription table, `ctx.effect` ownership for the calling fiber, registration-order delivery with listener failures contained) and subscribes to `'remote/host-event'`; `dispatch` stays private | -| `api/remotes` | New `src/types.ts` (allowlist, type projection, selection seat) listed in both faces' `files`; a `./types` export with `lib/types/**/*.js` added to `files`; the Host face adds the shape assertion and `import type {}` for the three owner `./types`; the Client half re-exports those three plus `@deepseek-ai/dsh-api-gateway/client`; `./invariant` asserts the runtime relation for allowlisted events (`thisArg === null` and `mode === 'emit'`) | +| `dsh-type-meta` | `src/types.ts` gains `TypeRTForwardableEvent`, `TypeRTRemoteEventSelection`, and `TypeRTRemoteEvent`; `TypeRTClientRemote` gains `$on` and `$dispatch`. Types only, no runtime | +| `api/gateway` Client half | `ClientRemoteService` implements `$on` (subscriptions addressed by registration, `ctx.effect` ownership for the calling fiber) and `$dispatch` (snapshot delivery in registration order, containing a listener that throws or rejects) | +| `api/remotes` | New `src/remote-events.ts` (the allowlist value) and `src/types.ts` (type projection, selection seat), both listed in both faces' `files`; a `./types` export with `lib/types/**/*.js` added to `files`; the Host face adds the shape assertion and `import type {}` for the three owner `./types`; the Client half re-exports those three plus `@deepseek-ai/dsh-api-gateway/client` | | Root `tsconfig.base.json` | Three `paths` entries (`dsh-settings/types`, `dsh-credentials/types`, `dsh-api-remotes/types`), all pointing at the **source** plane | | `dsh-commands` / `dsh-settings` / `dsh-credentials` | The `interface Events` sub-block moves into each package's client-safe `./types` (settings and credentials create that export, moving the brands and pure types with it; `index` keeps re-exporting them and keeps the constructors; `files` gains `lib/types/**/*.js`) | -| `host/apiproxy` | `HostFrame` gains `host/remote-event` and loses `host/commands-changed`/`-settings-changed`/`-credentials-changed` with their zod branches; `events.host()` subscribes by allowlist ahead of the `settings/document-updated` listener and validates through `assertJsonArgs`; the `settings/document-updated` listener stays to keep feeding `host/models-changed` | +| `host/apiproxy` | `HostFrame` gains `host/remote-event` and loses `host/commands-changed`/`-settings-changed`/`-credentials-changed` with their zod branches; `events.host()` subscribes by allowlist ahead of the `settings/document-updated` listener and validates through `assertJsonArgs`; that listener stays to keep feeding `host/models-changed` | | `dsh-session` | `src/types.ts` re-exports `JsonValue` so wire contract files can use the client-safe subpath | -| `client/runtime` | The bridge's three `ctx.emit` branches collapse into `ctx.emit('remote/host-event', frame.event, frame.args)`; the `Events` merge drops `commands/changed`, `settings/changed`, and `credentials/changed` (`models/changed` stays) | +| `client/runtime` | The bridge's three `ctx.emit` branches collapse into `ctx.remote.$dispatch(frame.event, frame.args)`, adding a `remote` injection; the `Events` merge drops `commands/changed`, `settings/changed`, and `credentials/changed` (`models/changed` stays) | | Five consumers | ui-command / ui-models / ui-settings-general / ui-permission / ui-agent-preset subscribe through `ctx.remote.$on(...)`, following `ui-goal`'s precedent for the type-only facade import and the `'remote'` injection | | `client/connection` | The fixture's `emitHost` produces `host/remote-event` | | `apps/web/tests` + `apps/cli` | Client symbols mirrored on the test side (see above); `apps/cli/tsconfig.json` drops its 15 Client project references | @@ -148,28 +146,28 @@ This change therefore **mirrors** the few Client-owned symbols on the test side **Move the apps/web browser e2e into the Client aggregate.** "Client tests belong to the Client face" looks right and fails immediately with 21 errors: those tests use Host services, and in the Client program `ctx.sessions` is `ISessions`. -**Split `directory-picker-browse`/`-native` into Host and Client faces** so no Client package reaches the Host graph. The direction is right — they are genuinely unsplit dual-half packages — but it is a separate concern from this capability seam and lands in another owner's packages; recorded as its own follow-up. +**Split `directory-picker-browse`/`-native` into Host and Client faces** so no Client package reaches the Host graph. The direction is right — they are genuinely unsplit dual-half packages — but the change lands in another owner's packages and buys only a cleaner build graph; once this design mirrors the Client symbols on the test side, it no longer needs the split. **Assessed and declined.** -## Acceptance criteria +## Verification -- Emitting the three Host events puts one `host/remote-event` frame each on the real host stream, with `event` the Host name and `args` equal element for element (a real composition test). -- The allowlist rejects three candidate classes at the type level: a name that is not an event, a Scope-bound event (`goal/changed`), and an event whose return is not `void`. -- `$on`'s key surface equals the allowlist: `$on('slots/changed', …)` and `$on('skills/change', …)` must both fail to compile. -- `TypeRTClientRemote` has **no** `$dispatch`: the developer-visible contract is `$on` plus the existing `$mount` and generated namespaces. -- A non-JSON-safe argument makes `assertJsonArgs` throw rather than degrade silently; that function is unit-tested directly instead of driving a malformed emit through the event bus. -- `ctx.remote.$on`'s disposer belongs to the calling fiber: disposing the fiber removes the subscription. One throwing listener affects neither its siblings nor later frames. -- For one emit, the forwarded frame and the invalidation frame derived from the same Host event keep the pre-change relative order. -- On the consumer side, `$on('settings/document-updated', …)` resolves `ns` as `SettingsNamespace` — the brand survives the wire. -- The three `HostFrame` variants, the three Client-side `Events` declarations, and the three bridge branches disappear in the same change; `host/models-changed` behavior is unchanged. -- `pnpm run build` passes. +What pins this behavior: -## Risks +- A real composition test puts one `host/remote-event` frame on the real host stream per Host emit, with `event` the Host name and `args` equal element for element. +- Type-level negatives reject three candidate classes: a name that is not an event, a Scope-bound event (`goal/changed`), and an event whose return is not `void`. `$on('slots/changed', …)` (Client-local) and `$on('skills/change', …)` (declared but unselected) both fail to compile, so `$on`'s key surface equals the allowlist. +- On the consumer side, `$on('settings/document-updated', …)` resolves `ns` as `SettingsNamespace`: the brand survives the wire. +- `$on`'s disposer belongs to the calling fiber, and two registrations of one function object retire independently — a table keyed on listener identity would collapse them, so subscriptions are addressed by registration. +- Delivery contains a listener that throws AND one that rejects a returned promise: the declared return is `void`, so nobody awaits an async listener, and its rejection would otherwise escape this containment entirely. Delivery iterates a snapshot, so subscribing or disposing mid-frame cannot change who receives that frame. +- For one emit, the forwarded frame and the invalidation frame derived from the same Host event keep the pre-change relative order (two config cases observe it). +- `assertJsonArgs` is unit-tested directly rather than by driving a malformed emit through the event bus: a typed `ctx.emit` cannot construct one, since every allowlisted event has a statically JSON-safe payload. +- The three `HostFrame` variants, the three Client-side `Events` declarations, and the three bridge branches are gone in the same change; `host/models-changed` behavior is unchanged. -- **Tenancy inside a legacy frame union.** The new contract temporarily lives in apiproxy's `HostFrame`, so a reader may assume apiproxy owns Remote events. The frame's JSDoc names `api-remotes` as the allowlist owner, and apiproxy's README records the tenancy under known limitations. -- **The shared file breaks api/remotes' face-disjointness contract.** `src/types.ts` belongs to both projects, so each emits an identical declaration into the shared `lib/types`. Content is byte-identical and the `.tsbuildinfo` files stay separate, so this is harmless in practice — but the README's build-boundary section must state the exception and its cause (the `paths` entry points at source). -- **Any Client plugin can `ctx.emit('remote/host-event', …)`** and synthesize a Host event. This is the same exposure `connection/reset` already has for a fabricated reconnect; the Client is one trust domain. Tests pin the event-to-`$on` conversion and do not pretend the port authenticates its source. -- **The allowlist's shape assertion is currently commented out** in `packages/api/remotes/src/index.ts`, together with the allowlist import and the three owner `./types` type-only imports it needs. The three static guarantees described above are therefore inactive right now: adding a Scope-bound or misspelled name would not fail to compile. Restoring it does not change the build graph (those four consumers already left the Host graph) and is required before the pull request. -- **Mirrored test values can drift.** Nothing mechanically checks the Client constants mirrored in `apps/web/tests` against their source; the safety net is only that a drift misses a selector. A grep-level gate forbidding `@deepseek-ai/dsh-client-*` imports under `apps/web/tests` would close this and is not part of this change. -- **The dynamic subscription erases a handler type.** Subscribing by allowlist requires one erasure at `ctx.on(name, …)`; if the predicate is later relaxed, that assertion loses its static backing. -- **Capabilities given up.** No projected or redacted payloads, no Scope-bound events (`agentCtx.remote.$on`), and no replay on reconnect — these are pure invalidation signals, and the existing `connection/reset` already covers refetching after a reconnect. The mux stream's session events, answerable frames, and snapshot baselines stay out of scope. -- **Client packages remain in the Host graph.** Twelve projects (`connection`, `runtime`, `ui-slots`, and kin) still reach it through the unsplit `directory-picker-browse`/`-native` pair and `api/gateway → client/connection`. They compile and no longer implicate api/remotes' Client face, so they do not block this change; the root fix is the follow-up above. +## Consequences + +- **Tenancy inside a legacy frame union.** The contract lives in apiproxy's `HostFrame`, so a reader may assume apiproxy owns Remote events. The frame's JSDoc names `api-remotes` as the allowlist owner, and apiproxy's README records the tenancy under known limitations. When the host stream moves off that package, the wrapper moves with it and the consumer contract does not change. +- **Two files break api/remotes' face-disjointness contract.** `src/remote-events.ts` and `src/types.ts` belong to both projects, so each emits an identical declaration into the shared `lib/types`. Content is byte-identical and the `.tsbuildinfo` files stay separate, so this is harmless in practice; the README's build-boundary section states the exception and its cause (the `paths` entry points at source). +- **The carrier handoff is developer-visible.** Any Client plugin holding `ctx.remote` can call `$dispatch` and synthesize a forwarded event. That exposure predates the verb — `ctx.emit` was equally reachable while an internal event relayed the frame — and matches what `connection/reset` already allows for a fabricated reconnect; the Client is one trust domain. Tests pin the handoff-to-`$on` conversion and do not pretend the port authenticates its caller. +- **A malformed argument fails in the emitter's containment, not at load.** `assertJsonArgs` throws inside the forwarding listener, so the emitting seam's listener containment logs it and drops that frame: loud in the Host log rather than at load or at the emit point. +- **Mirrored test values can drift.** Nothing mechanically checks the Client constants mirrored in `apps/web/tests` against their source; the safety net is only that a drift misses a selector. The rule lives in `apps/web/tests/README.md` and is held by review — a grep-level gate was considered and deliberately skipped. +- **Capabilities given up.** No projected or redacted payloads, no Scope-bound events (`agentCtx.remote.$on`), and no replay on reconnect — these are pure invalidation signals, and `connection/reset` already covers refetching after a reconnect. The mux stream's session events, answerable frames, and snapshot baselines stay out of scope. +- **Client packages remain in the Host graph.** Twelve projects (`connection`, `runtime`, `ui-slots`, and kin) still reach it through the unsplit `directory-picker-browse`/`-native` pair and `api/gateway → client/connection`. They compile and no longer implicate api/remotes' Client face, so they did not block this change; splitting those packages would remove a few but was assessed and declined. The two chat e2e importing `dsh-client-runtime/client` rely on `runtime` already being in that graph — incidental, not a guarantee. +- **The invariant companion holds no runtime check.** An earlier revision asserted the dispatch shape (`thisArg === null`, `mode === 'emit'`) over the live event bus, which coupled the companion to the allowlist value and made rolldown hoist it into a third bundle chunk the mechanical publication list does not carry. The Host face's `TypeRTForwardableEvent` assertion already refuses both deviations at compile time, so the companion is an explained empty installer. diff --git a/.agents/notes/proposed/architecture/2026-08-10-remote-event-delivery.zh.md b/.agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.zh.md similarity index 54% rename from .agents/notes/proposed/architecture/2026-08-10-remote-event-delivery.zh.md rename to .agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.zh.md index 2d34b2555e..dd96e6082d 100644 --- a/.agents/notes/proposed/architecture/2026-08-10-remote-event-delivery.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.zh.md @@ -1,30 +1,30 @@ # Agent Note: Remote 事件投递(ctx.remote.$on) -Status: proposed +Status: implemented [English](2026-08-10-remote-event-delivery.md) | 中文 ## 问题 -[TypeRT Remote 方法调用](2026-08-02-typert-remote-method-calls.zh.md)只覆盖「一次请求一个结果」的定向调用,明确把 Session 事件流与有状态交互留在别处;Host 向消费端的**单向事件推送**因此仍然全部压在遗留的 API Proxy 上。 +[TypeRT Remote 方法调用](../../implemented/architecture/2026-08-02-typert-remote-method-calls.md)只覆盖「一次请求一个结果」的定向调用,明确把 Session 事件流与有状态交互留在别处;Host 向消费端的**单向事件推送**因此仍然全部压在遗留的 API Proxy 上。 Host 上一族「注册表变了,重新拉一次」的纯失效事件(`commands/change`、`credentials/updated`、`settings/document-updated`)既不依赖 AgentScope、载荷也本来就是 JSON,却要穿过四跳才能到达一个 UI 订阅者:host cordis 事件 → apiproxy 手写 `HostFrame` 变体 + zod → client/runtime 手写桥 `ctx.emit(...)` → 消费者 `ctx.on(...)`。每加一个这类事件要改 5 处(帧联合、zod 联合、host 流监听、client 桥、client 侧重复的 `Events` 声明),而这 5 处没有一处是在陈述新事实——事件名、载荷类型、发射时机全都由 owner 包早已在 cordis `Events` 里声明过。 那份重复声明还是**有损**的:client 侧写成 `settings/changed(ns: string)`,brand 类型在这一跳被拍平成裸 `string`,与 Remote 方法侧「消费端类型指向业务包唯一符号」的既有契约相反。 -## 提案 +## 决策 -给消费端 Remote 面补一个单向事件订阅动词 `ctx.remote.$on(event, listener)`;**名单驱动、原样转发**: +消费端 Remote 面持有一个单向事件订阅动词 `ctx.remote.$on(event, listener)`;**名单驱动、原样转发**: -- `packages/api/remotes/src/types.ts` 持有一份可转发 host 事件名单,它同时是「消费端能订阅什么」的唯一控制点。该文件**同时列进本包 host 与 client 两个 face 的 `files`**,两侧读同一份。 +- `packages/api/remotes/src/remote-events.ts` 持有一份可转发 host 事件名单,它同时是「消费端能订阅什么」的唯一控制点。旁边的 `src/types.ts` 由它派生类型投影并填充 selection 座位,按包约定保持纯类型。两个文件**都同时列进本包 host 与 client 两个 face 的 `files`**,两侧读同一份。 - wire 上的事件名 **就是 host cordis 事件原名**(`settings/document-updated`),不加 `host/` 前缀;载荷 **就是 host 的实参列表**,逐元素原样过 JSON,无投影、无脱敏、无改名。 - 载体**寄生现有 host 流**:`HostFrame` 加一个包裹帧 `host/remote-event`,不新开下行通道。 - 事件**签名**不另立表:owner 包把自己的 cordis `Events` 声明搬进 client-safe 的 `./types` 纯类型出口,两侧读**同一份**——`$on` 的 listener 类型就是 `Events[Event]` 本身。「原样」不需要证明,是构造性成立的。 - 但**只借 cordis 的类型形状,不接 cordis 的事件系统**:投递语义、注册表、异常处置全归 TypeRT 自己。 -一条 `Events` 条目若签名里够到了 host-only 符号(Service、`Agent`、Context 等),处理方式是**把代码拆到能干净落进 `./types` 为止**;不接受「一半留 index、一半搬走」的分裂声明,也不接受在 `./types` 里造结构等价的影子类型。本次三个包都不需要拆:它们的条目只够到 `SettingsNamespace`、`SettingsUpdateSource`、`CredentialRef`,全是纯类型。 +一条 `Events` 条目若签名里够到了 host-only 符号(Service、`Agent`、Context 等),处理方式是**把代码拆到能干净落进 `./types` 为止**;不接受「一半留 index、一半搬走」的分裂声明,也不接受在 `./types` 里造结构等价的影子类型。这三个包都不需要拆:它们的条目只够到 `SettingsNamespace`、`SettingsUpdateSource`、`CredentialRef`,全是纯类型。 -本次只迁**纯透传**的三条并删除对应 `HostFrame` 变体;带派生逻辑的一律不动:`host/models-changed`(`llm/adapters-updated` 与 provider/agent-default 命名空间过滤的 fan-in)、`host/workspace-changed`/`-removed`/`host/archived-sessions-changed`(需 view 派生 + 每连接 dedup 状态)、`host/session-added`/`-removed`/`host/session-status`/`host/agent-error`(需活对象投影或帧时派生字段)。 +**纯透传**的三条走这条路径,对应的 `HostFrame` 变体已删除;带派生逻辑的一律不动:`host/models-changed`(`llm/adapters-updated` 与 provider/agent-default 命名空间过滤的 fan-in)、`host/workspace-changed`/`-removed`/`host/archived-sessions-changed`(需 view 派生 + 每连接 dedup 状态)、`host/session-added`/`-removed`/`host/session-status`/`host/agent-error`(需活对象投影或帧时派生字段)。 `skills/change`、`tools/change`、`system-prompt/change` 是同形状的纯失效事件但目前**没有任何消费者**,按「每个抽象都要有当前 owner 与需求」不进名单,只作为扩展位记录在此。 @@ -32,51 +32,51 @@ Host 上一族「注册表变了,重新拉一次」的纯失效事件(`comma type-meta 加一个**形状谓词**、一个**选择座位**和 `TypeRTClientRemote` 的**一个**成员;零运行时代码: -```ts -/** 形状上可以单向远程投递的 cordis 事件名:不绑 Scope,且返回 void。 */ +```ts ignore-check +/** Cordis events shaped for one-way remote delivery: no Scope binding, void return. */ export type TypeRTForwardableEvent = { [Event in keyof Events]: unknown extends ThisParameterType ? ReturnType extends void ? Event : never : never }[keyof Events] -/** Host 装配声明的转发选择;由 api/remotes 的名单一次性填满,其他包不填。 */ +/** The Host assembly's forwarding selection; api/remotes' allowlist fills it, no other package does. */ export interface TypeRTRemoteEventSelection {} -/** `$on` 的合法键:被选中且当前编译面确实存在的事件。 */ +/** `$on`'s legal keys: selected, and present in the current compilation face. */ export type TypeRTRemoteEvent = Extract ``` -```ts -/** 订阅一条被转发的 host 事件;返回的 disposer 归调用方 fiber。 */ +```ts ignore-check +/** Subscribe to one forwarded Host event; the returned disposer belongs to the calling fiber. */ $on(event: Event, listener: Events[Event]): () => void ``` `Events` 按程序解析:host 程序里是 host 事件全集,client 程序里是 client 编译面看得见的那些——同一个谓词在两侧各自成立,不需要把 host 声明拖进 client。 -**消费端只有 `$on`,没有 `$dispatch`。** 帧到订阅表的投递口不进开发者可见契约,也**不能**是一个跨插件的模块级函数:client bundle 纯度门禁(`packages/client/tsdown.client.ts`)只放行 `CLIENT_EXTERNALS`、`INLINE_SAFE` 那层 wire 契约与 `/remote` 生成物三类值导入,而靠 inline 绕过会把 `ClientRemoteService` 复制一份进 runtime bundle、令 `instanceof` 恒假。 +**契约把消费动词与载体交接分开**:消费方用 `$on` 订阅,持有 host 帧 sink 的一方用 `$dispatch` 把解码后的帧交进来。它**不能**是一个跨插件的模块级函数:client bundle 纯度门禁(`packages/client/tsdown.client.ts`)只放行 `CLIENT_EXTERNALS`、`INLINE_SAFE` 那层 wire 契约与 `/remote` 生成物三类值导入,而靠 inline 绕过会把 `ClientRemoteService` 复制一份进 runtime bundle、令 `instanceof` 恒假。cordis 服务方法正是该门禁指定的协作形态: -投递口因此是**一条客户端内部 cordis 事件**,声明在 `dsh-type-meta`(两侧共用的单 face 包,runtime 本来就依赖它,所以零新增依赖): - -```ts -'remote/host-event'(event: string, args: readonly unknown[]): void +```ts ignore-check +$dispatch(event: string, args: readonly unknown[]): void ``` -持有 host 帧 sink 的 client/runtime 发射它,`ClientRemoteService` 是唯一订阅者并转成 `$on` 回调(`dispatch` 是私有方法,不进 `TypeRTClientRemote`)。这是仓内既有的跨插件 plumbing 形态——`connection/reset` 就是 runtime 声明+发射、`ui-command` 订阅,并有 `runtime/tests/wire-events.spec.ts` 钉住。`event` 形参是 `string` 而非 `TypeRTRemoteEvent`:这是 wire 边界,收到无人订阅的名字即静默丢弃。 +持有 host 帧 sink 的 client/runtime 直接调用它,帧不经中转事件即到达订阅表。`event` 形参是 `string` 而非 `TypeRTRemoteEvent`:这是 wire 边界,收到无人订阅的名字即静默丢弃。 投递语义与 cordis 事件系统不共用实现:只有单向投递,没有 waterfall / bail / parallel / serial 模式,也没有 `@mode` 概念(`ReturnType extends void` 是这条纪律的静态表达);不绑 `this`;没有 `EventOptions`、`prepend`、优先级;按注册顺序逐个调用,单个 listener 抛错就地隔离并记日志——它绝不能拖垮帧泵(沿用 `ConnectionController` 对 sink 异常的既有处置)。 -### 名单:两个 face 共读的一个文件 +### 名单:两个 face 共读的同一份声明 -`packages/api/remotes/src/types.ts` 同时列进 `tsconfig.host.json` 与 `tsconfig.client.json` 的 `files`,是名单的**唯一家**: +`packages/api/remotes/src/remote-events.ts` 同时列进 `tsconfig.host.json` 与 `tsconfig.client.json` 的 `files`,是名单的**唯一家**;`src/types.ts` 由它派生类型面: -```ts +```ts ignore-check +// remote-events.ts — the value export const API_REMOTE_FORWARDED_EVENTS = [ 'commands/change', 'credentials/updated', 'settings/document-updated', ] as const +// types.ts — the type face, derived export type ApiRemoteForwardedEvent = typeof API_REMOTE_FORWARDED_EVENTS[number] declare module '@deepseek-ai/dsh-type-meta' { @@ -88,7 +88,7 @@ declare module '@deepseek-ai/dsh-type-meta' { host 半再加一处形状断言,把 host 事件词汇的约束落到同一份名单上: -```ts +```ts ignore-check API_REMOTE_FORWARDED_EVENTS satisfies readonly TypeRTForwardableEvent[] ``` @@ -100,7 +100,7 @@ API_REMOTE_FORWARDED_EVENTS satisfies readonly TypeRTForwardableEvent[] ### 线协议(apiproxy) -```ts +```ts ignore-check | { type: 'host/remote-event'; event: string; args: JsonValue[] } ``` @@ -108,7 +108,6 @@ zod 侧 `args: z.array(z.unknown())`:帧本身来自 `JSON.parse`,元素必 `events.host()` 打开时按名单挂监听(host 流每条自持 disposers,无需新增广播集合)。**注册位置是契约的一部分**:这段必须挂在 `settings/document-updated` 监听**之前**。cordis 按注册序触发,而 `host/models-changed` 是由同一条 host 事件**派生**出来的失效帧——转发帧排到派生帧之后会让同一次 emit 的两帧顺序相对改动前颠倒(已被两条 config 用例实测到)。规则:**转发帧必须先于由它派生的失效帧**。 -cordis `on` 的键是字面量泛型,按动态名单订阅必须在此擦除一次 handler 类型;这是本变更唯一的类型断言点,其安全性由名单谓词与 `isJsonValue` 校验共同承担。 `api/events.ts` 是浏览器侧也要编译的 wire 契约文件,所以它引用的每个类型都必须走 owner 包的 **client-safe type-only 子路径**,绝不能走包根出口。实证:从 `@deepseek-ai/dsh-session` 根引一个类型,就把根出口的 `declare module 'cordis' { interface Context { sessions: SessionStore } }` 拖进 client 编译面、把 client 的 `ctx.sessions: ISessions` 顶掉,在完全无关的 `ui-slash` / `ui-conversation` 里炸出 18 条错。`JsonValue` 因此需要 `dsh-session/src/types.ts` 补一条 re-export。 @@ -118,20 +117,20 @@ cordis `on` 的键是字面量泛型,按动态名单订阅必须在此擦除 由此得到一条对本设计要紧的连带纪律:**这些测试从客户端包 import 值或类型,会把该包的整个 project——以及它引用的每个 project——拖进 Host 构建图**。`ui-settings-general`/`ui-models`/`ui-permission`/`ui-command` 四个消费者 references `api/remotes` 的 client face,而该 face 必须等 host tsdown 生成 `@deepseek-ai/dsh-goal/remote` 才能编译,于是形成构建期死锁:host tsc → api/remotes client face → `goal/remote` → host tsdown → 排在 host tsc 之后。 -本次的处置是在测试侧**镜像**所需的客户端符号(`scaffold.ts` 导出镜像后的 welcome-notice 常量,两个 chat e2e 直接引 `dsh-client-runtime/client` 因为 `runtime` 工程本来就在 host 图里),从而让那 4 个消费者离开 host 图;`apps/cli/tsconfig.json` 里 15 条 client 工程引用随之失去 owner-map 职责,一并删除。镜像值与源逐字一致,漂移的表现是选择器失配或通知未被抑制,都是响亮失败。 +所需的客户端符号在测试侧**镜像**了一份(`scaffold.ts` 导出镜像后的 welcome-notice 常量,两个 chat e2e 直接引 `dsh-client-runtime/client` 因为 `runtime` 工程本来就在 host 图里),从而让那 4 个消费者离开了 host 图;`apps/cli/tsconfig.json` 里 15 条 client 工程引用随之失去 owner-map 职责,已一并删除。镜像值与源逐字一致,漂移的表现是选择器失配或通知未被抑制,都是响亮失败。 ### 改动清单 | 位置 | 改动 | |---|---| -| `dsh-type-meta` | `src/types.ts` 加 `TypeRTForwardableEvent`、`TypeRTRemoteEventSelection`、`TypeRTRemoteEvent` 与 `'remote/host-event'` 声明;`TypeRTClientRemote` 增 `$on`。纯类型,零运行时 | -| `api/gateway` client 半 | `ClientRemoteService` 实现 `$on`(订阅表、`ctx.effect` 归属调用方 fiber、按注册顺序派发并隔离 listener 异常)+ 订阅 `'remote/host-event'`,`dispatch` 保持私有 | -| `api/remotes` | 新增 `src/types.ts`(名单 + 类型投影 + 选择座位),双列进两个 face 的 `files`;`./types` 出口 + `files` 补 `lib/types/**/*.js`;host 半加形状断言并 `import type {}` 三个 owner 包的 `./types`;client 半 `export type {}` 那三个 `./types` 与 `@deepseek-ai/dsh-api-gateway/client`;`./invariant` 断言名单内事件的运行期关系(`thisArg === null` + `mode === 'emit'`) | +| `dsh-type-meta` | `src/types.ts` 加 `TypeRTForwardableEvent`、`TypeRTRemoteEventSelection`、`TypeRTRemoteEvent`;`TypeRTClientRemote` 增 `$on` 与 `$dispatch`。纯类型,零运行时 | +| `api/gateway` client 半 | `ClientRemoteService` 实现 `$on`(订阅按注册项寻址、`ctx.effect` 归属调用方 fiber)与 `$dispatch`(快照后按注册顺序派发,收容抛出或拒绝的 listener) | +| `api/remotes` | 新增 `src/remote-events.ts`(名单值)与 `src/types.ts`(类型投影 + 选择座位),两者都双列进两个 face 的 `files`;`./types` 出口 + `files` 补 `lib/types/**/*.js`;host 半加形状断言并 `import type {}` 三个 owner 包的 `./types`;client 半 `export type {}` 那三个 `./types` 与 `@deepseek-ai/dsh-api-gateway/client` | | 根 `tsconfig.base.json` | 加 `dsh-settings/types`、`dsh-credentials/types`、`dsh-api-remotes/types` 三条 `paths`,全部指向**源**平面 | | `dsh-commands` / `dsh-settings` / `dsh-credentials` | `interface Events` 子块移入各自 client-safe 的 `./types`(settings/credentials 新建该出口,brand 与纯类型一并移入,index 继续 re-export 并留住构造器;`files` 补 `lib/types/**/*.js`) | -| `host/apiproxy` | `HostFrame` 增 `host/remote-event`、删 `host/commands-changed`/`-settings-changed`/`-credentials-changed` 三变体及其 zod;`events.host()` 按名单挂监听(位置在 `settings/document-updated` 之前)+ `assertJsonArgs`;`settings/document-updated` 监听保留以继续喂 `host/models-changed` | +| `host/apiproxy` | `HostFrame` 增 `host/remote-event`、删 `host/commands-changed`/`-settings-changed`/`-credentials-changed` 三变体及其 zod;`events.host()` 按名单挂监听(位置在 `settings/document-updated` 之前)+ `assertJsonArgs`;该监听保留以继续喂 `host/models-changed` | | `dsh-session` | `src/types.ts` 补 `export type { JsonValue }`,让 wire 契约文件能走 client-safe 子路径 | -| `client/runtime` | 桥里三条 `ctx.emit` 换成一行 `ctx.emit('remote/host-event', frame.event, frame.args)`;`Events` 声明删 `commands/changed`/`settings/changed`/`credentials/changed`(`models/changed` 保留) | +| `client/runtime` | 桥里三条 `ctx.emit` 换成一行 `ctx.remote.$dispatch(frame.event, frame.args)` 并新增 `remote` 注入;`Events` 声明删 `commands/changed`/`settings/changed`/`credentials/changed`(`models/changed` 保留) | | 5 个消费者 | ui-command / ui-models / ui-settings-general / ui-permission / ui-agent-preset 改订 `ctx.remote.$on(...)`;照 `ui-goal` 先例 type-only 引 `@deepseek-ai/dsh-api-remotes/client` 并把 `'remote'` 加进 `inject` | | `client/connection` | fixture 的 `emitHost` 造 `host/remote-event` | | `apps/web/tests` + `apps/cli` | 客户端符号镜像(见上节);`apps/cli/tsconfig.json` 删 15 条 client 工程引用 | @@ -148,28 +147,28 @@ cordis `on` 的键是字面量泛型,按动态名单订阅必须在此擦除 **把 apps/web 的 browser e2e 搬进 client 聚合**。看似「客户端测试归客户端面」,实测立刻 21 条错:它们用 host 服务,而 client 程序里 `ctx.sessions` 是 `ISessions`。已否。 -**给 `directory-picker-browse`/`-native` 做 host/client 双 face 切分**,从根上让客户端包不进 host 图。方向正确(它们确实是未切分的双半包),但与本单的 capability seam 是两件事,且改动落在别人属地——记为独立后续单。 +**给 `directory-picker-browse`/`-native` 做 host/client 双 face 切分**,从根上让客户端包不进 host 图。方向正确(它们确实是未切分的双半包),但改动落在别人属地,而收益只是「构建图更干净」——本设计在测试侧镜像客户端符号之后已经不需要它。**已评估不做**。 -## 验收标准 +## 验证 -- host emit 三条事件后,真实 host 流各出一帧 `host/remote-event`,`event` 为 host 原名、`args` 与实参逐元素相等(真组合测试)。 -- 名单在类型层拒绝三类候选:不存在的事件名、绑 Scope 的事件(`goal/changed`)、非 `void` 返回的事件。 -- `$on` 的键面等于名单:`$on('slots/changed', …)` 与 `$on('skills/change', …)` 都必须编译失败。 -- `TypeRTClientRemote` 上**不存在** `$dispatch`:开发者可见契约只有 `$on`(加既有 `$mount` 与生成的 namespace)。 -- 名单内事件发射非 JSON-safe 实参时,`assertJsonArgs` 抛错而非静默降级(对该函数直接单测,不从事件总线造畸形 emit)。 -- `ctx.remote.$on` 的 disposer 归属调用方 fiber:处置 fiber 后订阅消失。一个 listener 抛错不影响同事件其余 listener,也不中断后续帧投递。 -- 转发帧与由同一条 host 事件派生的失效帧在同一次 emit 里的顺序与改动前逐帧一致。 -- 消费端 `$on('settings/document-updated', …)` 的 `ns` 形参解析为 `SettingsNamespace`(brand 未丢)。 -- 三条 `HostFrame` 变体、client 侧三条 `Events` 声明、client 手写桥的三条分支在同一 PR 内消失;`host/models-changed` 行为不变。 -- `pnpm run build` 全量通过。 +钉住该行为的东西: -## 风险 +- 一个真组合测试:host 每 emit 一次,真实 host 流就出一帧 `host/remote-event`,`event` 为 host 原名、`args` 与实参逐元素相等。 +- 类型层负例拒绝三类候选:不是事件的名字、绑 Scope 的事件(`goal/changed`)、返回值非 `void` 的事件。`$on('slots/changed', …)`(client 本地事件)与 `$on('skills/change', …)`(已声明但未选中)都编译失败——因此 `$on` 的键面恰好等于名单。 +- 消费端 `$on('settings/document-updated', …)` 把 `ns` 解析为 `SettingsNamespace`:brand 穿过 wire 存活。 +- `$on` 的 disposer 归属调用方 fiber;同一个函数对象订阅两次时两条注册各自独立退订——按 listener 身份做键的表会把它们合并,所以订阅按注册项寻址。 +- 投递同时收容抛出的 listener 与拒绝所返回 promise 的 listener:声明返回值是 `void`,没人 await 异步 listener,其拒绝否则会完全逃出这层收容。投递遍历快照,因此派发中订阅或退订都不会改变本帧的接收者集合。 +- 同一次 emit 下,转发帧与由同一条 host 事件派生的失效帧保持改动前的相对顺序(两条 config 用例实测)。 +- `assertJsonArgs` 直接单测,而不是从事件总线造畸形 emit:类型化的 `ctx.emit` 造不出来——名单内每条事件的载荷在静态上都是 JSON-safe 的。 +- 三个 `HostFrame` 变体、三条 client 侧 `Events` 声明、三条桥分支在同一变更内消失;`host/models-changed` 行为不变。 -- **寄生 legacy 帧联合**:新契约暂时住在 apiproxy 的 `HostFrame` 里,读者会误以为 Remote 事件归 apiproxy 拥有。缓解=帧注释指明名单归 `api-remotes`,并在 apiproxy README 的已知欠账里记这条寄居关系。 -- **共享文件破了 api/remotes 的 face 互斥契约**:`src/types.ts` 同时属于两个 project,两侧各自 emit 一份同名声明到共用的 `lib/types`。内容逐字相同、`.tsbuildinfo` 各自独立,实际无害,但 README 的 Build boundary 节必须写明这条例外及其成因(paths 指向 src)。 -- **任一 client 插件都能 `ctx.emit('remote/host-event', …)` 伪造一条 host 事件**:与 `connection/reset` 可被伪造成重连同一量级(client 是单一信任域)。测试只钉「事件到 `$on` 的转换」,不假装它有来源鉴别。 -- **名单的形状断言当前处于注释态**(`packages/api/remotes/src/index.ts`,连同它所需的名单 import 与三条 owner `./types` 的 `import type {}`),因此本节描述的三条静态保证暂未生效:此刻往名单里塞一个 scoped 事件或拼错的名字不会有编译错误。恢复它对构建图无影响(那四个消费者已不在 host 图里),是 PR 前必做项。 -- **测试侧的镜像会漂移**:`apps/web/tests` 里镜像的客户端常量与源之间没有机械校验,只能靠「漂移即选择器失配」这种响亮失败兜底。理想上该加一条 grep 级门禁禁止 `apps/web/tests` 引入 `@deepseek-ai/dsh-client-*`,本轮未加。 -- **动态订阅的类型擦除**:按名单 `ctx.on(name, …)` 必须擦一次 handler 类型;若名单谓词将来被放宽,这处断言就不再有静态支撑。 -- **放弃的能力**:不支持带载荷投影/脱敏的事件、不支持 Scope 化事件(`agentCtx.remote.$on`)、不支持重连重放(纯失效信号,重连后的重新拉取由既有 `connection/reset` 覆盖)。mux 流的 session 事件、可答帧与快照基线不在范围内。 -- **host 图里仍有客户端包**:`connection`、`runtime`、`ui-slots` 等 12 个工程经 `directory-picker-browse|native`(未切分的双半包)与 `api/gateway → client/connection` 仍在 host 构建图内。它们当前都能编译、且不再牵连 api/remotes 的 client face,所以不阻塞;根治留给上面那条独立后续单。 +## 后果 + +- **寄居在 legacy 帧联合里**:契约住在 apiproxy 的 `HostFrame` 中,读者可能误以为 apiproxy 拥有 Remote 事件。该帧的 JSDoc 点名名单归 `api-remotes`,apiproxy README 在 known limitations 记录这项寄居。host 流将来整体搬家时,包裹帧随之搬走,消费端契约不变。 +- **两个文件打破了 api/remotes 的 face 互斥约定**:`src/remote-events.ts` 与 `src/types.ts` 同属两个工程,各自向共享的 `lib/types` 发射一份相同声明。内容逐字节相同、`.tsbuildinfo` 各自独立,实践上无害;README 的构建边界节陈述了这个例外及其成因(`paths` 指向源码面)。 +- **载体交接是开发者可见的**:任何持有 `ctx.remote` 的 client 插件都能调 `$dispatch` 合成一条转发事件。这个暴露面早于该动词存在——先前由内部事件中转帧时,`ctx.emit` 同样可达——与 `connection/reset` 可被伪造成重连同一量级(client 是单一信任域)。测试只钉「交接到 `$on` 的转换」,不假装该端口鉴别调用方。 +- **畸形实参在发射方的收容里失败,而非加载期**:`assertJsonArgs` 在转发监听内抛出,因此由发射 seam 自己的 listener 收容记录并丢弃该帧——响亮地出现在 host 日志里,而不是加载时或 emit 点。 +- **测试侧镜像值可能漂移**:没有任何机制核对 `apps/web/tests` 中镜像的 client 常量与其源;安全网只是漂移会让选择器失配。规则写在 `apps/web/tests/README.md`,由 review 守;grep 级门禁经评估后刻意不做。 +- **放弃的能力**:不支持投影或脱敏载荷、不支持 Scope 化事件(`agentCtx.remote.$on`)、重连不重放——这些都是纯失效信号,且 `connection/reset` 已覆盖重连后的重新拉取。mux 流的会话事件、可应答帧与快照基线不在范围内。 +- **仍有 client 包留在 host 图里**:12 个工程(`connection`、`runtime`、`ui-slots` 等)经未拆分的 `directory-picker-browse`/`-native` 与 `api/gateway → client/connection` 仍可达 host 图。它们都能编译且不再牵连 api/remotes 的 client face,因此没有阻塞本次改动;拆分那些包能减少几个,但经评估后不做。两个 chat e2e 直接引 `dsh-client-runtime/client` 依赖 `runtime` 本来就在图里——属偶然而非保证。 +- **invariant companion 不做运行期检查**:早先的修订曾在活事件总线上断言投递形状(`thisArg === null`、`mode === 'emit'`),这让 companion 与名单值耦合,并使 rolldown 把它提成第三个 bundle chunk——而机械推导的发布文件清单并不携带它。host 面的 `TypeRTForwardableEvent` 断言在编译期已拒绝这两种偏离,因此该 companion 是一个带说明的空 installer。 diff --git a/.agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.i18n.yaml index 874c0c0d34..f789ed37a0 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.md -2026-08-06-onboarding-step-owned-takeover-chrome.md: 4b3bbbc03c4494359297ae6e54bcc9a74c387e80 -2026-08-06-onboarding-step-owned-takeover-chrome.zh.md: ba4f503f7bc318fcb16f4413465a86609fa1bc61 +2026-08-06-onboarding-step-owned-takeover-chrome.md: 35b9d16aaba4ca9f124a9972c43b2a107ea0309f +2026-08-06-onboarding-step-owned-takeover-chrome.zh.md: 2f3331965aa94aa01bf0ce21d4bbb421397ee5bd diff --git a/.agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.md b/.agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.md index 4b3bbbc03c..35b9d16aab 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.md +++ b/.agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.md @@ -32,4 +32,4 @@ A future step that registers without wrapping its visible content in `Onboarding ## Testing -`packages/client/ui-primitives/tests/onboarding-surface.spec.tsx` pins the primitive: body portal around the content, mask/stage class presence, `#root` inert held for exactly the mount lifetime, and the no-`#root` composition. `packages/client/ui-settings/tests/settings-root.spec.tsx` pins the inverted shell contract: no takeover chrome and no inert while a mounted step renders nothing. `apps/web/tests/onboarding-deepseek-config.e2e.ts` gains the defect's assembled regression pin: a configured world reloads while every `settings.describe` response is held open at the browser's network boundary — widening the steps' deciding window from loopback-invisible to hundreds of milliseconds, which is what keeps the assertions non-vacuous — and an 8 ms in-page sampler proves the takeover chrome never mounts and `#root` never turns inert. The file's existing scenarios and the step specs (`ui-settings-general`, `ui-models`) pass unchanged — the mask selector and geometry pins survive because the stylesheet moved verbatim. +`packages/client/ui-primitives/tests/onboarding-surface.spec.tsx` pins the primitive: body portal around the content, mask/stage class presence, `#root` inert held for exactly the mount lifetime, and the no-`#root` composition. `packages/client/ui-settings-general/tests/settings-root.spec.tsx` pins the inverted shell contract: no takeover chrome and no inert while a mounted step renders nothing. `apps/web/tests/onboarding-deepseek-config.e2e.ts` gains the defect's assembled regression pin: a configured world reloads while every `settings.describe` response is held open at the browser's network boundary — widening the steps' deciding window from loopback-invisible to hundreds of milliseconds, which is what keeps the assertions non-vacuous — and an 8 ms in-page sampler proves the takeover chrome never mounts and `#root` never turns inert. The file's existing scenarios and the step specs (`ui-settings-general`, `ui-models`) pass unchanged — the mask selector and geometry pins survive because the stylesheet moved verbatim. diff --git a/.agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.zh.md b/.agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.zh.md index ba4f503f7b..2f3331965a 100644 --- a/.agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-08-06-onboarding-step-owned-takeover-chrome.zh.md @@ -32,4 +32,4 @@ ## 测试 -`packages/client/ui-primitives/tests/onboarding-surface.spec.tsx` 钉住原语行为:内容外的 body portal、遮罩/展示层类名存在、`#root` 的 `inert` 恰好持续挂载生命周期,以及无 `#root` 的组合。`packages/client/ui-settings/tests/settings-root.spec.tsx` 钉住反转后的外壳约定:已挂载步骤什么都不渲染时,无接管界面框架、无 inert。`apps/web/tests/onboarding-deepseek-config.e2e.ts` 新增本缺陷的整装回归钉:已配置世界刷新页面,同时在浏览器网络边界扣住所有 `settings.describe` 响应——把步骤的判定窗口从 loopback 下不可见拉宽到数百毫秒,这正是断言保持非空洞的关键——页内 8ms 采样器证明接管界面框架从未挂载、`#root` 从未变为 inert。该文件的既有场景与步骤 spec(`ui-settings-general`、`ui-models`)原样通过——样式表逐字迁移,遮罩选择器与几何钉子得以幸存。 +`packages/client/ui-primitives/tests/onboarding-surface.spec.tsx` 钉住原语行为:内容外的 body portal、遮罩/展示层类名存在、`#root` 的 `inert` 恰好持续挂载生命周期,以及无 `#root` 的组合。`packages/client/ui-settings-general/tests/settings-root.spec.tsx` 钉住反转后的外壳约定:已挂载步骤什么都不渲染时,无接管界面框架、无 inert。`apps/web/tests/onboarding-deepseek-config.e2e.ts` 新增本缺陷的整装回归钉:已配置世界刷新页面,同时在浏览器网络边界扣住所有 `settings.describe` 响应——把步骤的判定窗口从 loopback 下不可见拉宽到数百毫秒,这正是断言保持非空洞的关键——页内 8ms 采样器证明接管界面框架从未挂载、`#root` 从未变为 inert。该文件的既有场景与步骤 spec(`ui-settings-general`、`ui-models`)原样通过——样式表逐字迁移,遮罩选择器与几何钉子得以幸存。 diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml index 0762a4d131..45d0a37bf6 100644 --- a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md -2026-07-30-versioned-gui-welcome-onboarding.md: 4707769d4fa9fbf184e09a2e73087dfd326070be -2026-07-30-versioned-gui-welcome-onboarding.zh.md: 002a04b1fc6acc4fc7cfee053bdbbb2abaef047e +2026-07-30-versioned-gui-welcome-onboarding.md: ff4bfc860492d7eca345d55449e4e4726b988854 +2026-07-30-versioned-gui-welcome-onboarding.zh.md: dd293fe9e1e77414c4b352aa50e6b0456f866cce diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md index 4707769d4f..ff4bfc8604 100644 --- a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md +++ b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md @@ -16,7 +16,7 @@ The GUI's credential onboarding begins with a DeepSeek-specific readiness check, **Loopback acknowledgement is durable per Harness profile.** The Host half registers a `ui-onboarding` section in the user-settings seam, stored under the active `$DSH_HOME/settings.yaml`. The connection plugin publishes whether the current page uses a loopback authority as `ctx.connection.isLoopback`; hostname classification remains internal to the connection package, and other client plugins consume the service state instead of importing its implementation. A loopback browser shows the notice unless `welcomeNoticeVersion` equals the owner constant exactly. Continue applies one path mutation with the current version and calls `complete()` only after the Host commits it; a failed write leaves the notice open, and closing the page or process writes nothing. Bumping the constant intentionally makes every profile acknowledge the revised copy once. A non-loopback browser must not call the loopback-only settings API. It presents the same notice, but explicit Continue completes the step only in the current browser process; reload or a new process presents it again. -**Concurrent loopback views converge without stale replacement.** The acknowledgement write omits `expectedRevision` deliberately: every loopback tab writes the same version to one path, so the operation is idempotent and preserves sibling fields instead of rebuilding the section. `settings/document-updated` becomes `host/settings-changed`; an already mounted loopback tab refetches and advances when another tab or an external editor commits the current version. The API proxy exposes this one product namespace through a closed allowlist beside configurable-provider namespaces, without treating its changes as model-catalog invalidations. +**Concurrent loopback views converge without stale replacement.** The acknowledgement write omits `expectedRevision` deliberately: every loopback tab writes the same version to one path, so the operation is idempotent and preserves sibling fields instead of rebuilding the section. `settings/document-updated` reaches the client as an invalidation — through `host/settings-changed` then, and forwarded verbatim now ([forwarded Remote events](../architecture/2026-08-10-remote-event-delivery.md)); an already mounted loopback tab refetches and advances when another tab or an external editor commits the current version. The API proxy exposes this one product namespace through a closed allowlist beside configurable-provider namespaces, without treating its changes as model-catalog invalidations. **Onboarding temporarily owns the viewport as one continuous stage.** A solid product surface replaces the complete application view through a body-level portal and marks the underlying app root inert; the exact required mask remains mounted behind that surface with `position:absolute`, zero left/right/bottom offsets, `top:80px`, `rgba(0, 0, 0, 0.24)`, and `backdrop-filter: blur(2px)`. Welcome and conditional credential setup render as successive pages in this stage instead of independent modals. Both pages reuse the Web UI's black `BrandWordmark`. The welcome page preserves the four authored paragraphs verbatim under the `内测声明` title; every paragraph uses one 16/28 body scale, and only the requested action clause inside the final paragraph receives a subtle 500 weight. A short staggered opacity/vertical entrance supplies pacing without blocking interaction and disappears under reduced motion. The title receives initial focus, Continue is the sole button, and no close, Escape, or mask-click path exists. diff --git a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md index 002a04b1fc..dd293fe9e1 100644 --- a/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md +++ b/.agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.zh.md @@ -16,7 +16,7 @@ GUI 的凭据引导从 DeepSeek 专用的就绪状态检查开始,但内部测 **loopback 确认状态按 Harness profile 持久化。** 宿主端在 user-settings seam 中注册 `ui-onboarding` 分节,并存入当前 `$DSH_HOME/settings.yaml`。connection 插件通过 `ctx.connection.isLoopback` 统一发布当前页面是否使用 loopback authority;hostname 判定留在 connection 包内,其他客户端插件只消费服务状态,而不导入其实现。除非 `welcomeNoticeVersion` 与文案所有者文件中的常量精确相等,否则 loopback 浏览器会显示通知。「继续」会以当前版本执行一次路径变更,并且仅在宿主端提交成功后调用 `complete()`;写入失败时通知保持打开,关闭页面或进程则不会写入任何内容。更新该常量会有意要求每个 profile 对修订后的文案重新确认一次。非 loopback 浏览器不能调用仅限 loopback 的 settings API;它仍显示同一通知,但显式点击「继续」只会在当前浏览器进程中完成该步骤,重新加载或新进程会再次显示通知。 -**并发 loopback 视图无需陈旧的整体替换即可收敛。** 确认写入有意省略 `expectedRevision`:每个 loopback 标签页都向同一路径写入相同版本,因此该操作是幂等的,并会保留同级字段,而不是重建整个分节。`settings/document-updated` 会转为 `host/settings-changed`;另一个标签页或外部编辑器提交当前版本后,已挂载的 loopback 标签页会重新拉取状态并推进。API Proxy(`@deepseek-ai/dsh-host-apiproxy`)在可配置提供方 namespace 之外,通过封闭的允许列表暴露这一个产品 namespace,同时不会把它的变更视为模型目录失效事件。 +**并发 loopback 视图无需陈旧的整体替换即可收敛。** 确认写入有意省略 `expectedRevision`:每个 loopback 标签页都向同一路径写入相同版本,因此该操作是幂等的,并会保留同级字段,而不是重建整个分节。`settings/document-updated` 以失效通知形式到达客户端——当时经 `host/settings-changed`,现在则是原样转发([转发的 Remote 事件](../architecture/2026-08-10-remote-event-delivery.md));另一个标签页或外部编辑器提交当前版本后,已挂载的 loopback 标签页会重新拉取状态并推进。API Proxy(`@deepseek-ai/dsh-host-apiproxy`)在可配置提供方 namespace 之外,通过封闭的允许列表暴露这一个产品 namespace,同时不会把它的变更视为模型目录失效事件。 **引导流程会暂时接管视口,形成一个连续阶段。** 纯色产品界面通过挂载到 `body` 的 portal 取代完整的应用视图,并将底层应用根节点标记为 inert;严格符合要求的遮罩仍挂载在该界面后方,并保留 `position:absolute`、left/right/bottom 偏移量为零、`top:80px`、`rgba(0, 0, 0, 0.24)` 和 `backdrop-filter: blur(2px)`。欢迎页和按条件显示的凭据设置页在这一阶段中依次呈现,而不是各自作为独立的模态窗口。两个页面都复用 Web UI 的黑色 `BrandWordmark`。欢迎页在 `内测声明` 标题下逐字保留既定的四段文案;所有段落统一采用 16/28 的正文字号与行高,只有最后一段中指定的行动语句使用较为克制的 500 字重。短暂的错落式透明度与纵向位移动画营造出节奏感,但不会阻碍交互,并会在用户启用减少动态效果时禁用。初始焦点落在标题上,「继续」是唯一按钮,且不存在关闭、Escape 或点击遮罩的退出路径。 diff --git a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.i18n.yaml b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.i18n.yaml index 4970aa2c7d..0c25e37283 100644 --- a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.md -2026-07-31-permission-default-for-new-sessions.md: ffa4c8a07bdd08ca52edbc14fe10372ad76e8cf8 -2026-07-31-permission-default-for-new-sessions.zh.md: c70d431ec9bbe8cdeabfdc8a0052a9c436844f02 +2026-07-31-permission-default-for-new-sessions.md: 63df4514f8503f13eec4b4a47daf057d07062ab3 +2026-07-31-permission-default-for-new-sessions.zh.md: 969fa1aae667e171ba0427a026ef16124fe5ee88 diff --git a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.md b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.md index ffa4c8a07b..63df4514f8 100644 --- a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.md +++ b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.md @@ -16,7 +16,7 @@ The service reads the current Settings value synchronously at `session/created`. The existing `/permission` command and `permissions` projection remain the current-session path. The browser plugin now contributes the Permission row to `settings.general.item`, reads the dynamic enum from the redacted Settings descriptor, and writes only `defaultPreset` through a revision-checked `settings.mutate`. The row injects its observable through the slot `hooks` compartment instead of binding a renderer-specific hook, and the Permission service sweeps already-live sessions when it mounts so HMR cannot leave an unpinned session. The ownerless General-settings package contributes no placeholder rows. -ApiProxy explicitly adds `permission` to its Web settings allowlist beside the configurable-provider namespaces. This is a local boundary decision, not a general registration flag or a `local-client` access model: registering another Settings namespace still does not expose it. Permission changes emit `host/settings-changed` but not `host/models-changed`. +ApiProxy explicitly adds `permission` to its Web settings allowlist beside the configurable-provider namespaces. This is a local boundary decision, not a general registration flag or a `local-client` access model: registering another Settings namespace still does not expose it. Permission changes reach the client as a settings invalidation — `host/settings-changed` then, the forwarded `settings/document-updated` now ([forwarded Remote events](../architecture/2026-08-10-remote-event-delivery.md)) — but never as `host/models-changed`. ## Consequences diff --git a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.zh.md b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.zh.md index c70d431ec9..969fa1aae6 100644 --- a/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.zh.md +++ b/.agents/notes/implemented/feature/2026-07-31-permission-default-for-new-sessions.zh.md @@ -16,7 +16,7 @@ Web「通用」设置页将「权限」显示为禁用的骨架控件,尽管 ` 现有 `/permission` 命令和 `permissions` 投影仍是当前会话的操作路径。浏览器插件现在向 `settings.general.item` 贡献「权限」行,从脱敏后的 Settings 描述符读取动态 enum,并只通过经过 revision 校验的 `settings.mutate` 写入 `defaultPreset`。该行通过 slot 的 `hooks` 格注入 observable,而不是绑定渲染器专用钩子;权限服务挂载时会遍历并固定所有已存活会话,因此 HMR(热模块替换)不会遗留未固定的会话。无归属的「通用」设置包不贡献任何占位行。 -ApiProxy 在可配置提供方 namespace 之外,将 `permission` 显式加入 Web Settings allowlist。这是局部的边界决策,而不是通用注册标志或 `local-client` 访问模型:注册其他 Settings namespace 仍不会将其暴露。权限变更会发出 `host/settings-changed`,但不会发出 `host/models-changed`。 +ApiProxy 在可配置提供方 namespace 之外,将 `permission` 显式加入 Web Settings allowlist。这是局部的边界决策,而不是通用注册标志或 `local-client` 访问模型:注册其他 Settings namespace 仍不会将其暴露。权限变更以 settings 失效通知形式到达客户端——当时是 `host/settings-changed`,现在是转发的 `settings/document-updated`([转发的 Remote 事件](../architecture/2026-08-10-remote-event-delivery.md))——但绝不会是 `host/models-changed`。 ## 后果 diff --git a/apps/web/tests/README.i18n.yaml b/apps/web/tests/README.i18n.yaml new file mode 100644 index 0000000000..a6005ade32 --- /dev/null +++ b/apps/web/tests/README.i18n.yaml @@ -0,0 +1,6 @@ +# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each +# 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 apps/web/tests/README.md +README.md: 68e5db5af5f816cc982bacb7989d996c859be204 +README.zh.md: f366c28024dab89d0243a60d93a706f220fa8fb8 diff --git a/apps/web/tests/README.md b/apps/web/tests/README.md new file mode 100644 index 0000000000..68e5db5af5 --- /dev/null +++ b/apps/web/tests/README.md @@ -0,0 +1,46 @@ +# apps/web browser e2e + +English | [中文](README.zh.md) + +These tests boot the real web composition in-process and drive it with a real +Chromium over real HTTP. The lane's mechanics — modes, fixtures, goldens, and +the deliberate composition divergences from `dsh web` — are documented in +[`scaffold.ts`](scaffold.ts) and the +[browser e2e Agent Note](../../../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md). + +## These are Host-face tests + +They type-check in the root `tsconfig.host.json`, not in the Client aggregate, +because they read Host services directly: `ctx.apiProxy`, the Host +`SessionStore`, `ctx.sessionProjectionCache`. Driving a browser at runtime does +not make a file part of the Client program — the two faces merge cordis +`Context` under the same keys with different services, so one program cannot see +both. Moving these files into the Client aggregate makes every Host-service +access fail to compile. + +## Do not import `@deepseek-ai/dsh-client-*` here + +Importing a Client package — a value or a type — pulls its whole TypeScript +project, and every project it references, into the **Host build graph**. That has +bitten this lane once already: four Client consumer packages reference +`api/remotes`' Client face, which cannot compile until Host tsdown has generated +`@deepseek-ai/dsh-goal/remote`, so the Host build phase ended up waiting on an +artifact it produces itself. + +When a scenario needs a Client-owned constant or pure function, mirror it here +instead, next to the commented-out import that names the source module. A drift +then surfaces as a missed selector or an unsuppressed notice — a loud failure, +never a silent pass. `scaffold.ts` holds the mirrored welcome-notice values and +exports them for the scenarios that assert on them. + +Two kinds of Client import stand. `assembled-boot.ts` drives the shell itself, so +it imports `AppWebEntry` from `@deepseek-ai/dsh-client-web` and the boot-manifest +type from `@deepseek-ai/dsh-client-modules/client`: booting the real shell is what +that harness is for, and both packages are already in the Host graph. Separately, +the chat scenarios import `conversationContextKey` from +`@deepseek-ai/dsh-client-runtime/client` because `client/runtime` is reachable +through the unsplit `directory-picker` packages and pulls nothing further in. +That reachability is incidental, not a guarantee — if it ever leaves the graph, +mirror the helper like the rest. + +Nothing mechanically enforces this rule; keep it in review. diff --git a/apps/web/tests/README.zh.md b/apps/web/tests/README.zh.md new file mode 100644 index 0000000000..f366c28024 --- /dev/null +++ b/apps/web/tests/README.zh.md @@ -0,0 +1,37 @@ +# apps/web 浏览器 e2e + +[English](README.md) | 中文 + +这些测试在进程内启动真实的 web 组合,并用真实 Chromium 通过真实 HTTP 驱动它。该 lane +的运行机制——模式、fixture、golden,以及与 `dsh web` 之间刻意保留的组合差异——记录在 +[`scaffold.ts`](scaffold.ts) 和 +[浏览器 e2e Agent Note](../../../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md)中。 + +## 这些是 Host 面的测试 + +它们在根 `tsconfig.host.json` 中做类型检查,而不在 Client aggregate 中,因为它们直接读取 +Host 服务:`ctx.apiProxy`、Host 侧 `SessionStore`、`ctx.sessionProjectionCache`。运行时驱动 +浏览器并不使一个文件成为 Client 程序的一部分——两个 face 在相同的键上以不同服务合并 cordis +`Context`,因此单个程序无法同时看见两者。把这些文件挪进 Client aggregate 会让每一处 +Host 服务访问都无法编译。 + +## 不要在此 import `@deepseek-ai/dsh-client-*` + +import 一个 Client 包——无论值还是类型——都会把它整个 TypeScript 工程、以及它引用的每个工程 +拉进 **Host 构建图**。这已经坑过本 lane 一次:四个 Client 消费方包引用了 `api/remotes` 的 +Client face,而该 face 必须等 Host tsdown 生成 `@deepseek-ai/dsh-goal/remote` 之后才能编译, +于是 Host 构建阶段变成在等一个由它自己产出的产物。 + +当某个场景需要 Client 持有的常量或纯函数时,改为在此处镜像一份,并紧挨着一条注释掉的 +import 点明源模块。这样漂移会表现为选择器未命中或提示未被抑制——是响亮的失败,绝不会是静默 +通过。`scaffold.ts` 持有镜像的 welcome-notice 取值,并导出给断言它们的场景使用。 + +有两类 Client import 是长期成立的。`assembled-boot.ts` 驱动 shell 本身,因此它从 +`@deepseek-ai/dsh-client-web` import `AppWebEntry`、从 +`@deepseek-ai/dsh-client-modules/client` import boot manifest 类型:启动真实 shell 正是该 +harness 的用途,且这两个包本来就在 Host 图中。另外,chat 场景从 +`@deepseek-ai/dsh-client-runtime/client` import `conversationContextKey`,因为 +`client/runtime` 经未拆分的 `directory-picker` 包可达,且不会再牵入别的东西。这种可达性是 +偶然而非保证——一旦它离开该图,就像其余情形那样镜像该 helper。 + +没有任何机制强制这条规则;靠 review 守住它。 diff --git a/apps/web/tests/onboarding-deepseek-config.e2e.ts b/apps/web/tests/onboarding-deepseek-config.e2e.ts index 77eba279ba..5572e834ad 100644 --- a/apps/web/tests/onboarding-deepseek-config.e2e.ts +++ b/apps/web/tests/onboarding-deepseek-config.e2e.ts @@ -10,7 +10,7 @@ import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import { acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, - WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_ACK_FIELD, + WELCOME_NOTICE_SETTINGS_NAMESPACE, WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_VERSION, WELCOME_NOTICE_COPY, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' diff --git a/docs/development.i18n.yaml b/docs/development.i18n.yaml index 9d8d8fba47..24d78bf62d 100644 --- a/docs/development.i18n.yaml +++ b/docs/development.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/development.md -development.md: 7d6ca74df1311560b0d61444c92e74927639ca31 -development.zh.md: 1461717cf496b1863f2f0c47ad4b42990dc789e9 +development.md: d5bd18f83f6f2f8fd3d507ec133eea4d4e06abef +development.zh.md: 0725cfaad74ec2aa3e52f6523a1dbdbdd9da8a39 diff --git a/docs/event-producer-consumer.i18n.yaml b/docs/event-producer-consumer.i18n.yaml index 6c8cfbdee0..92815faa9d 100644 --- a/docs/event-producer-consumer.i18n.yaml +++ b/docs/event-producer-consumer.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/event-producer-consumer.md -event-producer-consumer.md: d47142bfc3e0ea1430dcbc87b6f01d614d8a4b12 -event-producer-consumer.zh.md: 929fe71dfb109f8360ffa6771bb9b267a7f0ab1c +event-producer-consumer.md: a4802914ca5080dfb1e4ac7ed41d62f6c95a3f00 +event-producer-consumer.zh.md: 966555779a96d7aa2ac4ad6db55ede6b33d42870 diff --git a/docs/event-producer-consumer.md b/docs/event-producer-consumer.md index d47142bfc3..a4802914ca 100644 --- a/docs/event-producer-consumer.md +++ b/docs/event-producer-consumer.md @@ -21,8 +21,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:178`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `apiproxy`, [`compact-basic`](../packages/compact/compact-basic), [`goal-session`](../packages/goal/goal-session), `server` | | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:278`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/index.ts:30`](../packages/interaction/user-approval/src/index.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `apiproxy` | -| `commands/change` | `emit` | [`packages/interaction/commands/src/index.ts:134`](../packages/interaction/commands/src/index.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `apiproxy` | -| `credentials/updated` | `emit` | [`packages/credentials/credentials/src/index.ts:67`](../packages/credentials/credentials/src/index.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | `apiproxy`, [`credentials`](../packages/credentials/credentials) | +| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:33`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `apiproxy` | +| `credentials/updated` | `emit` | [`packages/credentials/credentials/src/types.ts:29`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | `apiproxy`, [`credentials`](../packages/credentials/credentials) | | `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | `apiproxy`, [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace) | | `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) | | `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-policy`](../packages/fs/fs-policy), [`skill-local`](../packages/skill/skill-local) | @@ -34,8 +34,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) | | `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) | -| `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` | -| `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) | +| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` | +| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | | `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:166`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) | | `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:140`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | @@ -61,17 +61,14 @@ This matrix shows which packages dispatch each harness-owned event and which pac | Event string | Dispatchers | Listeners | | --- | --- | --- | -| `commands/changed` | `runtime` (`emit`) | `ui-command` | -| `connection/reset` | `runtime` (`emit`) | `runtime`, `ui-command`, `ui-models`, `ui-permission`, `ui-settings-general` | -| `credentials/changed` | `runtime` (`emit`) | `ui-models` | +| `connection/reset` | `runtime` (`emit`) | `ui-settings` | | `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), [`pty-local`](../packages/pty/pty-local), `runtime`, [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) | -| `internal/plugin` | - | `hmr`, `loader`, [`lsp-local`](../packages/lsp/lsp-local), `modules`, `webserver` | +| `internal/plugin` | - | `loader`, [`lsp-local`](../packages/lsp/lsp-local), `webserver` | | `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` | | `internal/status` | - | [`agent`](../packages/core/agent) | | `locale/change` | `locale` (`emit`) | `locale` | -| `models/changed` | `runtime` (`emit`) | `ui-models` | -| `session/preset-changed` | `runtime` (`emit`) | `ui-command` | -| `settings/changed` | `runtime` (`emit`) | `runtime`, `ui-models`, `ui-permission`, `ui-settings-general` | +| `models/changed` | `runtime` (`emit`) | - | +| `session/preset-changed` | `runtime` (`emit`) | - | | `slash/input-begin-command` | - | `ui-conversation` | | `slash/input-consume-token` | - | `ui-conversation` | | `slash/input-insert-reference` | - | `ui-conversation` | diff --git a/docs/event-producer-consumer.zh.md b/docs/event-producer-consumer.zh.md index 929fe71dfb..966555779a 100644 --- a/docs/event-producer-consumer.zh.md +++ b/docs/event-producer-consumer.zh.md @@ -23,8 +23,8 @@ | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:178`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `apiproxy`, [`compact-basic`](../packages/compact/compact-basic), [`goal-session`](../packages/goal/goal-session), `server` | | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:278`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | | `approval/request` | `waterfall` | [`packages/interaction/user-approval/src/index.ts:30`](../packages/interaction/user-approval/src/index.ts) | [`user-approval`](../packages/interaction/user-approval) (`waterfall`) | [`acp`](../packages/acp/acp), `apiproxy` | -| `commands/change` | `emit` | [`packages/interaction/commands/src/index.ts:134`](../packages/interaction/commands/src/index.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `apiproxy` | -| `credentials/updated` | `emit` | [`packages/credentials/credentials/src/index.ts:67`](../packages/credentials/credentials/src/index.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | `apiproxy`, [`credentials`](../packages/credentials/credentials) | +| `commands/change` | `emit` | [`packages/interaction/commands/src/types.ts:33`](../packages/interaction/commands/src/types.ts) | [`commands`](../packages/interaction/commands) (`events.dispatch`) | `apiproxy` | +| `credentials/updated` | `emit` | [`packages/credentials/credentials/src/types.ts:29`](../packages/credentials/credentials/src/types.ts) | [`credentials`](../packages/credentials/credentials) (`events.dispatch`) | `apiproxy`, [`credentials`](../packages/credentials/credentials) | | `domain/changed` | `emit` | [`packages/storage/storage-domain/src/events.ts:46`](../packages/storage/storage-domain/src/events.ts) | [`storage-domain`](../packages/storage/storage-domain) (`emit`) | `apiproxy`, [`storage-domain`](../packages/storage/storage-domain), [`workspace`](../packages/workspace/workspace) | | `fs/edit-intent` | `waterfall` | [`packages/fs/fs/src/index.ts:66`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`waterfall`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`waterfall`) | [`fs-policy`](../packages/fs/fs-policy) | | `fs/observed` | `emit` | [`packages/fs/fs/src/index.ts:76`](../packages/fs/fs/src/index.ts) | [`tool-fs`](../packages/fs/tool-fs) (`emit`), [`tool-str-replace-editor`](../packages/fs/tool-str-replace-editor) (`emit`) | [`fs-policy`](../packages/fs/fs-policy), [`skill-local`](../packages/skill/skill-local) | @@ -36,13 +36,13 @@ | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:64`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`session-persistence`](../packages/session/session-persistence), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) | | `session/event` | `emit` | [`packages/core/session/src/index.ts:76`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`agent-loop`](../packages/core/agent-loop), `apiproxy`, [`compact`](../packages/compact/compact), [`compact-basic`](../packages/compact/compact-basic), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`loader-smoke`](../packages/support/loader-smoke), `server`, [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-telemetry-otel`](../packages/session/session-telemetry-otel), [`session-title`](../packages/session/session-title), [`token-meter`](../packages/llm/token-meter), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workspace-context`](../packages/context/workspace-context) | | `session/flush` | `parallel` | [`packages/core/session/src/index.ts:85`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`session-persistence`](../packages/session/session-persistence), [`session-telemetry`](../packages/session/session-telemetry) | -| `settings/document-updated` | `emit` | [`packages/settings/settings/src/index.ts:170`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` | -| `settings/updated` | `emit` | [`packages/settings/settings/src/index.ts:157`](../packages/settings/settings/src/index.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) | +| `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:48`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `apiproxy` | +| `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:35`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) | | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:297`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - | -| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:165`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) | -| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:139`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:145`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | -| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:156`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) | +| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:166`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), `server`, [`subagent`](../packages/subagent/subagent) | +| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:140`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:146`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) | +| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:157`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) | | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`system-prompt`](../packages/core/system-prompt) | | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - | | `telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - | @@ -51,7 +51,7 @@ | `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:149`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`timeout-policy`](../packages/guard/timeout-policy) | | `tools/post-execute` | `waterfall` | [`packages/core/tools/src/index.ts:161`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`repeat-tool-guard`](../packages/guard/repeat-tool-guard), [`spill-policy`](../packages/spill/spill-policy), [`tool-fs-search`](../packages/fs/tool-fs-search) | | `tools/pre-execute` | `waterfall` | [`packages/core/tools/src/index.ts:138`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex), [`tool-tasks`](../packages/tasks/tool-tasks) | -| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:182`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`workspace-context`](../packages/context/workspace-context) | +| `tools/result` | `emit` | [`packages/core/tools/src/index.ts:183`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`events.dispatch`) | [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`workspace-context`](../packages/context/workspace-context) | | `workflow/agent-end` | `emit` | [`packages/workflow/workflow/src/index.ts:79`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) | | `workflow/agent-start` | `emit` | [`packages/workflow/workflow/src/index.ts:68`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) | | `workflow/end` | `emit` | [`packages/workflow/workflow/src/index.ts:89`](../packages/workflow/workflow/src/index.ts) | [`workflow`](../packages/workflow/workflow) (`events.dispatch`) | [`workflow`](../packages/workflow/workflow) | @@ -63,17 +63,14 @@ | Event string | Dispatchers | Listeners | | --- | --- | --- | -| `commands/changed` | `runtime` (`emit`) | `ui-command` | -| `connection/reset` | `runtime` (`emit`) | `runtime`, `ui-command`, `ui-models`, `ui-permission`, `ui-settings-general` | -| `credentials/changed` | `runtime` (`emit`) | `ui-models` | +| `connection/reset` | `runtime` (`emit`) | `ui-settings` | | `internal/dispatch` | - | [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`fs`](../packages/fs/fs), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission`](../packages/interaction/permission), [`plan-mode`](../packages/plan/plan-mode), [`pty-local`](../packages/pty/pty-local), `runtime`, [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`subagent`](../packages/subagent/subagent), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval), [`workflow`](../packages/workflow/workflow) | -| `internal/plugin` | - | `hmr`, `loader`, [`lsp-local`](../packages/lsp/lsp-local), `modules`, `webserver` | -| `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets)、`gateway` | +| `internal/plugin` | - | `loader`, [`lsp-local`](../packages/lsp/lsp-local), `webserver` | +| `internal/service` | - | [`agent-presets`](../packages/preset/agent-presets), `gateway` | | `internal/status` | - | [`agent`](../packages/core/agent) | | `locale/change` | `locale` (`emit`) | `locale` | -| `models/changed` | `runtime` (`emit`) | `ui-models` | -| `session/preset-changed` | `runtime` (`emit`) | `ui-command` | -| `settings/changed` | `runtime` (`emit`) | `runtime`, `ui-models`, `ui-permission`, `ui-settings-general` | +| `models/changed` | `runtime` (`emit`) | - | +| `session/preset-changed` | `runtime` (`emit`) | - | | `slash/input-begin-command` | - | `ui-conversation` | | `slash/input-consume-token` | - | `ui-conversation` | | `slash/input-insert-reference` | - | `ui-conversation` | diff --git a/docs/module-graph.i18n.yaml b/docs/module-graph.i18n.yaml index cfcaa0803d..ce44c570d7 100644 --- a/docs/module-graph.i18n.yaml +++ b/docs/module-graph.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/module-graph.md -module-graph.md: 719e8f4cc28f84086d1cfad5588f8cf78bcfcf0c -module-graph.zh.md: c17ad236b083ef08ab3927670a03c9e5df0c496d +module-graph.md: 69577adc46b93de85e997030fc12638511ec3b0c +module-graph.zh.md: e483b1e093989f81260d32a8588fae9e4f44dbd0 diff --git a/docs/module-graph.md b/docs/module-graph.md index 719e8f4cc2..69577adc46 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -340,9 +340,6 @@ flowchart TD pkg_client_hmr --> pkg_client_modules pkg_client_hmr --> pkg_host_webserver pkg_client_hmr --> pkg_invariants - pkg_client_runtime --> pkg_invariants - pkg_client_runtime --> pkg_type_meta - pkg_client_runtime --> pkg_typert_registry pkg_credentials --> pkg_brand pkg_credentials --> pkg_invariants pkg_subprocess_e2b --> pkg_e2b @@ -377,22 +374,6 @@ flowchart TD pkg_attachment_local --> pkg_attachment pkg_attachment_local --> pkg_invariants pkg_attachment_local --> pkg_paths - pkg_client_locale --> pkg_client_connection - pkg_client_locale --> pkg_client_runtime - pkg_client_locale --> pkg_client_ui_primitives - pkg_client_locale --> pkg_client_ui_slots - pkg_client_locale --> pkg_invariants - pkg_client_ui_models --> pkg_client_connection - pkg_client_ui_models --> pkg_client_runtime - pkg_client_ui_models --> pkg_client_schema_form - pkg_client_ui_models --> pkg_client_ui_primitives - pkg_client_ui_models --> pkg_client_ui_slots - pkg_client_ui_models --> pkg_client_web_react - pkg_client_ui_models --> pkg_invariants - pkg_client_ui_settings --> pkg_client_runtime - pkg_client_ui_settings --> pkg_client_ui_primitives - pkg_client_ui_settings --> pkg_client_ui_slots - pkg_client_ui_settings --> pkg_invariants pkg_credentials_local --> pkg_atomic_write pkg_credentials_local --> pkg_credentials pkg_credentials_local --> pkg_environment @@ -428,38 +409,10 @@ flowchart TD pkg_skill --> pkg_scope pkg_web --> pkg_invariants pkg_web --> pkg_llm - pkg_client_ui_question --> pkg_client_locale - pkg_client_ui_question --> pkg_invariants - pkg_client_ui_settings_general --> pkg_client_connection - pkg_client_ui_settings_general --> pkg_client_locale - pkg_client_ui_settings_general --> pkg_client_runtime - pkg_client_ui_settings_general --> pkg_client_ui_primitives - pkg_client_ui_settings_general --> pkg_client_ui_settings - pkg_client_ui_settings_general --> pkg_client_ui_slots - pkg_client_ui_settings_general --> pkg_client_web_react - pkg_client_ui_settings_general --> pkg_invariants - pkg_client_ui_sidebar --> pkg_client_locale - pkg_client_ui_sidebar --> pkg_client_runtime - pkg_client_ui_sidebar --> pkg_client_ui_primitives - pkg_client_ui_sidebar --> pkg_client_ui_slots - pkg_client_ui_sidebar --> pkg_invariants - pkg_client_ui_slash --> pkg_client_locale - pkg_client_ui_slash --> pkg_client_runtime - pkg_client_ui_slash --> pkg_client_ui_primitives - pkg_client_ui_slash --> pkg_client_ui_slots - pkg_client_ui_slash --> pkg_invariants - pkg_client_ui_theme --> pkg_client_connection - pkg_client_ui_theme --> pkg_client_locale - pkg_client_ui_theme --> pkg_client_runtime - pkg_client_ui_theme --> pkg_client_ui_primitives - pkg_client_ui_theme --> pkg_client_ui_slots - pkg_client_ui_theme --> pkg_host_webserver - pkg_client_ui_theme --> pkg_invariants - pkg_client_ui_workspace --> pkg_client_locale - pkg_client_ui_workspace --> pkg_client_runtime - pkg_client_ui_workspace --> pkg_client_ui_primitives - pkg_client_ui_workspace --> pkg_client_ui_slots - pkg_client_ui_workspace --> pkg_invariants + pkg_client_runtime --> pkg_api_gateway + pkg_client_runtime --> pkg_invariants + pkg_client_runtime --> pkg_type_meta + pkg_client_runtime --> pkg_typert_registry pkg_lsp --> pkg_brand pkg_lsp --> pkg_invariants pkg_lsp --> pkg_llm @@ -490,24 +443,10 @@ flowchart TD pkg_app_boot --> pkg_invariants pkg_app_boot --> pkg_paths pkg_app_boot --> pkg_system_prompt - pkg_client_ui_layout --> pkg_client_runtime - pkg_client_ui_layout --> pkg_client_ui_slots - pkg_client_ui_layout --> pkg_client_ui_theme - pkg_client_ui_layout --> pkg_invariants pkg_code_runtime_worker --> pkg_code_runtime pkg_code_runtime_worker --> pkg_invariants pkg_code_runtime_worker --> pkg_session pkg_code_runtime_worker --> pkg_timeout - pkg_host_directory_picker_browse --> pkg_client_locale - pkg_host_directory_picker_browse --> pkg_client_runtime - pkg_host_directory_picker_browse --> pkg_client_ui_primitives - pkg_host_directory_picker_browse --> pkg_client_ui_slots - pkg_host_directory_picker_browse --> pkg_client_ui_workspace - pkg_host_directory_picker_browse --> pkg_invariants - pkg_host_directory_picker_native --> pkg_client_runtime - pkg_host_directory_picker_native --> pkg_client_ui_slots - pkg_host_directory_picker_native --> pkg_client_ui_workspace - pkg_host_directory_picker_native --> pkg_invariants pkg_persona --> pkg_invariants pkg_persona --> pkg_system_prompt pkg_sandbox --> pkg_invariants @@ -566,10 +505,6 @@ flowchart TD pkg_message_feedback --> pkg_session_persistence pkg_message_feedback --> pkg_storage_domain pkg_message_feedback --> pkg_type_meta - pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse - pkg_host_directory_picker_auto --> pkg_host_directory_picker_native - pkg_host_directory_picker_auto --> pkg_host_webserver - pkg_host_directory_picker_auto --> pkg_invariants pkg_commands --> pkg_agent pkg_commands --> pkg_brand pkg_commands --> pkg_invariants @@ -690,10 +625,14 @@ flowchart TD pkg_acp --> pkg_session pkg_acp --> pkg_user_approval pkg_api_remotes --> pkg_agent + pkg_api_remotes --> pkg_api_gateway + pkg_api_remotes --> pkg_commands + pkg_api_remotes --> pkg_credentials pkg_api_remotes --> pkg_goal pkg_api_remotes --> pkg_invariants pkg_api_remotes --> pkg_session pkg_api_remotes --> pkg_session_persistence + pkg_api_remotes --> pkg_settings pkg_api_remotes --> pkg_typert_registry pkg_headless --> pkg_agent pkg_headless --> pkg_agent_default_model @@ -894,13 +833,22 @@ flowchart TD pkg_client_test_runtime --> pkg_client_web_react pkg_client_test_runtime --> pkg_host_apiproxy pkg_client_test_runtime --> pkg_invariants - pkg_client_ui_trajectory --> pkg_agent - pkg_client_ui_trajectory --> pkg_client_locale - pkg_client_ui_trajectory --> pkg_client_runtime - pkg_client_ui_trajectory --> pkg_client_ui_primitives - pkg_client_ui_trajectory --> pkg_compact - pkg_client_ui_trajectory --> pkg_invariants - pkg_client_ui_trajectory --> pkg_tools + pkg_client_ui_models --> pkg_api_remotes + pkg_client_ui_models --> pkg_client_connection + pkg_client_ui_models --> pkg_client_runtime + pkg_client_ui_models --> pkg_client_schema_form + pkg_client_ui_models --> pkg_client_ui_primitives + pkg_client_ui_models --> pkg_client_ui_slots + pkg_client_ui_models --> pkg_client_web_react + pkg_client_ui_models --> pkg_invariants + pkg_client_ui_settings --> pkg_api_gateway + pkg_client_ui_settings --> pkg_api_remotes + pkg_client_ui_settings --> pkg_client_connection + pkg_client_ui_settings --> pkg_client_runtime + pkg_client_ui_settings --> pkg_client_schema_form + pkg_client_ui_settings --> pkg_client_ui_slots + pkg_client_ui_settings --> pkg_invariants + pkg_client_ui_settings --> pkg_settings pkg_session_reference --> pkg_agent pkg_session_reference --> pkg_compact pkg_session_reference --> pkg_invariants @@ -1063,21 +1011,13 @@ flowchart TD pkg_web_app --> pkg_bash_env pkg_web_app --> pkg_invariants pkg_web_app --> pkg_system_prompt - pkg_client_ui_conversation --> pkg_agent - pkg_client_ui_conversation --> pkg_attachment - pkg_client_ui_conversation --> pkg_brand - pkg_client_ui_conversation --> pkg_client_connection - pkg_client_ui_conversation --> pkg_client_locale - pkg_client_ui_conversation --> pkg_client_runtime - pkg_client_ui_conversation --> pkg_client_ui_primitives - pkg_client_ui_conversation --> pkg_client_ui_slash - pkg_client_ui_conversation --> pkg_client_ui_slots - pkg_client_ui_conversation --> pkg_commands - pkg_client_ui_conversation --> pkg_compact - pkg_client_ui_conversation --> pkg_invariants - pkg_client_ui_conversation --> pkg_llm_retry - pkg_client_ui_conversation --> pkg_token_meter - pkg_client_ui_conversation --> pkg_tools + pkg_client_locale --> pkg_api_remotes + pkg_client_locale --> pkg_client_connection + pkg_client_locale --> pkg_client_runtime + pkg_client_locale --> pkg_client_ui_primitives + pkg_client_locale --> pkg_client_ui_settings + pkg_client_locale --> pkg_client_ui_slots + pkg_client_locale --> pkg_invariants pkg_sdk_protocol --> pkg_invariants pkg_sdk_protocol --> pkg_llm pkg_sdk_protocol --> pkg_session @@ -1120,6 +1060,132 @@ flowchart TD pkg_subagent_spawn --> pkg_invariants pkg_subagent_spawn --> pkg_subagent pkg_subagent_spawn --> pkg_subagent_inprocess + pkg_client_ui_question --> pkg_client_locale + pkg_client_ui_question --> pkg_invariants + pkg_client_ui_sidebar --> pkg_client_locale + pkg_client_ui_sidebar --> pkg_client_runtime + pkg_client_ui_sidebar --> pkg_client_ui_primitives + pkg_client_ui_sidebar --> pkg_client_ui_slots + pkg_client_ui_sidebar --> pkg_invariants + pkg_client_ui_slash --> pkg_client_locale + pkg_client_ui_slash --> pkg_client_runtime + pkg_client_ui_slash --> pkg_client_ui_primitives + pkg_client_ui_slash --> pkg_client_ui_slots + pkg_client_ui_slash --> pkg_invariants + pkg_client_ui_theme --> pkg_api_remotes + pkg_client_ui_theme --> pkg_client_connection + pkg_client_ui_theme --> pkg_client_locale + pkg_client_ui_theme --> pkg_client_runtime + pkg_client_ui_theme --> pkg_client_ui_primitives + pkg_client_ui_theme --> pkg_client_ui_settings + pkg_client_ui_theme --> pkg_client_ui_slots + pkg_client_ui_theme --> pkg_host_webserver + pkg_client_ui_theme --> pkg_invariants + pkg_client_ui_trajectory --> pkg_agent + pkg_client_ui_trajectory --> pkg_client_locale + pkg_client_ui_trajectory --> pkg_client_runtime + pkg_client_ui_trajectory --> pkg_client_ui_primitives + pkg_client_ui_trajectory --> pkg_compact + pkg_client_ui_trajectory --> pkg_invariants + pkg_client_ui_trajectory --> pkg_tools + pkg_client_ui_workspace --> pkg_client_locale + pkg_client_ui_workspace --> pkg_client_runtime + pkg_client_ui_workspace --> pkg_client_ui_primitives + pkg_client_ui_workspace --> pkg_client_ui_slots + pkg_client_ui_workspace --> pkg_invariants + pkg_agent_spine_demo --> pkg_agent + pkg_agent_spine_demo --> pkg_agent_loop + pkg_agent_spine_demo --> pkg_bash_env + pkg_agent_spine_demo --> pkg_goal + pkg_agent_spine_demo --> pkg_goal_session + pkg_agent_spine_demo --> pkg_invariants + pkg_agent_spine_demo --> pkg_llm + pkg_agent_spine_demo --> pkg_llm_retry + pkg_agent_spine_demo --> pkg_paths + pkg_agent_spine_demo --> pkg_scope + pkg_agent_spine_demo --> pkg_session + pkg_agent_spine_demo --> pkg_session_title + pkg_agent_spine_demo --> pkg_skill + pkg_agent_spine_demo --> pkg_skill_local + pkg_agent_spine_demo --> pkg_system_prompt + pkg_agent_spine_demo --> pkg_tasks_local + pkg_agent_spine_demo --> pkg_tool_bash + pkg_agent_spine_demo --> pkg_tool_goal + pkg_agent_spine_demo --> pkg_tool_skill + pkg_agent_spine_demo --> pkg_tool_tasks + pkg_agent_spine_demo --> pkg_tools + pkg_agent_spine_demo --> pkg_workspace_context + pkg_jsonrpc --> pkg_agent + pkg_jsonrpc --> pkg_invariants + pkg_jsonrpc --> pkg_llm + pkg_jsonrpc --> pkg_llm_deepseek + pkg_jsonrpc --> pkg_scope + pkg_jsonrpc --> pkg_sdk_protocol + pkg_jsonrpc --> pkg_session + pkg_jsonrpc --> pkg_subagent + pkg_sdk_client --> pkg_invariants + pkg_sdk_client --> pkg_llm + pkg_sdk_client --> pkg_sdk_protocol + pkg_sdk_client --> pkg_session + pkg_subagent_dsh_sdk --> pkg_agent + pkg_subagent_dsh_sdk --> pkg_invariants + pkg_subagent_dsh_sdk --> pkg_llm + pkg_subagent_dsh_sdk --> pkg_sdk_client + pkg_subagent_dsh_sdk --> pkg_session + pkg_subagent_dsh_sdk --> pkg_subagent + pkg_subagent_dsh_sdk --> pkg_subprocess + pkg_client_ui_conversation --> pkg_agent + pkg_client_ui_conversation --> pkg_api_remotes + pkg_client_ui_conversation --> pkg_attachment + pkg_client_ui_conversation --> pkg_brand + pkg_client_ui_conversation --> pkg_client_connection + pkg_client_ui_conversation --> pkg_client_locale + pkg_client_ui_conversation --> pkg_client_runtime + pkg_client_ui_conversation --> pkg_client_ui_primitives + pkg_client_ui_conversation --> pkg_client_ui_settings + pkg_client_ui_conversation --> pkg_client_ui_slash + pkg_client_ui_conversation --> pkg_client_ui_slots + pkg_client_ui_conversation --> pkg_commands + pkg_client_ui_conversation --> pkg_compact + pkg_client_ui_conversation --> pkg_invariants + pkg_client_ui_conversation --> pkg_llm_retry + pkg_client_ui_conversation --> pkg_token_meter + pkg_client_ui_conversation --> pkg_tools + pkg_client_ui_layout --> pkg_client_runtime + pkg_client_ui_layout --> pkg_client_ui_slots + pkg_client_ui_layout --> pkg_client_ui_theme + pkg_client_ui_layout --> pkg_invariants + pkg_client_ui_settings_general --> pkg_api_remotes + pkg_client_ui_settings_general --> pkg_client_connection + pkg_client_ui_settings_general --> pkg_client_locale + pkg_client_ui_settings_general --> pkg_client_runtime + pkg_client_ui_settings_general --> pkg_client_ui_primitives + pkg_client_ui_settings_general --> pkg_client_ui_settings + pkg_client_ui_settings_general --> pkg_client_ui_sidebar + pkg_client_ui_settings_general --> pkg_client_ui_slots + pkg_client_ui_settings_general --> pkg_client_web_react + pkg_client_ui_settings_general --> pkg_invariants + pkg_acp_demo --> pkg_acp + pkg_acp_demo --> pkg_agent_spine_demo + pkg_acp_demo --> pkg_app_boot + pkg_acp_demo --> pkg_invariants + pkg_acp_demo --> pkg_session_checkpoint_policy + pkg_acp_demo --> pkg_session_persistence_jsonl + pkg_acp_demo --> pkg_session_query + pkg_acp_demo --> pkg_session_query_sqlite + pkg_acp_demo --> pkg_tools + pkg_acp_demo --> pkg_workspace_context + pkg_host_directory_picker_browse --> pkg_client_locale + pkg_host_directory_picker_browse --> pkg_client_runtime + pkg_host_directory_picker_browse --> pkg_client_ui_primitives + pkg_host_directory_picker_browse --> pkg_client_ui_slots + pkg_host_directory_picker_browse --> pkg_client_ui_workspace + pkg_host_directory_picker_browse --> pkg_invariants + pkg_host_directory_picker_native --> pkg_client_runtime + pkg_host_directory_picker_native --> pkg_client_ui_slots + pkg_host_directory_picker_native --> pkg_client_ui_workspace + pkg_host_directory_picker_native --> pkg_invariants + pkg_client_ui_agent_preset --> pkg_api_remotes pkg_client_ui_agent_preset --> pkg_client_connection pkg_client_ui_agent_preset --> pkg_client_locale pkg_client_ui_agent_preset --> pkg_client_runtime @@ -1129,6 +1195,7 @@ flowchart TD pkg_client_ui_agent_preset --> pkg_client_ui_slots pkg_client_ui_agent_preset --> pkg_client_web_react pkg_client_ui_agent_preset --> pkg_invariants + pkg_client_ui_command --> pkg_api_remotes pkg_client_ui_command --> pkg_client_connection pkg_client_ui_command --> pkg_client_locale pkg_client_ui_command --> pkg_client_runtime @@ -1189,47 +1256,10 @@ flowchart TD pkg_client_ui_workflow_run --> pkg_session pkg_client_ui_workflow_run --> pkg_tool_workflow pkg_client_ui_workflow_run --> pkg_workflow - pkg_agent_spine_demo --> pkg_agent - pkg_agent_spine_demo --> pkg_agent_loop - pkg_agent_spine_demo --> pkg_bash_env - pkg_agent_spine_demo --> pkg_goal - pkg_agent_spine_demo --> pkg_goal_session - pkg_agent_spine_demo --> pkg_invariants - pkg_agent_spine_demo --> pkg_llm - pkg_agent_spine_demo --> pkg_llm_retry - pkg_agent_spine_demo --> pkg_paths - pkg_agent_spine_demo --> pkg_scope - pkg_agent_spine_demo --> pkg_session - pkg_agent_spine_demo --> pkg_session_title - pkg_agent_spine_demo --> pkg_skill - pkg_agent_spine_demo --> pkg_skill_local - pkg_agent_spine_demo --> pkg_system_prompt - pkg_agent_spine_demo --> pkg_tasks_local - pkg_agent_spine_demo --> pkg_tool_bash - pkg_agent_spine_demo --> pkg_tool_goal - pkg_agent_spine_demo --> pkg_tool_skill - pkg_agent_spine_demo --> pkg_tool_tasks - pkg_agent_spine_demo --> pkg_tools - pkg_agent_spine_demo --> pkg_workspace_context - pkg_jsonrpc --> pkg_agent - pkg_jsonrpc --> pkg_invariants - pkg_jsonrpc --> pkg_llm - pkg_jsonrpc --> pkg_llm_deepseek - pkg_jsonrpc --> pkg_scope - pkg_jsonrpc --> pkg_sdk_protocol - pkg_jsonrpc --> pkg_session - pkg_jsonrpc --> pkg_subagent - pkg_sdk_client --> pkg_invariants - pkg_sdk_client --> pkg_llm - pkg_sdk_client --> pkg_sdk_protocol - pkg_sdk_client --> pkg_session - pkg_subagent_dsh_sdk --> pkg_agent - pkg_subagent_dsh_sdk --> pkg_invariants - pkg_subagent_dsh_sdk --> pkg_llm - pkg_subagent_dsh_sdk --> pkg_sdk_client - pkg_subagent_dsh_sdk --> pkg_session - pkg_subagent_dsh_sdk --> pkg_subagent - pkg_subagent_dsh_sdk --> pkg_subprocess + pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse + pkg_host_directory_picker_auto --> pkg_host_directory_picker_native + pkg_host_directory_picker_auto --> pkg_host_webserver + pkg_host_directory_picker_auto --> pkg_invariants pkg_client_ui_model --> pkg_client_connection pkg_client_ui_model --> pkg_client_locale pkg_client_ui_model --> pkg_client_runtime @@ -1239,12 +1269,14 @@ flowchart TD pkg_client_ui_model --> pkg_client_ui_slash pkg_client_ui_model --> pkg_client_ui_slots pkg_client_ui_model --> pkg_invariants + pkg_client_ui_permission --> pkg_api_remotes pkg_client_ui_permission --> pkg_client_connection pkg_client_ui_permission --> pkg_client_locale pkg_client_ui_permission --> pkg_client_runtime pkg_client_ui_permission --> pkg_client_schema_form pkg_client_ui_permission --> pkg_client_ui_command pkg_client_ui_permission --> pkg_client_ui_primitives + pkg_client_ui_permission --> pkg_client_ui_settings pkg_client_ui_permission --> pkg_client_ui_slash pkg_client_ui_permission --> pkg_client_ui_slots pkg_client_ui_permission --> pkg_invariants @@ -1257,16 +1289,6 @@ flowchart TD pkg_client_ui_skill --> pkg_client_ui_slots pkg_client_ui_skill --> pkg_client_ui_tool pkg_client_ui_skill --> pkg_invariants - pkg_acp_demo --> pkg_acp - pkg_acp_demo --> pkg_agent_spine_demo - pkg_acp_demo --> pkg_app_boot - pkg_acp_demo --> pkg_invariants - pkg_acp_demo --> pkg_session_checkpoint_policy - pkg_acp_demo --> pkg_session_persistence_jsonl - pkg_acp_demo --> pkg_session_query - pkg_acp_demo --> pkg_session_query_sqlite - pkg_acp_demo --> pkg_tools - pkg_acp_demo --> pkg_workspace_context ``` | Package | Group | Depends on | @@ -1303,7 +1325,6 @@ flowchart TD | [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) | | [`client-connection`](../packages/client/connection) | `client` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | | [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | -| [`client-runtime`](../packages/client/runtime) | `client` | [`invariants`](../packages/support/invariants), [`type-meta`](../packages/typert/type-meta), [`typert-registry`](../packages/typert/registry) | | [`credentials`](../packages/credentials/credentials) | `credentials` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) | | [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`frontend-static`](../packages/host/frontend-static) | `host` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | @@ -1317,9 +1338,6 @@ flowchart TD | [`llm`](../packages/llm/llm) | `llm` | [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout) | | [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/support/invariants), [`typert-registry`](../packages/typert/registry) | | [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) | -| [`client-locale`](../packages/client/locale) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | -| [`client-ui-models`](../packages/client/ui-models) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | -| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) | | [`settings-local`](../packages/settings/settings-local) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`settings`](../packages/settings/settings) | | [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) | @@ -1328,12 +1346,7 @@ flowchart TD | [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | | [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | | [`web`](../packages/web/web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | -| [`client-ui-question`](../packages/client/ui-question) | `client` | [`client-locale`](../packages/client/locale), [`invariants`](../packages/support/invariants) | -| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | -| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | -| [`client-ui-slash`](../packages/client/ui-slash) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | -| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | -| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | +| [`client-runtime`](../packages/client/runtime) | `client` | [`api-gateway`](../packages/api/gateway), [`invariants`](../packages/support/invariants), [`type-meta`](../packages/typert/type-meta), [`typert-registry`](../packages/typert/registry) | | [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | | [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`type-meta`](../packages/typert/type-meta) | | [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`invariants`](../packages/support/invariants), [`skill`](../packages/skill/skill) | @@ -1343,10 +1356,7 @@ flowchart TD | [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`app-boot`](../packages/boot/app-boot) | `boot` | [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`system-prompt`](../packages/core/system-prompt) | -| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) | | [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | -| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | -| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | | [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) | | [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | @@ -1361,7 +1371,6 @@ flowchart TD | [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`type-meta`](../packages/typert/type-meta) | -| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) | | [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | | [`user-interaction`](../packages/interaction/user-interaction) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | @@ -1389,7 +1398,7 @@ flowchart TD | [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-title`](../packages/session/session-title) | | [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-approval`](../packages/interaction/user-approval) | -| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`typert-registry`](../packages/typert/registry) | +| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) | | [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) | @@ -1424,7 +1433,8 @@ flowchart TD | [`agent-loop-testkit`](../packages/support/agent-loop-testkit) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`llm-replay`](../packages/support/llm-replay) | `support` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) | -| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) | +| [`client-ui-models`](../packages/client/ui-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | +| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-gateway`](../packages/api/gateway), [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`settings`](../packages/settings/settings) | | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) | | [`workspace-context`](../packages/context/workspace-context) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | | [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) | @@ -1452,7 +1462,7 @@ flowchart TD | [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | | [`web-app`](../packages/bundle/web-app) | `bundle` | [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) | -| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) | +| [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | @@ -1460,8 +1470,24 @@ flowchart TD | [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`subagent-fork`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | | [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | -| [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | -| [`client-ui-command`](../packages/client/ui-command) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | +| [`client-ui-question`](../packages/client/ui-question) | `client` | [`client-locale`](../packages/client/locale), [`invariants`](../packages/support/invariants) | +| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | +| [`client-ui-slash`](../packages/client/ui-slash) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | +| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | +| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) | +| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | +| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`bash-env`](../packages/bash/bash-env), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks-local`](../packages/tasks/tasks-local), [`tool-bash`](../packages/bash/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | +| [`jsonrpc`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | +| [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) | +| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) | +| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) | +| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) | +| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | +| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/support/invariants), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | +| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | +| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | +| [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | +| [`client-ui-command`](../packages/client/ui-command) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-deliverables`](../packages/client/ui-deliverables) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) | | [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`plan-mode`](../packages/plan/plan-mode) | @@ -1469,11 +1495,7 @@ flowchart TD | [`client-ui-task`](../packages/client/ui-task) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-workflow-run`](../packages/client/ui-workflow-run) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) | -| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`bash-env`](../packages/bash/bash-env), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks-local`](../packages/tasks/tasks-local), [`tool-bash`](../packages/bash/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | -| [`jsonrpc`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | -| [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) | -| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) | +| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | | [`client-ui-model`](../packages/client/ui-model) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-command`](../packages/client/ui-command), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | -| [`client-ui-permission`](../packages/client/ui-permission) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-command`](../packages/client/ui-command), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`permission`](../packages/interaction/permission) | +| [`client-ui-permission`](../packages/client/ui-permission) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-command`](../packages/client/ui-command), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`permission`](../packages/interaction/permission) | | [`client-ui-skill`](../packages/client/ui-skill) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-tool`](../packages/client/ui-tool), [`invariants`](../packages/support/invariants) | -| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/support/invariants), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | diff --git a/docs/module-graph.zh.md b/docs/module-graph.zh.md index c17ad236b0..e483b1e093 100644 --- a/docs/module-graph.zh.md +++ b/docs/module-graph.zh.md @@ -342,9 +342,6 @@ flowchart TD pkg_client_hmr --> pkg_client_modules pkg_client_hmr --> pkg_host_webserver pkg_client_hmr --> pkg_invariants - pkg_client_runtime --> pkg_invariants - pkg_client_runtime --> pkg_type_meta - pkg_client_runtime --> pkg_typert_registry pkg_credentials --> pkg_brand pkg_credentials --> pkg_invariants pkg_subprocess_e2b --> pkg_e2b @@ -379,22 +376,6 @@ flowchart TD pkg_attachment_local --> pkg_attachment pkg_attachment_local --> pkg_invariants pkg_attachment_local --> pkg_paths - pkg_client_locale --> pkg_client_connection - pkg_client_locale --> pkg_client_runtime - pkg_client_locale --> pkg_client_ui_primitives - pkg_client_locale --> pkg_client_ui_slots - pkg_client_locale --> pkg_invariants - pkg_client_ui_models --> pkg_client_connection - pkg_client_ui_models --> pkg_client_runtime - pkg_client_ui_models --> pkg_client_schema_form - pkg_client_ui_models --> pkg_client_ui_primitives - pkg_client_ui_models --> pkg_client_ui_slots - pkg_client_ui_models --> pkg_client_web_react - pkg_client_ui_models --> pkg_invariants - pkg_client_ui_settings --> pkg_client_runtime - pkg_client_ui_settings --> pkg_client_ui_primitives - pkg_client_ui_settings --> pkg_client_ui_slots - pkg_client_ui_settings --> pkg_invariants pkg_credentials_local --> pkg_atomic_write pkg_credentials_local --> pkg_credentials pkg_credentials_local --> pkg_environment @@ -430,38 +411,10 @@ flowchart TD pkg_skill --> pkg_scope pkg_web --> pkg_invariants pkg_web --> pkg_llm - pkg_client_ui_question --> pkg_client_locale - pkg_client_ui_question --> pkg_invariants - pkg_client_ui_settings_general --> pkg_client_connection - pkg_client_ui_settings_general --> pkg_client_locale - pkg_client_ui_settings_general --> pkg_client_runtime - pkg_client_ui_settings_general --> pkg_client_ui_primitives - pkg_client_ui_settings_general --> pkg_client_ui_settings - pkg_client_ui_settings_general --> pkg_client_ui_slots - pkg_client_ui_settings_general --> pkg_client_web_react - pkg_client_ui_settings_general --> pkg_invariants - pkg_client_ui_sidebar --> pkg_client_locale - pkg_client_ui_sidebar --> pkg_client_runtime - pkg_client_ui_sidebar --> pkg_client_ui_primitives - pkg_client_ui_sidebar --> pkg_client_ui_slots - pkg_client_ui_sidebar --> pkg_invariants - pkg_client_ui_slash --> pkg_client_locale - pkg_client_ui_slash --> pkg_client_runtime - pkg_client_ui_slash --> pkg_client_ui_primitives - pkg_client_ui_slash --> pkg_client_ui_slots - pkg_client_ui_slash --> pkg_invariants - pkg_client_ui_theme --> pkg_client_connection - pkg_client_ui_theme --> pkg_client_locale - pkg_client_ui_theme --> pkg_client_runtime - pkg_client_ui_theme --> pkg_client_ui_primitives - pkg_client_ui_theme --> pkg_client_ui_slots - pkg_client_ui_theme --> pkg_host_webserver - pkg_client_ui_theme --> pkg_invariants - pkg_client_ui_workspace --> pkg_client_locale - pkg_client_ui_workspace --> pkg_client_runtime - pkg_client_ui_workspace --> pkg_client_ui_primitives - pkg_client_ui_workspace --> pkg_client_ui_slots - pkg_client_ui_workspace --> pkg_invariants + pkg_client_runtime --> pkg_api_gateway + pkg_client_runtime --> pkg_invariants + pkg_client_runtime --> pkg_type_meta + pkg_client_runtime --> pkg_typert_registry pkg_lsp --> pkg_brand pkg_lsp --> pkg_invariants pkg_lsp --> pkg_llm @@ -492,24 +445,10 @@ flowchart TD pkg_app_boot --> pkg_invariants pkg_app_boot --> pkg_paths pkg_app_boot --> pkg_system_prompt - pkg_client_ui_layout --> pkg_client_runtime - pkg_client_ui_layout --> pkg_client_ui_slots - pkg_client_ui_layout --> pkg_client_ui_theme - pkg_client_ui_layout --> pkg_invariants pkg_code_runtime_worker --> pkg_code_runtime pkg_code_runtime_worker --> pkg_invariants pkg_code_runtime_worker --> pkg_session pkg_code_runtime_worker --> pkg_timeout - pkg_host_directory_picker_browse --> pkg_client_locale - pkg_host_directory_picker_browse --> pkg_client_runtime - pkg_host_directory_picker_browse --> pkg_client_ui_primitives - pkg_host_directory_picker_browse --> pkg_client_ui_slots - pkg_host_directory_picker_browse --> pkg_client_ui_workspace - pkg_host_directory_picker_browse --> pkg_invariants - pkg_host_directory_picker_native --> pkg_client_runtime - pkg_host_directory_picker_native --> pkg_client_ui_slots - pkg_host_directory_picker_native --> pkg_client_ui_workspace - pkg_host_directory_picker_native --> pkg_invariants pkg_persona --> pkg_invariants pkg_persona --> pkg_system_prompt pkg_sandbox --> pkg_invariants @@ -568,10 +507,6 @@ flowchart TD pkg_message_feedback --> pkg_session_persistence pkg_message_feedback --> pkg_storage_domain pkg_message_feedback --> pkg_type_meta - pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse - pkg_host_directory_picker_auto --> pkg_host_directory_picker_native - pkg_host_directory_picker_auto --> pkg_host_webserver - pkg_host_directory_picker_auto --> pkg_invariants pkg_commands --> pkg_agent pkg_commands --> pkg_brand pkg_commands --> pkg_invariants @@ -692,10 +627,14 @@ flowchart TD pkg_acp --> pkg_session pkg_acp --> pkg_user_approval pkg_api_remotes --> pkg_agent + pkg_api_remotes --> pkg_api_gateway + pkg_api_remotes --> pkg_commands + pkg_api_remotes --> pkg_credentials pkg_api_remotes --> pkg_goal pkg_api_remotes --> pkg_invariants pkg_api_remotes --> pkg_session pkg_api_remotes --> pkg_session_persistence + pkg_api_remotes --> pkg_settings pkg_api_remotes --> pkg_typert_registry pkg_headless --> pkg_agent pkg_headless --> pkg_agent_default_model @@ -896,13 +835,22 @@ flowchart TD pkg_client_test_runtime --> pkg_client_web_react pkg_client_test_runtime --> pkg_host_apiproxy pkg_client_test_runtime --> pkg_invariants - pkg_client_ui_trajectory --> pkg_agent - pkg_client_ui_trajectory --> pkg_client_locale - pkg_client_ui_trajectory --> pkg_client_runtime - pkg_client_ui_trajectory --> pkg_client_ui_primitives - pkg_client_ui_trajectory --> pkg_compact - pkg_client_ui_trajectory --> pkg_invariants - pkg_client_ui_trajectory --> pkg_tools + pkg_client_ui_models --> pkg_api_remotes + pkg_client_ui_models --> pkg_client_connection + pkg_client_ui_models --> pkg_client_runtime + pkg_client_ui_models --> pkg_client_schema_form + pkg_client_ui_models --> pkg_client_ui_primitives + pkg_client_ui_models --> pkg_client_ui_slots + pkg_client_ui_models --> pkg_client_web_react + pkg_client_ui_models --> pkg_invariants + pkg_client_ui_settings --> pkg_api_gateway + pkg_client_ui_settings --> pkg_api_remotes + pkg_client_ui_settings --> pkg_client_connection + pkg_client_ui_settings --> pkg_client_runtime + pkg_client_ui_settings --> pkg_client_schema_form + pkg_client_ui_settings --> pkg_client_ui_slots + pkg_client_ui_settings --> pkg_invariants + pkg_client_ui_settings --> pkg_settings pkg_session_reference --> pkg_agent pkg_session_reference --> pkg_compact pkg_session_reference --> pkg_invariants @@ -1065,21 +1013,13 @@ flowchart TD pkg_web_app --> pkg_bash_env pkg_web_app --> pkg_invariants pkg_web_app --> pkg_system_prompt - pkg_client_ui_conversation --> pkg_agent - pkg_client_ui_conversation --> pkg_attachment - pkg_client_ui_conversation --> pkg_brand - pkg_client_ui_conversation --> pkg_client_connection - pkg_client_ui_conversation --> pkg_client_locale - pkg_client_ui_conversation --> pkg_client_runtime - pkg_client_ui_conversation --> pkg_client_ui_primitives - pkg_client_ui_conversation --> pkg_client_ui_slash - pkg_client_ui_conversation --> pkg_client_ui_slots - pkg_client_ui_conversation --> pkg_commands - pkg_client_ui_conversation --> pkg_compact - pkg_client_ui_conversation --> pkg_invariants - pkg_client_ui_conversation --> pkg_llm_retry - pkg_client_ui_conversation --> pkg_token_meter - pkg_client_ui_conversation --> pkg_tools + pkg_client_locale --> pkg_api_remotes + pkg_client_locale --> pkg_client_connection + pkg_client_locale --> pkg_client_runtime + pkg_client_locale --> pkg_client_ui_primitives + pkg_client_locale --> pkg_client_ui_settings + pkg_client_locale --> pkg_client_ui_slots + pkg_client_locale --> pkg_invariants pkg_sdk_protocol --> pkg_invariants pkg_sdk_protocol --> pkg_llm pkg_sdk_protocol --> pkg_session @@ -1122,6 +1062,132 @@ flowchart TD pkg_subagent_spawn --> pkg_invariants pkg_subagent_spawn --> pkg_subagent pkg_subagent_spawn --> pkg_subagent_inprocess + pkg_client_ui_question --> pkg_client_locale + pkg_client_ui_question --> pkg_invariants + pkg_client_ui_sidebar --> pkg_client_locale + pkg_client_ui_sidebar --> pkg_client_runtime + pkg_client_ui_sidebar --> pkg_client_ui_primitives + pkg_client_ui_sidebar --> pkg_client_ui_slots + pkg_client_ui_sidebar --> pkg_invariants + pkg_client_ui_slash --> pkg_client_locale + pkg_client_ui_slash --> pkg_client_runtime + pkg_client_ui_slash --> pkg_client_ui_primitives + pkg_client_ui_slash --> pkg_client_ui_slots + pkg_client_ui_slash --> pkg_invariants + pkg_client_ui_theme --> pkg_api_remotes + pkg_client_ui_theme --> pkg_client_connection + pkg_client_ui_theme --> pkg_client_locale + pkg_client_ui_theme --> pkg_client_runtime + pkg_client_ui_theme --> pkg_client_ui_primitives + pkg_client_ui_theme --> pkg_client_ui_settings + pkg_client_ui_theme --> pkg_client_ui_slots + pkg_client_ui_theme --> pkg_host_webserver + pkg_client_ui_theme --> pkg_invariants + pkg_client_ui_trajectory --> pkg_agent + pkg_client_ui_trajectory --> pkg_client_locale + pkg_client_ui_trajectory --> pkg_client_runtime + pkg_client_ui_trajectory --> pkg_client_ui_primitives + pkg_client_ui_trajectory --> pkg_compact + pkg_client_ui_trajectory --> pkg_invariants + pkg_client_ui_trajectory --> pkg_tools + pkg_client_ui_workspace --> pkg_client_locale + pkg_client_ui_workspace --> pkg_client_runtime + pkg_client_ui_workspace --> pkg_client_ui_primitives + pkg_client_ui_workspace --> pkg_client_ui_slots + pkg_client_ui_workspace --> pkg_invariants + pkg_agent_spine_demo --> pkg_agent + pkg_agent_spine_demo --> pkg_agent_loop + pkg_agent_spine_demo --> pkg_bash_env + pkg_agent_spine_demo --> pkg_goal + pkg_agent_spine_demo --> pkg_goal_session + pkg_agent_spine_demo --> pkg_invariants + pkg_agent_spine_demo --> pkg_llm + pkg_agent_spine_demo --> pkg_llm_retry + pkg_agent_spine_demo --> pkg_paths + pkg_agent_spine_demo --> pkg_scope + pkg_agent_spine_demo --> pkg_session + pkg_agent_spine_demo --> pkg_session_title + pkg_agent_spine_demo --> pkg_skill + pkg_agent_spine_demo --> pkg_skill_local + pkg_agent_spine_demo --> pkg_system_prompt + pkg_agent_spine_demo --> pkg_tasks_local + pkg_agent_spine_demo --> pkg_tool_bash + pkg_agent_spine_demo --> pkg_tool_goal + pkg_agent_spine_demo --> pkg_tool_skill + pkg_agent_spine_demo --> pkg_tool_tasks + pkg_agent_spine_demo --> pkg_tools + pkg_agent_spine_demo --> pkg_workspace_context + pkg_jsonrpc --> pkg_agent + pkg_jsonrpc --> pkg_invariants + pkg_jsonrpc --> pkg_llm + pkg_jsonrpc --> pkg_llm_deepseek + pkg_jsonrpc --> pkg_scope + pkg_jsonrpc --> pkg_sdk_protocol + pkg_jsonrpc --> pkg_session + pkg_jsonrpc --> pkg_subagent + pkg_sdk_client --> pkg_invariants + pkg_sdk_client --> pkg_llm + pkg_sdk_client --> pkg_sdk_protocol + pkg_sdk_client --> pkg_session + pkg_subagent_dsh_sdk --> pkg_agent + pkg_subagent_dsh_sdk --> pkg_invariants + pkg_subagent_dsh_sdk --> pkg_llm + pkg_subagent_dsh_sdk --> pkg_sdk_client + pkg_subagent_dsh_sdk --> pkg_session + pkg_subagent_dsh_sdk --> pkg_subagent + pkg_subagent_dsh_sdk --> pkg_subprocess + pkg_client_ui_conversation --> pkg_agent + pkg_client_ui_conversation --> pkg_api_remotes + pkg_client_ui_conversation --> pkg_attachment + pkg_client_ui_conversation --> pkg_brand + pkg_client_ui_conversation --> pkg_client_connection + pkg_client_ui_conversation --> pkg_client_locale + pkg_client_ui_conversation --> pkg_client_runtime + pkg_client_ui_conversation --> pkg_client_ui_primitives + pkg_client_ui_conversation --> pkg_client_ui_settings + pkg_client_ui_conversation --> pkg_client_ui_slash + pkg_client_ui_conversation --> pkg_client_ui_slots + pkg_client_ui_conversation --> pkg_commands + pkg_client_ui_conversation --> pkg_compact + pkg_client_ui_conversation --> pkg_invariants + pkg_client_ui_conversation --> pkg_llm_retry + pkg_client_ui_conversation --> pkg_token_meter + pkg_client_ui_conversation --> pkg_tools + pkg_client_ui_layout --> pkg_client_runtime + pkg_client_ui_layout --> pkg_client_ui_slots + pkg_client_ui_layout --> pkg_client_ui_theme + pkg_client_ui_layout --> pkg_invariants + pkg_client_ui_settings_general --> pkg_api_remotes + pkg_client_ui_settings_general --> pkg_client_connection + pkg_client_ui_settings_general --> pkg_client_locale + pkg_client_ui_settings_general --> pkg_client_runtime + pkg_client_ui_settings_general --> pkg_client_ui_primitives + pkg_client_ui_settings_general --> pkg_client_ui_settings + pkg_client_ui_settings_general --> pkg_client_ui_sidebar + pkg_client_ui_settings_general --> pkg_client_ui_slots + pkg_client_ui_settings_general --> pkg_client_web_react + pkg_client_ui_settings_general --> pkg_invariants + pkg_acp_demo --> pkg_acp + pkg_acp_demo --> pkg_agent_spine_demo + pkg_acp_demo --> pkg_app_boot + pkg_acp_demo --> pkg_invariants + pkg_acp_demo --> pkg_session_checkpoint_policy + pkg_acp_demo --> pkg_session_persistence_jsonl + pkg_acp_demo --> pkg_session_query + pkg_acp_demo --> pkg_session_query_sqlite + pkg_acp_demo --> pkg_tools + pkg_acp_demo --> pkg_workspace_context + pkg_host_directory_picker_browse --> pkg_client_locale + pkg_host_directory_picker_browse --> pkg_client_runtime + pkg_host_directory_picker_browse --> pkg_client_ui_primitives + pkg_host_directory_picker_browse --> pkg_client_ui_slots + pkg_host_directory_picker_browse --> pkg_client_ui_workspace + pkg_host_directory_picker_browse --> pkg_invariants + pkg_host_directory_picker_native --> pkg_client_runtime + pkg_host_directory_picker_native --> pkg_client_ui_slots + pkg_host_directory_picker_native --> pkg_client_ui_workspace + pkg_host_directory_picker_native --> pkg_invariants + pkg_client_ui_agent_preset --> pkg_api_remotes pkg_client_ui_agent_preset --> pkg_client_connection pkg_client_ui_agent_preset --> pkg_client_locale pkg_client_ui_agent_preset --> pkg_client_runtime @@ -1131,6 +1197,7 @@ flowchart TD pkg_client_ui_agent_preset --> pkg_client_ui_slots pkg_client_ui_agent_preset --> pkg_client_web_react pkg_client_ui_agent_preset --> pkg_invariants + pkg_client_ui_command --> pkg_api_remotes pkg_client_ui_command --> pkg_client_connection pkg_client_ui_command --> pkg_client_locale pkg_client_ui_command --> pkg_client_runtime @@ -1191,47 +1258,10 @@ flowchart TD pkg_client_ui_workflow_run --> pkg_session pkg_client_ui_workflow_run --> pkg_tool_workflow pkg_client_ui_workflow_run --> pkg_workflow - pkg_agent_spine_demo --> pkg_agent - pkg_agent_spine_demo --> pkg_agent_loop - pkg_agent_spine_demo --> pkg_bash_env - pkg_agent_spine_demo --> pkg_goal - pkg_agent_spine_demo --> pkg_goal_session - pkg_agent_spine_demo --> pkg_invariants - pkg_agent_spine_demo --> pkg_llm - pkg_agent_spine_demo --> pkg_llm_retry - pkg_agent_spine_demo --> pkg_paths - pkg_agent_spine_demo --> pkg_scope - pkg_agent_spine_demo --> pkg_session - pkg_agent_spine_demo --> pkg_session_title - pkg_agent_spine_demo --> pkg_skill - pkg_agent_spine_demo --> pkg_skill_local - pkg_agent_spine_demo --> pkg_system_prompt - pkg_agent_spine_demo --> pkg_tasks_local - pkg_agent_spine_demo --> pkg_tool_bash - pkg_agent_spine_demo --> pkg_tool_goal - pkg_agent_spine_demo --> pkg_tool_skill - pkg_agent_spine_demo --> pkg_tool_tasks - pkg_agent_spine_demo --> pkg_tools - pkg_agent_spine_demo --> pkg_workspace_context - pkg_jsonrpc --> pkg_agent - pkg_jsonrpc --> pkg_invariants - pkg_jsonrpc --> pkg_llm - pkg_jsonrpc --> pkg_llm_deepseek - pkg_jsonrpc --> pkg_scope - pkg_jsonrpc --> pkg_sdk_protocol - pkg_jsonrpc --> pkg_session - pkg_jsonrpc --> pkg_subagent - pkg_sdk_client --> pkg_invariants - pkg_sdk_client --> pkg_llm - pkg_sdk_client --> pkg_sdk_protocol - pkg_sdk_client --> pkg_session - pkg_subagent_dsh_sdk --> pkg_agent - pkg_subagent_dsh_sdk --> pkg_invariants - pkg_subagent_dsh_sdk --> pkg_llm - pkg_subagent_dsh_sdk --> pkg_sdk_client - pkg_subagent_dsh_sdk --> pkg_session - pkg_subagent_dsh_sdk --> pkg_subagent - pkg_subagent_dsh_sdk --> pkg_subprocess + pkg_host_directory_picker_auto --> pkg_host_directory_picker_browse + pkg_host_directory_picker_auto --> pkg_host_directory_picker_native + pkg_host_directory_picker_auto --> pkg_host_webserver + pkg_host_directory_picker_auto --> pkg_invariants pkg_client_ui_model --> pkg_client_connection pkg_client_ui_model --> pkg_client_locale pkg_client_ui_model --> pkg_client_runtime @@ -1241,12 +1271,14 @@ flowchart TD pkg_client_ui_model --> pkg_client_ui_slash pkg_client_ui_model --> pkg_client_ui_slots pkg_client_ui_model --> pkg_invariants + pkg_client_ui_permission --> pkg_api_remotes pkg_client_ui_permission --> pkg_client_connection pkg_client_ui_permission --> pkg_client_locale pkg_client_ui_permission --> pkg_client_runtime pkg_client_ui_permission --> pkg_client_schema_form pkg_client_ui_permission --> pkg_client_ui_command pkg_client_ui_permission --> pkg_client_ui_primitives + pkg_client_ui_permission --> pkg_client_ui_settings pkg_client_ui_permission --> pkg_client_ui_slash pkg_client_ui_permission --> pkg_client_ui_slots pkg_client_ui_permission --> pkg_invariants @@ -1259,16 +1291,6 @@ flowchart TD pkg_client_ui_skill --> pkg_client_ui_slots pkg_client_ui_skill --> pkg_client_ui_tool pkg_client_ui_skill --> pkg_invariants - pkg_acp_demo --> pkg_acp - pkg_acp_demo --> pkg_agent_spine_demo - pkg_acp_demo --> pkg_app_boot - pkg_acp_demo --> pkg_invariants - pkg_acp_demo --> pkg_session_checkpoint_policy - pkg_acp_demo --> pkg_session_persistence_jsonl - pkg_acp_demo --> pkg_session_query - pkg_acp_demo --> pkg_session_query_sqlite - pkg_acp_demo --> pkg_tools - pkg_acp_demo --> pkg_workspace_context ``` | Package | Group | Depends on | @@ -1305,7 +1327,6 @@ flowchart TD | [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) | | [`client-connection`](../packages/client/connection) | `client` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | | [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | -| [`client-runtime`](../packages/client/runtime) | `client` | [`invariants`](../packages/support/invariants), [`type-meta`](../packages/typert/type-meta), [`typert-registry`](../packages/typert/registry) | | [`credentials`](../packages/credentials/credentials) | `credentials` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants) | | [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`invariants`](../packages/support/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`frontend-static`](../packages/host/frontend-static) | `host` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | @@ -1319,9 +1340,6 @@ flowchart TD | [`llm`](../packages/llm/llm) | `llm` | [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`timeout`](../packages/util/timeout) | | [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/support/invariants), [`typert-registry`](../packages/typert/registry) | | [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) | -| [`client-locale`](../packages/client/locale) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | -| [`client-ui-models`](../packages/client/ui-models) | `client` | [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | -| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths) | | [`settings-local`](../packages/settings/settings-local) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`settings`](../packages/settings/settings) | | [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`credentials`](../packages/credentials/credentials), [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) | @@ -1330,12 +1348,7 @@ flowchart TD | [`system-prompt`](../packages/core/system-prompt) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | | [`skill`](../packages/skill/skill) | `skill` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope) | | [`web`](../packages/web/web) | `web` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | -| [`client-ui-question`](../packages/client/ui-question) | `client` | [`client-locale`](../packages/client/locale), [`invariants`](../packages/support/invariants) | -| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | -| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | -| [`client-ui-slash`](../packages/client/ui-slash) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | -| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | -| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | +| [`client-runtime`](../packages/client/runtime) | `client` | [`api-gateway`](../packages/api/gateway), [`invariants`](../packages/support/invariants), [`type-meta`](../packages/typert/type-meta), [`typert-registry`](../packages/typert/registry) | | [`lsp`](../packages/lsp/lsp) | `lsp` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | | [`agent`](../packages/core/agent) | `core` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`type-meta`](../packages/typert/type-meta) | | [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`invariants`](../packages/support/invariants), [`skill`](../packages/skill/skill) | @@ -1345,10 +1358,7 @@ flowchart TD | [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`acp-snapshot`](../packages/support/acp-snapshot) | `support` | [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`app-boot`](../packages/boot/app-boot) | `boot` | [`environment`](../packages/util/environment), [`invariants`](../packages/support/invariants), [`paths`](../packages/util/paths), [`system-prompt`](../packages/core/system-prompt) | -| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) | | [`code-runtime-worker`](../packages/code-runtime/code-runtime-worker) | `code-runtime` | [`code-runtime`](../packages/code-runtime/code-runtime), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | -| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | -| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | | [`persona`](../packages/preset/persona) | `preset` | [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) | | [`sandbox`](../packages/sandbox/sandbox) | `sandbox` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`session-persistence`](../packages/session/session-persistence) | `session` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`timeout`](../packages/util/timeout) | @@ -1363,7 +1373,6 @@ flowchart TD | [`loader-smoke`](../packages/support/loader-smoke) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`time-context`](../packages/context/time-context) | `context` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`message-feedback`](../packages/feedback/message-feedback) | `feedback` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage-domain`](../packages/storage/storage-domain), [`type-meta`](../packages/typert/type-meta) | -| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | | [`commands`](../packages/interaction/commands) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`scope`](../packages/core/scope), [`session`](../packages/core/session) | | [`user-approval`](../packages/interaction/user-approval) | `interaction` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt) | | [`user-interaction`](../packages/interaction/user-interaction) | `interaction` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm) | @@ -1391,7 +1400,7 @@ flowchart TD | [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`session-query`](../packages/session-query/session-query) | `session-query` | [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-title`](../packages/session/session-title) | | [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`user-approval`](../packages/interaction/user-approval) | -| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`typert-registry`](../packages/typert/registry) | +| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`credentials`](../packages/credentials/credentials), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) | | [`headless`](../packages/bundle/headless) | `bundle` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`tmux-context`](../packages/context/tmux-context) | `context` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session) | | [`fs-e2b`](../packages/e2b/fs-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants) | @@ -1426,7 +1435,8 @@ flowchart TD | [`agent-loop-testkit`](../packages/support/agent-loop-testkit) | `support` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`llm-replay`](../packages/support/llm-replay) | `support` | [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`client-test-runtime`](../packages/client/test-runtime) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/support/invariants) | -| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) | +| [`client-ui-models`](../packages/client/ui-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | +| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-gateway`](../packages/api/gateway), [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`settings`](../packages/settings/settings) | | [`session-reference`](../packages/context/session-reference) | `context` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`session-query`](../packages/session-query/session-query) | | [`workspace-context`](../packages/context/workspace-context) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | | [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | `guard` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) | @@ -1454,7 +1464,7 @@ flowchart TD | [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | | [`web-app`](../packages/bundle/web-app) | `bundle` | [`bash-env`](../packages/bash/bash-env), [`invariants`](../packages/support/invariants), [`system-prompt`](../packages/core/system-prompt) | -| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) | +| [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`sdk-protocol`](../packages/sdk/protocol) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | | [`tool-ralph`](../packages/workflow/tool-ralph) | `workflow` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | @@ -1462,8 +1472,24 @@ flowchart TD | [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) | | [`subagent-fork`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | | [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`invariants`](../packages/support/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | -| [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | -| [`client-ui-command`](../packages/client/ui-command) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | +| [`client-ui-question`](../packages/client/ui-question) | `client` | [`client-locale`](../packages/client/locale), [`invariants`](../packages/support/invariants) | +| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | +| [`client-ui-slash`](../packages/client/ui-slash) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | +| [`client-ui-theme`](../packages/client/ui-theme) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | +| [`client-ui-trajectory`](../packages/client/ui-trajectory) | `client` | [`agent`](../packages/core/agent), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`tools`](../packages/core/tools) | +| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | +| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`bash-env`](../packages/bash/bash-env), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks-local`](../packages/tasks/tasks-local), [`tool-bash`](../packages/bash/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | +| [`jsonrpc`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | +| [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) | +| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) | +| [`client-ui-conversation`](../packages/client/ui-conversation) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compact`](../packages/compact/compact), [`invariants`](../packages/support/invariants), [`llm-retry`](../packages/llm/llm-retry), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) | +| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/support/invariants) | +| [`client-ui-settings-general`](../packages/client/ui-settings-general) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | +| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/support/invariants), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | +| [`host-directory-picker-browse`](../packages/host/directory-picker-browse) | `host` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | +| [`host-directory-picker-native`](../packages/host/directory-picker-native) | `host` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/support/invariants) | +| [`client-ui-agent-preset`](../packages/client/ui-agent-preset) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/support/invariants) | +| [`client-ui-command`](../packages/client/ui-command) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-deliverables`](../packages/client/ui-deliverables) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-goal`](../packages/client/ui-goal) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/support/invariants) | | [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`plan-mode`](../packages/plan/plan-mode) | @@ -1471,11 +1497,7 @@ flowchart TD | [`client-ui-task`](../packages/client/ui-task) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | | [`client-ui-workflow-run`](../packages/client/ui-workflow-run) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) | -| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`bash-env`](../packages/bash/bash-env), [`goal`](../packages/goal/goal), [`goal-session`](../packages/goal/goal-session), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`paths`](../packages/util/paths), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks-local`](../packages/tasks/tasks-local), [`tool-bash`](../packages/bash/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | -| [`jsonrpc`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`scope`](../packages/core/scope), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | -| [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) | -| [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) | +| [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`host-directory-picker-browse`](../packages/host/directory-picker-browse), [`host-directory-picker-native`](../packages/host/directory-picker-native), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/support/invariants) | | [`client-ui-model`](../packages/client/ui-model) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-command`](../packages/client/ui-command), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants) | -| [`client-ui-permission`](../packages/client/ui-permission) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-command`](../packages/client/ui-command), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`permission`](../packages/interaction/permission) | +| [`client-ui-permission`](../packages/client/ui-permission) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-schema-form`](../packages/client/schema-form), [`client-ui-command`](../packages/client/ui-command), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/support/invariants), [`permission`](../packages/interaction/permission) | | [`client-ui-skill`](../packages/client/ui-skill) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slash`](../packages/client/ui-slash), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-tool`](../packages/client/ui-tool), [`invariants`](../packages/support/invariants) | -| [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/support/invariants), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-query`](../packages/session-query/session-query), [`session-query-sqlite`](../packages/session-query/session-query-sqlite), [`tools`](../packages/core/tools), [`workspace-context`](../packages/context/workspace-context) | diff --git a/docs/persistence-catalog.i18n.yaml b/docs/persistence-catalog.i18n.yaml index 88fc306381..3574545cd5 100644 --- a/docs/persistence-catalog.i18n.yaml +++ b/docs/persistence-catalog.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/persistence-catalog.md -persistence-catalog.md: 3b6e5b742df6282ec0cde4b4581eb3909844b537 +persistence-catalog.md: 0ccb94ab8cb89c096b48a01b1232428dfc94f676 persistence-catalog.zh.md: 6b65038869b53536bcbb2bd15c04172a7b5531e5 diff --git a/docs/persistence-catalog.md b/docs/persistence-catalog.md index 3b6e5b742d..0ccb94ab8c 100644 --- a/docs/persistence-catalog.md +++ b/docs/persistence-catalog.md @@ -90,7 +90,7 @@ export type SessionEvent = { }[T] ``` -Sources: [`packages/core/session/src/types.ts:331`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:338`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:367`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:399`](../packages/core/session/src/types.ts) +Sources: [`packages/core/session/src/types.ts:336`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:343`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:372`](../packages/core/session/src/types.ts) · [`packages/core/session/src/types.ts:404`](../packages/core/session/src/types.ts) ## Events @@ -203,7 +203,7 @@ Source: [`packages/interaction/user-approval/src/index.ts:67`](../packages/inter Types: [StreamChunk](subsystems/llm-streaming.md) -Source: [`packages/core/session/src/types.ts:261`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:266`](../packages/core/session/src/types.ts) #### `assistant/message` — surface @@ -219,7 +219,7 @@ Source: [`packages/core/session/src/types.ts:261`](../packages/core/session/src/ Types: [TokenUsage](subsystems/llm-streaming.md) -Source: [`packages/core/session/src/types.ts:268`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:273`](../packages/core/session/src/types.ts) ### `command/*` @@ -240,7 +240,7 @@ Source: [`packages/core/session/src/types.ts:268`](../packages/core/session/src/ } ``` -Source: [`packages/interaction/commands/src/types.ts:41`](../packages/interaction/commands/src/types.ts) +Source: [`packages/interaction/commands/src/types.ts:56`](../packages/interaction/commands/src/types.ts) #### `command/run` — log-only @@ -258,7 +258,7 @@ Source: [`packages/interaction/commands/src/types.ts:41`](../packages/interactio 'command/run': { commandId: CommandId; name: string; args?: string; source: CommandSource } ``` -Source: [`packages/interaction/commands/src/types.ts:34`](../packages/interaction/commands/src/types.ts) +Source: [`packages/interaction/commands/src/types.ts:49`](../packages/interaction/commands/src/types.ts) ### `compact/*` @@ -499,7 +499,7 @@ Source: [`packages/plan/plan-mode/src/index.ts:53`](../packages/plan/plan-mode/s 'request/context': RequestContext ``` -Source: [`packages/core/session/src/types.ts:304`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:309`](../packages/core/session/src/types.ts) #### `request/header` — log-only @@ -511,7 +511,7 @@ Source: [`packages/core/session/src/types.ts:304`](../packages/core/session/src/ 'request/header': { header: EpochHeader; reason: RequestHeaderReason } ``` -Source: [`packages/core/session/src/types.ts:299`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:304`](../packages/core/session/src/types.ts) ### `sandbox/*` @@ -564,7 +564,7 @@ Source: [`packages/sandbox/sandbox-policy/src/session-mode.ts:33`](../packages/s 'session/end-seed': Record ``` -Source: [`packages/core/session/src/types.ts:327`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:332`](../packages/core/session/src/types.ts) #### `session/title` — log-only @@ -600,7 +600,7 @@ Source: [`packages/session/session-title-llm/src/index.ts:43`](../packages/sessi 'step/end': { turn: number; step: number } ``` -Source: [`packages/core/session/src/types.ts:251`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:256`](../packages/core/session/src/types.ts) #### `step/start` — log-only @@ -609,7 +609,7 @@ Source: [`packages/core/session/src/types.ts:251`](../packages/core/session/src/ 'step/start': { turn: number; step: number } ``` -Source: [`packages/core/session/src/types.ts:249`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:254`](../packages/core/session/src/types.ts) ### `subagent/*` @@ -639,7 +639,7 @@ Source: [`packages/subagent/subagent/src/descriptor.ts:37`](../packages/subagent Types: [TodoItem](subsystems/session.md) -Source: [`packages/core/session/src/types.ts:294`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:299`](../packages/core/session/src/types.ts) ### `tool/*` @@ -656,7 +656,7 @@ Source: [`packages/core/session/src/types.ts:294`](../packages/core/session/src/ Types: [CallId](subsystems/core.md) -Source: [`packages/core/session/src/types.ts:274`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:279`](../packages/core/session/src/types.ts) #### `tool/code-dispatch` — log-only @@ -725,7 +725,7 @@ Source: [`packages/core/tools/src/types.ts:40`](../packages/core/tools/src/types } ``` -Source: [`packages/core/session/src/types.ts:286`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:291`](../packages/core/session/src/types.ts) ### `tool-workflow/*` @@ -795,7 +795,7 @@ Source: [`packages/workflow/tool-workflow/src/types.ts:47`](../packages/workflow Types: [TurnEndReason](subsystems/session.md) -Source: [`packages/core/session/src/types.ts:247`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:252`](../packages/core/session/src/types.ts) #### `turn/start` — log-only @@ -809,7 +809,7 @@ Source: [`packages/core/session/src/types.ts:247`](../packages/core/session/src/ 'turn/start': { turn: number } ``` -Source: [`packages/core/session/src/types.ts:238`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:243`](../packages/core/session/src/types.ts) ### `user/*` @@ -826,7 +826,7 @@ Source: [`packages/core/session/src/types.ts:238`](../packages/core/session/src/ 'user/message': UserMessage ``` -Source: [`packages/core/session/src/types.ts:259`](../packages/core/session/src/types.ts) +Source: [`packages/core/session/src/types.ts:264`](../packages/core/session/src/types.ts) ### `web/*` diff --git a/docs/subsystems/commands.i18n.yaml b/docs/subsystems/commands.i18n.yaml index f04e332ad9..9a8c979de4 100644 --- a/docs/subsystems/commands.i18n.yaml +++ b/docs/subsystems/commands.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/commands.md -commands.md: 03cb068418ff3e157349989e421f33d76cd7afe6 -commands.zh.md: b900baab4b0fffb50ac10265f4ea3ac2d8f9d01b +commands.md: 843210ccfa967ed03cd704d2b4b93bc7a9323102 +commands.zh.md: 9e60b160733c99841a7a26c6aa1809450a9b5ff5 diff --git a/docs/subsystems/commands.md b/docs/subsystems/commands.md index 03cb068418..843210ccfa 100644 --- a/docs/subsystems/commands.md +++ b/docs/subsystems/commands.md @@ -161,7 +161,7 @@ async execute( agent: Agent, line: string, signal: AbortSignal, ): Promise @@ -183,5 +183,5 @@ A command was registered or unregistered. This is an unfiltered registry notific 'commands/change'(): void ``` -Source: [`packages/interaction/commands/src/index.ts:134`](../../packages/interaction/commands/src/index.ts) +Source: [`packages/interaction/commands/src/types.ts:33`](../../packages/interaction/commands/src/types.ts) diff --git a/docs/subsystems/commands.zh.md b/docs/subsystems/commands.zh.md index b900baab4b..9e60b16073 100644 --- a/docs/subsystems/commands.zh.md +++ b/docs/subsystems/commands.zh.md @@ -161,7 +161,7 @@ async execute( agent: Agent, line: string, signal: AbortSignal, ): Promise @@ -183,5 +183,5 @@ A command was registered or unregistered. This is an unfiltered registry notific 'commands/change'(): void ``` -Source: [`packages/interaction/commands/src/index.ts:134`](../../packages/interaction/commands/src/index.ts) +Source: [`packages/interaction/commands/src/types.ts:33`](../../packages/interaction/commands/src/types.ts) diff --git a/docs/subsystems/credentials.i18n.yaml b/docs/subsystems/credentials.i18n.yaml index a7c9218e1b..74e7f89cf7 100644 --- a/docs/subsystems/credentials.i18n.yaml +++ b/docs/subsystems/credentials.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/credentials.md -credentials.md: 5ac023231c7eac85edbeda1c72f871ca37c0a891 -credentials.zh.md: b3f6e19ca76fb680ab243715530cb03770b543dc +credentials.md: 9bfc0f46fbc13bef7f0132f17aa0de9007edf26d +credentials.zh.md: 649771f53a1e00fd2b4302d8c31f35235a2961a3 diff --git a/docs/subsystems/credentials.md b/docs/subsystems/credentials.md index 5ac023231c..9bfc0f46fb 100644 --- a/docs/subsystems/credentials.md +++ b/docs/subsystems/credentials.md @@ -101,7 +101,7 @@ abstract set(ref: CredentialRef, value: string): Promise abstract unset(ref: CredentialRef): Promise ``` -Source: [`packages/credentials/credentials/src/index.ts:77`](../../packages/credentials/credentials/src/index.ts) +Source: [`packages/credentials/credentials/src/index.ts:60`](../../packages/credentials/credentials/src/index.ts) @@ -129,5 +129,5 @@ Committed change to a provider-managed credential source: a `set`, an `unset`, o 'credentials/updated'(ref: CredentialRef): void ``` -Source: [`packages/credentials/credentials/src/index.ts:67`](../../packages/credentials/credentials/src/index.ts) +Source: [`packages/credentials/credentials/src/types.ts:29`](../../packages/credentials/credentials/src/types.ts) diff --git a/docs/subsystems/credentials.zh.md b/docs/subsystems/credentials.zh.md index b3f6e19ca7..649771f53a 100644 --- a/docs/subsystems/credentials.zh.md +++ b/docs/subsystems/credentials.zh.md @@ -101,7 +101,7 @@ abstract set(ref: CredentialRef, value: string): Promise abstract unset(ref: CredentialRef): Promise ``` -Source: [`packages/credentials/credentials/src/index.ts:77`](../../packages/credentials/credentials/src/index.ts) +Source: [`packages/credentials/credentials/src/index.ts:60`](../../packages/credentials/credentials/src/index.ts) @@ -129,5 +129,5 @@ Committed change to a provider-managed credential source: a `set`, an `unset`, o 'credentials/updated'(ref: CredentialRef): void ``` -Source: [`packages/credentials/credentials/src/index.ts:67`](../../packages/credentials/credentials/src/index.ts) +Source: [`packages/credentials/credentials/src/types.ts:29`](../../packages/credentials/credentials/src/types.ts) diff --git a/docs/subsystems/settings.i18n.yaml b/docs/subsystems/settings.i18n.yaml index 8d243e8eb2..78abf3bd5f 100644 --- a/docs/subsystems/settings.i18n.yaml +++ b/docs/subsystems/settings.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/settings.md -settings.md: 6499c8260ad90c6aa4576d76abe30f261fdb0192 -settings.zh.md: 2b9f89d54feebe4436a182ff9e42d31ac080101d +settings.md: 04360b2c332c37e6602bf792a28b77f6755a5b34 +settings.zh.md: 206c305059126c5cf95abfc1980214352b9ce840 diff --git a/docs/subsystems/settings.md b/docs/subsystems/settings.md index 6499c8260a..04360b2c33 100644 --- a/docs/subsystems/settings.md +++ b/docs/subsystems/settings.md @@ -252,7 +252,7 @@ async replace(ns: SettingsNamespace, section: object, expectedRevision?: number) async mutate(ns: SettingsNamespace, ops: readonly SettingsPathOp[], expectedRevision?: number): Promise ``` -Source: [`packages/settings/settings/src/index.ts:387`](../../packages/settings/settings/src/index.ts) +Source: [`packages/settings/settings/src/index.ts:350`](../../packages/settings/settings/src/index.ts) @@ -279,7 +279,7 @@ One registered namespace's RAW user section changed, whether or not the resolved 'settings/document-updated'(ns: SettingsNamespace, revision: number): void ``` -Source: [`packages/settings/settings/src/index.ts:170`](../../packages/settings/settings/src/index.ts) +Source: [`packages/settings/settings/src/types.ts:48`](../../packages/settings/settings/src/types.ts) @@ -306,5 +306,5 @@ Committed change to one registered namespace's resolved value. Emitted after the 'settings/updated'(ns: SettingsNamespace, next: unknown, prev: unknown, source: SettingsUpdateSource): void ``` -Source: [`packages/settings/settings/src/index.ts:157`](../../packages/settings/settings/src/index.ts) +Source: [`packages/settings/settings/src/types.ts:35`](../../packages/settings/settings/src/types.ts) diff --git a/docs/subsystems/settings.zh.md b/docs/subsystems/settings.zh.md index 2b9f89d54f..206c305059 100644 --- a/docs/subsystems/settings.zh.md +++ b/docs/subsystems/settings.zh.md @@ -252,7 +252,7 @@ async replace(ns: SettingsNamespace, section: object, expectedRevision?: number) async mutate(ns: SettingsNamespace, ops: readonly SettingsPathOp[], expectedRevision?: number): Promise ``` -Source: [`packages/settings/settings/src/index.ts:387`](../../packages/settings/settings/src/index.ts) +Source: [`packages/settings/settings/src/index.ts:350`](../../packages/settings/settings/src/index.ts) @@ -279,7 +279,7 @@ One registered namespace's RAW user section changed, whether or not the resolved 'settings/document-updated'(ns: SettingsNamespace, revision: number): void ``` -Source: [`packages/settings/settings/src/index.ts:170`](../../packages/settings/settings/src/index.ts) +Source: [`packages/settings/settings/src/types.ts:48`](../../packages/settings/settings/src/types.ts) @@ -306,5 +306,5 @@ Committed change to one registered namespace's resolved value. Emitted after the 'settings/updated'(ns: SettingsNamespace, next: unknown, prev: unknown, source: SettingsUpdateSource): void ``` -Source: [`packages/settings/settings/src/index.ts:157`](../../packages/settings/settings/src/index.ts) +Source: [`packages/settings/settings/src/types.ts:35`](../../packages/settings/settings/src/types.ts) diff --git a/docs/subsystems/typert.i18n.yaml b/docs/subsystems/typert.i18n.yaml index aa7ab3aafc..1cc0814d1d 100644 --- a/docs/subsystems/typert.i18n.yaml +++ b/docs/subsystems/typert.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write docs/subsystems/typert.md -typert.md: 3c593952687a76089fcd6d66958c282294ae5abd -typert.zh.md: 5d9c987de8c6d54394ccd1165ad1d430d41a31e5 +typert.md: d279bcb1c0f20f75a82f89e8bfab731f82779729 +typert.zh.md: 4153cf8b6ebec65e0f9391911f9a0e9e0f869a63 diff --git a/docs/subsystems/typert.md b/docs/subsystems/typert.md index 3c59395268..d279bcb1c0 100644 --- a/docs/subsystems/typert.md +++ b/docs/subsystems/typert.md @@ -199,6 +199,27 @@ interface TypeRTClientRemote extends TypeRTRemoteNamespaceMap { * @returns disposer after namespace services and concrete methods are ready. */ $mount(contribution: TypeRTRemoteContribution): Promise + /** + * Subscribe to one forwarded Host event; delivery is one-way, in registration + * order, and isolates a throwing listener from the rest. + * @template Event - forwarded event name selected by the Host assembly. + * @param event - forwarded Host event name, unchanged on the wire. + * @param listener - receives the Host's argument list as declared by Cordis `Events`. + * @returns disposer owned by the calling fiber. + */ + $on(event: Event, listener: Events[Event]): () => void + /** + * Hand one decoded forwarded frame to the subscription table. The carrier + * owning the Host frame sink calls this; a consumer subscribes with + * {@link TypeRTClientRemote.$on} and never calls it. + * + * `event` is a plain string because this is the wire boundary: the name is + * whatever the Host assembly's allowlist selected, and one nobody subscribed + * to is dropped silently. + * @param event - forwarded Host event name, exactly as the Host emitted it. + * @param args - the Host argument list, already JSON-decoded. + */ + $dispatch(event: string, args: readonly unknown[]): void } ``` diff --git a/docs/subsystems/typert.zh.md b/docs/subsystems/typert.zh.md index 5d9c987de8..4153cf8b6e 100644 --- a/docs/subsystems/typert.zh.md +++ b/docs/subsystems/typert.zh.md @@ -199,6 +199,27 @@ interface TypeRTClientRemote extends TypeRTRemoteNamespaceMap { * @returns disposer after namespace services and concrete methods are ready. */ $mount(contribution: TypeRTRemoteContribution): Promise + /** + * Subscribe to one forwarded Host event; delivery is one-way, in registration + * order, and isolates a throwing listener from the rest. + * @template Event - forwarded event name selected by the Host assembly. + * @param event - forwarded Host event name, unchanged on the wire. + * @param listener - receives the Host's argument list as declared by Cordis `Events`. + * @returns disposer owned by the calling fiber. + */ + $on(event: Event, listener: Events[Event]): () => void + /** + * Hand one decoded forwarded frame to the subscription table. The carrier + * owning the Host frame sink calls this; a consumer subscribes with + * {@link TypeRTClientRemote.$on} and never calls it. + * + * `event` is a plain string because this is the wire boundary: the name is + * whatever the Host assembly's allowlist selected, and one nobody subscribed + * to is dropped silently. + * @param event - forwarded Host event name, exactly as the Host emitted it. + * @param args - the Host argument list, already JSON-decoded. + */ + $dispatch(event: string, args: readonly unknown[]): void } ``` diff --git a/knip.json b/knip.json index 11b25e3f80..1f3dcefea8 100644 --- a/knip.json +++ b/knip.json @@ -126,6 +126,9 @@ "project": [ "src/**/*.ts", "tests/**/*.ts" + ], + "ignoreDependencies": [ + "@deepseek-ai/dsh-api-gateway" ] }, "packages/client/ui-primitives": { @@ -653,18 +656,11 @@ }, "packages/client/ui-settings": { "entry": [ - "tests/**/*.spec.ts", - "tests/**/*.spec.tsx" + "tests/**/*.spec.ts" ], "project": [ "src/**/*.ts", - "src/**/*.tsx", - "tests/**/*.ts", - "tests/**/*.tsx" - ], - "ignoreDependencies": [ - "@types/react-dom", - "react-dom" + "tests/**/*.ts" ] }, "apps/web": { diff --git a/packages/api/gateway/README.i18n.yaml b/packages/api/gateway/README.i18n.yaml index 8e1e98774a..4782fc1aa8 100644 --- a/packages/api/gateway/README.i18n.yaml +++ b/packages/api/gateway/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/api/gateway/README.md -README.md: 0e1a03d2016b8cfbe165dbf1b0a9802290b29502 -README.zh.md: f8c01b489f51fb5e78b608dd9c24a36c7bc64c3a +README.md: 96f55eead6aec50e2f39f5bcefe71b51cc853298 +README.zh.md: 8985fd1f57833bc05f045e72135bb69a5a198d50 diff --git a/packages/api/gateway/README.md b/packages/api/gateway/README.md index ffdec7ac4d..96f55eead6 100644 --- a/packages/api/gateway/README.md +++ b/packages/api/gateway/README.md @@ -20,7 +20,7 @@ A cancellation-aware Remote method declares `signal: AbortSignal` as its final H Each call validates positional inputs, constructs the descriptor's exact named `args`, and sends it through `ctx.connection.rpc.call('/api', endpoint, ...)`. Generated cancellation-aware methods accept a final optional `AbortSignal`; the Client combines it with the contribution mount lifetime before calling Connection. The returned value is validated before reaching application code. Withdrawing a contribution removes its descriptors and methods together, aborts in-flight calls, and makes retained method handles reject. -`ctx.remote.$on()` subscribes to one forwarded Host event. Its legal keys are exactly the Host assembly's forwarding selection, and the listener type is the owning package's own Cordis `Events` declaration, so no second signature can drift from it. Each subscription belongs to the calling fiber and disappears with it. Delivery is one-way and follows registration order; a listener that throws is logged and isolated from the remaining listeners, which never affects the frame pump. The delivery port is not part of the Remote contract: the Client half owning the Host frame sink emits the internal `remote/host-event` Cordis event, the Remote service is its only subscriber, and an event name nobody subscribes to is dropped, since the wire carries whatever the Host selected. +`ctx.remote.$on()` subscribes to one forwarded Host event. Its legal keys are exactly the Host assembly's forwarding selection, and the listener type is the owning package's own Cordis `Events` declaration, so no second signature can drift from it. Each subscription belongs to the calling fiber and disappears with it. Delivery is one-way and follows registration order; a listener that throws is logged and isolated from the remaining listeners, which never affects the frame pump. `ctx.remote.$dispatch()` is the other half of that surface, and it is the carrier's: the Client half owning the Host frame sink hands each decoded frame over, and an event name nobody subscribes to is dropped, since the wire carries whatever the Host selected. A consumer subscribes and never calls it. Generated declaration merges provide the TypeScript API through the shared `TypeRTClientRemote` contract. The Client entry contains no Host Service or Host Cordis interface merge, and method lookup and invocation use ordinary objects and functions rather than a JavaScript Proxy. diff --git a/packages/api/gateway/README.zh.md b/packages/api/gateway/README.zh.md index 0137eb77db..8985fd1f57 100644 --- a/packages/api/gateway/README.zh.md +++ b/packages/api/gateway/README.zh.md @@ -20,7 +20,7 @@ Connection 可用时,Host 入口会在 Connection 共享的 `/api` FetchHandle 每次调用都会校验位置参数,构造与描述符完全匹配的具名 `args`,再通过 `ctx.connection.rpc.call('/api', endpoint, ...)` 发送。生成的支持取消的方法接受最后一个可选 `AbortSignal`;Client 会在调用 Connection 前将它与贡献项的挂载生命周期合并。返回值经过校验后才会交给应用代码。撤回贡献项会同时移除其描述符和方法、中止正在进行的调用,并使外部仍持有的方法句柄在调用时返回拒绝。 -`ctx.remote.$on()` 订阅一条被转发的 Host 事件。它的合法键恰好等于 Host 装配声明的转发选择,listener 类型就是事件所属包自己的 Cordis `Events` 声明,因此不存在会与之漂移的第二份签名。每个订阅归属发起调用的 fiber,并随该 fiber 一起消失。投递是单向的,并按注册顺序进行;抛错的 listener 会被记录并与其余 listener 隔离,绝不影响帧泵。投递口不属于 Remote 约定:持有 Host 帧 sink 的 Client 半发射内部 Cordis 事件 `remote/host-event`,Remote 服务是它唯一的订阅方,收到无人订阅的事件名即丢弃,因为 wire 上出现什么取决于 Host 的转发选择。 +`ctx.remote.$on()` 订阅一条被转发的 Host 事件。它的合法键恰好等于 Host 装配声明的转发选择,listener 类型就是事件所属包自己的 Cordis `Events` 声明,因此不存在会与之漂移的第二份签名。每个订阅归属发起调用的 fiber,并随该 fiber 一起消失。投递是单向的,并按注册顺序进行;抛错的 listener 会被记录并与其余 listener 隔离,绝不影响帧泵。`ctx.remote.$dispatch()` 是该面的另一半,且属于载体:持有 Host 帧 sink 的 Client 半把每个解码后的帧交进来,收到无人订阅的事件名即丢弃,因为 wire 上出现什么取决于 Host 的转发选择。消费方只订阅,绝不调用它。 生成的声明合并通过共享的 `TypeRTClientRemote` 约定提供 TypeScript API。Client 入口不包含 Host 服务或 Host Cordis 接口合并;方法查找和调用使用普通对象与函数,而不使用 JavaScript Proxy。 diff --git a/packages/api/gateway/src/client/index.ts b/packages/api/gateway/src/client/index.ts index 00df34f9b4..c064a3eef9 100644 --- a/packages/api/gateway/src/client/index.ts +++ b/packages/api/gateway/src/client/index.ts @@ -75,16 +75,24 @@ export function apply(ctx: Context): void { /** One subscribed listener after `$on` erased its per-event argument list. */ type RemoteEventListener = (...args: never[]) => void +/** + * One subscription, identified by the registration rather than by its listener: + * two fibers may subscribe the same function object to the same event, and each + * disposer must retire only its own registration. + */ +interface RemoteEventSubscription { + readonly listener: RemoteEventListener +} + class ClientRemoteService extends Service implements TypeRTClientRemote { private readonly ownerCtx: Context private readonly namespaces = new Map() - private readonly subscriptions = new Map>() + private readonly subscriptions = new Map() private mutations = Promise.resolve() constructor(ctx: Context) { super(ctx, 'remote') this.ownerCtx = ctx - ctx.on('remote/host-event', (event, args) => { this.dispatch(event, args) }) ctx.effect(() => () => { this.subscriptions.clear() }, 'api-gateway.client.subscriptions') } @@ -105,37 +113,51 @@ class ClientRemoteService extends Service implements TypeRTClientRemote { // The table is keyed by the runtime event name, so the argument list this // signature pins per event cannot survive in it; `$deliver` restores it // from the frame the Host emitted for that same name. - const erased: RemoteEventListener = listener + const subscription: RemoteEventSubscription = { listener } const owned = this.ctx.effect(() => { const listeners = this.listeners(event) - listeners.add(erased) - return () => { listeners.delete(erased) } + listeners.push(subscription) + return () => { + const at = listeners.indexOf(subscription) + if (at >= 0) listeners.splice(at, 1) + } }, `api-gateway.client.$on(${JSON.stringify(event)})`) return () => { void owned() } } /** - * Deliver one forwarded event in registration order, isolating a throwing - * listener; an event name nobody subscribes to is dropped, since the wire - * carries whatever the Host forwarding allowlist selected. + * Deliver one forwarded event in registration order, isolating a listener + * that fails either synchronously or by rejecting a returned promise; see + * {@link TypeRTClientRemote.$dispatch} for the caller contract. */ - private dispatch(event: string, args: readonly unknown[]): void { + $dispatch(event: string, args: readonly unknown[]): void { const listeners = this.subscriptions.get(event) if (listeners === undefined) return - for (const listener of listeners) { - try { - listener(...args as never[]) - } catch (error) { + // Snapshot: a listener may subscribe or dispose during delivery, and this + // round's recipients are the ones registered when the frame arrived. + for (const { listener } of [...listeners]) { + const report = (error: unknown): void => { console.error(`client api: Remote event ${JSON.stringify(event)} listener threw:`, error) } + try { + /* oxlint-disable-next-line typescript/no-confusing-void-expression -- + * The declared return is void, so nobody awaits an async listener; the + * runtime value is still a promise, and reading it is the only way to + * keep its rejection inside this containment instead of surfacing as an + * unhandled one. */ + const settled: unknown = listener(...args as never[]) + if (settled instanceof Promise) settled.catch(report) + } catch (error) { + report(error) + } } } - /** Subscription set for one event name; empty sets are retained, bounded by the Host's selection. */ - private listeners(event: string): Set { + /** Subscriptions for one event name; empty arrays are retained, bounded by the Host's selection. */ + private listeners(event: string): RemoteEventSubscription[] { let listeners = this.subscriptions.get(event) if (listeners === undefined) { - listeners = new Set() + listeners = [] this.subscriptions.set(event, listeners) } return listeners diff --git a/packages/api/gateway/tests/client.spec.ts b/packages/api/gateway/tests/client.spec.ts index a2c0c980cf..3c9a1b5ff4 100644 --- a/packages/api/gateway/tests/client.spec.ts +++ b/packages/api/gateway/tests/client.spec.ts @@ -625,11 +625,11 @@ describe('Client TypeRT API', () => { )) await subscriber - ctx.emit('remote/host-event', 'fixture/changed', ['settings']) + ctx.remote.$dispatch('fixture/changed', ['settings']) expect(seen).toEqual(['settings']) await subscriber.dispose() - ctx.emit('remote/host-event', 'fixture/changed', ['after fiber disposal']) + ctx.remote.$dispatch('fixture/changed', ['after fiber disposal']) expect(seen).toEqual(['settings']) await client.dispose() @@ -645,7 +645,7 @@ describe('Client TypeRT API', () => { }) ctx.remote.$on('fixture/changed', (namespace) => { seen.push(namespace) }) try { - ctx.emit('remote/host-event', 'fixture/changed', ['credentials']) + ctx.remote.$dispatch('fixture/changed', ['credentials']) expect(seen).toEqual(['credentials']) expect(consoleError).toHaveBeenCalledWith( @@ -653,7 +653,7 @@ describe('Client TypeRT API', () => { expect.any(Error), ) disposeFirst() - ctx.emit('remote/host-event', 'fixture/changed', ['commands']) + ctx.remote.$dispatch('fixture/changed', ['commands']) expect(seen).toEqual(['credentials', 'commands']) expect(consoleError).toHaveBeenCalledTimes(1) } finally { @@ -661,9 +661,62 @@ describe('Client TypeRT API', () => { } }) - it('exposes subscription as the only forwarded-event verb', () => { + it('contains an async listener whose promise rejects', async () => { + const ctx = await bench(vi.fn()) + const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined) + const seen: string[] = [] + // The declared return is void, so nobody awaits an async listener: the + // rejection has to be contained here or it escapes as an unhandled one. + /* oxlint-disable-next-line typescript/no-misused-promises -- + * Deliberately the shape the contract does not invite: `$on` declares a void + * listener, and this pins what the service does when a caller hands it an + * async one anyway. */ + ctx.remote.$on('fixture/changed', () => Promise.reject(new Error('fixture async failure'))) + ctx.remote.$on('fixture/changed', (namespace) => { seen.push(namespace) }) + try { + ctx.remote.$dispatch('fixture/changed', ['credentials']) + await Promise.resolve() + await Promise.resolve() + + expect(seen).toEqual(['credentials']) + expect(consoleError).toHaveBeenCalledWith( + 'client api: Remote event "fixture/changed" listener threw:', + expect.any(Error), + ) + } finally { + consoleError.mockRestore() + } + }) + + it('retires only its own registration when one listener subscribes twice', async () => { + const ctx = await bench(vi.fn()) + const seen: string[] = [] + // One function object, two registrations. A table keyed by listener identity + // stores it once, so the first frame would reach it once instead of twice + // and either disposer would silence both. + const listener = (namespace: string): void => { seen.push(namespace) } + const disposeFirst = ctx.remote.$on('fixture/changed', listener) + ctx.remote.$on('fixture/changed', listener) + + ctx.remote.$dispatch('fixture/changed', ['both']) + expect(seen).toEqual(['both', 'both']) + + // The surviving registration keeps receiving after its twin retires. + disposeFirst() + ctx.remote.$dispatch('fixture/changed', ['survivor']) + expect(seen).toEqual(['both', 'both', 'survivor']) + + // Disposing twice is inert: the record is already gone, so the second call + // must not splice the surviving twin out from under its own owner. + disposeFirst() + ctx.remote.$dispatch('fixture/changed', ['still here']) + expect(seen).toEqual(['both', 'both', 'survivor', 'still here']) + }) + + it('separates the consumer verb from the carrier handoff', () => { expectTypeOf().toHaveProperty('$on') - expectTypeOf().not.toHaveProperty('$dispatch') + // The carrier owning the frame sink calls this; a consumer subscribes instead. + expectTypeOf().toHaveProperty('$dispatch') }) it('drops a forwarded event nobody subscribes to', async () => { @@ -671,7 +724,7 @@ describe('Client TypeRT API', () => { const seen: string[] = [] ctx.remote.$on('fixture/changed', (namespace) => { seen.push(namespace) }) - ctx.emit('remote/host-event', 'fixture/idle', [1]) + ctx.remote.$dispatch('fixture/idle', [1]) expect(seen).toEqual([]) }) diff --git a/packages/api/remotes/README.i18n.yaml b/packages/api/remotes/README.i18n.yaml index dd8e3d4891..5492938176 100644 --- a/packages/api/remotes/README.i18n.yaml +++ b/packages/api/remotes/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/api/remotes/README.md -README.md: 567ece0fb58d4b9c0b022dd2ce4d8ee87caccc57 -README.zh.md: 12add6f8efc5b6af3e9b74b26a1abb2bb3936e0a +README.md: 34c78caf985c93137e7369329302ffd5feb67595 +README.zh.md: 4fff19673f10b0d173c9e3f724ca205845a0b9ed diff --git a/packages/api/remotes/src/client/index.ts b/packages/api/remotes/src/client/index.ts index 417937c2c3..78f83cb48d 100644 --- a/packages/api/remotes/src/client/index.ts +++ b/packages/api/remotes/src/client/index.ts @@ -16,10 +16,9 @@ export type {} from '@deepseek-ai/dsh-commands/types' export type {} from '@deepseek-ai/dsh-credentials/types' export type {} from '@deepseek-ai/dsh-settings/types' /** - * The Gateway Client face's own declaration merges, type-only: the internal - * `remote/host-event` delivery event a carrier owner emits and the Remote - * service subscribes to. Erased at emit, so this facade still carries no - * runtime edge to the Gateway implementation. + * The Gateway Client face's own declaration merges, type-only: `ctx.remote` and + * with it the `$on`/`$dispatch` surface. Erased at emit, so this facade still + * carries no runtime edge to the Gateway implementation. */ export type {} from '@deepseek-ai/dsh-api-gateway/client' diff --git a/packages/api/remotes/src/index.ts b/packages/api/remotes/src/index.ts index 1d592f49f9..705c8ee573 100644 --- a/packages/api/remotes/src/index.ts +++ b/packages/api/remotes/src/index.ts @@ -1,7 +1,7 @@ /** Host BFF entry and Loader shell for the Remote contribution assembly. */ import type { TypeRTForwardableEvent } from '@deepseek-ai/dsh-type-meta' -import { API_REMOTE_FORWARDED_EVENTS } from './types.ts' +import { API_REMOTE_FORWARDED_EVENTS } from './remote-events.ts' // The owner packages' client-safe `./types` exports carry the cordis `Events` // declarations for every allowlisted event. Pulling them into this face is what @@ -24,7 +24,7 @@ export type { ApiRemoteAgentResult, ApiRemoteLookupError, } from './agent-lookup.ts' -export { API_REMOTE_FORWARDED_EVENTS } from './types.ts' +export { API_REMOTE_FORWARDED_EVENTS } from './remote-events.ts' export type { ApiRemoteForwardedEvent } from './types.ts' // Shape gate over the allowlist, kept in the Host face because the Host's event diff --git a/packages/api/remotes/src/invariant.ts b/packages/api/remotes/src/invariant.ts index 1e0598a423..f93b63e98b 100644 --- a/packages/api/remotes/src/invariant.ts +++ b/packages/api/remotes/src/invariant.ts @@ -1,8 +1,8 @@ /** Package-owned invariant companion for `@deepseek-ai/dsh-api-remotes`. */ +/* jscpd:ignore-start */ import type { Context } from '@deepseek-ai/cordis' -import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants' -import { API_REMOTE_FORWARDED_EVENTS } from './types.ts' +import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants' const PACKAGE_NAME = '@deepseek-ai/dsh-api-remotes' @@ -11,38 +11,8 @@ export const name = 'api-remotes-invariant' /** Service required before the companion can reserve package ownership. */ export const inject = ['invariants'] -/** The allowlist as a lookup over the live dispatch stream's plain event names. */ -const FORWARDED_EVENTS: ReadonlySet = new Set(API_REMOTE_FORWARDED_EVENTS) - -/** - * Judge one observed dispatch of an allowlisted event against what verbatim - * forwarding can carry. The Host face's `TypeRTForwardableEvent` assertion - * judges each name's DECLARED signature; only the dispatch stream shows how a - * producer actually emitted it, and neither deviation below is visible to the - * compiler. A Scope carrier would be silently dropped on the way to a consumer - * because `ctx.remote.$on` has no scoped form, and a waterfall or bail dispatch - * expects a return value that a one-way carrier can never deliver back. - * @param mode - dispatch mode reported by the event bus. - * @param event - dispatched event name. - * @param carrier - the dispatch `this`; `null` when the event is unscoped. - * @param fail - reporter bound to this package. - */ -function validateDispatch(mode: string, event: string, carrier: unknown, fail: InvariantFailure): void { - if (!FORWARDED_EVENTS.has(event)) return - if (carrier !== null) { - fail(`forwarded host event "${event}" was dispatched with a Scope carrier, which consumers can never receive`) - } - if (mode !== 'emit') { - fail(`forwarded host event "${event}" was dispatched as "${mode}", but forwarding to consumers is one-way`) - } -} - -/** Install the forwarded-event dispatch-shape check over the live event bus. */ -const install: InvariantInstaller = (ctx, fail) => { - ctx.on('internal/dispatch', (mode, event, _args, thisArg) => { - validateDispatch(mode, event, thisArg, fail) - }, { global: true }) -} +/** No runtime invariant: TypeRT and the Agent/Session registries own the observed relationships. */ +const install: InvariantInstaller = () => {} /** * Register this package's invariant companion. @@ -51,3 +21,4 @@ const install: InvariantInstaller = (ctx, fail) => { */ export const apply = (ctx: Context): Promise<() => void> => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install)) +/* jscpd:ignore-end */ diff --git a/packages/api/remotes/src/remote-events.ts b/packages/api/remotes/src/remote-events.ts new file mode 100644 index 0000000000..f8137b72cf --- /dev/null +++ b/packages/api/remotes/src/remote-events.ts @@ -0,0 +1,21 @@ +/** + * The one home of this application's forwarded-Host-event allowlist. Both + * compiler faces list this file, so the Host forwarding loop and the consumer + * `ctx.remote.$on` key face read one declaration instead of two copies that + * could drift; `./types.ts` derives the type projection from it and stays + * type-only. + */ + +/** + * Host events this application forwards to consumers verbatim: no projection, + * no redaction, no renaming. The wire name is the Host cordis event name and + * the payload is its argument list, so this array is simultaneously the whole + * control point over what a consumer can receive and the legal key set of + * `ctx.remote.$on`. Forwarding one more event is an entry here and nothing + * else. + */ +export const API_REMOTE_FORWARDED_EVENTS = [ + 'commands/change', + 'credentials/updated', + 'settings/document-updated', +] as const diff --git a/packages/api/remotes/src/types.ts b/packages/api/remotes/src/types.ts index ec0aa8a01b..8d92903848 100644 --- a/packages/api/remotes/src/types.ts +++ b/packages/api/remotes/src/types.ts @@ -1,25 +1,15 @@ /** - * The one home of this application's forwarded-Host-event allowlist, listed in - * `tsconfig.host.json` AND `tsconfig.client.json` so the Host forwarding loop - * and the consumer `ctx.remote.$on` key face read the same declaration instead - * of two copies that could drift. + * Type face of the forwarded-Host-event allowlist: the consumer key projection + * and the selection seat it fills. The allowlist VALUE lives in + * `./remote-events.ts`, keeping this module type-only per the package + * convention; both compiler faces list both files, so the Host forwarding loop + * and the consumer `ctx.remote.$on` key face read one declaration instead of + * two copies that could drift. * * @module @deepseek-ai/dsh-api-remotes/types */ -/** - * Host events this application forwards to consumers verbatim: no projection, - * no redaction, no renaming. The wire name is the Host cordis event name and - * the payload is its argument list, so this array is simultaneously the whole - * control point over what a consumer can receive and the legal key set of - * `ctx.remote.$on`. Forwarding one more event is an entry here and nothing - * else. - */ -export const API_REMOTE_FORWARDED_EVENTS = [ - 'commands/change', - 'credentials/updated', - 'settings/document-updated', -] as const +import type { API_REMOTE_FORWARDED_EVENTS } from './remote-events.ts' /** Type projection of the allowlist; the consumer and the Host read this one. */ export type ApiRemoteForwardedEvent = typeof API_REMOTE_FORWARDED_EVENTS[number] diff --git a/packages/api/remotes/tests/invariant.spec.ts b/packages/api/remotes/tests/invariant.spec.ts deleted file mode 100644 index 1037035a52..0000000000 --- a/packages/api/remotes/tests/invariant.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { Context } from '@deepseek-ai/cordis' -import { credentialRef } from '@deepseek-ai/dsh-credentials' -import InvariantService from '@deepseek-ai/dsh-invariants' -import { settingsNamespace } from '@deepseek-ai/dsh-settings' -import { API_REMOTE_FORWARDED_EVENTS } from '@deepseek-ai/dsh-api-remotes' -import type { ApiRemoteForwardedEvent } from '@deepseek-ai/dsh-api-remotes' -import * as ApiRemotesInvariant from '@deepseek-ai/dsh-api-remotes/invariant' - -async function setup(): Promise { - const ctx = new Context() - await ctx.plugin(InvariantService) - await ctx.plugin(ApiRemotesInvariant) - return ctx -} - -/** - * One legal emission per allowlisted event. `Events` types each emit by name, - * so the three arities (0, 2, 1) cannot share a single loop body; keying the - * table by {@link ApiRemoteForwardedEvent} makes the compiler reject it as soon - * as the allowlist grows, which keeps "every listed event is exercised" true - * without a single argument-list assertion. - */ -const legalEmission: Record void> = { - 'commands/change': ctx => { ctx.emit('commands/change') }, - 'credentials/updated': ctx => { ctx.emit('credentials/updated', credentialRef('DEMO_TOKEN')) }, - 'settings/document-updated': ctx => { - ctx.emit('settings/document-updated', settingsNamespace('demo'), 1) - }, -} - -describe('forwarded host event invariants', () => { - it('accepts an unscoped one-way dispatch of every allowlisted event', async () => { - const ctx = await setup() - for (const event of API_REMOTE_FORWARDED_EVENTS) { - expect(() => { legalEmission[event](ctx) }).not.toThrow() - } - }) - - it('ignores an owner package event the allowlist does not select', async () => { - const ctx = await setup() - // `settings/updated` is the resolved-value event, deliberately left out of - // the allowlist while its sibling `settings/document-updated` is in it, so - // this pins that the check discriminates by name rather than by owner. - expect(() => { - ctx.emit('settings/updated', settingsNamespace('demo'), { a: 1 }, { a: 2 }, 'update') - }).not.toThrow() - // The carrier that fails an allowlisted event must pass unremarked here. - expect(() => { - ctx.emit({}, 'settings/updated', settingsNamespace('demo'), { a: 1 }, { a: 2 }, 'update') - }).not.toThrow() - }) - - it('rejects an allowlisted event dispatched with a Scope carrier', async () => { - const ctx = await setup() - expect(() => { ctx.emit({}, 'commands/change') }) - .toThrow(/"commands\/change" was dispatched with a Scope carrier/) - }) - - it('rejects an allowlisted event dispatched as anything but one-way', async () => { - const ctx = await setup() - expect(() => { ctx.bail('commands/change') }) - .toThrow(/"commands\/change" was dispatched as "bail"/) - }) -}) diff --git a/packages/api/remotes/tsconfig.client.json b/packages/api/remotes/tsconfig.client.json index d537747e77..c02e98656a 100644 --- a/packages/api/remotes/tsconfig.client.json +++ b/packages/api/remotes/tsconfig.client.json @@ -7,6 +7,7 @@ }, "files": [ "src/client/index.ts", + "src/remote-events.ts", "src/types.ts" ], "references": [ diff --git a/packages/api/remotes/tsconfig.host.json b/packages/api/remotes/tsconfig.host.json index 0f7fd354d3..fbdfb6a94c 100644 --- a/packages/api/remotes/tsconfig.host.json +++ b/packages/api/remotes/tsconfig.host.json @@ -9,6 +9,7 @@ "src/agent-lookup.ts", "src/index.ts", "src/invariant.ts", + "src/remote-events.ts", "src/types.ts" ], "references": [ diff --git a/packages/client/locale/package.json b/packages/client/locale/package.json index 6415c26961..0b8c0a6c7e 100644 --- a/packages/client/locale/package.json +++ b/packages/client/locale/package.json @@ -33,7 +33,9 @@ "client": { "inject": [ "@deepseek-ai/dsh-client-connection", - "@deepseek-ai/dsh-client-runtime" + "@deepseek-ai/dsh-client-runtime", + "@deepseek-ai/dsh-client-ui-settings", + "@deepseek-ai/dsh-api-remotes" ], "platform": "web", "immediately": true @@ -41,17 +43,22 @@ }, "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/cordis": "workspace:^", + "@deepseek-ai/dsh-api-remotes": "workspace:^", "@deepseek-ai/dsh-client-connection": "workspace:^", "@deepseek-ai/dsh-client-runtime": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", + "@deepseek-ai/dsh-client-ui-settings": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", - "@deepseek-ai/cordis": "workspace:^", "react": "^18.2.0" }, "devDependencies": { + "@deepseek-ai/cordis": "^4.0.0-rc.7", + "@deepseek-ai/dsh-api-remotes": "workspace:^", "@deepseek-ai/dsh-client-runtime": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", + "@deepseek-ai/dsh-client-ui-settings": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@types/react": "~18.3.1", diff --git a/packages/client/locale/src/client/LanguageRow.tsx b/packages/client/locale/src/client/LanguageRow.tsx index febf732792..12da219572 100644 --- a/packages/client/locale/src/client/LanguageRow.tsx +++ b/packages/client/locale/src/client/LanguageRow.tsx @@ -7,7 +7,7 @@ import { useState } from 'react' import type { PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots' import { IconChevronDownOutline14, Menu } from '@deepseek-ai/dsh-client-ui-primitives' -import type {} from './settings-contract.ts' +import type {} from '@deepseek-ai/dsh-client-ui-settings/client' import type { createLanguageRowStore } from './settings-store.ts' import css from './LanguageRow.module.css' diff --git a/packages/client/locale/src/client/index.ts b/packages/client/locale/src/client/index.ts index 3eea9eede6..ede3ccc30b 100644 --- a/packages/client/locale/src/client/index.ts +++ b/packages/client/locale/src/client/index.ts @@ -13,9 +13,11 @@ import type { Context } from '@deepseek-ai/cordis' import { type BoundActions, type LocaleDictOf, type LocaleNamespaceMap, type Translate, type TranslateNS, } from '@deepseek-ai/dsh-client-ui-slots' -import { - bindSettingsScope, type ClientContext, type SettingsScope, -} from '@deepseek-ai/dsh-client-runtime/client' +import type { ClientContext, SettingsScope } from '@deepseek-ai/dsh-client-runtime/client' +// Type-only: the ctx.settingsScope Context merge and the settings slot types. +// Cross-plugin collaboration goes through the service, never a value import +// (client bundle purity gate). +import type {} from '@deepseek-ai/dsh-client-ui-settings/client' import { LOCALE_PREFERENCE_FIELD, LOCALE_SETTINGS_NAMESPACE, type LocaleId, type LocaleSettings, } from '../locale-settings.ts' @@ -29,7 +31,6 @@ import { createLanguageRowStore } from './settings-store.ts' export type { LanguageRowComponentProps, LanguageRowInjected } from './LanguageRow.tsx' export type { LanguageOptionRow, LanguageRowState } from './settings-store.ts' -export type { SettingsGeneralItemOwnerProps } from './settings-contract.ts' export type { CommonKey } from '../locales/index.ts' export type { LocaleId, LocaleSettings } from '../locale-settings.ts' @@ -343,7 +344,7 @@ function detectBrowserLocale(): LocaleId | undefined { } /** Required services: slot registration plus the settings transport. */ -export const inject = ['slots', 'connection'] +export const inject = ['slots', 'connection', 'remote', 'settingsScope'] /** * Client plugin body: provide the locale service with base dictionaries and @@ -352,7 +353,7 @@ export const inject = ['slots', 'connection'] * @param ctx - client cordis context. */ export function apply(ctx: ClientContext): void { - const host = bindSettingsScope(ctx, { namespace: LOCALE_SETTINGS_NAMESPACE }) + const host = ctx.settingsScope.bind({ namespace: LOCALE_SETTINGS_NAMESPACE }) const locale = new LocaleService(ctx, host) locale.register(COMMON_NS, { zh, en }) locale.register(SETTINGS_NS, { zh: settingsZh, en: settingsEn }) diff --git a/packages/client/locale/src/client/settings-contract.ts b/packages/client/locale/src/client/settings-contract.ts deleted file mode 100644 index e032707645..0000000000 --- a/packages/client/locale/src/client/settings-contract.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * The `settings.general.item` slot type — one preference row inside the - * settings General section, contributed by the feature plugin that owns the - * preference (locale → Language, ui-theme → Appearance). Options: `id` (row - * key), `order` (row position). Rows draw their own internals (row layout, - * separators via CSS); the section column only stacks them. - * - * TYPE HOME RATIONALE: the slot is declared at runtime by - * ui-settings-general's General entry, but its type lives here — this - * package is the common dependency of every item registrant (any settings - * row carries copy, so every registrant already depends on locale), whereas - * the declarer's own contract is unreachable for locale/ui-theme without a - * reference cycle. - */ -declare module '@deepseek-ai/dsh-client-ui-slots' { - interface SlotMap { - /** One preference row inside the settings General section (see module JSDoc). */ - 'settings.general.item': { kind: 'list'; scope: 'root'; owner: SettingsGeneralItemOwnerProps } - } -} - -/** Owner share of a General preference row (the section supplies nothing). */ -export interface SettingsGeneralItemOwnerProps { - /** Marker field: item owner props are intentionally empty. */ - children?: never -} diff --git a/packages/client/locale/tests/apply.spec.ts b/packages/client/locale/tests/apply.spec.ts index c82e7c7cb3..92e567893b 100644 --- a/packages/client/locale/tests/apply.spec.ts +++ b/packages/client/locale/tests/apply.spec.ts @@ -4,6 +4,8 @@ import { Context } from '@deepseek-ai/cordis' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client' +import { SettingsScopeService } from '@deepseek-ai/dsh-client-ui-settings/client' +import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime' import { apply, inject, SETTINGS_NS, } from '@deepseek-ai/dsh-client-locale/client' @@ -43,6 +45,9 @@ async function bench() { } }) ctx.provide('connection', { api: { settings: { describe, mutate } }, isLoopback: true } as never) + // The settings transport and the forwarded-event port the plugin injects. + new TestRemote(ctx) + await ctx.plugin(SettingsScopeService).await() return { ctx, slots: ctx.get('slots') as SlotsService, describe, mutate, setHostPreference: (next: string | undefined) => { preference = next; revision += 1 }, @@ -79,7 +84,7 @@ describe('locale apply', () => { }) it('declares the slot service', () => { - expect(inject).toEqual(['slots', 'connection']) + expect(inject).toEqual(['slots', 'connection', 'remote', 'settingsScope']) }) it('provides the service with base + settings dictionaries and registers the row (declaration before or after apply)', async () => { @@ -134,10 +139,10 @@ describe('locale apply', () => { const locale = b.ctx.get('locale') as LocaleService await vi.waitFor(() => { expect(locale.getLocale().active).toBe('en') }) b.setHostPreference(undefined) - b.ctx.emit('settings/changed', LOCALE_SETTINGS_NAMESPACE) + b.ctx.remote.$dispatch('settings/document-updated', [LOCALE_SETTINGS_NAMESPACE, 0]) await vi.waitFor(() => { expect(locale.getLocale().active).toBe('zh') }) b.setHostPreference('en') - b.ctx.emit('settings/changed', LOCALE_SETTINGS_NAMESPACE) + b.ctx.remote.$dispatch('settings/document-updated', [LOCALE_SETTINGS_NAMESPACE, 0]) await vi.waitFor(() => { expect(locale.getLocale().active).toBe('en') }) expect(b.describe).toHaveBeenCalledTimes(3) }) diff --git a/packages/client/locale/tests/invariant.spec.ts b/packages/client/locale/tests/invariant.spec.ts index c55efa9a12..7f1c51c008 100644 --- a/packages/client/locale/tests/invariant.spec.ts +++ b/packages/client/locale/tests/invariant.spec.ts @@ -6,6 +6,7 @@ import { apply as clientApply, COMMON_NS, LocaleService, inject } from '@deepsee import * as LocaleInvariant from '@deepseek-ai/dsh-client-locale/invariant' import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client' import InvariantService from '@deepseek-ai/dsh-invariants' +import { stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' describe('invariant companion', () => { it('registers under the package name with an empty installer', async () => { @@ -20,10 +21,13 @@ describe('invariant companion', () => { it('client apply provides ctx.locale seeded with the zh/en common namespace', async () => { // The feature registers its own Language settings row, hence the slots edge. - expect(inject).toEqual(['slots', 'connection']) + expect(inject).toEqual(['slots', 'connection', 'remote', 'settingsScope']) const ctx = new Context() new SlotsService(ctx) ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) + // The settings row's transport and the forwarded-event port. + ctx.provide('remote', { $on: () => () => {} } as never) + ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) await ctx.plugin({ inject, apply: clientApply }).await() const locale = ctx.get('locale') expect(locale).toBeInstanceOf(LocaleService) diff --git a/packages/client/locale/tsconfig.json b/packages/client/locale/tsconfig.json index 313c11f5bf..d0ecc2710e 100644 --- a/packages/client/locale/tsconfig.json +++ b/packages/client/locale/tsconfig.json @@ -25,6 +25,9 @@ }, { "path": "../../support/invariants" + }, + { + "path": "../ui-settings" } ] } diff --git a/packages/client/runtime/package.json b/packages/client/runtime/package.json index 86d51cd7a3..f57f9e4952 100644 --- a/packages/client/runtime/package.json +++ b/packages/client/runtime/package.json @@ -33,7 +33,8 @@ "client": { "inject": [ "@deepseek-ai/dsh-client-connection", - "@deepseek-ai/dsh-typert-registry" + "@deepseek-ai/dsh-typert-registry", + "@deepseek-ai/dsh-api-gateway" ], "platform": "web", "immediately": true @@ -44,7 +45,6 @@ "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-attachment": "workspace:^", "@deepseek-ai/dsh-client-connection": "workspace:^", - "@deepseek-ai/dsh-client-schema-form": "workspace:^", "@deepseek-ai/dsh-commands": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-host-apiproxy": "workspace:^", @@ -59,19 +59,20 @@ "zustand": "~4.4.7" }, "peerDependencies": { + "@deepseek-ai/dsh-api-gateway": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-type-meta": "workspace:^", "@deepseek-ai/dsh-typert-registry": "workspace:^", "@deepseek-ai/cordis": "workspace:^" }, "devDependencies": { + "@deepseek-ai/dsh-api-gateway": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-timeout": "workspace:^", "@deepseek-ai/dsh-type-meta": "workspace:^", "@deepseek-ai/dsh-typert-registry": "workspace:^", - "@types/react": "~18.3.1", "@deepseek-ai/cordis": "workspace:^", - "@deepseek-ai/schemastery": "workspace:^" + "@types/react": "~18.3.1" }, "files": [ "lib/index.js", diff --git a/packages/client/runtime/src/client/contract/settings-scope.ts b/packages/client/runtime/src/client/contract/settings-scope.ts new file mode 100644 index 0000000000..4e4f583d87 --- /dev/null +++ b/packages/client/runtime/src/client/contract/settings-scope.ts @@ -0,0 +1,63 @@ +/** + * The settings-namespace scope contract. The type lives here, in the common + * dependency of every feature that owns a preference, while the implementation + * and its Host transport live with the Settings surface + * (`dsh-client-ui-settings`): a feature service accepts a scope through + * `attachSettings` without depending on the surface that binds it, which would + * otherwise close a reference cycle. + */ + +/** Client-side sync state of one settings namespace. */ +export interface SettingsScopeSnapshot { + /** + * `loading` until the first accepted section, `ready` while one stands, and + * `unavailable` when the namespace is not exposed to this client or the + * connection keeps preferences process-local (memory mode). + */ + status: 'loading' | 'ready' | 'unavailable' + /** Last accepted schema-resolved section; undefined before the first acceptance. */ + value: T | undefined + /** Namespace revision fencing the next write; undefined before the first Host view. */ + revision: number | undefined + /** Whether the Host document accepts writes; memory mode never does. */ + writable: boolean + /** `host` syncs with the Host document; `memory` keeps a remote browser process-local. */ + mode: 'host' | 'memory' +} + +/** Domain-owned description of one settings namespace consumed by a browser plugin. */ +export interface SettingsScopeSpec { + /** Settings namespace registered by the owning Host plugin. */ + namespace: string + /** + * Narrow one wire section; undefined keeps the last accepted value. The + * default validates the section against the namespace's own serialized wire + * schema, so domains add a decoder only to narrow beyond that schema. + */ + decode?: (section: unknown) => T | undefined +} + +/** + * Reactive owner handle over one namespace's durable section — the browser + * mirror of the Host-side `SettingsScope` owner seam. Domain services read + * and observe the snapshot and route explicit user choices through `set`. + */ +export interface SettingsScope { + /** @returns the current sync snapshot (stable reference until the next change). */ + getSnapshot(): SettingsScopeSnapshot + /** + * Observe snapshot replacements. + * @param listener - invoked after each snapshot change. + * @returns the disposer removing this listener. + */ + subscribe(listener: () => void): () => void + /** + * Queue one field write. Rapid writes preserve mutation order, each carries + * the latest known namespace revision, and only the latest settlement may + * publish; a rejected or failed latest write reloads Host state instead. + * @param field - scalar field inside the namespace section. + * @param value - JSON-shaped value selected by the user. + * @returns settlement after the write and any latest-write recovery read. + */ + set(field: string, value: unknown): Promise +} diff --git a/packages/client/runtime/src/client/index.ts b/packages/client/runtime/src/client/index.ts index 5bba2ff7fc..0d8671eb4f 100644 --- a/packages/client/runtime/src/client/index.ts +++ b/packages/client/runtime/src/client/index.ts @@ -1,6 +1,10 @@ /** Browser runtime services for slots, sessions, workspaces, and connection-stream delivery. */ import type { Context } from '@deepseek-ai/cordis' import type { ConnectionHandle, SessionId } from '@deepseek-ai/dsh-client-connection/client' +// Type-only: the ctx.remote merge. Deliberately the gateway's Client half rather +// than api-remotes': that face imports a Host-tsdown-generated artifact, and this +// project sits in the Host build graph. +import type {} from '@deepseek-ai/dsh-api-gateway/client' import type { TypeRTContext } from '@deepseek-ai/dsh-type-meta' import type { MaybeSnapshotSelectorHook, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots' import { SlotsService } from './slots.ts' @@ -42,9 +46,12 @@ export type { SessionProvideChannelHost } from './sessions/provide.ts' export { createScope } from './agents/scope.ts' export type { AgentScopeHandle } from './agents/scope.ts' export { DirectoryBrowseError, WorkspaceCreateError, WorkspacesService } from './workspaces/service.ts' -export { bindSettingsScope, SettingsScopeController } from './settings-scope.ts' -export type { SettingsScope, SettingsScopeSnapshot, SettingsScopeSpec } from './settings-scope.ts' export { resolveWorkspacePath } from './workspaces/path.ts' +// Contract only: the scope implementation and its Host transport belong to +// dsh-client-ui-settings (see that package's settings-scope.ts). +export type { + SettingsScope, SettingsScopeSnapshot, SettingsScopeSpec, +} from './contract/settings-scope.ts' export type { Session } from './sessions/session.ts' export type { ISession, ProjectionsFace, SessionFace } from './contract/session.ts' export type { AgentContext, ISessions } from './contract/sessions.ts' @@ -190,7 +197,7 @@ declare module '@deepseek-ai/cordis' { } /** Required services: the wire handle and Client TypeRT registry. */ -export const inject = ['connection', 'typert'] +export const inject = ['connection', 'typert', 'remote'] /** Mounts the browser runtime services and connection stream. * @param ctx - Client Cordis context. @@ -219,11 +226,11 @@ export function apply(ctx: Context): void { sessions.handleHostEnvelope(envelope) workspaces.handleHostEnvelope(envelope) // Forwarded-event bridge: the session layer ignores registry frames (no - // session routing). This plugin only carries the frame onto the internal - // `remote/host-event` plumbing event; the Remote service subscribes there - // and fans out to `ctx.remote.$on`, so no consumer reads a frame. + // session routing). This plugin owns the frame sink, so it hands the + // decoded frame straight to the Remote service, which fans it out to + // `ctx.remote.$on` subscribers; no consumer reads a frame. const frame = envelope.payload - if (frame.type === 'host/remote-event') ctx.emit('remote/host-event', frame.event, frame.args) + if (frame.type === 'host/remote-event') ctx.remote.$dispatch(frame.event, frame.args) else if (frame.type === 'host/session-preset-changed') { ctx.emit('session/preset-changed', frame.sessionId, frame.agentPreset) } diff --git a/packages/client/runtime/tests/wire-events.spec.ts b/packages/client/runtime/tests/wire-events.spec.ts index 4c2550384c..ca47db2a0b 100644 --- a/packages/client/runtime/tests/wire-events.spec.ts +++ b/packages/client/runtime/tests/wire-events.spec.ts @@ -1,7 +1,7 @@ /** - * Wire-to-typed-event bridge: a `host/remote-event` frame is republished - * verbatim on the internal `remote/host-event` plumbing event (the Remote - * service's fan-out to `ctx.remote.$on` is api-gateway's own coverage); + * Wire-to-typed-event bridge: a `host/remote-event` frame is handed verbatim to + * the Remote service's `$dispatch` (its fan-out to `ctx.remote.$on` is + * api-gateway's own coverage); * host/session-preset-changed → ctx 'session/preset-changed'; * `host/models-changed` still broadcasts the typed `models/changed`; each * established connection generation → ctx 'connection/reset' (the forced @@ -43,13 +43,20 @@ void forwardedEventContracts interface Bench { ctx: Context sinks: ConnectionSinks | undefined + /** Every `$dispatch` the runtime made, as `[event, ...args]`. */ + dispatched: unknown[][] } async function mount(): Promise { const ctx = new Context() await ctx.plugin(TypertRegistry) const api = new FakeApiClient() - const bench: Bench = { ctx, sinks: undefined } + const bench: Bench = { ctx, sinks: undefined, dispatched: [] } + // Stands in for api-gateway's Remote service: this spec owns the carrier's + // handoff, not the fan-out behind it. + ctx.reflect.provide('remote', { + $dispatch: (event: string, args: readonly unknown[]) => { bench.dispatched.push([event, ...args]) }, + }) const handle: ConnectionHandle = { api, isLoopback: true, @@ -69,8 +76,7 @@ async function mount(): Promise { describe('wire event bridge', () => { it('republishes a forwarded host event verbatim, and routes no other host frame there', async () => { const bench = await mount() - const seen: unknown[][] = [] - bench.ctx.on('remote/host-event', (event, args) => { seen.push([event, ...args]) }) + const seen = bench.dispatched bench.sinks?.onHostEnvelope?.({ rpcId: 'r1' as never, payload: { type: 'host/remote-event', event: 'commands/change', args: [] }, @@ -86,8 +92,7 @@ describe('wire event bridge', () => { it('carries each forwarded event name with its own argument list, unfiltered', async () => { const bench = await mount() - const seen: unknown[][] = [] - bench.ctx.on('remote/host-event', (event, args) => { seen.push([event, ...args]) }) + const seen = bench.dispatched bench.sinks?.onHostEnvelope?.({ rpcId: 'r3' as never, diff --git a/packages/client/runtime/tsconfig.json b/packages/client/runtime/tsconfig.json index 7ccf95dd73..6641467cc2 100644 --- a/packages/client/runtime/tsconfig.json +++ b/packages/client/runtime/tsconfig.json @@ -23,9 +23,6 @@ { "path": "../connection" }, - { - "path": "../schema-form" - }, { "path": "../../host/apiproxy" }, @@ -61,6 +58,9 @@ }, { "path": "../../typert/registry" + }, + { + "path": "../../api/gateway" } ], "exclude": [ diff --git a/packages/client/test-runtime/src/remote.ts b/packages/client/test-runtime/src/remote.ts index 143352cb1c..fab7f9b1fd 100644 --- a/packages/client/test-runtime/src/remote.ts +++ b/packages/client/test-runtime/src/remote.ts @@ -7,29 +7,40 @@ import type { Context } from '@deepseek-ai/cordis' * host events to reach those subscribers, but not the generated namespaces or * the wire — so this double implements subscription and dispatch only. * - * Dispatch is driven the same way production drives it: by the internal - * `remote/host-event` event the connection sink emits. A spec therefore - * exercises its refresh chains with `ctx.emit('remote/host-event', name, args)`, - * the exact signal `client/runtime` republishes from a `host/remote-event` - * frame, rather than reaching into this double. + * Dispatch is driven the same way production drives it: `client/runtime` owns the + * host frame sink and hands each decoded `host/remote-event` frame to + * `$dispatch`. A spec therefore exercises its refresh chains by calling + * `$dispatch(name, args)` on this double. * * `$mount` rejects: a spec that reaches a generated namespace through this * double has outgrown it and needs the real Client Remote service. + * + * One deliberate asymmetry with production: a throwing listener propagates out + * of the emit instead of being contained and logged, so a spec cannot lean on + * this double for the containment guarantee `$on` documents — assert that + * against the real service. */ export class TestRemote { private readonly subscriptions = new Map void>>() /** - * Register the double as `ctx.remote` and bind its dispatch to the plumbing event. + * Register the double as `ctx.remote`. * @param ctx - the spec's root Context. */ constructor(ctx: Context) { ctx.provide('remote', this) - ctx.on('remote/host-event', (event, args) => { - const listeners = this.subscriptions.get(event) - if (listeners === undefined) return - for (const listener of [...listeners]) listener(...args as never[]) - }) + } + + /** + * Deliver one forwarded host event to its subscribers, standing in for the + * carrier that owns the frame sink. + * @param event - forwarded host event name. + * @param args - the Host argument list, verbatim. + */ + $dispatch(event: string, args: readonly unknown[]): void { + const listeners = this.subscriptions.get(event) + if (listeners === undefined) return + for (const listener of [...listeners]) listener(...args as never[]) } /** diff --git a/packages/client/test-runtime/tests/remote.spec.ts b/packages/client/test-runtime/tests/remote.spec.ts new file mode 100644 index 0000000000..3f11c82323 --- /dev/null +++ b/packages/client/test-runtime/tests/remote.spec.ts @@ -0,0 +1,43 @@ +/** + * TestRemote's own contract: subscription and disposal, dispatch driven by the + * internal plumbing event, the silent drop for an unsubscribed name, and the + * `$mount` refusal that sends a spec to the real Client Remote service. + */ +import { Context } from '@deepseek-ai/cordis' +import { describe, expect, it } from 'vitest' +import { TestRemote } from '../src/remote.ts' + +describe('TestRemote', () => { + it('delivers a forwarded event to its subscribers and stops after disposal', async () => { + const ctx = new Context() + const remote = new TestRemote(ctx) + const seen: string[] = [] + const off = remote.$on('settings/document-updated', (ns: string) => { + seen.push(ns) + }) + + ctx.remote.$dispatch('settings/document-updated', ['ui-theme', 1]) + expect(seen).toEqual(['ui-theme']) + + off() + ctx.remote.$dispatch('settings/document-updated', ['ui-theme', 2]) + expect(seen).toEqual(['ui-theme']) + await ctx.fiber.dispose() + }) + + it('drops a forwarded event nobody subscribed to', async () => { + const ctx = new Context() + new TestRemote(ctx) + // No subscriber for this name: the emit must be inert rather than throwing, + // because the wire carries whatever the Host allowlist selected. + expect(() => { ctx.remote.$dispatch('credentials/updated', ['DEEPSEEK_API_KEY']) }).not.toThrow() + await ctx.fiber.dispose() + }) + + it('refuses $mount, which needs the real Client Remote service', async () => { + const ctx = new Context() + const remote = new TestRemote(ctx) + await expect(remote.$mount()).rejects.toThrow('needs the real Client Remote service') + await ctx.fiber.dispose() + }) +}) diff --git a/packages/client/ui-agent-preset/src/client/section-store.ts b/packages/client/ui-agent-preset/src/client/section-store.ts index df430f6db4..a6f3cb4d62 100644 --- a/packages/client/ui-agent-preset/src/client/section-store.ts +++ b/packages/client/ui-agent-preset/src/client/section-store.ts @@ -137,9 +137,10 @@ export class AgentPresetSectionController { /** * Called after this page changes the roster DIRECTORY, so the other * surfaces reading the same roster re-read it. A settings field moving is - * already announced by the host through `settings/changed`; a directory - * copied or deleted here is not, and the new-session chip has no other - * way to learn a preset it should offer now exists. + * already announced by the host through the forwarded + * `settings/document-updated`; a directory copied or deleted here is not, + * and the new-session chip has no other way to learn a preset it should + * offer now exists. */ private readonly rosterChanged: () => void = () => {}, ) {} diff --git a/packages/client/ui-agent-preset/tests/apply.spec.ts b/packages/client/ui-agent-preset/tests/apply.spec.ts index cf87c541c8..f719f9f371 100644 --- a/packages/client/ui-agent-preset/tests/apply.spec.ts +++ b/packages/client/ui-agent-preset/tests/apply.spec.ts @@ -79,7 +79,7 @@ async function bench() { const locale = new LocaleService(ctx) ctx.provide('locale', locale) // The plugins inject `remote`; forwarded events reach them through the - // same `remote/host-event` signal the connection sink republishes. + // same `$dispatch` handoff the connection sink makes. new TestRemote(ctx) const calls: string[] = [] ctx.provide('connection', { @@ -253,11 +253,11 @@ describe('ui-agent-preset apply', () => { await section.load() const before = calls.length - ctx.emit('remote/host-event', 'settings/document-updated', ['agent-presets', 1]) + ctx.remote.$dispatch('settings/document-updated', ['agent-presets', 1]) await vi.waitFor(() => { expect(calls.length).toBe(before + 2) }) const afterRelevant = calls.length - ctx.emit('remote/host-event', 'settings/document-updated', ['llm-deepseek', 1]) + ctx.remote.$dispatch('settings/document-updated', ['llm-deepseek', 1]) await Promise.resolve() // Both surfaces re-read on their own namespace; an unrelated one moves @@ -285,7 +285,7 @@ describe('ui-agent-preset apply', () => { await ctx.plugin({ inject: [...inject], apply }).await() const before = calls.length - ctx.emit('remote/host-event', 'settings/document-updated', ['agent-presets', 1]) + ctx.remote.$dispatch('settings/document-updated', ['agent-presets', 1]) await vi.waitFor(() => { expect(calls.length).toBeGreaterThan(before) }) // Only the General row reloads: a section nobody opened has nothing to @@ -336,11 +336,11 @@ describe('ui-agent-preset apply', () => { // An unrelated namespace moves nothing: the chip re-reads on its own // setting, not on every settings write in the process. moveDefault() - ctx.emit('remote/host-event', 'settings/document-updated', ['llm-deepseek', 1]) + ctx.remote.$dispatch('settings/document-updated', ['llm-deepseek', 1]) await Promise.resolve() expect(seat.hooks.agentPresetSeat.getSnapshot().current).toBe('standard') - ctx.emit('remote/host-event', 'settings/document-updated', ['agent-presets', 1]) + ctx.remote.$dispatch('settings/document-updated', ['agent-presets', 1]) await vi.waitFor(() => { expect(seat.hooks.agentPresetSeat.getSnapshot().current).toBe('minimal') }) diff --git a/packages/client/ui-command/tests/service.spec.ts b/packages/client/ui-command/tests/service.spec.ts index 320b1bf600..839934eb53 100644 --- a/packages/client/ui-command/tests/service.spec.ts +++ b/packages/client/ui-command/tests/service.spec.ts @@ -80,7 +80,7 @@ async function bench(opts: BenchOptions = {}) { }) ctx.provide('connection', { api }) // CommandService injects `remote`; the directory invalidation arrives on the - // same `remote/host-event` signal the connection sink republishes. + // same `$dispatch` handoff the connection sink makes. new TestRemote(ctx) /** Notices the fake conversation face collected (runDetached routing). */ const notices: Array<{ scope: SessionId | undefined; level: 'info' | 'error'; text: string }> = [] @@ -602,7 +602,7 @@ describe('popupFor', () => { }) describe('directory invalidation events', () => { - it('commands/changed repulls in the background while the old snapshot serves', async () => { + it('commands/change repulls in the background while the old snapshot serves', async () => { let round = 0 const { ctx, source, warm } = await bench({ commands: () => { @@ -615,7 +615,7 @@ describe('directory invalidation events', () => { }, }) await warm(proj('s1')) - ctx.emit('remote/host-event', 'commands/change', []) + ctx.remote.$dispatch('commands/change', []) await new Promise(resolve => setTimeout(resolve, 0)) expect(source.matchSpace!(proj('s1'), '/fresh')).not.toBeUndefined() expect(source.matchSpace!(proj('s1'), '/goal')).toBeUndefined() diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json index e80432f79f..e11cec8289 100644 --- a/packages/client/ui-conversation/package.json +++ b/packages/client/ui-conversation/package.json @@ -35,6 +35,8 @@ "@deepseek-ai/dsh-client-connection", "@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-runtime", + "@deepseek-ai/dsh-client-ui-settings", + "@deepseek-ai/dsh-api-remotes", "@deepseek-ai/dsh-client-ui-layout" ], "platform": "web" @@ -51,47 +53,52 @@ "@deepseek-ai/schemastery": "workspace:^" }, "peerDependencies": { + "@deepseek-ai/cordis": "workspace:^", "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-api-remotes": "workspace:^", "@deepseek-ai/dsh-attachment": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-client-connection": "workspace:^", "@deepseek-ai/dsh-client-locale": "workspace:^", "@deepseek-ai/dsh-client-runtime": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", + "@deepseek-ai/dsh-client-ui-settings": "workspace:^", "@deepseek-ai/dsh-client-ui-slash": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", - "@deepseek-ai/dsh-compact": "workspace:^", "@deepseek-ai/dsh-commands": "workspace:^", + "@deepseek-ai/dsh-compact": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm-retry": "workspace:^", "@deepseek-ai/dsh-token-meter": "workspace:^", "@deepseek-ai/dsh-tools": "workspace:^", - "@deepseek-ai/cordis": "workspace:^", "react": "^18.2.0" }, "devDependencies": { + "@deepseek-ai/cordis": "^4.0.0-rc.7", "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-api-remotes": "workspace:^", "@deepseek-ai/dsh-attachment": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-client-connection": "workspace:^", "@deepseek-ai/dsh-client-locale": "workspace:^", "@deepseek-ai/dsh-client-runtime": "workspace:^", "@deepseek-ai/dsh-client-test-runtime": "workspace:^", - "@deepseek-ai/dsh-compact": "workspace:^", - "@deepseek-ai/dsh-commands": "workspace:^", - "@deepseek-ai/dsh-goal": "workspace:^", - "@deepseek-ai/dsh-plan-mode": "workspace:^", "@deepseek-ai/dsh-client-ui-layout": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", + "@deepseek-ai/dsh-client-ui-settings": "workspace:^", "@deepseek-ai/dsh-client-ui-slash": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", + "@deepseek-ai/dsh-commands": "workspace:^", + "@deepseek-ai/dsh-compact": "workspace:^", + "@deepseek-ai/dsh-goal": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm-retry": "workspace:^", "@deepseek-ai/dsh-permission": "workspace:^", + "@deepseek-ai/dsh-plan-mode": "workspace:^", "@deepseek-ai/dsh-session-projection": "workspace:^", "@deepseek-ai/dsh-token-meter": "workspace:^", - "@deepseek-ai/dsh-tools": "workspace:^", "@deepseek-ai/dsh-tool-todo": "workspace:^", + "@deepseek-ai/dsh-tools": "workspace:^", "@types/react": "~18.3.1", "@deepseek-ai/cordis": "workspace:^", "react": "^18.2.0" diff --git a/packages/client/ui-conversation/src/client/apply.ts b/packages/client/ui-conversation/src/client/apply.ts index 5449295055..4298328b33 100644 --- a/packages/client/ui-conversation/src/client/apply.ts +++ b/packages/client/ui-conversation/src/client/apply.ts @@ -2,8 +2,11 @@ import type { Context } from '@deepseek-ai/cordis' import { resolveSlotLabel, type BoundActions } from '@deepseek-ai/dsh-client-ui-slots' import { - bindSettingsScope, resolveWorkspacePath, type ISessions, type SessionId, + resolveWorkspacePath, type ISessions, type SessionId, } from '@deepseek-ai/dsh-client-runtime/client' +// Type-only: the ctx.settingsScope Context merge. Cross-plugin collaboration +// goes through the service, never a value import (client bundle purity gate). +import type {} from '@deepseek-ai/dsh-client-ui-settings/client' import type {} from '@deepseek-ai/dsh-client-ui-layout/client' // Type-only: pulls the locale plugin's Context merge (ctx.locale). import type {} from '@deepseek-ai/dsh-client-locale/client' @@ -46,7 +49,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { /** Services required by the conversation plugin. */ export const inject = [ - 'slots', 'layout', 'sessions', 'workspaces', 'locale', 'connection', + 'slots', 'layout', 'sessions', 'workspaces', 'locale', 'connection', 'remote', 'settingsScope', 'conversationEvents', 'conversationViews', ] @@ -128,7 +131,7 @@ export function apply(ctx: Context): void { // Apply-time construction keeps store identity bound to this fiber. const chatStore = createChatStore() const submissionPolicy = new ComposerSubmissionPolicy( - bindSettingsScope(ctx, { namespace: CONVERSATION_SETTINGS_NAMESPACE }), + ctx.settingsScope.bind({ namespace: CONVERSATION_SETTINGS_NAMESPACE }), ) ctx.slots.inject('settings.general.item', () => ctx.slots.register({ diff --git a/packages/client/ui-conversation/tests/apply-inject.spec.tsx b/packages/client/ui-conversation/tests/apply-inject.spec.tsx index 706db3eeb1..2017d84498 100644 --- a/packages/client/ui-conversation/tests/apply-inject.spec.tsx +++ b/packages/client/ui-conversation/tests/apply-inject.spec.tsx @@ -15,7 +15,7 @@ // chat-toolview-slot.spec.tsx. import { describe, expect, it, vi } from 'vitest' -import { SlotTestRuntime, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { SlotTestRuntime, usePinnedBrowserLanguages, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' import type { SessionBehaviorOverrides } from '@deepseek-ai/dsh-client-test-runtime' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' import type { ISession, SessionId } from '@deepseek-ai/dsh-client-runtime/client' @@ -48,6 +48,9 @@ function sessionFakeFor() { async function bench() { const runtime = await SlotTestRuntime.create() runtime.provide('connection', { api: { settings: {} }, isLoopback: false } as never) + // The plugin injects both; these specs exercise no settings path. + runtime.provide('remote', { $on: () => () => {} }) + runtime.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) const sessionFake = sessionFakeFor() await runtime.sessions.add({ id: ROOT, diff --git a/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx b/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx index 9e52d2a0d5..9d4004f8c0 100644 --- a/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx +++ b/packages/client/ui-conversation/tests/assembly-surfaces.spec.tsx @@ -6,7 +6,7 @@ import { useState } from 'react' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' import type { ISession, SessionId } from '@deepseek-ai/dsh-client-runtime/client' import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' -import { SlotTestRuntime, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { SlotTestRuntime, usePinnedBrowserLanguages, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' import { apply, inject, type EmptyWorkspaceOwnerProps } from '@deepseek-ai/dsh-client-ui-conversation/client' usePinnedBrowserLanguages('zh-CN') @@ -51,6 +51,9 @@ function WorkspaceProbe({ open }: EmptyWorkspaceOwnerProps) { async function bench(opts?: { blank?: boolean }) { const runtime = await SlotTestRuntime.create() runtime.provide('connection', { api: { settings: {} }, isLoopback: false } as never) + // The plugin injects both; these specs exercise no settings path. + runtime.provide('remote', { $on: () => () => {} }) + runtime.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() }) const locale = new LocaleService(runtime.ctx) runtime.provide('locale', locale) @@ -76,6 +79,9 @@ describe('resident composer', () => { it('renders the locked view state while no session exists at all', async () => { const runtime = await SlotTestRuntime.create() runtime.provide('connection', { api: { settings: {} }, isLoopback: false } as never) + // The plugin injects both; these specs exercise no settings path. + runtime.provide('remote', { $on: () => () => {} }) + runtime.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() }) const locale = new LocaleService(runtime.ctx) runtime.provide('locale', locale) @@ -103,6 +109,9 @@ describe('resident composer', () => { it('keeps the complete Hero tree mounted when the first Workspace session appears', async () => { const runtime = await SlotTestRuntime.create() runtime.provide('connection', { api: { settings: {} }, isLoopback: false } as never) + // The plugin injects both; these specs exercise no settings path. + runtime.provide('remote', { $on: () => () => {} }) + runtime.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() }) const locale = new LocaleService(runtime.ctx) runtime.provide('locale', locale) @@ -169,6 +178,9 @@ describe('prompt rejection through the assembled composer', () => { it('renders the promptError alert strip and keeps the draft in the machine', async () => { const runtime = await SlotTestRuntime.create() runtime.provide('connection', { api: { settings: {} }, isLoopback: false } as never) + // The plugin injects both; these specs exercise no settings path. + runtime.provide('remote', { $on: () => () => {} }) + runtime.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() }) const locale = new LocaleService(runtime.ctx) runtime.provide('locale', locale) diff --git a/packages/client/ui-conversation/tests/chat-apply.spec.tsx b/packages/client/ui-conversation/tests/chat-apply.spec.tsx index 7963e4b4f4..0ebb710311 100644 --- a/packages/client/ui-conversation/tests/chat-apply.spec.tsx +++ b/packages/client/ui-conversation/tests/chat-apply.spec.tsx @@ -6,7 +6,7 @@ // entries. Tool composition belongs to ui-tool and its machinery spec. import { describe, expect, it, vi } from 'vitest' -import { SlotTestRuntime, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { SlotTestRuntime, usePinnedBrowserLanguages, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client' @@ -22,6 +22,9 @@ const CHILD = 'child-1' as SessionId async function bench() { const runtime = await SlotTestRuntime.create() runtime.provide('connection', { api: { settings: {} }, isLoopback: false } as never) + // The plugin injects both; these specs exercise no settings path. + runtime.provide('remote', { $on: () => () => {} }) + runtime.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) await runtime.sessions.add({ id: ROOT, summary: { title: 'R', displayTitle: 'R' } }, { current: false }) await runtime.sessions.add( { id: CHILD, summary: { title: 'C', displayTitle: 'C', parentId: ROOT } }, { current: false }) diff --git a/packages/client/ui-conversation/tsconfig.json b/packages/client/ui-conversation/tsconfig.json index 49763e5fb8..b6190dc3ae 100644 --- a/packages/client/ui-conversation/tsconfig.json +++ b/packages/client/ui-conversation/tsconfig.json @@ -76,6 +76,9 @@ }, { "path": "../../interaction/permission" + }, + { + "path": "../ui-settings" } ], "exclude": [ diff --git a/packages/client/ui-deliverables/tests/produced-files.spec.tsx b/packages/client/ui-deliverables/tests/produced-files.spec.tsx index 31289b38de..78d3317d75 100644 --- a/packages/client/ui-deliverables/tests/produced-files.spec.tsx +++ b/packages/client/ui-deliverables/tests/produced-files.spec.tsx @@ -18,7 +18,7 @@ import type { } from '@deepseek-ai/dsh-client-runtime/client' import { apply as applyLocale } from '@deepseek-ai/dsh-client-locale/client' import type { ChatFileMentions, TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-conversation/client' -import { makeTranslate } from '@deepseek-ai/dsh-client-test-runtime' +import { makeTranslate, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' import { ProducedFiles } from '../src/client/ProducedFiles.tsx' import { basename, deliverablesDefinition, producedFileMentions, producedForClosing, selectProducedFiles, @@ -341,6 +341,9 @@ describe('plugin registration', () => { children: { 'conversation.chat.turnTail': { kind: 'chain', scope: 'session' } }, } as never, () => null) ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) + // ui-theme's Appearance row binds a durable scope through these two. + ctx.provide('remote', { $on: () => () => {} } as never) + ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) await ctx.plugin({ inject: ['slots'], apply: applyLocale }).await() const fiber = ctx.plugin({ inject: [...inject], apply }) diff --git a/packages/client/ui-layout/tests/apply.spec.ts b/packages/client/ui-layout/tests/apply.spec.ts index f1a9115059..9c0b42035e 100644 --- a/packages/client/ui-layout/tests/apply.spec.ts +++ b/packages/client/ui-layout/tests/apply.spec.ts @@ -7,6 +7,7 @@ // coverage gate still requires exercised. import { Context } from '@deepseek-ai/cordis' +import { stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' import { beforeEach, describe, expect, it, vi } from 'vitest' import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' @@ -26,6 +27,9 @@ async function bench() { // seam for persistence; model this bench as a remote, memory-only browser. ctx.provide('locale', new LocaleService(ctx)) ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) + // ui-theme's Appearance row binds a durable scope through these two. + ctx.provide('remote', { $on: () => () => {} } as never) + ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) await ctx.plugin({ inject: themeInject, apply: themeApply }).await() await slotsFiber.await() return { ctx, slots: ctx.get('slots') as SlotsService } diff --git a/packages/client/ui-models/tests/apply.spec.ts b/packages/client/ui-models/tests/apply.spec.ts index e077441fe0..a5aa39e281 100644 --- a/packages/client/ui-models/tests/apply.spec.ts +++ b/packages/client/ui-models/tests/apply.spec.ts @@ -19,7 +19,7 @@ async function bench() { const locale = new LocaleService(ctx) ctx.provide('locale', locale) // The plugins inject `remote`; forwarded events reach them through the - // same `remote/host-event` signal the connection sink republishes. + // same `$dispatch` handoff the connection sink makes. new TestRemote(ctx) // The apply path only captures the wire face; no call leaves this fake // until a section actually loads. @@ -138,8 +138,8 @@ describe('pushed invalidations', () => { declare(b.slots) await b.ctx.plugin({ inject: [...inject], apply }).await() // The fake wire face has no methods: a fetch attempt would throw. - b.ctx.emit('remote/host-event', 'settings/document-updated', ['llm-pi-ai', 1]) - b.ctx.emit('remote/host-event', 'credentials/updated', ['OPENAI_API_KEY']) + b.ctx.remote.$dispatch('settings/document-updated', ['llm-pi-ai', 1]) + b.ctx.remote.$dispatch('credentials/updated', ['OPENAI_API_KEY']) b.ctx.emit('models/changed') b.ctx.emit('connection/reset') }) @@ -170,7 +170,7 @@ describe('pushed invalidations', () => { )() injected.controller.store.update((state) => { state.status = 'ready' }) const load = vi.spyOn(injected.controller, 'load').mockResolvedValue() - b.ctx.emit('remote/host-event', 'credentials/updated', ['DEEPSEEK_API_KEY']) + b.ctx.remote.$dispatch('credentials/updated', ['DEEPSEEK_API_KEY']) expect(load).toHaveBeenCalledTimes(1) }) }) diff --git a/packages/client/ui-permission/package.json b/packages/client/ui-permission/package.json index f7391e2c3f..01e25b0d0a 100644 --- a/packages/client/ui-permission/package.json +++ b/packages/client/ui-permission/package.json @@ -36,7 +36,8 @@ "@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-runtime", "@deepseek-ai/dsh-client-ui-command", - "@deepseek-ai/dsh-api-remotes" + "@deepseek-ai/dsh-api-remotes", + "@deepseek-ai/dsh-client-ui-settings" ], "platform": "web" } @@ -55,6 +56,7 @@ "@deepseek-ai/dsh-client-schema-form": "workspace:^", "@deepseek-ai/dsh-client-ui-command": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", + "@deepseek-ai/dsh-client-ui-settings": "workspace:^", "@deepseek-ai/dsh-client-ui-slash": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", @@ -62,6 +64,7 @@ "react": "^18.2.0" }, "devDependencies": { + "@deepseek-ai/cordis": "^4.0.0-rc.7", "@deepseek-ai/dsh-api-remotes": "workspace:^", "@deepseek-ai/dsh-client-connection": "workspace:^", "@deepseek-ai/dsh-client-locale": "workspace:^", @@ -70,6 +73,7 @@ "@deepseek-ai/dsh-client-test-runtime": "workspace:^", "@deepseek-ai/dsh-client-ui-command": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", + "@deepseek-ai/dsh-client-ui-settings": "workspace:^", "@deepseek-ai/dsh-client-ui-slash": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-client-web-react": "workspace:^", diff --git a/packages/client/ui-permission/src/client/index.ts b/packages/client/ui-permission/src/client/index.ts index d055b10abe..1e9a9486c0 100644 --- a/packages/client/ui-permission/src/client/index.ts +++ b/packages/client/ui-permission/src/client/index.ts @@ -16,6 +16,8 @@ import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client' // Type-only: pulls the locale plugin's Context merge (ctx.locale). import type {} from '@deepseek-ai/dsh-client-locale/client' +// Type-only: the settings slot types (this package registers a General row). +import type {} from '@deepseek-ai/dsh-client-ui-settings/client' // Type-only: pulls the ctx.remote merge and the forwarded-event key face // (the settings invalidation rides the allowlist) into this program. import type {} from '@deepseek-ai/dsh-api-remotes/client' diff --git a/packages/client/ui-permission/tests/browser-plugin.spec.ts b/packages/client/ui-permission/tests/browser-plugin.spec.ts index 7b6f5e2a19..63bc9e2133 100644 --- a/packages/client/ui-permission/tests/browser-plugin.spec.ts +++ b/packages/client/ui-permission/tests/browser-plugin.spec.ts @@ -39,7 +39,7 @@ async function bench() { locale.setLocale('en') ctx.provide('locale', locale) // The plugin injects `remote`; forwarded events reach it through the same - // `remote/host-event` signal the connection sink republishes. + // `$dispatch` handoff the connection sink makes. new TestRemote(ctx) ctx.slots.register({ name: 'root', @@ -162,8 +162,8 @@ describe('ui-permission browser plugin', () => { it('disposal removes the decoration (HMR safety)', async () => { const b = await bench() expect(b.decoration()).toBeDefined() - b.ctx.emit('remote/host-event', 'settings/document-updated', ['another', 1]) - b.ctx.emit('remote/host-event', 'settings/document-updated', ['permission', 1]) + b.ctx.remote.$dispatch('settings/document-updated', ['another', 1]) + b.ctx.remote.$dispatch('settings/document-updated', ['permission', 1]) b.ctx.emit('connection/reset') await b.fiber.dispose() expect(b.decoration()).toBeUndefined() diff --git a/packages/client/ui-permission/tsconfig.json b/packages/client/ui-permission/tsconfig.json index 987c3e429f..18cb5ff3da 100644 --- a/packages/client/ui-permission/tsconfig.json +++ b/packages/client/ui-permission/tsconfig.json @@ -46,6 +46,9 @@ }, { "path": "../../api/remotes/tsconfig.client.json" + }, + { + "path": "../ui-settings" } ] } diff --git a/packages/client/ui-settings-general/README.i18n.yaml b/packages/client/ui-settings-general/README.i18n.yaml index 8d7a5f1d4b..87ff4b0946 100644 --- a/packages/client/ui-settings-general/README.i18n.yaml +++ b/packages/client/ui-settings-general/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/client/ui-settings-general/README.md -README.md: ab27e073dc76335efc619f56365d1705007f7ef2 -README.zh.md: 16ff5604bee5425569b783e27a29699344f630e3 +README.md: 93bb2b04982a0e7bc8a248c1127428aec509773f +README.zh.md: 5cf5da2f92a0b5a1f53f3306665c3c84381a0e85 diff --git a/packages/client/ui-settings-general/package.json b/packages/client/ui-settings-general/package.json index 8dc142fd91..89e5fcfa0d 100644 --- a/packages/client/ui-settings-general/package.json +++ b/packages/client/ui-settings-general/package.json @@ -36,7 +36,8 @@ "@deepseek-ai/dsh-client-ui-settings", "@deepseek-ai/dsh-client-locale", "@deepseek-ai/dsh-client-connection", - "@deepseek-ai/dsh-api-remotes" + "@deepseek-ai/dsh-api-remotes", + "@deepseek-ai/dsh-client-ui-sidebar" ], "platform": "web" } @@ -48,19 +49,21 @@ "license": "BSD-3-Clause", "dependencies": { "@deepseek-ai/dsh-settings": "workspace:^", - "@deepseek-ai/schemastery": "workspace:^" + "@deepseek-ai/schemastery": "workspace:^", + "clsx": "^2.0.0" }, "peerDependencies": { - "@deepseek-ai/cordis": "workspace:^", "@deepseek-ai/dsh-api-remotes": "workspace:^", "@deepseek-ai/dsh-client-connection": "workspace:^", "@deepseek-ai/dsh-client-locale": "workspace:^", "@deepseek-ai/dsh-client-runtime": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", "@deepseek-ai/dsh-client-ui-settings": "workspace:^", + "@deepseek-ai/dsh-client-ui-sidebar": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-client-web-react": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/cordis": "workspace:^", "react": "^18.2.0" }, "devDependencies": { @@ -71,11 +74,12 @@ "@deepseek-ai/dsh-client-test-runtime": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", "@deepseek-ai/dsh-client-ui-settings": "workspace:^", + "@deepseek-ai/dsh-client-ui-sidebar": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-client-web-react": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", - "@types/react": "~18.3.1", "@deepseek-ai/cordis": "workspace:^", + "@types/react": "~18.3.1", "react": "^18.2.0" }, "files": [ diff --git a/packages/client/ui-settings/src/client/SettingsRoot.module.css b/packages/client/ui-settings-general/src/client/SettingsRoot.module.css similarity index 100% rename from packages/client/ui-settings/src/client/SettingsRoot.module.css rename to packages/client/ui-settings-general/src/client/SettingsRoot.module.css diff --git a/packages/client/ui-settings/src/client/SettingsRoot.tsx b/packages/client/ui-settings-general/src/client/SettingsRoot.tsx similarity index 99% rename from packages/client/ui-settings/src/client/SettingsRoot.tsx rename to packages/client/ui-settings-general/src/client/SettingsRoot.tsx index de00fa372e..795b9f1308 100644 --- a/packages/client/ui-settings/src/client/SettingsRoot.tsx +++ b/packages/client/ui-settings-general/src/client/SettingsRoot.tsx @@ -16,7 +16,7 @@ import clsx from 'clsx' import { IconAgentPresetOutline16, IconCloseOutline16, IconDataOutline16, IconSettingsOutline16, } from '@deepseek-ai/dsh-client-ui-primitives' -import type { SettingsRootComponentProps, SettingsSectionRow } from './contract/slots.ts' +import type { SettingsRootComponentProps, SettingsSectionRow } from './shell-contract.ts' import css from './SettingsRoot.module.css' /** Nav glyph by section id; unknown ids fall back to the settings gear. */ diff --git a/packages/client/ui-settings-general/src/client/index.ts b/packages/client/ui-settings-general/src/client/index.ts index d4c1aa0ca5..947e278f99 100644 --- a/packages/client/ui-settings-general/src/client/index.ts +++ b/packages/client/ui-settings-general/src/client/index.ts @@ -7,14 +7,21 @@ */ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client' import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client' +import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots' import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react' -// Type-only: pulls the shell's SlotMap merges (trigger/header/section/item). +// Type-only: the settings slot declarations plus the ctx.settingsScope Context +// merge. Cross-plugin collaboration goes through the service, never a value +// import (client bundle purity gate). import type {} from '@deepseek-ai/dsh-client-ui-settings/client' -// Type-only: pulls ctx.locale and the 'settings.general.item' SlotMap merge. +// Type-only: pulls ctx.locale into this program. import type {} from '@deepseek-ai/dsh-client-locale/client' // Type-only: pulls the ctx.remote merge and the forwarded-event key face // (the settings invalidation rides the allowlist) into this program. import type {} from '@deepseek-ai/dsh-api-remotes/client' +import type { + SettingsOnboardingStep, SettingsRootInjected, SettingsSectionRow, +} from './shell-contract.ts' +import { SettingsRoot } from './SettingsRoot.tsx' import { CloseLabel, HeaderContent, TriggerContent } from './chrome.tsx' import { GeneralSection } from './GeneralSection.tsx' import { SettingsDocumentAction } from './SettingsDocumentAction.tsx' @@ -99,6 +106,77 @@ export function apply(ctx: ClientContext): void { ] return () => { for (const dispose of disposers) dispose() } }, 'ui-settings-general: metadata invalidations') + // The settings shell: this package occupies the sidebar-owned hole and + // declares the settings slots. Ledger → nav-row projection as an observable + // source (uSES contract: getSnapshot returns the cached rows until the + // ledger version moves). Labels may be locale-following thunks, so the cache + // key includes the locale revision and subscribers ride both sources. + let rowsVersion = -1 + let rowsRevision = -1 + let rows: readonly SettingsSectionRow[] = [] + let onboardingVersion = -1 + let onboardingSteps: readonly SettingsOnboardingStep[] = [] + const shellInjected = (): SettingsRootInjected => ({ + hooks: { + sections: { + getSnapshot: () => { + const version = ctx.slots.getVersion('settings.section') + const revision = ctx.locale.getSnapshot().revision + if (version !== rowsVersion || revision !== rowsRevision) { + rowsVersion = version + rowsRevision = revision + rows = ctx.slots.entries('settings.section') + .map(e => ({ + /* v8 ignore next -- list-slot registration requires id (SlotCore rejects an entry without one) */ + id: e.options.id ?? '', + order: e.options.order ?? 0, + label: resolveSlotLabel(e.options.label) ?? '', + })) + .sort((a, b) => a.order - b.order) + } + return rows + }, + subscribe: (listener) => { + const offLedger = ctx.slots.subscribe('settings.section', listener) + const offLocale = ctx.locale.subscribe(listener) + return () => { + offLedger() + offLocale() + } + }, + }, + onboardingSteps: { + getSnapshot: () => { + const version = ctx.slots.getVersion('settings.onboarding') + if (version !== onboardingVersion) { + onboardingVersion = version + onboardingSteps = ctx.slots.entries('settings.onboarding') + .map(e => ({ + /* v8 ignore next -- list-slot registration requires id */ + id: e.options.id ?? '', + order: e.options.order ?? 0, + })) + .sort((a, b) => a.order - b.order) + } + return onboardingSteps + }, + subscribe: listener => ctx.slots.subscribe('settings.onboarding', listener), + }, + }, + }) + ctx.slots.inject('sidebar.settings', () => ctx.slots.register({ + name: 'sidebar.settings', + children: { + 'settings.trigger': { kind: 'single', scope: 'root' }, + 'settings.header': { kind: 'single', scope: 'root' }, + 'settings.action': { kind: 'list', scope: 'root' }, + 'settings.close': { kind: 'single', scope: 'root' }, + 'settings.section': { kind: 'list', scope: 'root' }, + 'settings.onboarding': { kind: 'list', scope: 'root' }, + }, + inject: shellInjected, + }, SettingsRoot)) + ctx.slots.inject('settings.trigger', () => ctx.slots.register({ name: 'settings.trigger', locale: NS }, TriggerContent)) ctx.slots.inject('settings.header', () => diff --git a/packages/client/ui-settings-general/src/client/shell-contract.ts b/packages/client/ui-settings-general/src/client/shell-contract.ts new file mode 100644 index 0000000000..ad61b2111a --- /dev/null +++ b/packages/client/ui-settings-general/src/client/shell-contract.ts @@ -0,0 +1,59 @@ +/** + * Settings shell contract — the types of the `sidebar.settings` occupant this + * package renders. They live here rather than in ui-settings because they + * reference the sidebar's own slot type: ui-settings is the settings domain's + * base layer and must not depend on any `ui-*` presentation package, or the + * reference graph closes a cycle through ui-sidebar → ui-layout → ui-theme. + * The settings SLOT types (what registrants contribute) stay in ui-settings. + */ +import type { HostObservable, InjectFace, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' +// Type-only: pulls ui-sidebar's SlotMap merge (the 'sidebar.settings' entry) +// into every program that sees this contract. +import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client' +// Type-only: pulls the settings slot declarations the shell renders into. +import type {} from '@deepseek-ai/dsh-client-ui-settings/client' + +/** One nav row projected from a settings.section registration's options. */ +export interface SettingsSectionRow { + id: string + order: number + label: string +} + +/** One ordered onboarding step projected from a slot registration. */ +export interface SettingsOnboardingStep { + id: string + order: number +} + +/** + * Registrant-private injected share of the settings shell (assembled in + * apply): the ledger's nav-row projection as a hooks-compartment source — + * the shell reads no locale state and subscribes through the bound hook. + */ +export type SettingsRootInjected = { + hooks: { + /** settings.section ledger projected into ordered nav rows. */ + sections: HostObservable + /** settings.onboarding ledger projected into coordinator order. */ + onboardingSteps: HostObservable + } +} + +/** + * Full component props of the settings shell root: the sidebar owner share + * (wide/rail state) plus the declared render shares and the injected face + * (hooks compartment bound to useSections). No store is registered — modal + * open state and active section id are component-local viewing state. + */ +export type SettingsRootComponentProps = + PropsRuntime<'sidebar.settings'> + & PropsRenderSlots< + | 'settings.trigger' + | 'settings.header' + | 'settings.action' + | 'settings.close' + | 'settings.section' + | 'settings.onboarding' + > + & InjectFace diff --git a/packages/client/ui-settings-general/tests/apply.spec.ts b/packages/client/ui-settings-general/tests/apply.spec.ts index 98db5699df..3a2c333cfc 100644 --- a/packages/client/ui-settings-general/tests/apply.spec.ts +++ b/packages/client/ui-settings-general/tests/apply.spec.ts @@ -34,7 +34,7 @@ async function bench(isLoopback = true) { const locale = new LocaleService(ctx) ctx.provide('locale', locale) // The plugins inject `remote`; forwarded events reach them through the - // same `remote/host-event` signal the connection sink republishes. + // same `$dispatch` handoff the connection sink makes. new TestRemote(ctx) const settingsDescribe = vi.fn(() => Promise.resolve({ rpcId: 'settings-general' as never, @@ -170,9 +170,9 @@ describe('ui-settings-general apply', () => { const { controller } = (entry.inject as unknown as () => WelcomeNoticeInjected)() await controller.load() expect(b.settingsDescribe).toHaveBeenCalledOnce() - b.ctx.emit('remote/host-event', 'settings/document-updated', ['unrelated', 1]) + b.ctx.remote.$dispatch('settings/document-updated', ['unrelated', 1]) expect(b.settingsDescribe).toHaveBeenCalledOnce() - b.ctx.emit('remote/host-event', 'settings/document-updated', [WELCOME_NOTICE_SETTINGS_NAMESPACE, 1]) + b.ctx.remote.$dispatch('settings/document-updated', [WELCOME_NOTICE_SETTINGS_NAMESPACE, 1]) await vi.waitFor(() => { expect(b.settingsDescribe).toHaveBeenCalledTimes(2) }) b.ctx.emit('connection/reset') await vi.waitFor(() => { expect(b.settingsDescribe).toHaveBeenCalledTimes(3) }) diff --git a/packages/client/ui-settings/tests/settings-root.spec.tsx b/packages/client/ui-settings-general/tests/settings-root.spec.tsx similarity index 99% rename from packages/client/ui-settings/tests/settings-root.spec.tsx rename to packages/client/ui-settings-general/tests/settings-root.spec.tsx index 1f34a47cf5..a95e8e6533 100644 --- a/packages/client/ui-settings/tests/settings-root.spec.tsx +++ b/packages/client/ui-settings-general/tests/settings-root.spec.tsx @@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { useEffect, useState } from 'react' import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' -import type { SettingsRootComponentProps } from '../src/client/contract/slots.ts' +import type { SettingsRootComponentProps } from '../src/client/shell-contract.ts' import { SettingsRoot } from '../src/client/SettingsRoot.tsx' afterEach(cleanup) diff --git a/packages/client/ui-settings/tests/apply.spec.ts b/packages/client/ui-settings-general/tests/shell.spec.ts similarity index 83% rename from packages/client/ui-settings/tests/apply.spec.ts rename to packages/client/ui-settings-general/tests/shell.spec.ts index 2e8243fa26..8515d79423 100644 --- a/packages/client/ui-settings/tests/apply.spec.ts +++ b/packages/client/ui-settings-general/tests/shell.spec.ts @@ -2,13 +2,26 @@ import { Context } from '@deepseek-ai/cordis' import { describe, expect, it, vi } from 'vitest' import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client' -import { apply, inject } from '@deepseek-ai/dsh-client-ui-settings/client' -import type { SettingsRootInjected } from '@deepseek-ai/dsh-client-ui-settings/client' +import { apply, inject } from '../src/client/index.ts' +import type { SettingsRootInjected } from '../src/client/shell-contract.ts' import { SettingsRoot } from '../src/client/SettingsRoot.tsx' async function bench() { const ctx = new Context() await ctx.plugin(SlotsService).await() + // Copy machinery the shell only reads a revision from; the real locale + // plugin would drag its own settings-row dependencies into this bench. + ctx.provide('locale', { + register: () => () => {}, + bind: () => (key: string) => key, + getSnapshot: () => ({ active: 'zh', locales: [], revision: 0 }), + subscribe: () => () => {}, + } as never) + ctx.provide('connection', { + api: { settings: { describe: async () => ({ result: { ok: false } }) } }, + isLoopback: false, + } as never) + ctx.provide('remote', { $on: () => () => {} } as never) return { ctx, slots: ctx.get('slots') as SlotsService } } @@ -36,7 +49,7 @@ const CHILD_SPECS = { describe('ui-settings apply', () => { it('declares only the slot registry (a pure composition face, no locale)', () => { - expect(inject).toEqual(['slots']) + expect(inject).toEqual(['slots', 'locale', 'connection', 'remote']) }) it('registers the shell and declares every child slot, before or after the declaration', async () => { @@ -63,13 +76,16 @@ describe('ui-settings apply', () => { declare(b.slots) await b.ctx.plugin({ inject: [...inject], apply }).await() const { sections } = injectedOf(b.slots).hooks - // The shell ships no sections of its own — registrants fill the ledger. - expect(sections.getSnapshot()).toEqual([]) + // This package registers the General section itself; every other section + // arrives from a feature registrant. + const GENERAL = { id: 'general', order: 0, label: 'general.nav' } + expect(sections.getSnapshot()).toEqual([GENERAL]) b.slots.register({ name: 'settings.section', id: 'z', order: 20, label: 'Z' } as never, () => null) // No order and no label: both projection defaults apply. b.slots.register({ name: 'settings.section', id: 'a' } as never, () => null) const rows = sections.getSnapshot() expect(rows).toEqual([ + GENERAL, { id: 'a', order: 0, label: '' }, { id: 'z', order: 20, label: 'Z' }, ]) @@ -94,6 +110,8 @@ describe('ui-settings apply', () => { b.slots.register({ name: 'settings.onboarding', id: 'default-order' } as never, () => null) const steps = onboardingSteps.getSnapshot() expect(steps).toEqual([ + // This package's own onboarding page, registered by the same apply. + { id: 'welcome-notice', order: -100 }, { id: 'welcome', order: -100 }, { id: 'credential', order: 0 }, { id: 'default-order', order: 0 }, diff --git a/packages/client/ui-settings-general/tsconfig.json b/packages/client/ui-settings-general/tsconfig.json index 07f5269510..a4535df785 100644 --- a/packages/client/ui-settings-general/tsconfig.json +++ b/packages/client/ui-settings-general/tsconfig.json @@ -40,6 +40,9 @@ }, { "path": "../../api/remotes/tsconfig.client.json" + }, + { + "path": "../ui-sidebar" } ] } diff --git a/packages/client/ui-settings/package.json b/packages/client/ui-settings/package.json index 3447573398..d281c7d724 100644 --- a/packages/client/ui-settings/package.json +++ b/packages/client/ui-settings/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-client-ui-settings", - "description": "Settings shell plugin: sidebar trigger, modal panel, feature sections, and an ordered full-page onboarding stage", + "description": "Settings domain base plugin: the settings-namespace scope service and the canonical settings slot-type contract", "version": "0.0.1-rc.1", "publishConfig": { "access": "restricted" @@ -32,8 +32,9 @@ "dsh": { "client": { "inject": [ + "@deepseek-ai/dsh-client-connection", "@deepseek-ai/dsh-client-runtime", - "@deepseek-ai/dsh-client-ui-sidebar" + "@deepseek-ai/dsh-api-remotes" ], "platform": "web" } @@ -43,30 +44,32 @@ "watch": "tsdown --watch" }, "license": "BSD-3-Clause", - "dependencies": { - "clsx": "^2.0.0" - }, "peerDependencies": { + "@deepseek-ai/cordis": "workspace:^", + "@deepseek-ai/dsh-api-gateway": "workspace:^", + "@deepseek-ai/dsh-api-remotes": "workspace:^", + "@deepseek-ai/dsh-client-connection": "workspace:^", "@deepseek-ai/dsh-client-runtime": "workspace:^", - "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", + "@deepseek-ai/dsh-client-schema-form": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", - "@deepseek-ai/cordis": "workspace:^", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "@deepseek-ai/dsh-settings": "workspace:^", + "react": "^18.2.0" }, "devDependencies": { - "@deepseek-ai/dsh-client-locale": "workspace:^", + "@deepseek-ai/cordis": "^4.0.0-rc.7", + "@deepseek-ai/dsh-api-gateway": "workspace:^", + "@deepseek-ai/dsh-api-remotes": "workspace:^", + "@deepseek-ai/dsh-client-connection": "workspace:^", "@deepseek-ai/dsh-client-runtime": "workspace:^", - "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", - "@deepseek-ai/dsh-client-ui-sidebar": "workspace:^", + "@deepseek-ai/dsh-client-schema-form": "workspace:^", + "@deepseek-ai/dsh-client-test-runtime": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", - "@types/react-dom": "~18.3.0", + "@deepseek-ai/dsh-settings": "workspace:^", "@types/react": "~18.3.1", "@deepseek-ai/cordis": "workspace:^", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "react": "^18.2.0" }, "files": [ "lib/index.js", diff --git a/packages/client/ui-settings/src/client/contract/slots.ts b/packages/client/ui-settings/src/client/contract/slots.ts index 4158ee6c66..903e1b0225 100644 --- a/packages/client/ui-settings/src/client/contract/slots.ts +++ b/packages/client/ui-settings/src/client/contract/slots.ts @@ -7,10 +7,7 @@ * setting never means editing the shell; copy that belongs to no single * feature (chrome, the General section) is owned by ui-settings-general. */ -import type { HostObservable, InjectFace, PropsRenderSlots, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots' -// Type-only: pulls ui-sidebar's SlotMap merge (the 'sidebar.settings' entry) -// into every program that sees this contract. -import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client' + declare module '@deepseek-ai/dsh-client-ui-slots' { interface SlotMap { @@ -66,9 +63,26 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { * would render without mask or stage). */ 'settings.onboarding': { kind: 'list'; scope: 'root'; owner: SettingsOnboardingOwnerProps } + /** + * One preference row inside the General section, contributed by the + * feature plugin that owns the preference (locale → Language, ui-theme → + * Appearance, ui-conversation → Composer Enter). Options: `id` (row key), + * `order` (row position). Rows draw their own internals; the section + * column only stacks them. Declared at runtime by ui-settings-general's + * General entry — the type lives here with every other settings slot type, + * because this package is the settings domain's base layer and every + * registrant already depends on it for `ctx.settingsScope`. + */ + 'settings.general.item': { kind: 'list'; scope: 'root'; owner: SettingsGeneralItemOwnerProps } } } +/** Owner share of a General preference row (the section supplies nothing). */ +export interface SettingsGeneralItemOwnerProps { + /** Marker field: item owner props are intentionally empty. */ + children?: never +} + /** Owner share of the trigger content seat: the sidebar column state. */ export interface SettingsTriggerOwnerProps { /** Whether the sidebar renders wide content (false = 56px rail, icon only). */ @@ -103,47 +117,3 @@ export interface SettingsOnboardingOwnerProps { openSection: (id: string) => void } -/** One nav row projected from a settings.section registration's options. */ -export interface SettingsSectionRow { - id: string - order: number - label: string -} - -/** One ordered onboarding step projected from a slot registration. */ -export interface SettingsOnboardingStep { - id: string - order: number -} - -/** - * Registrant-private injected share of the settings shell (assembled in - * apply): the ledger's nav-row projection as a hooks-compartment source — - * the shell reads no locale state and subscribes through the bound hook. - */ -export type SettingsRootInjected = { - hooks: { - /** settings.section ledger projected into ordered nav rows. */ - sections: HostObservable - /** settings.onboarding ledger projected into coordinator order. */ - onboardingSteps: HostObservable - } -} - -/** - * Full component props of the settings shell root: the sidebar owner share - * (wide/rail state) plus the declared render shares and the injected face - * (hooks compartment bound to useSections). No store is registered — modal - * open state and active section id are component-local viewing state. - */ -export type SettingsRootComponentProps = - PropsRuntime<'sidebar.settings'> - & PropsRenderSlots< - | 'settings.trigger' - | 'settings.header' - | 'settings.action' - | 'settings.close' - | 'settings.section' - | 'settings.onboarding' - > - & InjectFace diff --git a/packages/client/ui-settings/src/client/index.ts b/packages/client/ui-settings/src/client/index.ts index 01c175954e..30f0c41af1 100644 --- a/packages/client/ui-settings/src/client/index.ts +++ b/packages/client/ui-settings/src/client/index.ts @@ -1,111 +1,32 @@ /** - * Settings shell plugin, browser half. A pure composition face: occupies the - * sidebar-owned `sidebar.settings` hole with the trigger chrome + modal - * panel, declares its chrome, section, and onboarding slots, and projects the - * section ledger into panel navigation. The shell ships no copy; it reads the - * optional locale revision only to resolve registrant-owned nav-label thunks. - * ui-settings-general owns the chrome and General content; features own their - * rows, sections, and onboarding pages. Export discipline: packages/client/AGENTS.md. + * Settings domain base plugin, browser half. Provides `ctx.settingsScope`, the + * settings-namespace Host transport every preference row binds its durable + * section through, and owns the canonical slot-type contract for the settings + * surface. It depends on no `ui-*` presentation package, so any feature that + * owns a preference can reach it: the settings SHELL — the `sidebar.settings` + * occupant, its navigation, and the chrome — lives in ui-settings-general, + * because a shell dependency on ui-sidebar would close a reference cycle + * through ui-layout and ui-theme. Export discipline: packages/client/AGENTS.md. */ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client' -// Type-only: the ctx.locale Context merge for the optional ctx.get('locale') -// read (nav labels may be locale-following thunks; the shell still ships no -// copy of its own and takes no hard locale dependency). -import type {} from '@deepseek-ai/dsh-client-locale/client' -import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots' -import type { - SettingsOnboardingStep, SettingsRootInjected, SettingsSectionRow, -} from './contract/slots.ts' -import { SettingsRoot } from './SettingsRoot.tsx' +import { SettingsScopeService } from './settings-scope.ts' export type { - SettingsHeaderOwnerProps, SettingsRootComponentProps, SettingsRootInjected, - SettingsOnboardingOwnerProps, SettingsOnboardingStep, SettingsSectionOwnerProps, - SettingsSectionRow, SettingsTriggerOwnerProps, + SettingsGeneralItemOwnerProps, SettingsHeaderOwnerProps, SettingsOnboardingOwnerProps, + SettingsSectionOwnerProps, SettingsTriggerOwnerProps, } from './contract/slots.ts' +export { SettingsScopeController, SettingsScopeService } from './settings-scope.ts' /** - * Required services (cordis fiber inject). The target slot is declared by - * ui-sidebar's apply, whose activation order relative to this one is NOT - * constrained (dsh.client.inject edges are informational); registration - * depends on the slot through `slots.inject()`. + * Required services: none. The transport is resolved per caller through + * `this.ctx` at `bind` time, so this plugin waits for nothing. */ -export const inject = ['slots'] +export const inject = [] /** - * Register the settings shell into `sidebar.settings` once the declaration is - * on the ledger. + * Provide the settings-namespace scope service. * @param ctx - client root context. */ export function apply(ctx: ClientContext): void { - // Ledger → nav-row projection as an observable source (uSES contract: - // getSnapshot returns the cached rows until the ledger version moves). - // Labels may be locale-following thunks, so the cache key includes the - // locale revision and subscribers ride both sources. - let rowsVersion = -1 - let rowsRevision = -1 - let rows: readonly SettingsSectionRow[] = [] - let onboardingVersion = -1 - let onboardingSteps: readonly SettingsOnboardingStep[] = [] - const localeRevision = (): number => ctx.get('locale')?.getSnapshot().revision ?? 0 - const injected = (): SettingsRootInjected => ({ - hooks: { - sections: { - getSnapshot: () => { - const version = ctx.slots.getVersion('settings.section') - const revision = localeRevision() - if (version !== rowsVersion || revision !== rowsRevision) { - rowsVersion = version - rowsRevision = revision - rows = ctx.slots.entries('settings.section') - .map(e => ({ - /* v8 ignore next -- list-slot registration requires id (SlotCore rejects an entry without one) */ - id: e.options.id ?? '', - order: e.options.order ?? 0, - label: resolveSlotLabel(e.options.label) ?? '', - })) - .sort((a, b) => a.order - b.order) - } - return rows - }, - subscribe: (listener) => { - const offLedger = ctx.slots.subscribe('settings.section', listener) - const offLocale = ctx.get('locale')?.subscribe(listener) - return () => { - offLedger() - offLocale?.() - } - }, - }, - onboardingSteps: { - getSnapshot: () => { - const version = ctx.slots.getVersion('settings.onboarding') - if (version !== onboardingVersion) { - onboardingVersion = version - onboardingSteps = ctx.slots.entries('settings.onboarding') - .map(e => ({ - /* v8 ignore next -- list-slot registration requires id */ - id: e.options.id ?? '', - order: e.options.order ?? 0, - })) - .sort((a, b) => a.order - b.order) - } - return onboardingSteps - }, - subscribe: listener => ctx.slots.subscribe('settings.onboarding', listener), - }, - }, - }) - ctx.slots.inject('sidebar.settings', () => ctx.slots.register({ - name: 'sidebar.settings', - children: { - 'settings.trigger': { kind: 'single', scope: 'root' }, - 'settings.header': { kind: 'single', scope: 'root' }, - 'settings.action': { kind: 'list', scope: 'root' }, - 'settings.close': { kind: 'single', scope: 'root' }, - 'settings.section': { kind: 'list', scope: 'root' }, - 'settings.onboarding': { kind: 'list', scope: 'root' }, - }, - inject: injected, - }, SettingsRoot)) + ctx.provide('settingsScope', new SettingsScopeService(ctx)) } diff --git a/packages/client/runtime/src/client/settings-scope.ts b/packages/client/ui-settings/src/client/settings-scope.ts similarity index 62% rename from packages/client/runtime/src/client/settings-scope.ts rename to packages/client/ui-settings/src/client/settings-scope.ts index cb7e933406..7af1c1eb97 100644 --- a/packages/client/runtime/src/client/settings-scope.ts +++ b/packages/client/ui-settings/src/client/settings-scope.ts @@ -1,66 +1,36 @@ -/** Host-backed settings-namespace synchronization for browser plugins. */ +/** + * Host transport for the settings-namespace scope contract. The contract types + * live in `dsh-client-runtime` (the common dependency of every feature that + * owns a preference); this file owns the wire behavior and the invalidation + * subscription, both of which are Settings-surface concerns. + */ +import { Service } from '@deepseek-ai/cordis' import type { Context } from '@deepseek-ai/cordis' import type { ConnectionHandle, IApiClient, SettingsNamespaceView, } from '@deepseek-ai/dsh-client-connection/client' import { rehydrateSchema, validateDraft } from '@deepseek-ai/dsh-client-schema-form' -import { createSnapshotStore, type SnapshotStore } from './contract/store.ts' - -/** Client-side sync state of one settings namespace. */ -export interface SettingsScopeSnapshot { - /** - * `loading` until the first accepted section, `ready` while one stands, and - * `unavailable` when the namespace is not exposed to this client or the - * connection keeps preferences process-local (memory mode). - */ - status: 'loading' | 'ready' | 'unavailable' - /** Last accepted schema-resolved section; undefined before the first acceptance. */ - value: T | undefined - /** Namespace revision fencing the next write; undefined before the first Host view. */ - revision: number | undefined - /** Whether the Host document accepts writes; memory mode never does. */ - writable: boolean - /** `host` syncs with the Host document; `memory` keeps a remote browser process-local. */ - mode: 'host' | 'memory' -} - -/** Domain-owned description of one settings namespace consumed by a browser plugin. */ -export interface SettingsScopeSpec { - /** Settings namespace registered by the owning Host plugin. */ - namespace: string - /** - * Narrow one wire section; undefined keeps the last accepted value. The - * default validates the section against the namespace's own serialized wire - * schema, so domains add a decoder only to narrow beyond that schema. - */ - decode?: (section: unknown) => T | undefined -} - -/** - * Reactive owner handle over one namespace's durable section — the browser - * mirror of the Host-side `SettingsScope` owner seam. Domain services read - * and observe the snapshot and route explicit user choices through `set`. - */ -export interface SettingsScope { - /** @returns the current sync snapshot (stable reference until the next change). */ - getSnapshot(): SettingsScopeSnapshot - /** - * Observe snapshot replacements. - * @param listener - invoked after each snapshot change. - * @returns the disposer removing this listener. - */ - subscribe(listener: () => void): () => void - /** - * Queue one field write. Rapid writes preserve mutation order, each carries - * the latest known namespace revision, and only the latest settlement may - * publish; a rejected or failed latest write reloads Host state instead. - * @param field - scalar field inside the namespace section. - * @param value - JSON-shaped value selected by the user. - * @returns settlement after the write and any latest-write recovery read. - */ - set(field: string, value: unknown): Promise -} +import { + createSnapshotStore, type SettingsScope, type SettingsScopeSnapshot, + type SettingsScopeSpec, type SnapshotStore, +} from '@deepseek-ai/dsh-client-runtime/client' +// Type-only, and deliberately NOT `@deepseek-ai/dsh-api-remotes/client`: this +// package is reachable from the Host build graph through its feature-package +// callers, and api-remotes' Client face imports a Host-tsdown-generated +// `/remote` artifact, which would deadlock the Host tsc phase. The gateway's +// Client half declares `ctx.remote` with no generated import, and the +// allowlist's `types` subpath is a pure-type source file, so the pair supplies +// `$on` and its key face without dragging a build artifact in. The runtime +// `remote` injection belongs to whoever calls bindSettingsScope: the +// subscription is registered on the caller's own context. +import type {} from '@deepseek-ai/dsh-api-gateway/client' +import type {} from '@deepseek-ai/dsh-api-remotes/types' +// The forwarded event's own declaration: `$on`'s key face is +// `Extract`, so the allowlist alone resolves to +// never — the owning package's client-safe, type-only subpath supplies the +// cordis `Events` entry (and with it the branded `SettingsNamespace`). +import type {} from '@deepseek-ai/dsh-settings/types' type SettingsFace = Pick @@ -224,38 +194,60 @@ export class SettingsScopeController implements SettingsScope { } } -/** - * Bind one namespace scope to settings and connection invalidations on the - * caller's plugin lifecycle. Listeners exist before the initial background - * read starts, so activation never blocks on the settings transport. - * @param ctx - owning browser plugin context. - * @param spec - domain-owned namespace contract. - * @returns the bound scope consumed by the domain's services and rows. - */ -export function bindSettingsScope( - ctx: Context, - spec: SettingsScopeSpec, -): SettingsScope { - const connection = ctx.get('connection') as ConnectionHandle - const controller = new SettingsScopeController( - connection.api, - spec, - connection.isLoopback ? 'host' : 'memory', - ) - ctx.effect(() => { - const refresh = (namespace?: string): void => { - if (namespace !== undefined && namespace !== spec.namespace) return - void controller.load() - } - const disposers = [ - ctx.on('settings/changed', refresh), - ctx.on('connection/reset', () => { refresh() }), - ] - void controller.load() - return async () => { - for (const dispose of disposers) dispose() - await controller.dispose() - } - }, `runtime: ${spec.namespace} settings scope`) - return controller +declare module '@deepseek-ai/cordis' { + interface Context { + settingsScope: SettingsScopeService + } +} + +/** + * The settings domain's base service. Features that own a preference reach the + * settings transport through this service rather than a shared function: the + * client bundle purity gate forbids cross-plugin value imports and directs + * cross-plugin collaboration through cordis services + * (`packages/client/tsdown.client.ts`). + */ +export class SettingsScopeService extends Service { + /** + * @param ctx - the providing plugin's context. + */ + constructor(ctx: Context) { + super(ctx, 'settingsScope') + } + + /** + * Bind one namespace scope to settings and connection invalidations on the + * CALLER's plugin lifecycle — the service proxy binds `this.ctx` to the + * caller at call time, so the scope's disposer belongs to the calling fiber. + * Listeners exist before the initial background read starts, so activation + * never blocks on the settings transport. The caller injects `connection` + * for the transport and `remote` for the forwarded settings invalidation. + * @param spec - domain-owned namespace contract. + * @returns the bound scope consumed by the domain's services and rows. + */ + bind(spec: SettingsScopeSpec): SettingsScope { + const ctx = this.ctx + const connection = ctx.get('connection') as ConnectionHandle + const controller = new SettingsScopeController( + connection.api, + spec, + connection.isLoopback ? 'host' : 'memory', + ) + ctx.effect(() => { + const refresh = (namespace?: string): void => { + if (namespace !== undefined && namespace !== spec.namespace) return + void controller.load() + } + const disposers = [ + ctx.remote.$on('settings/document-updated', refresh), + ctx.on('connection/reset', () => { refresh() }), + ] + void controller.load() + return async () => { + for (const dispose of disposers) dispose() + await controller.dispose() + } + }, `ui-settings: ${spec.namespace} settings scope`) + return controller + } } diff --git a/packages/client/runtime/tests/settings-scope.spec.ts b/packages/client/ui-settings/tests/settings-scope.spec.ts similarity index 93% rename from packages/client/runtime/tests/settings-scope.spec.ts rename to packages/client/ui-settings/tests/settings-scope.spec.ts index ae3c1db73c..e6b165e9f3 100644 --- a/packages/client/runtime/tests/settings-scope.spec.ts +++ b/packages/client/ui-settings/tests/settings-scope.spec.ts @@ -2,9 +2,9 @@ import { Context } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import { describe, expect, it, vi } from 'vitest' import type { RpcResponse, SettingsNamespaceView } from '@deepseek-ai/dsh-client-connection/client' -import { - bindSettingsScope, SettingsScopeController, type SettingsScope, -} from '../src/client/settings-scope.ts' +import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime' +import type { SettingsScope } from '@deepseek-ai/dsh-client-runtime/client' +import { SettingsScopeController, SettingsScopeService } from '../src/client/settings-scope.ts' interface UiTestSettings { preference: 'light' | 'dark' | 'system' @@ -295,7 +295,7 @@ describe('SettingsScopeController', () => { }) }) -describe('bindSettingsScope', () => { +describe('SettingsScopeService.bind', () => { it('subscribes before the initial read and converges to the latest queued invalidation', async () => { const initial = deferred>() const describeCall = vi.fn() @@ -308,16 +308,18 @@ describe('bindSettingsScope', () => { isLoopback: true, } as never) let scope!: SettingsScope + new TestRemote(ctx) + await ctx.plugin(SettingsScopeService).await() const fiber = ctx.plugin({ - inject: ['connection'], + inject: ['connection', 'remote', 'settingsScope'], apply: (plugin: Context) => { - scope = bindSettingsScope(plugin, { namespace: 'ui-test' }) + scope = plugin.settingsScope.bind({ namespace: 'ui-test' }) }, }) await fiber.await() await vi.waitFor(() => { expect(describeCall).toHaveBeenCalledOnce() }) - ctx.emit('settings/changed', 'unrelated') - ctx.emit('settings/changed', 'ui-test') + ctx.remote.$dispatch('settings/document-updated', ['unrelated', 0]) + ctx.remote.$dispatch('settings/document-updated', ['ui-test', 0]) ctx.emit('connection/reset') initial.resolve(described({ preference: 'dark' }, 1)) await vi.waitFor(() => { expect(describeCall).toHaveBeenCalledTimes(3) }) @@ -325,7 +327,7 @@ describe('bindSettingsScope', () => { expect(scope.getSnapshot()).toMatchObject({ value: { preference: 'system' }, revision: 3 }) }) await fiber.dispose() - ctx.emit('settings/changed', 'ui-test') + ctx.remote.$dispatch('settings/document-updated', ['ui-test', 0]) await Promise.resolve() expect(describeCall).toHaveBeenCalledTimes(3) }) @@ -338,10 +340,12 @@ describe('bindSettingsScope', () => { isLoopback: false, } as never) let scope!: SettingsScope + new TestRemote(ctx) + await ctx.plugin(SettingsScopeService).await() const fiber = ctx.plugin({ - inject: ['connection'], + inject: ['connection', 'remote', 'settingsScope'], apply: (plugin: Context) => { - scope = bindSettingsScope(plugin, { namespace: 'ui-test' }) + scope = plugin.settingsScope.bind({ namespace: 'ui-test' }) }, }) await fiber.await() @@ -350,3 +354,4 @@ describe('bindSettingsScope', () => { expect(describeCall).not.toHaveBeenCalled() }) }) + diff --git a/packages/client/ui-settings/tsconfig.json b/packages/client/ui-settings/tsconfig.json index c5ed0161c8..193107b1d2 100644 --- a/packages/client/ui-settings/tsconfig.json +++ b/packages/client/ui-settings/tsconfig.json @@ -11,20 +11,23 @@ { "path": "../../../vendor/cordis" }, - { - "path": "../locale" - }, { "path": "../ui-slots" }, - { - "path": "../ui-primitives" - }, { "path": "../runtime" }, { - "path": "../ui-sidebar" + "path": "../connection" + }, + { + "path": "../schema-form" + }, + { + "path": "../../api/gateway" + }, + { + "path": "../../settings/settings" }, { "path": "../../support/invariants" diff --git a/packages/client/ui-subagent/tests/browser-plugin.spec.ts b/packages/client/ui-subagent/tests/browser-plugin.spec.ts index 426dadaa60..1636349ca5 100644 --- a/packages/client/ui-subagent/tests/browser-plugin.spec.ts +++ b/packages/client/ui-subagent/tests/browser-plugin.spec.ts @@ -12,6 +12,7 @@ * source's own contract. */ import { Context } from '@deepseek-ai/cordis' +import { stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' import { describe, expect, it } from 'vitest' import { SlotsService, type ConversationSnapshot, type SessionId, type SessionListState, @@ -87,6 +88,9 @@ async function fullBench(sessions: SessionSummary[]) { ctx.provide('slash', { registerSource: (src: SlashSource) => { captured = src; return () => {} } }) ctx.provide('sessions', face) ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) + // ui-theme's Appearance row binds a durable scope through these two. + ctx.provide('remote', { $on: () => () => {} } as never) + ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) await provideSlotFaces(ctx) await ctx.plugin({ inject: localeInject, apply: applyLocale }).await() await ctx.plugin({ inject: [...inject], apply }).await() @@ -123,6 +127,9 @@ describe('apply', () => { await ctx.plugin(SlashService).await() ctx.provide('sessions', sessionsWith(FAMILY)) ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) + // ui-theme's Appearance row binds a durable scope through these two. + ctx.provide('remote', { $on: () => () => {} } as never) + ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) await provideSlotFaces(ctx) await ctx.plugin({ inject: localeInject, apply: applyLocale }).await() const fiber = ctx.plugin({ inject: [...inject], apply }) diff --git a/packages/client/ui-task/tests/browser-plugin.spec.ts b/packages/client/ui-task/tests/browser-plugin.spec.ts index 142668387f..c03a5125bf 100644 --- a/packages/client/ui-task/tests/browser-plugin.spec.ts +++ b/packages/client/ui-task/tests/browser-plugin.spec.ts @@ -8,6 +8,7 @@ import { Context } from '@deepseek-ai/cordis' import { describe, expect, it } from 'vitest' import InvariantService from '@deepseek-ai/dsh-invariants' import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client' +import { stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' import { apply as applyLocale, inject as localeInject } from '@deepseek-ai/dsh-client-locale/client' import { apply, inject } from '../src/client/index.ts' import { apply as applyNode } from '../src/index.ts' @@ -32,8 +33,11 @@ async function bench(): Promise<{ ctx: Context; fiber: ReturnType null) ctx.provide('sessions', {}) - // The locale plugin binds a settings scope, which reads the connection handle. + // The locale plugin binds a settings scope, which reads the connection handle + // and the forwarded-event port. ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) + ctx.provide('remote', { $on: () => () => {} } as never) + ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) await ctx.plugin({ inject: localeInject, apply: applyLocale }).await() const fiber = ctx.plugin({ inject: [...inject], apply }) await fiber.await() diff --git a/packages/client/ui-theme/package.json b/packages/client/ui-theme/package.json index 1d2e1e6b19..7a5e6b37cf 100644 --- a/packages/client/ui-theme/package.json +++ b/packages/client/ui-theme/package.json @@ -35,7 +35,9 @@ "inject": [ "@deepseek-ai/dsh-client-connection", "@deepseek-ai/dsh-client-runtime", - "@deepseek-ai/dsh-client-locale" + "@deepseek-ai/dsh-client-locale", + "@deepseek-ai/dsh-client-ui-settings", + "@deepseek-ai/dsh-api-remotes" ], "platform": "web", "immediately": true @@ -43,21 +45,26 @@ }, "license": "BSD-3-Clause", "peerDependencies": { + "@deepseek-ai/cordis": "workspace:^", + "@deepseek-ai/dsh-api-remotes": "workspace:^", "@deepseek-ai/dsh-client-connection": "workspace:^", "@deepseek-ai/dsh-client-locale": "workspace:^", "@deepseek-ai/dsh-client-runtime": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", + "@deepseek-ai/dsh-client-ui-settings": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-host-webserver": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", - "@deepseek-ai/cordis": "workspace:^", "react": "^18.2.0" }, "devDependencies": { + "@deepseek-ai/cordis": "^4.0.0-rc.7", + "@deepseek-ai/dsh-api-remotes": "workspace:^", "@deepseek-ai/dsh-client-locale": "workspace:^", "@deepseek-ai/dsh-client-runtime": "workspace:^", "@deepseek-ai/dsh-client-test-runtime": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", + "@deepseek-ai/dsh-client-ui-settings": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", "@deepseek-ai/dsh-host-webserver": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", diff --git a/packages/client/ui-theme/src/client/AppearanceRow.tsx b/packages/client/ui-theme/src/client/AppearanceRow.tsx index e482f5ed2e..b1db97d009 100644 --- a/packages/client/ui-theme/src/client/AppearanceRow.tsx +++ b/packages/client/ui-theme/src/client/AppearanceRow.tsx @@ -12,7 +12,7 @@ import { import type { PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots' import type { ThemePreference } from '../theme-settings.ts' import type { ThemeKey } from './locales.ts' -import type {} from './settings-contract.ts' +import type {} from '@deepseek-ai/dsh-client-ui-settings/client' import type { createAppearanceRowStore } from './settings-store.ts' import css from './AppearanceRow.module.css' diff --git a/packages/client/ui-theme/src/client/index.ts b/packages/client/ui-theme/src/client/index.ts index aaa37ecca8..ed1e9dc4a5 100644 --- a/packages/client/ui-theme/src/client/index.ts +++ b/packages/client/ui-theme/src/client/index.ts @@ -9,9 +9,10 @@ */ import type { Context } from '@deepseek-ai/cordis' import type { BoundActions } from '@deepseek-ai/dsh-client-ui-slots' -import { - bindSettingsScope, type ClientContext, type SettingsScope, -} from '@deepseek-ai/dsh-client-runtime/client' +import type { ClientContext, SettingsScope } from '@deepseek-ai/dsh-client-runtime/client' +// Type-only: the ctx.settingsScope Context merge. Cross-plugin collaboration +// goes through the service, never a value import (client bundle purity gate). +import type {} from '@deepseek-ai/dsh-client-ui-settings/client' // Type-only: pulls the locale plugin's Context merge (ctx.locale). import type {} from '@deepseek-ai/dsh-client-locale/client' import type { AppearanceRowInjected } from './AppearanceRow.tsx' @@ -212,8 +213,12 @@ export class ThemeService { } } -/** Required services: settings transport plus slots/locale for the Appearance row. */ -export const inject = ['slots', 'locale', 'connection'] +/** + * Required services: settings transport plus slots/locale for the Appearance + * row. `remote` carries the forwarded settings invalidation that + * `bindSettingsScope` subscribes to on this context. + */ +export const inject = ['slots', 'locale', 'connection', 'remote', 'settingsScope'] /** * Client plugin body: provide the theme service and register the @@ -222,7 +227,7 @@ export const inject = ['slots', 'locale', 'connection'] * @param ctx - client cordis context. */ export function apply(ctx: ClientContext): void { - const host = bindSettingsScope(ctx, { namespace: THEME_SETTINGS_NAMESPACE }) + const host = ctx.settingsScope.bind({ namespace: THEME_SETTINGS_NAMESPACE }) const theme = new ThemeService(ctx, host) ctx.provide('theme', theme) diff --git a/packages/client/ui-theme/src/client/settings-contract.ts b/packages/client/ui-theme/src/client/settings-contract.ts deleted file mode 100644 index f4fc5fe8da..0000000000 --- a/packages/client/ui-theme/src/client/settings-contract.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Re-export outlet for the `settings.general.item` slot type consumed by this - * package's Appearance row. The canonical home is the locale package (the - * common dependency of every item registrant); this file exists so row - * modules import the type from within their own package. - */ -export type { SettingsGeneralItemOwnerProps } from '@deepseek-ai/dsh-client-locale/client' -// Side-effect type import: pulls the SlotMap merge into this program. -import type {} from '@deepseek-ai/dsh-client-locale/client' diff --git a/packages/client/ui-theme/tests/apply.spec.ts b/packages/client/ui-theme/tests/apply.spec.ts index 006d64d621..65009edc26 100644 --- a/packages/client/ui-theme/tests/apply.spec.ts +++ b/packages/client/ui-theme/tests/apply.spec.ts @@ -5,7 +5,8 @@ import { Context } from '@deepseek-ai/cordis' import { describe, expect, it, vi } from 'vitest' import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' -import { usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { TestRemote, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { SettingsScopeService } from '@deepseek-ai/dsh-client-ui-settings/client' import { apply, inject, SETTINGS_NS } from '@deepseek-ai/dsh-client-ui-theme/client' import type { AppearanceRowInjected, ThemeService } from '@deepseek-ai/dsh-client-ui-theme/client' import { THEME_SETTINGS_NAMESPACE, ThemeSettingsSchema } from '../src/theme-settings.ts' @@ -53,6 +54,9 @@ async function bench(isLoopback = true) { }) }) ctx.provide('connection', { api: { settings: { describe, mutate } }, isLoopback } as never) + // The settings transport and the forwarded-event port the plugin injects. + new TestRemote(ctx) + await ctx.plugin(SettingsScopeService).await() return { ctx, slots: ctx.get('slots') as SlotsService, locale, describe, mutate, setHostPreference: (next: string) => { preference = next }, @@ -79,7 +83,7 @@ function faceOf(slots: SlotsService) { describe('ui-theme apply', () => { it('declares the slot and locale services', () => { - expect(inject).toEqual(['slots', 'locale', 'connection']) + expect(inject).toEqual(['slots', 'locale', 'connection', 'remote', 'settingsScope']) }) it('provides the service, registers localized copy, and registers the row (declaration before or after apply)', async () => { @@ -128,10 +132,10 @@ describe('ui-theme apply', () => { await b.ctx.plugin({ inject: [...inject], apply }).await() const theme = b.ctx.get('theme') as ThemeService await vi.waitFor(() => { expect(theme.getTheme().preference).toBe('dark') }) - b.ctx.emit('settings/changed', 'unrelated') + b.ctx.remote.$dispatch('settings/document-updated', ['unrelated', 0]) expect(b.describe).toHaveBeenCalledOnce() b.setHostPreference('light') - b.ctx.emit('settings/changed', THEME_SETTINGS_NAMESPACE) + b.ctx.remote.$dispatch('settings/document-updated', [THEME_SETTINGS_NAMESPACE, 0]) await vi.waitFor(() => { expect(theme.getTheme().preference).toBe('light') }) b.setHostPreference('dark') b.ctx.emit('connection/reset') diff --git a/packages/client/ui-theme/tests/invariant.spec.ts b/packages/client/ui-theme/tests/invariant.spec.ts index c559d14e0d..514b8e2f60 100644 --- a/packages/client/ui-theme/tests/invariant.spec.ts +++ b/packages/client/ui-theme/tests/invariant.spec.ts @@ -7,6 +7,7 @@ import * as ThemeInvariant from '@deepseek-ai/dsh-client-ui-theme/invariant' import { apply as localeApply, inject as localeInject } from '@deepseek-ai/dsh-client-locale/client' import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client' import InvariantService from '@deepseek-ai/dsh-invariants' +import { stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' describe('invariant companion', () => { it('registers under the package name with an empty installer', async () => { @@ -23,7 +24,7 @@ describe('invariant companion', () => { it('client apply provides ctx.theme over the slots/locale edges', async () => { // The feature registers its own Appearance settings row with localized // copy, hence the slots + locale edges. - expect(inject).toEqual(['slots', 'locale', 'connection']) + expect(inject).toEqual(['slots', 'locale', 'connection', 'remote', 'settingsScope']) const ctx = new Context() new SlotsService(ctx) ctx.provide('connection', { @@ -33,6 +34,9 @@ describe('invariant companion', () => { }) } }, isLoopback: true, } as never) + // The settings row's transport and the forwarded-event port. + ctx.provide('remote', { $on: () => () => {} } as never) + ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) await ctx.plugin({ inject: localeInject, apply: localeApply }).await() await ctx.plugin({ inject, apply: clientApply }).await() expect(ctx.get('theme')).toBeInstanceOf(ThemeService) diff --git a/packages/client/ui-theme/tsconfig.json b/packages/client/ui-theme/tsconfig.json index 2cc728b31c..8da8ac5e6e 100644 --- a/packages/client/ui-theme/tsconfig.json +++ b/packages/client/ui-theme/tsconfig.json @@ -31,6 +31,9 @@ }, { "path": "../../support/invariants" + }, + { + "path": "../ui-settings" } ] } diff --git a/packages/client/ui-tool/tests/assembly-surfaces.spec.tsx b/packages/client/ui-tool/tests/assembly-surfaces.spec.tsx index e006b106a8..80ee97d157 100644 --- a/packages/client/ui-tool/tests/assembly-surfaces.spec.tsx +++ b/packages/client/ui-tool/tests/assembly-surfaces.spec.tsx @@ -5,7 +5,7 @@ import { cleanup, fireEvent, waitFor } from '@testing-library/react' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' import type { ISession, SessionId, TodoItem, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client' import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' -import { SlotTestRuntime, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime' +import { SlotTestRuntime, usePinnedBrowserLanguages, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' import { apply as applyConversation, inject as injectConversation } from '@deepseek-ai/dsh-client-ui-conversation/client' import { apply as applyTool, inject as injectTool } from '../src/client/apply.ts' import { toolChatSnapshot } from './tool-details-render.tsx' @@ -69,6 +69,9 @@ const LAYOUT_CHILDREN = { async function bench(nodes: ToolResultNode[]) { const runtime = await SlotTestRuntime.create() runtime.provide('connection', { api: { settings: {} }, isLoopback: false } as never) + // ui-theme's Appearance row binds a durable scope through these two. + runtime.provide('remote', { $on: () => () => {} }) + runtime.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() }) const locale = new LocaleService(runtime.ctx) runtime.provide('locale', locale) diff --git a/packages/client/ui-tool/tests/chat-code-subcalls.spec.tsx b/packages/client/ui-tool/tests/chat-code-subcalls.spec.tsx index 5939655118..fbdfc4b75e 100644 --- a/packages/client/ui-tool/tests/chat-code-subcalls.spec.tsx +++ b/packages/client/ui-tool/tests/chat-code-subcalls.spec.tsx @@ -9,6 +9,7 @@ // (runningCalls) nest their so-far dispatches the same way. import { Context } from '@deepseek-ai/cordis' +import { stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { cleanup, fireEvent, render } from '@testing-library/react' import { @@ -158,6 +159,9 @@ async function bench(snapshot: ConversationSnapshot) { ctx.provide('workspaces', workspaces) ctx.provide('layout', layout) ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) + // ui-theme's Appearance row binds a durable scope through these two. + ctx.provide('remote', { $on: () => () => {} } as never) + ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) const locale = new LocaleService(ctx) ctx.provide('locale', locale) slots.installLocale(locale) diff --git a/packages/client/ui-tool/tests/toolview-slot.spec.tsx b/packages/client/ui-tool/tests/toolview-slot.spec.tsx index cab91c6457..2ab4ba9b12 100644 --- a/packages/client/ui-tool/tests/toolview-slot.spec.tsx +++ b/packages/client/ui-tool/tests/toolview-slot.spec.tsx @@ -13,7 +13,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { cleanup, fireEvent } from '@testing-library/react' import type { ISession, SessionId, ToolResultNode } from '@deepseek-ai/dsh-client-runtime/client' import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots' -import { SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime' +import { SlotTestRuntime, stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' import { LocaleService } from '@deepseek-ai/dsh-client-locale/client' import { apply as applyConversation, inject as injectConversation } from '@deepseek-ai/dsh-client-ui-conversation/client' import { apply as applyTool, inject as injectTool } from '@deepseek-ai/dsh-client-ui-tool/client' @@ -65,6 +65,9 @@ const LAYOUT_CHILDREN = { async function bench(nodes: ToolResultNode[]) { const runtime = await SlotTestRuntime.create() runtime.provide('connection', { api: { settings: {} }, isLoopback: false } as never) + // ui-theme's Appearance row binds a durable scope through these two. + runtime.provide('remote', { $on: () => () => {} }) + runtime.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) const layout = { openDetails: vi.fn(), closeDetails: vi.fn() } runtime.provide('layout', layout) const locale = new LocaleService(runtime.ctx) @@ -198,6 +201,9 @@ describe('registrant declaration injection', () => { it('runs a registrant before ui-tool and waits on the actual toolview declaration', async () => { const runtime = await SlotTestRuntime.create() runtime.provide('connection', { api: { settings: {} }, isLoopback: false } as never) + // ui-theme's Appearance row binds a durable scope through these two. + runtime.provide('remote', { $on: () => () => {} }) + runtime.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() }) const locale = new LocaleService(runtime.ctx) runtime.provide('locale', locale) diff --git a/packages/client/ui-trajectory/package.json b/packages/client/ui-trajectory/package.json index 49b100620b..725dc11d5f 100644 --- a/packages/client/ui-trajectory/package.json +++ b/packages/client/ui-trajectory/package.json @@ -64,6 +64,7 @@ "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-client-locale": "workspace:^", "@deepseek-ai/dsh-client-runtime": "workspace:^", + "@deepseek-ai/dsh-client-test-runtime": "workspace:^", "@deepseek-ai/dsh-client-ui-primitives": "workspace:^", "@deepseek-ai/dsh-client-ui-conversation": "workspace:^", "@deepseek-ai/dsh-client-ui-slots": "workspace:^", diff --git a/packages/client/ui-trajectory/tests/views.spec.tsx b/packages/client/ui-trajectory/tests/views.spec.tsx index ceacfdaf09..9a4e6b3ca8 100644 --- a/packages/client/ui-trajectory/tests/views.spec.tsx +++ b/packages/client/ui-trajectory/tests/views.spec.tsx @@ -30,6 +30,7 @@ import { import { createChatStore } from '@deepseek-ai/dsh-client-ui-conversation/src/client/stores.ts' import { zh as conversationZh } from '@deepseek-ai/dsh-client-ui-conversation/src/client/locales.ts' import { apply as localeApply, inject as localeInject } from '@deepseek-ai/dsh-client-locale/client' +import { stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' import type { LocaleKeysOf } from '@deepseek-ai/dsh-client-ui-slots' import { zh, type TrajectoryKey } from '../src/client/locales.ts' import { apply, inject } from '@deepseek-ai/dsh-client-ui-trajectory/client' @@ -202,8 +203,11 @@ async function bench(snapshot = historySnapshot(NODES)) { slots.register( { name: 'conversation.view', id: 'chat', order: 0, label: 'Chat' } as never, chatBody as never) // The locale plugin backs the locale-aware view tab label ('locale' in - // inject); its settings scope needs a connection handle. + // inject); its settings scope needs a connection handle and the + // forwarded-event port. ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) + ctx.provide('remote', { $on: () => () => {} } as never) + ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) ctx.plugin({ inject: [...localeInject], apply: localeApply }) const fiber = ctx.plugin({ inject: [...inject], apply }) await fiber.await() diff --git a/packages/credentials/credentials/README.i18n.yaml b/packages/credentials/credentials/README.i18n.yaml index 756ba1ba04..703bca4432 100644 --- a/packages/credentials/credentials/README.i18n.yaml +++ b/packages/credentials/credentials/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/credentials/credentials/README.md -README.md: 5cfeb8e6656fabd638ea2126e56bc66391f0ca01 -README.zh.md: 28a029f0e4909fb14a1208458dcd1d22afe4abb9 +README.md: 62f5c6318ec04e9820a3d18ffa159d1561215760 +README.zh.md: 10a98217b3229a7d7fb372eb9ec7f418d9d4d764 diff --git a/packages/host/apiproxy/README.i18n.yaml b/packages/host/apiproxy/README.i18n.yaml index e020c6f93f..61ffd7dcfd 100644 --- a/packages/host/apiproxy/README.i18n.yaml +++ b/packages/host/apiproxy/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/host/apiproxy/README.md -README.md: 2101c785a613477c04ecbfec6a39a0f403af40ef -README.zh.md: 3ba37967ff88ca89911017945aeed857e4b4ff19 +README.md: 426aa32c6b56b8be597969125206b4e1e7025622 +README.zh.md: 75383798555e3b775685b926630eedd97794005a diff --git a/packages/host/apiproxy/README.md b/packages/host/apiproxy/README.md index 0d3b68c588..426aa32c6b 100644 --- a/packages/host/apiproxy/README.md +++ b/packages/host/apiproxy/README.md @@ -72,7 +72,7 @@ None; this package neither assembles nor sends a provider request. ## Known Limitations and Deferred Work -- **Forwarded Remote events are parasitic on this legacy frame union** — `host/remote-event` lives in `HostFrame` so the delivery path could reuse the existing host stream instead of opening a third downlink, which makes it read as if this package owned the Remote event contract. It does not: the allowlist is `dsh-api-remotes`' and the consumer verb is `ctx.remote.$on`. When the host stream moves off this package, the frame moves with it and the consumer contract is unaffected ([rationale](../../../.agents/notes/proposed/architecture/2026-08-10-remote-event-delivery.md)). +- **Forwarded Remote events are parasitic on this legacy frame union** — `host/remote-event` lives in `HostFrame` so the delivery path could reuse the existing host stream instead of opening a third downlink, which makes it read as if this package owned the Remote event contract. It does not: the allowlist is `dsh-api-remotes`' and the consumer verb is `ctx.remote.$on`. When the host stream moves off this package, the frame moves with it and the consumer contract is unaffected ([rationale](../../../.agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.md)). - **Pending-interaction state is host-side** — the wire uses POST `/api/respond` plus `RpcReceipt`; the table in `src/api-proxy.ts` handles questions only and has no approval entries. - **Reserved seams stay out of `RpcMethodMap`** — `prompt.mode: 'inject'`, `task.list`, and a describe `hostInstanceId` are documented reservations; model discovery uses `llm.models`. An unknown method fails loud at envelope parse rather than getting a not-implemented code. - **No protocol version field** — client and host ship together; `host.describe` gains a version negotiation field only when an independently released client exists. diff --git a/packages/host/apiproxy/README.zh.md b/packages/host/apiproxy/README.zh.md index e9bed071e0..7538379855 100644 --- a/packages/host/apiproxy/README.zh.md +++ b/packages/host/apiproxy/README.zh.md @@ -72,7 +72,7 @@ Workspace 列表与 Session 列表是相互独立的重连基线。`workspace.cr ## 已知限制与暂缓事项 -- **转发的 Remote 事件寄居在这套 legacy 帧联合里**:`host/remote-event` 住在 `HostFrame` 中,是为了让投递路径复用现有宿主流、不必新开第三条下行通道,因此读起来像是本包拥有 Remote 事件契约。并非如此:名单归 `dsh-api-remotes`,消费端动词是 `ctx.remote.$on`。将来宿主流整体搬离本包时,该帧随之搬走,消费端契约不受影响([原委](../../../.agents/notes/proposed/architecture/2026-08-10-remote-event-delivery.zh.md))。 +- **转发的 Remote 事件寄居在这套 legacy 帧联合里**:`host/remote-event` 住在 `HostFrame` 中,是为了让投递路径复用现有宿主流、不必新开第三条下行通道,因此读起来像是本包拥有 Remote 事件契约。并非如此:名单归 `dsh-api-remotes`,消费端动词是 `ctx.remote.$on`。将来宿主流整体搬离本包时,该帧随之搬走,消费端契约不受影响([原委](../../../.agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.md))。 - **待处理交互状态位于宿主侧**:wire 使用 POST `/api/respond` 加 `RpcReceipt`;`src/api-proxy.ts` 中的表只处理问题,不包含审批条目。 - **预留 seam 不进入 `RpcMethodMap`**:`prompt.mode: 'inject'`、`task.list` 和描述字段 `hostInstanceId` 都是已记录的预留项;模型发现使用 `llm.models`。未知方法会在信封解析时直接失败,而不会返回「尚未实现」错误码。 - **没有协议版本字段**:客户端与宿主一同发布;只有出现独立发布的客户端后,`host.describe` 才会增加版本协商字段。 diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index 147dc9ed7d..16390a3331 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -6,7 +6,7 @@ import { randomUUID } from 'node:crypto' import { mkdir, stat } from 'node:fs/promises' import { dirname } from 'node:path' -import type { Context, Events } from '@deepseek-ai/cordis' +import type { Context } from '@deepseek-ai/cordis' import { installModelSelection } from '@deepseek-ai/dsh-agent' import type { Agent, ModelSelection, ModelSelectionRef, AgentOptions, AgentStatus } from '@deepseek-ai/dsh-agent' import { AGENT_DEFAULT_MODEL_SETTINGS_NAMESPACE } from '@deepseek-ai/dsh-agent-default-model' @@ -418,9 +418,11 @@ function frame(payload: F): RpcRequest { /** * Narrow one allowlisted host event's argument list to the JSON values the * wrapper frame carries. A rejected argument is an allowlist mistake (the - * forwarded path applies no projection), not hostile input, so it fails loud - * here rather than degrading to a dropped or lossy frame. Exported for the - * test that owns this decision: every currently allowlisted event has a + * forwarded path applies no projection), not hostile input, so it throws rather + * than degrading to a lossy frame. The throw surfaces where the forwarding + * listener runs, so the emitter's own listener containment logs it and drops + * that frame — loud in the Host log, not at load or at the emit. Exported for + * the test that owns this decision: every currently allowlisted event has a * statically JSON-safe payload, so a type-legal `ctx.emit` cannot reach the * rejection branch. * @param event - forwarded host event name, named in the failure. @@ -3471,18 +3473,17 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro // its `host/models-changed`), which is the order a client sees. ...API_REMOTE_FORWARDED_EVENTS.map(name => ctx.on( name, - // cordis keys `on` by literal event name, so subscribing from a - // runtime list erases the handler type once. The erasure is safe - // because the allowlist's shape assertion already proves each name - // is a real, non-scoped, void-returning event, and assertJsonArgs - // proves the payload is JSON-safe before it reaches the queue. + // The allowlist's shape assertion proves each name is a real, + // non-scoped, void-returning event, so the rest-parameter handler + // satisfies every member of the union `on` accepts here; + // assertJsonArgs proves the payload is JSON-safe before it queues. ((...args: unknown[]) => { queue.push(frame({ type: 'host/remote-event', event: name, args: assertJsonArgs(name, args), })) - }) as Events[typeof name], + }), )), // The recompose itself registers nothing (it re-parents the agent's // scope onto a standing mount that may already exist), so the diff --git a/packages/host/apiproxy/tests/api-proxy-config.spec.ts b/packages/host/apiproxy/tests/api-proxy-config.spec.ts index 2115d1491a..4f5d40191f 100644 --- a/packages/host/apiproxy/tests/api-proxy-config.spec.ts +++ b/packages/host/apiproxy/tests/api-proxy-config.spec.ts @@ -229,7 +229,10 @@ function forwardedSettings(ns: string): HostFrame { return { type: 'host/remote-event', event: 'settings/document-updated', - args: [ns, expect.any(Number) as unknown as number], + /* oxlint-disable-next-line typescript/no-unsafe-assignment -- + * expect.any is typed `any`; the frame's args are JsonValue[]. The + * revision is the Host's own counter, so the matcher is the assertion. */ + args: [ns, expect.any(Number)], } } diff --git a/packages/settings/settings/README.i18n.yaml b/packages/settings/settings/README.i18n.yaml index fba3913f75..b6e05fac88 100644 --- a/packages/settings/settings/README.i18n.yaml +++ b/packages/settings/settings/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/settings/settings/README.md -README.md: 7917f38017bfb23dc4718ee533c1f9a92b519d41 -README.zh.md: f46cf433b4d2207b17b0f40cc3b9cf70794b512c +README.md: b1c32c7f4cd05b05802400b33dfdd40f7ed20e61 +README.zh.md: 47fa75d13ad607554de1aa51de03557a769e2366 diff --git a/packages/typert/type-meta/README.i18n.yaml b/packages/typert/type-meta/README.i18n.yaml index 99482aeefc..764bd5c5ac 100644 --- a/packages/typert/type-meta/README.i18n.yaml +++ b/packages/typert/type-meta/README.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write packages/typert/type-meta/README.md -README.md: 5bf35dabf80e035ec8eeeef1598873bb4032fa98 -README.zh.md: 68b690d2143b4420f2e10efceaca0dfc1149d0ff +README.md: 658baa8bae68c33acf3fe22dba5bf463356a551b +README.zh.md: 7c0fb603e8a235aa9786bcd2721a7d7626703d46 diff --git a/packages/typert/type-meta/README.md b/packages/typert/type-meta/README.md index 33524355f0..658baa8bae 100644 --- a/packages/typert/type-meta/README.md +++ b/packages/typert/type-meta/README.md @@ -20,7 +20,7 @@ Decorator initializers retain markers in a module-private `WeakMap` keyed by the Business packages extend `TypeRTLookupMap` and `TypeRTContextMap` to associate Host objects or scoped Contexts with their wire identities. Generated artifacts extend `TypeRTRemoteMap`, `TypeRTRemoteScopeMap`, and `TypeRTRemoteNamespaceMap` so Client imports expose only selected Remote methods. `InvocationDescriptor` is the shared runtime form consumed by the registry, Gateway, and Client Remote. -The Host assembly extends `TypeRTRemoteEventSelection` with the Host events it forwards to consumers, which narrows the `ctx.remote.$on` key face; `TypeRTForwardableEvent` states the shapes a one-way delivery can carry at all, excluding Scope-bound and answered events. The `remote/host-event` Cordis event is declared here because both compilation faces share this package, but only the consumer side participates: the Client half owning the host frame sink emits it and the Client Remote service is its only subscriber. +The Host assembly extends `TypeRTRemoteEventSelection` with the Host events it forwards to consumers, which narrows the `ctx.remote.$on` key face; `TypeRTForwardableEvent` states the shapes a one-way delivery can carry at all, excluding Scope-bound and answered events. `TypeRTClientRemote` carries both roles of that surface: consumers subscribe through `$on`, and the Client half owning the host frame sink hands frames over through `$dispatch`. Lookup and Context packages own both sides of their contract: declaration merging supplies the static association, while runtime providers register identity resolution with `ctx.typert`. A lookup or Host Context provider supplies the stable declaration and default resolver, while Host composition may separately configure a synchronous or asynchronous resolver; policy rejections may use `TypeRTLookupFailure` to carry a failure value owned by the boundary adapter. Strict codecs carry generated schemas; `src-json` codecs identify the weaker source-launch path. diff --git a/packages/typert/type-meta/README.zh.md b/packages/typert/type-meta/README.zh.md index 1e7f434a72..7c0fb603e8 100644 --- a/packages/typert/type-meta/README.zh.md +++ b/packages/typert/type-meta/README.zh.md @@ -20,7 +20,7 @@ Host 方法通过将 `signal: AbortSignal` 声明为最后一个参数来启用 业务包扩展 `TypeRTLookupMap` 和 `TypeRTContextMap`,以关联 Host 对象或作用域 Context 与其协议身份。生成的产物扩展 `TypeRTRemoteMap`、`TypeRTRemoteScopeMap` 和 `TypeRTRemoteNamespaceMap`,使 Client 导入后仅暴露选定的 Remote 方法。`InvocationDescriptor` 是供注册表、Gateway 和 Client Remote 使用的共享运行时形式。 -Host 装配扩展 `TypeRTRemoteEventSelection` 来声明转发给消费端的 Host 事件,从而收窄 `ctx.remote.$on` 的键面;`TypeRTForwardableEvent` 陈述单向投递根本能承载哪些形状,把 Scope 化事件与有返回值的事件排除在外。`remote/host-event` 这条 Cordis 事件声明在此包,是因为两个编译面共用它,但只有消费端参与:持有 Host 帧 sink 的 Client 半发射它,Client Remote 服务是唯一的订阅方。 +Host 装配扩展 `TypeRTRemoteEventSelection` 来声明转发给消费端的 Host 事件,从而收窄 `ctx.remote.$on` 的键面;`TypeRTForwardableEvent` 陈述单向投递根本能承载哪些形状,把 Scope 化事件与有返回值的事件排除在外。`TypeRTClientRemote` 承载该面的两种角色:消费方经 `$on` 订阅,持有 Host 帧 sink 的 Client 半经 `$dispatch` 交出帧。 查找包与 Context 包同时负责其约定的两侧:声明合并提供静态关联,运行时提供方则向 `ctx.typert` 注册身份解析。lookup 或 Host Context provider 提供稳定声明与默认 resolver,Host 组合可以另行配置同步或异步 resolver;策略拒绝可用 `TypeRTLookupFailure` 携带由边界适配器拥有的失败值。严格编解码器携带生成的 schema;`src-json` 编解码器标识约束更弱的源码启动路径。 diff --git a/packages/typert/type-meta/src/types.ts b/packages/typert/type-meta/src/types.ts index a4899a7249..c08fb209ee 100644 --- a/packages/typert/type-meta/src/types.ts +++ b/packages/typert/type-meta/src/types.ts @@ -211,6 +211,18 @@ export interface TypeRTClientRemote extends TypeRTRemoteNamespaceMap { * @returns disposer owned by the calling fiber. */ $on(event: Event, listener: Events[Event]): () => void + /** + * Hand one decoded forwarded frame to the subscription table. The carrier + * owning the Host frame sink calls this; a consumer subscribes with + * {@link TypeRTClientRemote.$on} and never calls it. + * + * `event` is a plain string because this is the wire boundary: the name is + * whatever the Host assembly's allowlist selected, and one nobody subscribed + * to is dropped silently. + * @param event - forwarded Host event name, exactly as the Host emitted it. + * @param args - the Host argument list, already JSON-decoded. + */ + $dispatch(event: string, args: readonly unknown[]): void } /** @@ -452,18 +464,4 @@ declare module '@deepseek-ai/cordis' { interface Context { typert: TypeRTService } - - interface Events { - /** - * The carrier received one allowlisted host event forwarded over the wire. - * Declared here because both compilation faces share this package; only the - * consumer side participates, where the Client half owning the host frame - * sink emits it and the Remote service is its only subscriber, turning it - * into `$on` callbacks. The Host neither emits nor observes it. - * @mode emit - * @param event - forwarded host event name, exactly as the Host emitted it. - * @param args - the Host argument list, already JSON-decoded. - */ - 'remote/host-event'(event: string, args: readonly unknown[]): void - } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index adb6aa6c5c..c5de1e08c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,9 +150,6 @@ importers: '@deepseek-ai/dsh-base': specifier: workspace:^ version: link:../../packages/bundle/base - '@deepseek-ai/dsh-client-ui-agent-preset': - specifier: workspace:^ - version: link:../../packages/client/ui-agent-preset '@deepseek-ai/dsh-cmdline': specifier: workspace:^ version: link:../../packages/boot/cmdline @@ -825,6 +822,15 @@ importers: '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent + '@deepseek-ai/dsh-api-gateway': + specifier: workspace:^ + version: link:../gateway + '@deepseek-ai/dsh-commands': + specifier: workspace:^ + version: link:../../interaction/commands + '@deepseek-ai/dsh-credentials': + specifier: workspace:^ + version: link:../../credentials/credentials '@deepseek-ai/dsh-goal': specifier: workspace:^ version: link:../../goal/goal @@ -837,6 +843,9 @@ importers: '@deepseek-ai/dsh-session-persistence': specifier: workspace:^ version: link:../../session/session-persistence + '@deepseek-ai/dsh-settings': + specifier: workspace:^ + version: link:../../settings/settings '@deepseek-ai/dsh-typert-registry': specifier: workspace:^ version: link:../../typert/registry @@ -1735,12 +1744,18 @@ importers: '@deepseek-ai/cordis': specifier: workspace:^ version: link:../../../vendor/cordis + '@deepseek-ai/dsh-api-remotes': + specifier: workspace:^ + version: link:../../api/remotes '@deepseek-ai/dsh-client-runtime': specifier: workspace:^ version: link:../runtime '@deepseek-ai/dsh-client-ui-primitives': specifier: workspace:^ version: link:../ui-primitives + '@deepseek-ai/dsh-client-ui-settings': + specifier: workspace:^ + version: link:../ui-settings '@deepseek-ai/dsh-client-ui-slots': specifier: workspace:^ version: link:../ui-slots @@ -1780,9 +1795,6 @@ importers: '@deepseek-ai/dsh-client-connection': specifier: workspace:^ version: link:../connection - '@deepseek-ai/dsh-client-schema-form': - specifier: workspace:^ - version: link:../schema-form '@deepseek-ai/dsh-client-ui-slots': specifier: workspace:^ version: link:../ui-slots @@ -1823,6 +1835,9 @@ importers: '@deepseek-ai/cordis': specifier: workspace:^ version: link:../../../vendor/cordis + '@deepseek-ai/dsh-api-gateway': + specifier: workspace:^ + version: link:../../api/gateway '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../support/invariants @@ -1835,9 +1850,6 @@ importers: '@deepseek-ai/dsh-typert-registry': specifier: workspace:^ version: link:../../typert/registry - '@deepseek-ai/schemastery': - specifier: link:../../../vendor/schemastery - version: link:../../../vendor/schemastery '@types/react': specifier: ~18.3.1 version: 18.3.31 @@ -1903,6 +1915,9 @@ importers: '@deepseek-ai/cordis': specifier: workspace:^ version: link:../../../vendor/cordis + '@deepseek-ai/dsh-api-remotes': + specifier: workspace:^ + version: link:../../api/remotes '@deepseek-ai/dsh-client-connection': specifier: workspace:^ version: link:../connection @@ -1949,6 +1964,9 @@ importers: '@deepseek-ai/cordis': specifier: workspace:^ version: link:../../../vendor/cordis + '@deepseek-ai/dsh-api-remotes': + specifier: workspace:^ + version: link:../../api/remotes '@deepseek-ai/dsh-client-connection': specifier: workspace:^ version: link:../connection @@ -2001,6 +2019,9 @@ importers: '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent + '@deepseek-ai/dsh-api-remotes': + specifier: workspace:^ + version: link:../../api/remotes '@deepseek-ai/dsh-attachment': specifier: workspace:^ version: link:../../attachment/attachment @@ -2025,6 +2046,9 @@ importers: '@deepseek-ai/dsh-client-ui-primitives': specifier: workspace:^ version: link:../ui-primitives + '@deepseek-ai/dsh-client-ui-settings': + specifier: workspace:^ + version: link:../ui-settings '@deepseek-ai/dsh-client-ui-slash': specifier: workspace:^ version: link:../ui-slash @@ -2224,6 +2248,9 @@ importers: '@deepseek-ai/cordis': specifier: workspace:^ version: link:../../../vendor/cordis + '@deepseek-ai/dsh-api-remotes': + specifier: workspace:^ + version: link:../../api/remotes '@deepseek-ai/dsh-client-connection': specifier: workspace:^ version: link:../connection @@ -2266,6 +2293,9 @@ importers: '@deepseek-ai/cordis': specifier: workspace:^ version: link:../../../vendor/cordis + '@deepseek-ai/dsh-api-remotes': + specifier: workspace:^ + version: link:../../api/remotes '@deepseek-ai/dsh-client-connection': specifier: workspace:^ version: link:../connection @@ -2278,12 +2308,18 @@ importers: '@deepseek-ai/dsh-client-schema-form': specifier: workspace:^ version: link:../schema-form + '@deepseek-ai/dsh-client-test-runtime': + specifier: workspace:^ + version: link:../test-runtime '@deepseek-ai/dsh-client-ui-command': specifier: workspace:^ version: link:../ui-command '@deepseek-ai/dsh-client-ui-primitives': specifier: workspace:^ version: link:../ui-primitives + '@deepseek-ai/dsh-client-ui-settings': + specifier: workspace:^ + version: link:../ui-settings '@deepseek-ai/dsh-client-ui-slash': specifier: workspace:^ version: link:../ui-slash @@ -2474,44 +2510,43 @@ importers: version: 18.3.31 packages/client/ui-settings: - dependencies: - clsx: - specifier: ^2.0.0 - version: 2.1.1 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ version: link:../../../vendor/cordis - '@deepseek-ai/dsh-client-locale': + '@deepseek-ai/dsh-api-gateway': specifier: workspace:^ - version: link:../locale + version: link:../../api/gateway + '@deepseek-ai/dsh-api-remotes': + specifier: workspace:^ + version: link:../../api/remotes + '@deepseek-ai/dsh-client-connection': + specifier: workspace:^ + version: link:../connection '@deepseek-ai/dsh-client-runtime': specifier: workspace:^ version: link:../runtime - '@deepseek-ai/dsh-client-ui-primitives': + '@deepseek-ai/dsh-client-schema-form': specifier: workspace:^ - version: link:../ui-primitives - '@deepseek-ai/dsh-client-ui-sidebar': + version: link:../schema-form + '@deepseek-ai/dsh-client-test-runtime': specifier: workspace:^ - version: link:../ui-sidebar + version: link:../test-runtime '@deepseek-ai/dsh-client-ui-slots': specifier: workspace:^ version: link:../ui-slots '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../support/invariants + '@deepseek-ai/dsh-settings': + specifier: workspace:^ + version: link:../../settings/settings '@types/react': specifier: ~18.3.1 version: 18.3.31 - '@types/react-dom': - specifier: ~18.3.0 - version: 18.3.7(@types/react@18.3.31) react: specifier: ^18.2.0 version: 18.3.1 - react-dom: - specifier: ^18.2.0 - version: 18.3.1(react@18.3.1) packages/client/ui-settings-general: dependencies: @@ -2521,10 +2556,16 @@ importers: '@deepseek-ai/schemastery': specifier: link:../../../vendor/schemastery version: link:../../../vendor/schemastery + clsx: + specifier: ^2.0.0 + version: 2.1.1 devDependencies: '@deepseek-ai/cordis': specifier: workspace:^ version: link:../../../vendor/cordis + '@deepseek-ai/dsh-api-remotes': + specifier: workspace:^ + version: link:../../api/remotes '@deepseek-ai/dsh-client-connection': specifier: workspace:^ version: link:../connection @@ -2543,6 +2584,9 @@ importers: '@deepseek-ai/dsh-client-ui-settings': specifier: workspace:^ version: link:../ui-settings + '@deepseek-ai/dsh-client-ui-sidebar': + specifier: workspace:^ + version: link:../ui-sidebar '@deepseek-ai/dsh-client-ui-slots': specifier: workspace:^ version: link:../ui-slots @@ -2782,6 +2826,9 @@ importers: '@deepseek-ai/cordis': specifier: workspace:^ version: link:../../../vendor/cordis + '@deepseek-ai/dsh-api-remotes': + specifier: workspace:^ + version: link:../../api/remotes '@deepseek-ai/dsh-client-locale': specifier: workspace:^ version: link:../locale @@ -2794,6 +2841,9 @@ importers: '@deepseek-ai/dsh-client-ui-primitives': specifier: workspace:^ version: link:../ui-primitives + '@deepseek-ai/dsh-client-ui-settings': + specifier: workspace:^ + version: link:../ui-settings '@deepseek-ai/dsh-client-ui-slots': specifier: workspace:^ version: link:../ui-slots @@ -2880,6 +2930,9 @@ importers: '@deepseek-ai/dsh-client-runtime': specifier: workspace:^ version: link:../runtime + '@deepseek-ai/dsh-client-test-runtime': + specifier: workspace:^ + version: link:../test-runtime '@deepseek-ai/dsh-client-ui-conversation': specifier: workspace:^ version: link:../ui-conversation diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index e1c65bc8cd..b94eaa80b7 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -122,6 +122,7 @@ export const SERVICE_WALK_EXEMPTIONS: Record = { apiProxy: 'interface-typed (ApiProxy) with the class in api-proxy.ts, not index.ts — packages/host/apiproxy/README.md owns the API', appShell: 'client-side interface-typed browser service — packages/client/web/README.md owns the API', connection: 'client-side interface-typed browser service — packages/client/connection/README.md owns the API', + settingsScope: 'client-side settings-namespace transport service — packages/client/ui-settings/README.md owns the API', chatFileMentions: 'client-side slot-contract accessor (ChatFileMentions) — packages/client/ui-conversation/README.md owns the API', command: 'client-side interface-typed browser service — packages/client/ui-command/README.md owns the API', conversation: 'client-side interface-typed browser service — packages/client/ui-conversation/README.md owns the API', diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index 4685e045ba..84e7ca6912 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -1483,7 +1483,7 @@ { "doc": "docs/subsystems/settings.md", "symbol": "SettingsNamespace", - "source": "packages/settings/settings/src/index.ts" + "source": "packages/settings/settings/src/types.ts" }, { "doc": "docs/subsystems/settings.md", @@ -1508,7 +1508,7 @@ { "doc": "docs/subsystems/settings.md", "symbol": "SettingsUpdateSource", - "source": "packages/settings/settings/src/index.ts" + "source": "packages/settings/settings/src/types.ts" }, { "doc": "docs/subsystems/credentials.md", @@ -1713,7 +1713,7 @@ { "doc": "docs/subsystems/credentials.md", "symbol": "CredentialRef", - "source": "packages/credentials/credentials/src/index.ts" + "source": "packages/credentials/credentials/src/types.ts" }, { "doc": "docs/subsystems/permission.md",