Просмотр исходного кода

Merge remote-tracking branch 'origin/master' into fix/locale-default-english

# Conflicts:
#	packages/client/ui-settings-models/tests/apply.client.spec.ts
#	packages/client/ui-settings-plugins/tests/apply.client.spec.ts
#	packages/client/ui-theme/tests/apply.client.spec.ts
Chinesezjc 1 месяц назад
Родитель
Сommit
acc9359a53
100 измененных файлов с 1939 добавлено и 961 удалено
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.i18n.yaml
  2. 4 4
      .agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md
  3. 4 4
      .agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.i18n.yaml
  5. 9 10
      .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md
  6. 9 10
      .agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md
  7. 2 2
      .agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.i18n.yaml
  8. 4 4
      .agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md
  9. 4 4
      .agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.zh.md
  10. 2 2
      .agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.i18n.yaml
  11. 23 51
      .agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md
  12. 23 51
      .agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.zh.md
  13. 2 2
      .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.i18n.yaml
  14. 1 1
      .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md
  15. 1 1
      .agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md
  16. 2 2
      .agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.i18n.yaml
  17. 1 1
      .agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.md
  18. 1 1
      .agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.zh.md
  19. 2 2
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.i18n.yaml
  20. 1 1
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.md
  21. 1 1
      .agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md
  22. 2 2
      .agents/notes/implemented/architecture/2026-08-09-cordis-event-walk-backstop.i18n.yaml
  23. 1 1
      .agents/notes/implemented/architecture/2026-08-09-cordis-event-walk-backstop.md
  24. 1 1
      .agents/notes/implemented/architecture/2026-08-09-cordis-event-walk-backstop.zh.md
  25. 2 2
      .agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.i18n.yaml
  26. 1 1
      .agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.md
  27. 1 1
      .agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.zh.md
  28. 6 0
      .agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.i18n.yaml
  29. 86 0
      .agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md
  30. 86 0
      .agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.zh.md
  31. 6 0
      .agents/notes/implemented/architecture/2026-08-17-dynamic-client-render-and-attachment-ownership.i18n.yaml
  32. 43 0
      .agents/notes/implemented/architecture/2026-08-17-dynamic-client-render-and-attachment-ownership.md
  33. 43 0
      .agents/notes/implemented/architecture/2026-08-17-dynamic-client-render-and-attachment-ownership.zh.md
  34. 2 2
      .agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.i18n.yaml
  35. 1 1
      .agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md
  36. 1 1
      .agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.zh.md
  37. 2 2
      .agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.i18n.yaml
  38. 1 1
      .agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md
  39. 1 1
      .agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.zh.md
  40. 2 2
      .agents/notes/implemented/bug-fix/2026-08-10-pre-plugin-theme-bootstrap.i18n.yaml
  41. 3 3
      .agents/notes/implemented/bug-fix/2026-08-10-pre-plugin-theme-bootstrap.md
  42. 3 3
      .agents/notes/implemented/bug-fix/2026-08-10-pre-plugin-theme-bootstrap.zh.md
  43. 2 2
      .agents/notes/implemented/feature/2026-08-11-web-attachment-display-alignment.i18n.yaml
  44. 3 3
      .agents/notes/implemented/feature/2026-08-11-web-attachment-display-alignment.md
  45. 3 3
      .agents/notes/implemented/feature/2026-08-11-web-attachment-display-alignment.zh.md
  46. 2 2
      .agents/notes/implemented/process/2026-07-26-web-syntax-highlighting-shiki.i18n.yaml
  47. 1 1
      .agents/notes/implemented/process/2026-07-26-web-syntax-highlighting-shiki.md
  48. 1 1
      .agents/notes/implemented/process/2026-07-26-web-syntax-highlighting-shiki.zh.md
  49. 2 2
      .agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.i18n.yaml
  50. 1 1
      .agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md
  51. 1 1
      .agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md
  52. 4 7
      apps/web/package.json
  53. 1 1
      apps/web/src/main.ts
  54. 125 38
      apps/web/tests/assembled-boot.ts
  55. 6 3
      apps/web/tests/built-boot.snapshot.ts
  56. 8 0
      apps/web/tests/cordis-tool-round.e2e.ts
  57. 7 5
      apps/web/tests/image-display.snapshot.ts
  58. 1 1
      apps/web/tests/settings-chrome.e2e.ts
  59. 11 7
      apps/web/tests/snapshots/cordis-tool-round/session.jsonl
  60. 14 9
      apps/web/tests/snapshots/cordis-tool-round/ui.expected.md
  61. 14 17
      apps/web/vite.config.ts
  62. 2 2
      docs/config-catalog.i18n.yaml
  63. 2 3
      docs/config-catalog.md
  64. 2 3
      docs/config-catalog.zh.md
  65. 2 2
      docs/module-graph.i18n.yaml
  66. 171 197
      docs/module-graph.md
  67. 171 197
      docs/module-graph.zh.md
  68. 2 2
      docs/subsystems/client-modules.i18n.yaml
  69. 11 3
      docs/subsystems/client-modules.md
  70. 11 3
      docs/subsystems/client-modules.zh.md
  71. 9 14
      knip.json
  72. 2 1
      package.json
  73. 6 0
      packages/bundle/web-app/cordis.patch.yml
  74. 2 0
      packages/bundle/web-app/package.json
  75. 41 2
      packages/client/AGENTS.md
  76. 2 2
      packages/client/README.i18n.yaml
  77. 2 3
      packages/client/README.md
  78. 2 3
      packages/client/README.zh.md
  79. 14 8
      packages/client/connection/package.json
  80. 5 5
      packages/client/locale/package.json
  81. 3 2
      packages/client/locale/tests/apply.client.spec.ts
  82. 93 0
      packages/client/locale/tests/document-language.client.spec.ts
  83. 1 1
      packages/client/locale/tests/language-row.client.spec.tsx
  84. 2 2
      packages/client/modules/README.i18n.yaml
  85. 7 3
      packages/client/modules/README.md
  86. 7 3
      packages/client/modules/README.zh.md
  87. 3 1
      packages/client/modules/package.json
  88. 44 16
      packages/client/modules/src/client/index.ts
  89. 113 44
      packages/client/modules/src/client/manifest.ts
  90. 84 60
      packages/client/modules/src/client/system.ts
  91. 150 28
      packages/client/modules/src/index.ts
  92. 149 40
      packages/client/modules/tests/loader.client.spec.ts
  93. 185 1
      packages/client/modules/tests/node-half.client.spec.ts
  94. 4 1
      packages/client/modules/tsdown.config.ts
  95. 2 2
      packages/client/runtime/README.i18n.yaml
  96. 2 2
      packages/client/runtime/README.md
  97. 2 2
      packages/client/runtime/README.zh.md
  98. 24 13
      packages/client/runtime/package.json
  99. 3 3
      packages/client/runtime/src/client/contract/store.ts
  100. 1 1
      packages/client/runtime/src/client/index.ts

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.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-19-gui-layering-and-rpc-protocol.md
-2026-07-19-gui-layering-and-rpc-protocol.md: 22bebb0951ca3ffa0a0679396c4c54ef32eca279
-2026-07-19-gui-layering-and-rpc-protocol.zh.md: c38328cc2e692e8eed44a3dbf4149207820265c9
+2026-07-19-gui-layering-and-rpc-protocol.md: 620803668e88f5a462ab2a75e6e916a85d433ed6
+2026-07-19-gui-layering-and-rpc-protocol.zh.md: 8145b6e5af149f79b45e03c80aeea90da90b3729

+ 4 - 4
.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.md

@@ -26,7 +26,7 @@ Directories layer as follows:
 - `packages/host/*`: packages provide host-side capability only (representing the Node.js engineering core built on the existing harness plugin system), and additionally
     - the unified backend protocol (fetch, HTTP, streaming interfaces…) — definitions and support, see the "Message protocol" sections below
 - `packages/client/*`: packages provide client-side capability only; every package stays single-sided. Three kinds live here (the axes are owned by the [client plugin loading note](2026-07-23-client-plugin-loading-model.md)):
-    - **Pure libraries** (`ui-slots`, `web-react`, `ui-primitives`, plus the `loader` kernel package): ordinary root-index packages, statically bundled into the shell; the first three are seeded into the module table.
+    - **Pure libraries** (`ui-slots`, `ui-primitives`, plus the `loader` kernel package): ordinary root-index packages, statically bundled into the shell; the two client libraries are seeded into the module table.
     - **Static-arrival entry packages** (`connection`, `runtime`, `ui-theme`, `i18n`, `hmr`): no `dsh.client` key and no browser bundle — the shell bundles their `src/client/` half and registers it with `ctx.modules`; they are governed as entries of the host-authored graph like everything else.
     - **Fetch-arrival plugin packages** (`ui-layout`, `ui-sidebar`, `ui-conversation`, `ui-trajectory`): dual-entry — the root index is the node half (an empty `apply`, existing so the host Loader governs lifecycle and the web plugin registry discovers the package.json `dsh.client` declaration); the implementation lives under `src/client/`, shipped as the `./client` subpath (a tsdown closure-factory bundle). Cross-plugin consumption of `/client` is type-only; value cooperation goes through cordis services.
 - `apps/` holds the externally exported applications, assembled from Client / Host mixtures.
@@ -39,7 +39,7 @@ apps/*  (applications: apps/web = vite app, apps/cli = bin dispatch)
   │ consume
 packages/host/*                      packages/client/*
-  apiproxy   front layer: protocol     pure libs: ui-slots / web-react / ui-primitives
+  apiproxy   front layer: protocol     pure libs: ui-slots / ui-primitives
   runtime    assembly / host entity    dsh.client plugins ×8 (node half = empty apply,
   webserver  Web HTTP carriage                              client half = src/client/)
   │ ctx.plugin(...)                      ▲ import only apiproxy's /api /client subpaths
@@ -65,8 +65,8 @@ On the protocol side: TS interfaces (`packages/host/apiproxy/src/api/`, zero Nod
 | Front layer | `dsh-host-apiproxy` | TS/zod definitions (api/) + the fetch abstraction (fetch/: handler + client base class) | Keep it simple — every consumer needs it; importable from Node and browser alike; protocol content in the "Message protocol" sections below; clients must not bypass api through ctx |
 | Assembly layer | `dsh-host-runtime` | Plugin composition + ApiProxy integration + the web UI plugin mount (in-memory Loader tree over the eight dsh.client packages); home of host-level configuration (defaults/persistenceRoot, future user profile) | Which plugins mount and with what defaults is decided only here; shells must not alter the assembly |
 | Carrier layer | `dsh-host-webserver` | Web HTTP and upgrade: static serving + `/api/*`→handler forwarding + WebSocket upgrade route + close semantics; plugin bundle endpoint + `__DSH_BOOT__` manifest injection (fed by the web plugin registry) | Web (browser access) only; zero workspace dependencies (the registry arrives by structural injection); Electron does not reuse it |
-| Client libraries | `dsh-client-ui-slots` / `dsh-client-web-react` / `dsh-client-ui-primitives` | Slot registry core / ctx↔React glue / pure React atoms | Zero cordis runtime dependency in components; seeded into the loader module table by the shell |
-| Client plugins | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-i18n` / `dsh-client-ui-layout` / `dsh-client-ui-sidebar` / `dsh-client-ui-conversation` / `dsh-client-ui-trajectory` | Browser-side cordis plugin tree (wire consumer, core services, theme, i18n, layout, sidebar, conversation, trajectory) — see the web client architecture note | Dual entry (node half = empty apply; implementation in `src/client/`); the consumption face goes exclusively through ApiProxy |
+| Client libraries | `dsh-client-ui-slots` / `dsh-client-ui-primitives` | Slot contracts / pure React atoms | Seeded into the loader module table by the shell |
+| Client plugins | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-ui-renderer` / feature UI packages | Browser-side Cordis plugin tree: wire consumer, core services, theme, React rendering, and feature composition — see the web client architecture note | Dual entry (node half = empty apply; implementation in `src/client/`); cross-plugin value cooperation uses services and slots |
 | Application | `@deepseek-ai/dsh` (apps/cli) + `dsh-web-frontend` (apps/web, the vite application) | Coarse bin dispatch + one assembly module per application (web.ts / headless.ts); the vite app is a thin main over the `dsh-client-web` shell surface | Applications use dynamic imports so they never load each other; workspace knowledge like dist location stays in the app |
 
 #### Naming rule

+ 4 - 4
.agents/notes/implemented/architecture/2026-07-19-gui-layering-and-rpc-protocol.zh.md

@@ -24,7 +24,7 @@ Status: implemented
 - `packages/host/*`:包只提供 Host 侧能力(代表了以现在 Harness 实体插件系统为主体的 Node.js 代码核心工程),除此之外,还包含
     - 统一后端协议(fetch、HTTP、流式接口等)定义和支持,见本篇「消息协议」起各节
 - `packages/client/*`:包只提供 Client 侧能力,每包单边不混。这里住三类包(两条轴归 [client 插件装载笔记](2026-07-23-client-plugin-loading-model.md) 所有):
-    - **纯库**(`ui-slots`、`web-react`、`ui-primitives`,外加内核包 `loader`):普通根入口包,静态打包进壳;前三者播种进模块表。
+    - **纯库**(`ui-slots`、`ui-primitives`,外加内核包 `loader`):普通根入口包,静态打包进壳;两个客户端库播种进模块表。
     - **静态到达 entry 包**(`connection`、`runtime`、`ui-theme`、`i18n`、`hmr`):无 `dsh.client` 键、无浏览器 bundle——壳把它们的 `src/client/` 半边打进自己的 bundle 并向 `ctx.modules` 登记;它们与其余单元一样,作为 host 独家撰写的图里的 entry 受治理。
     - **fetch 到达插件包**(`ui-layout`、`ui-sidebar`、`ui-conversation`、`ui-trajectory`):双入口——根入口是 node 半边(空 `apply`,其存在是为了让 host Loader 管辖生命周期、让 web 插件注册表发现 package.json 的 `dsh.client` 声明);实现住在 `src/client/` 下,经 `./client` 子路径发布(tsdown 闭包工厂 bundle)。跨插件消费 `/client` 只限类型;值层面的协作走 cordis 服务。
 - `apps/` 作为对外导出的应用入口,可以由 Client / Host 混合组装。
@@ -37,7 +37,7 @@ apps/*  (applications: apps/web = vite app, apps/cli = bin dispatch)
   │ consume
 packages/host/*                      packages/client/*
-  apiproxy   front layer: protocol     pure libs: ui-slots / web-react / ui-primitives
+  apiproxy   front layer: protocol     pure libs: ui-slots / ui-primitives
   runtime    assembly / host entity    dsh.client plugins ×8 (node half = empty apply,
   webserver  Web HTTP carriage                              client half = src/client/)
   │ ctx.plugin(...)                      ▲ import only apiproxy's /api /client subpaths
@@ -63,8 +63,8 @@ TypeScript 以 solution 根引用的**两个聚合 program** 检查(`tsconfig.
 | 前置层 | `dsh-host-apiproxy` | TS/zod 定义 (api/)+ fetch 抽象 (fetch/:handler + 客户端基类) | 做简单、每个消费方都要;Node/浏览器皆可 import;协议内容见下文「消息协议」起各节;client 不得经 ctx 绕开 api |
 | 装配层 | `dsh-host-runtime` | 插件组合 + ApiProxy 集成 + web UI 插件挂载(覆盖八个 dsh.client 包的内存 Loader 树);host 级配置归属地(defaults/persistenceRoot,将来用户 profile) | 装什么插件、给什么默认值只在这里定;壳不得改装配 |
 | 承载层 | `dsh-host-webserver` | Web HTTP 与 upgrade:静态服务 + `/api/*`→handler 转发 + WebSocket upgrade route + close 语义;插件 bundle 端点 + `__DSH_BOOT__` manifest(元数据清单)注入(由 web 插件注册表供给) | Web(浏览器访问)专用;零 workspace 依赖(注册表经结构注入到达);Electron 不复用它 |
-| client 库 | `dsh-client-ui-slots` / `dsh-client-web-react` / `dsh-client-ui-primitives` | slot 注册表核心 / ctx↔React 胶合 / 纯 React 原子组件 | 组件零 cordis 运行时依赖;由壳播种进 loader 模块表 |
-| client 插件 | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-i18n` / `dsh-client-ui-layout` / `dsh-client-ui-sidebar` / `dsh-client-ui-conversation` / `dsh-client-ui-trajectory` | 浏览器侧 cordis 插件树(wire 消费方、核心服务、主题、i18n、布局、侧栏、对话、轨迹)——见 Web 客户端架构笔记 | 双入口(node 半边=空 apply;实现在 `src/client/`);消费面唯一经 ApiProxy |
+| client 库 | `dsh-client-ui-slots` / `dsh-client-ui-primitives` | slot 约定 / 纯 React 原子组件 | 由壳播种进 loader 模块表 |
+| client 插件 | `dsh-client-connection` / `dsh-client-runtime` / `dsh-client-ui-theme` / `dsh-client-ui-renderer` / 功能 UI 包 | 浏览器侧 Cordis 插件树:wire 消费方、核心服务、主题、React 渲染与功能组合——见 Web 客户端架构笔记 | 双入口(node 半边=空 apply;实现在 `src/client/`);跨插件值协作经服务与 slot 完成 |
 | 应用 | `@deepseek-ai/dsh`(apps/cli)+ `dsh-web-frontend`(apps/web,vite 应用) | bin 粗分发 + 每个应用一个拼装模块(web.ts / headless.ts);vite 应用是 `dsh-client-web` 壳表面之上的薄 main | 各应用使用动态 import,因此不会互相加载;dist 定位等 workspace 知识留在 app |
 
 #### 命名规则

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.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-19-gui-web-client-architecture.md
-2026-07-19-gui-web-client-architecture.md: 070b857f14007f429826ab83b17b5c8fbd3d3d0b
-2026-07-19-gui-web-client-architecture.zh.md: 37e082985b3e6dbf3effd33a5aaeb54fce96d713
+2026-07-19-gui-web-client-architecture.md: 8b4f940299cbba78d403c34b1e5fc9740e44f2c2
+2026-07-19-gui-web-client-architecture.zh.md: a86f9d6f71bed0ce147ffee39bd921037d6c1c4c

+ 9 - 10
.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md

@@ -22,23 +22,23 @@ Both ends run cordis. The host is a cordis plugin tree; the browser runs a secon
 │  ├ GET /plugins/<id>/client.js │   │  │   ui-theme/i18n(fetch bundle,boot 预拉)       │
 │  └ GET / 注入 __DSH_BOOT__ 图  │   │  ├ lazy entries: layout/sidebar/                   │
 │                                │   │  │   conversation/trajectory(fetch bundle,按需) │
-└────────────────────────────────┘   │  ├ app-shell 伪行(壳内静态注册,同一治理) 
+└────────────────────────────────┘   │  ├ ui-renderer(fetch bundle,React 根)
                                      │  └ session scope ×N(观看驱动,惰性建)            │
-                                     │ React: loading 页 → settled → 整 UI 一次成型       │
+                                     │ DOM loading 页 → settled → React UI 一次成型       │
                                      └────────────────────────────────────────────────────┘
 ```
 
 ## The client cordis tree and the loading chain
 
-The loading chain — the two package kinds (plain vs dsh.client plugin), the module-system/plugin-governor split, the two-phase boot over the host-authored entry graph with revisions, and hot reload — is owned by the [client plugin loading note](2026-07-23-client-plugin-loading-model.md). The load-bearing facts for this document: the browser boots the same vendored `@cordisjs/plugin-loader` as the host with a client module system (`ctx.modules`, `packages/client/modules`) filling its `internal` contract; every unit with product behavior is an entry in the host-authored `__DSH_BOOT__` graph — every production plugin package (infrastructure included) carries the `dsh.client` declaration and arrives as a fetched `./client` tsdown closure bundle, `immediately` rows differing only in boot phase-one prefetch, while plain packages (react family, cordis, the not-yet-promoted libraries) stay shell-bundled, seeded, and invisible to the graph; bundles execute `window.__ModuleLoader__.load({ id, factory })` and their `require` is answered from the lazy CJS module table (seed words + registered factories, materialized and memoized on first require — cross-plugin value imports are a build error, cooperation goes through cordis services); plugin CSS is inlined in the bundle and injected as `<style data-plugin="<id>">` at materialization (CSS Modules hashing + ownership tag = isolation, removal on reload); hot reload is live in dev graphs — the webserver stat-polls the bundles it serves and broadcasts `rebuilt` SSE frames, and the `client-hmr` plugin swaps one fiber per frame. The settled flip (`loader.await()` + an all-ACTIVE sweep) still switches the shell from the loading page to the real UI in one pass — settled means every entry is created and every fiber reached ACTIVE, with FAILED/PENDING fibers listed loud; there is no partial-availability mode (progressive rendering is deferred work).
+The loading chain — the two package kinds (plain vs dsh.client plugin), the module-system/plugin-governor split, the two-phase boot over the host-authored entry graph with revisions, and hot reload — is owned by the [client plugin loading note](2026-07-23-client-plugin-loading-model.md). The load-bearing facts for this document: the browser boots the same vendored `@cordisjs/plugin-loader` as the host with a client module system (`ctx.modules`, `packages/client/modules`) filling its `internal` contract; every unit with product behavior is an entry in the host-authored `__DSH_BOOT__` graph — every production plugin package (infrastructure included) carries the `dsh.client` declaration and arrives as a fetched `./client` tsdown closure bundle, `immediately` rows differing only in boot phase-one prefetch, while plain packages (react family, cordis, the not-yet-promoted libraries) stay shell-bundled, seeded, and invisible to the graph; bundles execute `window.__ModuleLoader__.load({ id, factory })` and their `require` is answered from the lazy CJS module table (seed words + registered factories, materialized and memoized on first require — cross-plugin value imports are a build error, cooperation goes through cordis services); global styles and CSS Modules are inlined in their owning plugin bundle and injected as `<style data-plugin="<id>">` at materialization (CSS Modules also receive hashed names; ownership tags make reload removal possible); hot reload is live in dev graphs — the webserver stat-polls the bundles it serves and broadcasts `rebuilt` SSE frames, and the `client-hmr` plugin swaps one fiber per frame. After `loader.await()` and an all-ACTIVE sweep, the framework-free kernel calls the dynamic UI renderer's `ctx.uiRenderer.mount(container)` once — every entry is created and every fiber reached ACTIVE, with FAILED/PENDING fibers listed loud; there is no partial-availability mode (progressive rendering is deferred work).
 
 Type universes stay split at the aggregate level — `tsconfig.host.json` is the host program and `tsconfig.client.json` the client program, both referenced by the solution root `tsconfig.json` — because both sides merge cordis `Context` under the same keys (`sessions`, `loader`) with different services; client packages consume the wire vocabulary through pure type subpaths (`@deepseek-ai/dsh-session/types` and kin) so no host augmentation rides into the client program.
 
 ## The slot system: how the page composes
 
-The slot system has its own note — the [slot system standard](2026-07-22-slot-type-chain-implementation.md) — and this document defers to it entirely. The one-paragraph summary for orientation: the shell renders only `'root'`; a plugin composes UI through a single `register` call that occupies a slot, declares+authorizes its child slots (`children` spec object), declares its store, and injects its business face; component props arrive in four auto-derived shares (`PropsRuntime<K>` / `PropsRenderSlots<S>` / `PropsStore<H>` / inject), each from its single source of truth. `SlotMap` declaration merging is the type authority and entries carry only the owner share ("whoever injects it, owns its type"); every rendered entry sits in a per-entry error boundary.
+The slot system has its own note — the [slot system standard](2026-07-22-slot-type-chain-implementation.md) — and this document defers to it entirely. The one-paragraph summary for orientation: ui-renderer renders only `'root'`; a plugin composes UI through a single `register` call that occupies a slot, declares+authorizes its child slots (`children` spec object), declares its store, and injects its business face; component props arrive in four auto-derived shares (`PropsRuntime<K>` / `PropsRenderSlots<S>` / `PropsStore<H>` / inject), each from its single source of truth. `SlotMap` declaration merging is the type authority and entries carry only the owner share ("whoever injects it, owns its type"); every rendered entry sits in a per-entry error boundary.
 
-Implementation homes: registry core and the props-share types in `packages/client/ui-slots`, outlet/renderer/uSES bridge in `packages/client/web-react`.
+Implementation homes: registry core and the props-share types live in `packages/client/ui-slots`; the outlet renderer, uSES bridge, application-level installation, and root mounting live in `packages/client/ui-renderer`.
 
 ## Services and scope addressing
 
@@ -75,18 +75,17 @@ Notifier 微任务合批 ──► ConversationSnapshot 缓存 ──uSES──
 - **ConversationNodeAssembler** (`runtime/src/client/conversation/`): the Session-owned incremental engine runs independently registered Definitions over raw events. `match(event)` selects `(kind, id)` without Context scans; start/update build Definition state; engine-computed Locations carry Turn/Step closure; backward Context reads record dependencies repaired by later prepends; `buildViewNode(target)` materializes only dirty Contexts. The Chat builder preserves structural order and per-key value identity, `useSession` selectors isolate consumption, and Assistant token publication coalesces to one animation frame. The [Conversation Node decision](2026-08-09-client-conversation-node-assembly.md) owns assembly, while [Tool presentation ownership](2026-08-08-client-tool-presentation-ownership.md) owns recursive Tool rendering.
 - **ConnectionController** (in `packages/client/connection`): opens the mux/host streams, pumps with for-await, reconnects with exponential backoff (500ms doubling to 10s, jitter, unlimited) behind a generation fence; sinks are injected one-way (the Controller does not know Session). Reconnect = rebuild: `onConnected` → list refresh + per-open-session resync. The object layer faces only `IApiClient`; Web carriage uses HTTP POST for the two client→server quadrants and [one WebSocket per logical stream](2026-08-04-websocket-downlink-carrier.md) for the two server→client quadrants, while the client class family remains the layering note's territory.
 
-## The React face (`packages/client/web-react`)
+## The React face (`packages/client/ui-renderer`)
 
-The glue package is the whole ctx↔React boundary; components stay framework-free.
+The dynamic ui-renderer plugin owns the ctx↔React adapter, application-level installation, root mount, and title projection. Business components receive bound hooks through slot props and do not value-import the renderer.
 
-- The snapshot store engine **lives in the runtime package** (zustand vanilla with draft-based updates, `flush: 'sync'` by default with opt-in `'raf'` batching, opt-in whole-value localStorage persistence, dev-mode deep freeze — all exported from `runtime`'s `./client` main entry, no subpath): store products are bare observable sources with no hook members. Plugins reach the engine only through `defineStore` declarations per the [slot system standard](2026-07-22-slot-type-chain-implementation.md). web-react composes every hook at the binding site (`bindSnapshotSelector`, per-source cached) from the one data contract React consumes: `ObservableSnapshot<T>` (`getSnapshot`/`subscribe`) — a Session object and a snapshot store both satisfy it. Business plugin packages depend on runtime and ui-slots only; web-react is shell-only glue.
+- The snapshot store engine **lives in the runtime package** (zustand vanilla with draft-based updates, `flush: 'sync'` by default with opt-in `'raf'` batching, opt-in whole-value localStorage persistence, dev-mode deep freeze — all exported from `runtime`'s `./client` main entry, no subpath): store products are bare observable sources with no hook members. Plugins reach the engine only through `defineStore` declarations per the [slot system standard](2026-07-22-slot-type-chain-implementation.md). ui-renderer composes every hook at the binding site (`bindSnapshotSelector`, per-source cached) from the one data contract React consumes: `ObservableSnapshot<T>` (`getSnapshot`/`subscribe`) — a Session object and a snapshot store both satisfy it.
 - `bindSnapshotSelector(source)`: binds a source into a typed selector hook over uSES-with-selector. The four uSES contract clauses hold by construction: getSnapshot returns the cached reference; subscribe is a bind-time closure (reference-stable forever); pure CSR passes no server snapshot; equality defaults to `Object.is` with `shallowEqual` opt-in per call.
-- `useInvoke(fn)`: wraps an async action into a stable trigger plus pending flag; pending rides a per-hook external store read through uSES (no setState on the render path), concurrent invocations are counted, and the invoke reference never changes.
 - Equality protocol, whole chain: producers use structural sharing; consumers short-circuit with `Object.is` or `shallowEqual`; `React.memo` shallow. Deep comparison is banned everywhere.
 
 ## Directory shape
 
-Client packages live under `packages/client/*`, with `apps/web` as the thin Vite application over the shell's boot export. Plugin packages keep their browser half under `src/client/`; **every build artifact lands in `lib/`** — the node half as `lib/index.js`/`lib/invariant.js`, the browser bundle as `lib/client.js` (the shared tsdown client preset emits both; there is no `dist/` directory, and `exports["./client"]` points at `./lib/client.js`). `ui-slots`, web-react, and runtime form the infrastructure direction; feature plugins cooperate through services and slots rather than importing presentation implementations.
+Client packages live under `packages/client/*`, with `apps/web` as the thin Vite application over the shell's boot export. Plugin packages keep their browser half under `src/client/`; **every build artifact lands in `lib/`** — the node half as `lib/index.js`/`lib/invariant.js`, the browser bundle as `lib/client.js` (the shared tsdown client preset emits both; there is no `dist/` directory, and `exports["./client"]` points at `./lib/client.js`). `ui-slots`, runtime, and ui-renderer form the infrastructure direction; feature plugins cooperate through services and slots rather than importing presentation implementations.
 
 A multi-domain plugin package additionally splits its client half by future package boundaries — ui-conversation is the exemplar:
 

+ 9 - 10
.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.zh.md

@@ -22,23 +22,23 @@ Status: implemented
 │  ├ GET /plugins/<id>/client.js │   │  │   ui-theme/i18n(fetch bundle,boot 预拉)       │
 │  └ GET / 注入 __DSH_BOOT__ 图  │   │  ├ lazy entries: layout/sidebar/                   │
 │                                │   │  │   conversation/trajectory(fetch bundle,按需) │
-└────────────────────────────────┘   │  ├ app-shell 伪行(壳内静态注册,同一治理) 
+└────────────────────────────────┘   │  ├ ui-renderer(fetch bundle,React 根)
                                      │  └ session scope ×N(观看驱动,惰性建)            │
-                                     │ React: loading 页 → settled → 整 UI 一次成型       │
+                                     │ DOM loading 页 → settled → React UI 一次成型       │
                                      └────────────────────────────────────────────────────┘
 ```
 
 ## client cordis 树与装载链
 
-装载链——两类包(普通包 vs dsh.client 插件)、模块系统/插件治理器之分、host 独家撰写的带修订号 entry 图之上的双阶段 boot、热重载——归 [client 插件装载笔记](2026-07-23-client-plugin-loading-model.md) 所有。本篇赖以立足的事实:浏览器启动与 host 相同的 vendored `@cordisjs/plugin-loader`,由 client 模块系统(`ctx.modules`,`packages/client/modules`)填上其 `internal` 约定;凡带产品行为的单元都是 host 独家撰写的 `__DSH_BOOT__` 图里的 entry——每个生产插件包(含基础设施)都携带 `dsh.client` 声明、以 fetch 到达的 `./client` tsdown 闭包 bundle 供给,`immediately` 行的差别仅在 boot 第一阶段预取,而普通包(react 家族、cordis、尚未升格的库)保持打进壳、已播种、对图不可见;bundle 执行 `window.__ModuleLoader__.load({ id, factory })`,其 `require` 由 lazy CJS 模块表应答(种子词条 + 已登记工厂,首次 require 时物化并记忆化——跨插件值 import 是构建错误,协作走 cordis 服务);插件 CSS 内联在 bundle 里、物化时注入为 `<style data-plugin="<id>">`(CSS Modules 哈希 + 归属标记 = 隔离,重载时移除);热重载已在 dev 图落地——webserver 对自己供给的 bundle 做 stat 轮询并广播 `rebuilt` SSE 帧,`client-hmr` 插件每帧换掉一个 fiber。settled 翻转(`loader.await()` + 一次全 ACTIVE 扫描)依旧让壳从 loading 页一次切换到真 UI——settled 意味着每个 entry 已创建、每个 fiber 都到达 ACTIVE,FAILED/PENDING 的 fiber 被大声列出;不存在部分可用模式(渐进渲染为后置工作)。
+装载链——两类包(普通包 vs dsh.client 插件)、模块系统/插件治理器之分、host 独家撰写的带修订号 entry 图之上的双阶段 boot、热重载——归 [client 插件装载笔记](2026-07-23-client-plugin-loading-model.md) 所有。本篇赖以立足的事实:浏览器启动与 host 相同的 vendored `@cordisjs/plugin-loader`,由 client 模块系统(`ctx.modules`,`packages/client/modules`)填上其 `internal` 约定;凡带产品行为的单元都是 host 独家撰写的 `__DSH_BOOT__` 图里的 entry——每个生产插件包(含基础设施)都携带 `dsh.client` 声明、以 fetch 到达的 `./client` tsdown 闭包 bundle 供给,`immediately` 行的差别仅在 boot 第一阶段预取,而普通包(react 家族、cordis、尚未升格的库)保持打进壳、已播种、对图不可见;bundle 执行 `window.__ModuleLoader__.load({ id, factory })`,其 `require` 由 lazy CJS 模块表应答(种子词条 + 已登记工厂,首次 require 时物化并记忆化——跨插件值 import 是构建错误,协作走 cordis 服务);全局样式与 CSS Modules 都内联在其持有插件的 bundle 中,物化时注入为 `<style data-plugin="<id>">`(CSS Modules 还会取得哈希类名;归属标签使重载时移除成为可能);热重载已在 dev 图落地——webserver 对自己供给的 bundle 做 stat 轮询并广播 `rebuilt` SSE 帧,`client-hmr` 插件每帧换掉一个 fiber。`loader.await()` 与全 ACTIVE 扫描完成后,不依赖框架的内核会调用动态 UI 渲染器的 `ctx.uiRenderer.mount(container)` 一次——此时每个 entry 已创建、每个 fiber 都到达 ACTIVE,FAILED/PENDING 的 fiber 被大声列出;不存在部分可用模式(渐进渲染为后置工作)。
 
 类型宇宙在聚合层拆分——`tsconfig.host.json` 是 host program、`tsconfig.client.json` 是 client program,二者由 solution 根 `tsconfig.json` 引用,因为两侧都在相同键(`sessions`、`loader`)上对 cordis `Context` 做声明合并且服务不同;client 包经纯类型子路径(`@deepseek-ai/dsh-session/types` 等)消费协议词汇,host 侧的声明合并不会搭车进入 client program。
 
 ## slot 体系:页面怎么拼
 
-slot 体系有自己的笔记——[slot 体系标准](2026-07-22-slot-type-chain-implementation.md)——本文整体移交给它。此处只留一段定位摘要:只渲染 `'root'`;插件用单独一次 `register` 调用组合 UI——占用 slot、声明并授权子 slot(`children` spec 对象)、声明 store、注入业务面;组件 props 分四份额自动推导到达(`PropsRuntime<K>` / `PropsRenderSlots<S>` / `PropsStore<H>` / inject),各有唯一真源。`SlotMap` 声明合并仍是类型权威,entry 只携带 owner 份额(「谁注入的,类型归谁」);每个被渲染的注册项都在 per-entry 错误边界之内。
+slot 体系有自己的笔记——[slot 体系标准](2026-07-22-slot-type-chain-implementation.md)——本文整体移交给它。此处只留一段定位摘要:ui-renderer 只渲染 `'root'`;插件用单独一次 `register` 调用组合 UI——占用 slot、声明并授权子 slot(`children` spec 对象)、声明 store、注入业务面;组件 props 分四份额自动推导到达(`PropsRuntime<K>` / `PropsRenderSlots<S>` / `PropsStore<H>` / inject),各有唯一真源。`SlotMap` 声明合并仍是类型权威,entry 只携带 owner 份额(「谁注入的,类型归谁」);每个被渲染的注册项都在 per-entry 错误边界之内。
 
-实现的家:注册表核心与 props 份额类型在 `packages/client/ui-slots`,出口组件/渲染器/uSES 桥在 `packages/client/web-react`。
+实现的家:注册表核心与 props 份额类型在 `packages/client/ui-slots`;outlet 渲染器、uSES 桥、应用级安装与根挂载在 `packages/client/ui-renderer`。
 
 ## 服务与 scope 寻址
 
@@ -75,18 +75,17 @@ Notifier 微任务合批 ──► ConversationSnapshot 缓存 ──uSES──
 - **ConversationNodeAssembler**(`runtime/src/client/conversation/`):Session 拥有的增量引擎在原始事件上运行各自独立注册的 Definition。`match(event)` 无须扫描 Context 即可选出 `(kind, id)`;start/update 构造 Definition state;引擎计算的 Location 携带 Turn/Step 关闭信息;向前查询 Context 时记录依赖,并由后续 prepend 修复;`buildViewNode(target)` 只物化 dirty Context。Chat builder 保留结构顺序和 per-key value identity,`useSession` selector 负责消费隔离,Assistant token 发布则合并到每个 animation frame 一次。[Conversation Node 决策](2026-08-09-client-conversation-node-assembly.md)拥有组装边界,[Tool 展示所有权](2026-08-08-client-tool-presentation-ownership.md)拥有 Tool 递归渲染。
 - **ConnectionController**(在 `packages/client/connection`):开 mux/host 双流、for-await 泵入,代际围栏之内指数退避重连(500ms 翻倍至 10s 封顶、抖动、无限重试);sinks 单向注入(Controller 不认识 Session)。重连 = 重建:`onConnected` → 列表刷新 + 各已打开会话 resync。对象层只面向 `IApiClient`;Web 承载以 HTTP POST 载两个 client→server 象限、以[每逻辑流一条 WebSocket](2026-08-04-websocket-downlink-carrier.md)载两个 server→client 象限,客户端类族归分层笔记属地。
 
-## React 面(`packages/client/web-react`)
+## React 面(`packages/client/ui-renderer`)
 
-胶水包就是整条 ctx↔React 边界;组件保持零框架依赖
+动态 ui-renderer 插件持有 ctx↔React 适配器、应用级安装、根挂载与标题投影。业务组件通过 slot props 接收绑定后的钩子,不对渲染器做值 import
 
-- 快照 store 引擎**住 runtime 包**(zustand vanilla + 草稿式更新,缺省 `flush: 'sync'`,可选 `'raf'` 合批,可选整值 localStorage 持久化,dev 深冻结——全部从 `runtime` 的 `./client` 主出口导出,无子路径):store 产物是裸的可观察源,不带任何钩子成员。插件只经 [slot 体系标准](2026-07-22-slot-type-chain-implementation.md) 的 `defineStore` 声明触及引擎。web-react 在绑定处(`bindSnapshotSelector`,按源缓存)从 React 消费的唯一数据约定合成每个钩子:`ObservableSnapshot<T>`(`getSnapshot`/`subscribe`)——Session 对象与快照 store 同构满足它。业务插件包只依赖 runtime 与 ui-slots;web-react 是仅壳可用的胶水。
+- 快照 store 引擎**住 runtime 包**(zustand vanilla + 草稿式更新,缺省 `flush: 'sync'`,可选 `'raf'` 合批,可选整值 localStorage 持久化,dev 深冻结——全部从 `runtime` 的 `./client` 主出口导出,无子路径):store 产物是裸的可观察源,不带任何钩子成员。插件只经 [slot 体系标准](2026-07-22-slot-type-chain-implementation.md) 的 `defineStore` 声明触及引擎。ui-renderer 在绑定处(`bindSnapshotSelector`,按源缓存)从 React 消费的唯一数据约定合成每个钩子:`ObservableSnapshot<T>`(`getSnapshot`/`subscribe`)——Session 对象与快照 store 同构满足它。
 - `bindSnapshotSelector(source)`:把一个源绑定为经 uSES-with-selector 的带类型 selector 钩子。uSES 约定四条按构造成立:getSnapshot 恒返缓存引用;subscribe 是绑定期闭包(引用永稳);纯 CSR 不传 server snapshot;相等性缺省 `Object.is`,按调用可选 `shallowEqual`。
-- `useInvoke(fn)`:把异步动作包成引用恒定的触发器加 pending 标志;pending 走每个钩子的外部 store 经 uSES 读出(渲染路径零 setState),并发调用计数,invoke 引用永不变。
 - 相等性协议,全链一致:生产端结构共享;消费方以 `Object.is` 或 `shallowEqual` 短路;`React.memo` 浅比较。深比较全链禁止。
 
 ## 目录形态
 
-Client 包位于 `packages/client/*`,`apps/web` 是壳 boot 导出之上的薄 Vite 应用。插件包的浏览器半边在 `src/client/` 下;**一切构建产物落 `lib/`**——node 半边为 `lib/index.js`/`lib/invariant.js`,浏览器 bundle 为 `lib/client.js`(共享 tsdown client 预设两者皆出;无 `dist/` 目录,`exports["./client"]` 指向 `./lib/client.js`)。`ui-slots`、web-react 与 runtime 构成基础设施方向;功能插件通过服务与 slot 协作,不导入展示实现。
+Client 包位于 `packages/client/*`,`apps/web` 是壳 boot 导出之上的薄 Vite 应用。插件包的浏览器半边在 `src/client/` 下;**一切构建产物落 `lib/`**——node 半边为 `lib/index.js`/`lib/invariant.js`,浏览器 bundle 为 `lib/client.js`(共享 tsdown client 预设两者皆出;无 `dist/` 目录,`exports["./client"]` 指向 `./lib/client.js`)。`ui-slots`、runtime 与 ui-renderer 构成基础设施方向;功能插件通过服务与 slot 协作,不导入展示实现。
 
 多域插件包的 client 半边还按未来包边界再拆——ui-conversation 即样板:
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.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-22-slot-type-chain-implementation.md
-2026-07-22-slot-type-chain-implementation.md: 48c3319cdab26b2e053cb08ed1dafa2b96a980d7
-2026-07-22-slot-type-chain-implementation.zh.md: 496677ecdef7d5b691edfc0bf43c9ff9d3d80449
+2026-07-22-slot-type-chain-implementation.md: e4072f95678f13de302acf84c0b35218fa5dd8d4
+2026-07-22-slot-type-chain-implementation.zh.md: 10db92b3a5847bd8cb58a63aa546860002810c70

+ 4 - 4
.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.md

@@ -12,11 +12,11 @@ The page is composed at runtime from independently loaded plugins, so the UI nee
 
 ## Decision
 
-One sentence: **the shell renders only `'root'`; a plugin composes UI through a single `register` call that simultaneously occupies a slot, declares+authorizes its child slots, declares its store, and injects its business face; components are pure functions whose props arrive in four shares, each auto-derived from its single source of truth.**
+One sentence: **the ui-renderer renders only `'root'`; a plugin composes UI through a single `register` call that simultaneously occupies a slot, declares+authorizes its child slots, declares its store, and injects its business face; components are pure functions whose props arrive in four shares, each auto-derived from its single source of truth.**
 
 ### 'root' is the only a-priori slot
 
-`SlotRegistry` (client runtime) declares `'root'` at construction — single/root, `owner: {}` — and its `SlotMap` merge lives in the runtime package. The shell's entire assembly is `ctx.slots.renderSlot('root', {})`: the only ctx-level render entry; any other key, a missing renderer, or an unregistered root fails loud (no fallback).
+`SlotRegistry` (client runtime) declares `'root'` at construction — single/root, `owner: {}` — and its `SlotMap` merge lives in the runtime package. The ui-renderer's entire assembly is `ctx.slots.renderSlot('root', {})`: the only ctx-level render entry; any other key, a missing renderer, or an unregistered root fails loud (no fallback).
 
 ### register is the single API; children = declaration + authorization + runtime spec
 
@@ -61,7 +61,7 @@ In the type chain, a chain entry's SlotMap shape is `{ kind: 'chain'; scope; own
 
 ### The store seat: framework engine, registrant schema
 
-The framework owns exactly one subscription machine: the snapshot store engine (zustand vanilla + immer + optional localStorage persistence) lives in the **runtime package** (`./client` main entry — no subpath), producing bare observable sources; web-react binds them into hooks at the outlet (per-source cached uSES binding). What a store *contains* is the registrant's declaration, written as a factory so no module-level handle exists (a module-scoped handle would be a de-facto singleton surviving plugin reloads):
+The framework owns exactly one subscription machine: the snapshot store engine (zustand vanilla + immer + optional localStorage persistence) lives in the **runtime package** (`./client` main entry — no subpath), producing bare observable sources; ui-renderer binds them into hooks at the outlet (per-source cached uSES binding). What a store *contains* is the registrant's declaration, written as a factory so no module-level handle exists (a module-scoped handle would be a de-facto singleton surviving plugin reloads):
 
 ```ts ignore-check
 export function createChatStore() {
@@ -92,7 +92,7 @@ Hooks are framework-made only: `useSession`, `useSessions`, `useWorkspaces`, `us
 
 `SessionProvider` is a framework component **delivered as a standard-kit seat**: an entry whose `children` declare a session-scope slot receives it as a prop (type in ui-slots, value injected by the renderer) — components never value-import it. It is self-wired (it reads the runtime's current-session state internally; the assembler passes nothing), render-prop shaped — `children(sessionId)` with an `empty` branch, remounting under `key={sessionId}`. `BindingContext` is machinery-internal; business components see zero React contexts. Inject factories execute inside the outlet on purpose (per-entry error boundaries catch them; a crashing registrant blacks out only its own entry while assembly errors rethrow); the outlet reads tree context as a machinery-only implicit parameter — the "identity from the register closure, situation from the tree position" split.
 
-Rendering lives behind an installation contract so the runtime stays React-free: `SlotRenderer` (interface in ui-slots, implementation `createSlotRenderer()` in web-react) is installed once at shell boot via `ctx.slots.install(...)`; double install and render-before-install throw. Ownership bookkeeping is a single `Map<key, entry>` in the service — ledger, slots, contributions, render bindings, and store instances all live and die on the one entry axis, which closes the stale-authority window across plugin reloads by construction (a disposed entry's captured `renderSlot` throws a stale-authorization error on entry).
+Rendering lives behind an installation contract so the runtime stays React-free: `SlotRenderer` (interface in ui-slots, implementation `createSlotRenderer()` in ui-renderer) is installed once at shell boot via `ctx.slots.install(...)`; double install and render-before-install throw. Ownership bookkeeping is a single `Map<key, entry>` in the service — ledger, slots, contributions, render bindings, and store instances all live and die on the one entry axis, which closes the stale-authority window across plugin reloads by construction (a disposed entry's captured `renderSlot` throws a stale-authorization error on entry).
 
 ### Type-chain implementation rulings
 

+ 4 - 4
.agents/notes/implemented/architecture/2026-07-22-slot-type-chain-implementation.zh.md

@@ -12,11 +12,11 @@ Status: implemented
 
 ## 决策
 
-一句话:**只渲染 `'root'`;插件用单独一次 `register` 调用组合 UI——这一次调用同时占用 slot、声明并授权子 slot、声明 store、注入业务面;组件是纯函数,props 分四份额到达,每一份额都从各自唯一的真源自动推导。**
+一句话:**ui-renderer 只渲染 `'root'`;插件用单独一次 `register` 调用组合 UI——这一次调用同时占用 slot、声明并授权子 slot、声明 store、注入业务面;组件是纯函数,props 分四份额到达,每一份额都从各自唯一的真源自动推导。**
 
 ### 'root' 是唯一的先验 slot
 
-`SlotRegistry`(client 运行时)在构造时声明 `'root'`——single/root、`owner: {}`——其 `SlotMap` 合并声明位于运行时包。的全部装配就是 `ctx.slots.renderSlot('root', {})`:唯一的 ctx 级渲染入口;传任何其他键、渲染器未安装、root 无人注册,一律大声失败(无 fallback)。
+`SlotRegistry`(client 运行时)在构造时声明 `'root'`——single/root、`owner: {}`——其 `SlotMap` 合并声明位于运行时包。ui-renderer 的全部装配就是 `ctx.slots.renderSlot('root', {})`:唯一的 ctx 级渲染入口;传任何其他键、渲染器未安装、root 无人注册,一律大声失败(无 fallback)。
 
 ### register 是唯一 API;children = 声明+授权+运行时 spec
 
@@ -61,7 +61,7 @@ ctx.slots.register({
 
 ### store 席位:引擎归框架,schema 归注册方
 
-框架只拥有一套订阅机制:快照 store 引擎(zustand vanilla + immer + 可选 localStorage 持久化)住 **运行时包**(`./client` 主出口——无子路径),产出裸的可观察源;web-react 在 outlet 处把它们绑定成钩子(按源缓存的 uSES 绑定)。store 里*装什么*是注册方的声明,且必须写成工厂函数,使模块级句柄根本无从存在(模块级句柄会成为跨插件重载存活的事实单例):
+框架只拥有一套订阅机制:快照 store 引擎(zustand vanilla + immer + 可选 localStorage 持久化)住 **运行时包**(`./client` 主出口——无子路径),产出裸的可观察源;ui-renderer 在 outlet 处把它们绑定成钩子(按源缓存的 uSES 绑定)。store 里*装什么*是注册方的声明,且必须写成工厂函数,使模块级句柄根本无从存在(模块级句柄会成为跨插件重载存活的事实单例):
 
 ```ts ignore-check
 export function createChatStore() {
@@ -92,7 +92,7 @@ inject 工厂只接收其声明所授权的形参——session slot 获得 `sess
 
 `SessionProvider` 是框架组件,**以标配席位形式送达**:`children` 里声明了 session scope slot 的 entry 经 prop 收到它(类型住 ui-slots,值由渲染器注入)——组件永不对它做值 import。它框架自接线(内部自读运行时的当前会话状态,装配方零传参),render-prop 形——`children(sessionId)` 外加 `empty` 分支,以 `key={sessionId}` 重挂。`BindingContext` 属机械内部;业务组件可见的 React Context 为零。inject 工厂有意在 outlet 内部执行(per-entry 错误边界接得住它们;崩溃的注册方只黑掉自己那一格,装配错误则重抛);outlet 将树上下文作为仅供框架机制使用的隐式参数读取——即「身份出自 register 闭包、现场出自树位置」的分工。
 
-渲染位于一份安装约定之后,因此运行时不依赖 React:`SlotRenderer`(接口住 ui-slots,实现 `createSlotRenderer()` 住 web-react)在壳 boot 时经 `ctx.slots.install(...)` 安装一次;双重安装与安装前渲染均 throw。归属记账是服务里的单一 `Map<key, entry>`——账本、slot、贡献、渲染绑定、store 实例全部沿同一条 entry 轴生灭,跨插件重载的陈旧权威窗口由此在构造上关闭(已 dispose 的 entry 所捕获的 `renderSlot`,一进入口即抛陈旧授权(stale-authorization)错误)。
+渲染位于一份安装约定之后,因此运行时不依赖 React:`SlotRenderer`(接口住 ui-slots,实现 `createSlotRenderer()` 住 ui-renderer)在壳 boot 时经 `ctx.slots.install(...)` 安装一次;双重安装与安装前渲染均 throw。归属记账是服务里的单一 `Map<key, entry>`——账本、slot、贡献、渲染绑定、store 实例全部沿同一条 entry 轴生灭,跨插件重载的陈旧权威窗口由此在构造上关闭(已 dispose 的 entry 所捕获的 `renderSlot`,一进入口即抛陈旧授权(stale-authorization)错误)。
 
 ### 类型链实现裁定
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.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-23-client-plugin-loading-model.md
-2026-07-23-client-plugin-loading-model.md: a9a5055eccf65c12c7ca26a85ea323e69dfc1b15
-2026-07-23-client-plugin-loading-model.zh.md: e7f758f5c8684bc006ebbf9db6b118c1e428812e
+2026-07-23-client-plugin-loading-model.md: 02dadf6e1dc1f2c4fd99907446bc6d07b35ba471
+2026-07-23-client-plugin-loading-model.zh.md: 06ba5512a5a46b2a0d447024e143871ab56088fb

+ 23 - 51
.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.md

@@ -1,10 +1,10 @@
-# Agent Note: Client plugin loading — plain packages, dsh.client plugins, and the two-phase boot
+# Agent Note: Client plugin loading — lazy factories, Cordis lifecycle, and hot reload
 
 Status: implemented
 
 English | [中文](2026-07-23-client-plugin-loading-model.zh.md)
 
-> Scope: the browser-side plugin loading machinery — what is a plugin, how code arrives, and how hot reload rides on that model. This note owns the loading chain; the [web client architecture note](2026-07-19-gui-web-client-architecture.md) defers to it for loading and keeps owning slots, the data object layer, and the React face.
+> Scope: the browser-side plugin loading machinery — how code arrives, how Cordis governs it, and how hot reload rides on that model. This note owns the loading chain; the [client shell layering note](2026-08-15-client-shells-and-dynamic-packages.md) owns package categories, build faces, shared-module requests, and npm dependency declarations, while the [web client architecture note](2026-07-19-gui-web-client-architecture.md) owns slots and the data object layer.
 
 ## Problem
 
@@ -24,31 +24,17 @@ The first-generation client loader (`createClientLoader`) hand-wrote both layers
 
 ## Decision
 
-### Two package kinds; `dsh.client` means plugin, period
+### Package membership and module requests
 
-What makes a package a plugin? One rule: **a package is a plugin package once its consumption is cordis dependency injection; until then it is a plain package.** How code reaches the page is not part of the taxonomy — arrival follows from the kind instead of defining it.
+The [client shell layering note](2026-08-15-client-shells-and-dynamic-packages.md) defines the current static and dynamic package sets and the import rules between them. The loading machinery treats every `dsh.client` package as a host-graph row with one ordinary `lib/client.js` factory bundle. Its declaration carries Cordis `inject` edges, synchronous module-table `external` requests, and the optional `immediately` prefetch mark; the composing app owns only the mounted roster.
 
-- **Plain packages** are the absolute base the module system itself needs, plus libraries not yet converted to DI: the react family, cordis, `@deepseek-ai/dsh-client-modules` (the module system itself — it can never be a plugin, because modules precede all modules), the web shell kernel, and — for now — ui-slots, web-react, ui-primitives. Plain packages are shell-bundled, seeded into the module table, and invisible to the host graph.
-- **Plugin packages** are everything else. Each one carries a `dsh.client` manifest declaration (`{ platform, inject, immediately? }`) and one uniform shape: the shared tsdown preset emits `lib/client.js`, and `exports["./client"]` points at that bundle. Each is a governed entry of the host-authored graph. The current set is connection, runtime, ui-theme, i18n, hmr (dev graphs only), ui-layout, ui-sidebar, ui-conversation, ui-model-selector, ui-user-questions, and ui-trajectory.
-
-The manifest owns the package's loading contract: its `inject` dependency edges, plus the optional `immediately` prefetch mark (absent means lazy). The composing app owns only the roster.
-
-To add a plugin package: declare `dsh.client`, emit the `./client` bundle through the shared preset, add the name to the composing app's roster. Nothing else changes hands.
-
-When does a plain package become a plugin? The upgrade law, recorded so the migration path stays honest: **a plain package becomes a plugin package when its consumers switch to cordis DI, not before.** Three promotions are queued: ui-slots (the slots machinery now living in runtime — SlotRegistry, the renderer contract, the root slot), web-react (the renderer install moving into its own `apply`), and ui-primitives (once components are served through slots/services). Until then they stay plain, and their symbol exports stay ordinary static imports.
-
-Four edge rules govern imports across the two kinds. None of them depends on any per-package mark:
-
-- **Plugin ↔ plugin value imports are a build error.** This holds regardless of either side's `immediately` declaration — the rule must not depend on a mark someone can flip. Cooperation goes through cordis inject/services. `import type` is exempt; the type chain is untouched. This rule is why `scopeOf` is a `SessionRuntime` method and why `transportError` lives in `dsh-host-apiproxy`'s wire layer (its `RpcResult` home, inline-safe).
-- **Plugin → plain package value imports are externals**, judged against the platform list. That list is one constant in the shell (`platform.ts`: react family, cordis, ui-slots, web-react, ui-primitives), imported by both the tsdown preset (for the external judgement) and `seed.ts` (for the table warm-up). One constant, two consumers — the hand-sync drift class stays dead.
-- **The purity gate covers every plugin package.** Its three branches: platform imports become externals; INLINE_SAFE wire layers are inlined; any other workspace leak is a build error. The uniform bundle shape is what makes this coverage total — every plugin builds through the same preset, so no package can sit outside the gate.
-- **The shell is self-sufficient.** The kernel (boot + loading page) value-imports no plugin package; its status stores are hand-rolled. The fail-loud presentation must not depend on the system whose failure it reports.
+The web kernel remains framework-free and imports no dynamic package value. Modules is itself a dynamic row, but the host parser delivers its ordinary factory before the Vite main module. The HTML-installed `__ModuleLoader__` facade uses that factory to construct the module system when the kernel calls `create()`. Runtime arrives through the same pending queue; static React, Cordis, and UI library identities come from the shell seed.
 
 ### One module system, one plugin governor
 
 The browser mirrors the host's division of labor. `dsh-client-modules` (`ClientModuleSystem`) takes the module-system seat that Node's internal ESM loader holds host-side; the same vendored `@cordisjs/plugin-loader` keeps the governance seat on both sides. The line between them in one sentence: **the module system owns module identity and bytes — how code arrives, registers, and becomes an exports; the Loader owns plugin lifecycle — when a plugin mounts, what it waits for, and how it is torn down.**
 
-`ClientModuleSystem` is a lazy CJS table. Executing a bundle only **registers** its factory — the bundle calls `window.__ModuleLoader__.load({ id, factory })` and nothing else happens. Every module body side effect, CSS injection included, lives inside the factory closure and runs at materialization: the first `require`/import of that id, memoized after that. A factory that requires a registered-but-unmaterialized sibling materializes it recursively, so no sort order exists anywhere. When asked to import an id, the table resolves through a fixed branch order: seed word → memoized record → static registration (shell-own modules, e.g. app-shell) → registered factory → graph-row external classic-script load → loud throw. That final throw is the runtime mirror of the build-time purity gate. The system also keeps per-module bookkeeping — owned `<style data-plugin>` tag ids, observed require edges — and exposes the two verbs HMR needs: `prefetch(id)` (load the script and register its factory; concurrent calls share one in-flight task) and `invalidate(id)` (drop the factory and record so the next arrival reloads it).
+`ClientModuleSystem` is a lazy CJS table. Executing a bundle only **registers** its factory — the bundle calls `window.__ModuleLoader__.load({ id, factory })` and nothing else happens. Every module body side effect, CSS injection included, lives inside the factory closure and runs at materialization: the first `require`/import of that id, memoized after that. Import and prefetch recursively register declared dynamic requests before their consumer; a factory then materializes any registered-but-unmaterialized request synchronously. The table resolves through a fixed branch order: seed word → memoized record → graph-row classic-script registration → registered-factory materialization → loud throw. The modules factory is the bootstrap exception: the HTML facade materializes it first, and construction places those same exports directly in the memoized table. That final throw is the runtime mirror of the build-time purity gate. The system also keeps per-module bookkeeping — owned `<style data-plugin>` tag ids, observed require edges — and exposes the two verbs HMR needs: `prefetch(id)` (register the requested dynamic factories and the row's own factory; concurrent arrivals share one task) and `invalidate(id)` (drop a non-bootstrap factory and record so the next arrival reloads it).
 
 The vendored Loader consumes the module system through its `internal` contract — the only call site is `tree.import` — and owns everything entry-shaped: entry creation, fiber activation through cordis service waiting (PENDING until injected services exist, cascading when a service is provided), update/refresh, teardown. The governance code is byte-identical to the host side, per vendor policy. Browserization is compile-time mapping in the shell's vite config: a `node:module` stub alias plus `process.*` defines make `ModuleLoader.fromInternal()` return undefined — exactly the empty slot the shell fills. The module system mounts as `ctx.modules`.
 
@@ -58,29 +44,29 @@ Each graph row's `url` goes to a same-origin external classic `<script src>` wit
 
 The shared tsdown preset emits `client.js.map` for every plugin and rewrites first-party source paths into the browser-resolvable repository shape `/packages/<group>/<package>/src/...`. Other workspace sources inlined into a bundle likewise resolve to their `packages/` owner, while dependency paths remain unchanged; `sourcesContent` carries the source, so the host only serves the map at `/plugins/<id>/client.js.map` and exposes no source route. The Vite shell also emits source maps, letting both shell code and out-of-graph plugins map stacks and performance profiles back to TypeScript/TSX.
 
-`rev` remains the script URL's query parameter and content-consistency anchor, and the bundle and map are both served with `no-cache`. An external script's `error` event exposes neither response status nor body, so failure diagnostics name only the URL; the same-origin host and build-stamped handoff id form the identity boundary, while the post-`load` factory-presence check rejects an artifact that did not register the expected id.
+`rev` remains the script URL's query parameter and content-consistency anchor, and the bundle and map are both served with `no-cache`. An external script's `error` event exposes neither response status nor body, so failure diagnostics name only the URL; the same-origin host and build-stamped registration id form the identity boundary, while the post-`load` factory-presence check rejects an artifact that did not register the expected id.
 
 ### The loading flow, end to end
 
-What happens between `dsh web` starting and the UI appearing? Three stages: the host composes and serves a graph, the shell prefetches, then cordis orchestrates.
+What happens between `dsh web` starting and the UI appearing? Three stages: the host composes a graph and parser-preloads bootstrap factories, the HTML facade creates the module system and the shell prefetches, then Cordis orchestrates.
 
 **Host side — compose the graph.**
 
 1. The composing app (`apps/cli`) ships the roster as ordinary rows in its `cordis.yml` config tree — client plugin packages are entry rows like every host plugin, including the always-mounted `client-hmr` row. A roster row that fails to import is caught by `assertEntriesLoaded`; a row whose fiber rejects is reported with its original stack by `assertEntriesActivated` ([host boot decision](2026-07-24-web-config-tree-boot-and-transport-layering.md)).
-2. The `dsh-client-modules` node half (the package is dual-face: its browser half is the module table) scans loader entries' package.json `dsh.client` declarations and composes `window.__DSH_BOOT__`: `{ rev, entries: [{ id, url, rev, inject?, immediately? }] }`. The `inject` edges and the `immediately` mark come from manifests, never hand-copied. It refuses declared plugins without built `./client` bundles and groups their package/path rows under one required source-build instruction; malformed declaration fields also fail activation, and the host audit reports either error from the FAILED fiber.
+2. The `dsh-client-modules` node half (the package is dual-face: its browser half is the module table) scans loader entries' package.json `dsh.client` declarations and composes `window.__DSH_BOOT__`: `{ rev, entries: [{ id, url, rev, inject?, immediately?, external? }] }`. The three optional fields come from manifests, never hand-copied. Composition orders requested dynamic rows before their consumers and rejects synchronous request cycles. It refuses declared plugins without built `./client` bundles and groups their package/path rows under one required source-build instruction; malformed declaration fields also fail activation, and the host audit reports either error from the FAILED fiber.
 3. Scanning is incremental per package — there is no full-rescan code path. Each cordis `internal/plugin` emission marks the fiber's entry name dirty (entry-less fibers drop O(1)); a microtask flush reconciles each dirty name against live loader entries, with package metadata (including the negative "not a client package" verdict) cached per name forever and bundle re-hashing reachable only through `rebuilt(id)`. The activation pass seeds the same dirty set from current entries and flushes synchronously, so first scan and steady state share one implementation. Each bundle's content hash is its `rev` (cache busting + HMR diff anchor), the row set hashes into `graph.rev`, and every row is served as a script resource at `/plugins/<id>/client.js?rev=…`, with its source map at the same path plus `.map`. The graph types are single-sourced in the modules package's `./client` export — the webserver knows nothing about the graph (it is a plain route-registration plugin; modules registers the bundle route and taps the index render itself).
 
 Why is the roster yml rows and not a scan? Because which plugins compose into a deployment is a composition decision, not a package property — a package declaring `dsh.client` in the repo does not mean this deployment mounts it, so discovery-by-scan cannot make that call; the node half scans only what the tree actually mounted.
 
-**Phase one — the module face.** The shell builds the module system over the graph, then prefetches every `immediately` row in parallel. Prefetch loads the external script and registers its factory only. A single row's prefetch failure is swallowed here: phase two's import retries the load and owns the loud failure, so one bad row cannot mask the others. `immediately` is a prefetch mark — not a barrier, not an identity. The package declares it, the registry carries it into the row. The infrastructure plugins (connection, runtime, ui-theme, i18n, plus hmr) declare it; UI plugins simply arrive on demand.
+**Phase one — the module face.** The injected HTML installs `window.__ModuleLoader__` in queue mode, executes the modules and runtime graph rows as blocking classic scripts, assigns `window.__DSH_BOOT__`, and then starts the Vite main module. The kernel calls the facade's `create()` with the raw graph and shell seeds. The facade removes and materializes the modules registration with a bootstrap `require` that rejects every external, then calls its `createClientModuleSystem` export. The modules bundle parses the graph, constructs the system, memoizes its own exports, and retains the instance in its module closure; construction switches the same facade to live registration before draining runtime's pending factory. The kernel then prefetches every `immediately` row in parallel; prefetch recursively registers declared dynamic requests and the row itself without materializing either. A row's prefetch failure is swallowed here because phase two's import retries and owns the loud failure. `immediately` remains an arrival mark, not a lifecycle barrier or package identity.
 
 **Phase two — the plugin face.**
 
 1. The kernel mounts the vendored Loader and injects the module system as `internal` before any entry exists. Ordering matters: `tree.import`'s bare-import fallback must never run in a browser.
-2. It creates one entry per graph row, plus the app-shell pseudo-row. The assembly entry is shell-own code the kernel appends itself — registered static with the module system, never part of the host graph — so it rides the same entry lifecycle and status coverage as everything else.
-3. Creation order carries no semantics; fibers activate through service waiting.
+2. It creates every graph row uniformly. Importing the modules row returns the memoized bootstrap exports, whose `apply()` provides the closed-over system as `ctx.modules`; rows that require that service remain PENDING until then, so the modules row needs no special creation position. Render assembly is an ordinary host-graph row provided by `dsh-client-ui-renderer`; the kernel appends no assembly pseudo-entry.
+3. Graph order governs synchronous factory availability; Cordis activation remains independent and proceeds through service waiting.
 4. `settled` = every entry created + `loader.await()` quiescent + an all-ACTIVE sweep. The sweep lists each import-failed, FAILED, or PENDING fiber with its missing services. It exists because cordis inject waits have no timeout — the sweep is the fail-loud floor.
-5. The loading page's boot status is a projection of real fiber states via `internal/status`. The settled flip switches to the real UI in one pass.
+5. The framework-free loading page projects real fiber states via `internal/status`. After the sweep, the kernel calls `ctx.uiRenderer.mount(container)` and replaces the page with the real UI in one pass.
 
 ### Hot reload: one driver plugin, self-watched bundles
 
@@ -100,33 +86,19 @@ On the browser side, the driver reloads one plugin per frame, serialized:
 
 Every plugin shares this one semantics; an `immediately` row reloads exactly like a lazy one. Dependency cascade costs zero client code: a fiber's activation epoch strings its service providers' uids, so replacing a provider's fiber re-loads every dependent through cordis itself. Reloading connection or runtime cascades the whole UI — correct, if heavy.
 
-The support boundary, stated honestly. Reload is coarse by design: fresh fiber, fresh components, React state lost, data layer untouched — react-refresh-grade state preservation conflicts with "re-executing the bundle re-runs the factory" and is deliberately out. Plain packages (react family, shell kernel, not-yet-promoted libraries) are not entries: changing them means a shell rebuild and a full page reload. No rollback in v1: an import failure leaves the entry fiberless and the next rebuilt frame retries from scratch; an apply failure leaves a FAILED fiber for the status projection; both log loudly. Self-reload works — the in-flight reload finishes in the old bundle's closure and the new apply opens a fresh SSE channel — but frames arriving in the gap are lost, and the next rebuild renotifies. One known dev-only race: a rebuilt frame overlapping a still-in-flight boot arrival shares that arrival's task and may materialize the pre-rebuild bytes; the next frame self-heals.
-
-## Package inventory (today → long term)
-
-| Package | Role | Today | Long term |
-|---|---|---|---|
-| react family / cordis | platform singletons | shell-bundled, seeded | plain forever (absolute base) |
-| vendored `@cordisjs/plugin-loader` | entry governance (same code both sides) | compile-time browserization, kernel-mounted | untouched (vendor policy) |
-| `dsh-client-modules` | the client module system | lazy CJS table; two-phase boot | plain forever (modules precede modules) |
-| `dsh-client-web` | shell kernel + AppRoot + app-shell assembly | self-sufficient (hand-rolled status stores, no plugin value imports) | keeps shrinking |
-| `dsh-client-ui-slots` | slot registry core | plain, seeded | promote to plugin; receive runtime's slots machinery |
-| `dsh-client-web-react` | ctx↔React glue | plain, seeded | promote to plugin; renderer install moves into its apply |
-| `dsh-client-ui-primitives` | base components | plain, seeded | promote to plugin (components via slots/services) |
-| `dsh-client-connection` | wire layer | plugin (`dsh.client` + bundle), declares `immediately` | transport swap (Electron IPC carrier) |
-| `dsh-client-runtime` | session object layer + slots service + store engine | plugin, declares `immediately` | keeps shrinking toward a pure session object layer |
-| `dsh-client-ui-theme` | theme tokens/service | plugin, declares `immediately`, plus the `./styles/*` source channel | Theme Registry (separate ruling) |
-| `dsh-client-i18n` | I18nService | plugin, declares `immediately` | per-deployment locale composition |
-| `dsh-client-hmr` | hot reload driver | plugin, declares `immediately` | rollback; reconnect handshake |
-| ui-layout / ui-sidebar / ui-conversation / ui-trajectory | UI features | plugins, on-demand | conversation domain split; trajectory real implementation |
+The support boundary, stated honestly. Reload is coarse by design: fresh fiber, fresh components, React state lost, data layer untouched — react-refresh-grade state preservation conflicts with "re-executing the bundle re-runs the factory" and is deliberately out. Static assembly packages and the shell kernel are not entries: changing them means a shell rebuild and a full page reload. Reload has no rollback: an import failure leaves the entry fiberless and the next rebuilt frame retries from scratch; an apply failure leaves a FAILED fiber for the status projection; both log loudly. Self-reload works — the in-flight reload finishes in the old bundle's closure and the new apply opens a fresh SSE channel — but frames arriving in the gap are lost, and the next rebuild renotifies. One known dev-only race: a rebuilt frame overlapping a still-in-flight boot arrival shares that arrival's task and may materialize the pre-rebuild bytes; the next frame self-heals.
+
+## Package ownership
+
+The current package inventory and build forms live in the [client shell layering note](2026-08-15-client-shells-and-dynamic-packages.md). This note retains only the loading properties that apply to every dynamic row: lazy factory registration, Cordis entry governance, external-script arrival, source maps, and HMR.
 
 ## Consequences
 
-One governance implementation runs on both sides of the wire; the browser-specific layer is one module system plus one reload plugin. Plugin packages have one shape, so the purity gate covers them all. Dependency edges and the boot tier live with their owners — the manifests — while the composing app holds only the roster. The drift classes stay structurally closed: share-list hand-sync, load-order coupling, cross-plugin imports, roster/tier double bookkeeping. Browser-native script loading preserves the standard mapping among plugin network resources, generated bundles, and TypeScript/TSX sources, while the module system keeps only one replaceable `loadBundle` hook.
+One governance implementation runs on both sides of the wire; the browser-specific layer is one module system plus one reload plugin. Dynamic packages have one artifact form, so the purity check covers them all. Cordis dependencies, module requests, and the boot tier live with their owners — the manifests — while the composing app holds only the roster. Host graph validation and recursive request arrival keep synchronous factory dependencies explicit. Browser-native script loading preserves the standard mapping among plugin network resources, generated bundles, and TypeScript/TSX sources, while the module system keeps only one replaceable `loadBundle` hook.
 
-Costs accepted: the vendored Loader carries idle machinery in the browser (EntryTree persistence is a no-op, groups/isolation unused); every plugin edit in dev pays a bundle rebuild plus fiber remount; graph `inject` rows are informational — activation truth is service-level — so a mismatch appears at the settled sweep, not at graph validation; the three not-yet-promoted libraries keep their static-import exports until their DI conversions land; every bundle gains a source-map artifact; and external-script failures provide only coarse URL diagnostics instead of the HTTP status available to an explicit fetch.
+Costs accepted: the vendored Loader carries idle machinery in the browser (EntryTree persistence is a no-op, groups/isolation unused); every plugin edit in dev pays a bundle rebuild plus fiber remount; graph `inject` rows are informational — activation truth is service-level — so a mismatch appears at the settled sweep, not at graph validation; the static UI libraries keep direct value exports; every bundle gains a source-map artifact; and external-script failures provide only coarse URL diagnostics instead of the HTTP status available to an explicit fetch.
 
-Roster: it lives in the web bundle's config tree (`packages/bundle/web-app/cordis.patch.yml`); `mountWebPlugins` and the `CLIENT_PACKAGES` constant are gone, and recomposing a deployment means swapping the yml/overlay. The graph composer moved from a webserver-side registry into the `dsh-client-modules` node half (the package upgraded to dual-face per this note's promotion rule — its consumer now reaches it through cordis DI), and the transport split landed alongside: the webserver became a plain route-registration plugin, `/api/*` binding moved to the connection node half over the upgraded `api-gateway` plugin (`dsh-host-apiproxy` providing `ctx.apiProxy`), and the dev bundle watch + SSE channel moved to the hmr node half.
+Roster: it lives in the web bundle's config tree (`packages/bundle/web-app/cordis.patch.yml`); `mountWebPlugins` and the `CLIENT_PACKAGES` constant are gone, and recomposing a deployment means swapping the yml/overlay. The graph composer lives in the `dsh-client-modules` node half, while the parser-preloaded client face bootstraps the browser module table. The webserver remains a plain route-registration plugin; `/api/*` binding belongs to the connection node half over `api-gateway` (`dsh-host-apiproxy` providing `ctx.apiProxy`), and the dev bundle watch plus SSE channel belongs to the hmr node half.
 
 ## Alternatives considered
 
@@ -137,7 +109,7 @@ Roster: it lives in the web bundle's config tree (`packages/bundle/web-app/cordi
 | Reuse `@cordisjs/plugin-hmr` in the browser | ~80% solves problems the browser doesn't have (fs watching, deep graph coloring, Node's dual caches); the reload skeleton is copied as a shape |
 | Module federation | Independently built remote bundles are exactly the form vite federation does not support |
 | Import maps | Ruled out earlier; the DI require table is the terminal mechanism |
-| Full ctx-ification now (react and libraries via services, no module table) | The module-axis extreme; parked — the upgrade law walks there one package at a time instead |
-| Eager instantiation with a frozen table | Requires arrival-time ordering; lazy CJS registration makes recursive `require` self-ordering and matches the naive-puller phase split |
+| Full ctx-ification now (React and libraries via services, no module table) | Static UI libraries still expose synchronous values, so removing the table would leave those imports without a shared identity |
+| Eager instantiation with a frozen table | Runs bundle side effects at script arrival; lazy registration keeps execution at Cordis import while recursive `require` materializes registered requests |
 | Fetch response text, then inject an inline `<script>` | Makes the module system buffer the complete source and maintain separate fetch/execute paths; dynamic source execution also breaks the browser-native association among the network resource, source map, and profile |
 | Builder-push rebuild channel (`POST /plugins/rebuilt` from the orchestrator's `onSuccess`) | Couples reload to one blessed builder process and a second wire protocol; the webserver already holds every bundle path, and stat polling covers the torn-write race (re-hash on every stat change) that once justified pushing |

+ 23 - 51
.agents/notes/implemented/architecture/2026-07-23-client-plugin-loading-model.zh.md

@@ -1,10 +1,10 @@
-# Agent Note: client 插件装载——普通包、dsh.client 插件与双阶段 boot
+# Agent Note: client 插件装载——惰性 factory、Cordis 生命周期与热重载
 
 Status: implemented
 
 [English](2026-07-23-client-plugin-loading-model.md) | 中文
 
-> 范围:浏览器侧的插件装载机件——什么是插件、代码怎么到达、热重载如何搭在这套模型上。装载链归本篇所有;[Web 客户端架构笔记](2026-07-19-gui-web-client-architecture.md) 在装载问题上以本篇为准,继续拥有 slot、数据对象层与 React 面
+> 范围:浏览器侧插件装载机件——代码如何到达、Cordis 如何治理代码,以及热重载如何搭乘这套模型。本 Note 拥有装载链;[client 外壳分层 Note](2026-08-15-client-shells-and-dynamic-packages.md)拥有包分类、构建 face、共享模块请求与 npm 依赖声明,[Web 客户端架构笔记](2026-07-19-gui-web-client-architecture.md)则拥有 slot 与数据对象层
 
 ## Problem
 
@@ -24,31 +24,17 @@ host 侧,cordis 插件装载站在 Node 的模块机制之上——require cac
 
 ## Decision
 
-### 两类包;`dsh.client` 即插件,别无他义
+### 包成员与模块请求
 
-什么让一个包成为插件?只有一条规则:**一个包的消费方式一旦是 cordis 依赖注入,它就是插件包;在此之前它是普通包。**代码怎么到达页面不属于分类体系——到达方式由包的类别推得,而不是反过来定义类别
+[Client 外壳分层 Note](2026-08-15-client-shells-and-dynamic-packages.md)定义当前的静态、动态包集合及其 import 规则。装载机件把每个 `dsh.client` 包视为一个 host graph row,且每个包只有一个普通 `lib/client.js` factory bundle。包声明携带 Cordis `inject` 边、同步模块表 `external` 请求,以及可选的 `immediately` 预取标记;负责组合的 app 只拥有挂载名册
 
-- **普通包**是模块系统自身所需的绝对基座,加上尚未转成 DI 的库:react 家族、cordis、`@deepseek-ai/dsh-client-modules`(模块系统本身——它永远不可能是插件,因为模块先于一切模块)、web 壳内核,以及——暂时——ui-slots、web-react、ui-primitives。普通包打进壳 bundle、播种进模块表、对 host 图不可见。
-- **插件包**是其余一切。每个都携带 `dsh.client` manifest(元数据清单)声明(`{ platform, inject, immediately? }`)和同一种统一形态:共享 tsdown 预设产出 `lib/client.js`,`exports["./client"]` 指向该 bundle。每个都是 host 编写的图里受治理的 entry。当前包括:connection、runtime、ui-theme、i18n、hmr(仅进 dev 图)、ui-layout、ui-sidebar、ui-conversation、ui-model-selector、ui-user-questions、ui-trajectory。
-
-manifest 拥有包的装载约定:它的 `inject` 依赖边,加可选的 `immediately` 预取标记(缺省即 lazy)。负责组合的 app 只拥有名册。
-
-新增一个插件包:声明 `dsh.client`,经共享预设产出 `./client` bundle,把包名加进负责组合的 app 的名册。除此之外无需任何交接。
-
-普通包何时升格为插件?升级法则,记录在案让迁移路径保持诚实:**普通包在其消费方改用 cordis DI 之时升格为插件包,绝不提前。**三项升格在排队:ui-slots(现居 runtime 的 slots 机件——SlotRegistry、渲染器约定、root slot)、web-react(渲染器安装移入自己的 `apply`)、ui-primitives(组件经 slot/服务供给之时)。在那之前它们保持普通包身份,符号导出保持普通的静态 import。
-
-四条边规则治理横跨两类包的 import。没有一条依赖任何单包标记:
-
-- **插件 ↔ 插件的值 import 是构建错误。**与两侧的 `immediately` 声明无关——规则不得依赖一个人人可翻转的标记。协作走 cordis inject/服务。`import type` 豁免;类型链分毫未动。这条规则正是 `scopeOf` 是 `SessionRuntime` 方法、`transportError` 住在 `dsh-host-apiproxy` wire 层(它的 `RpcResult` 老家,内联安全)的原因。
-- **插件 → 普通包的值 import 外置为 external**,按平台清单判定。清单是壳里的一个常量(`platform.ts`:react 家族、cordis、ui-slots、web-react、ui-primitives),tsdown 预设(external 判定)与 `seed.ts`(模块表预热)都 import 它。一个常量、两个消费方——人肉同步这一漂移缺陷类死透。
-- **纯度门禁覆盖每个插件包。**它的三条分支:平台 import 外置为 external;INLINE_SAFE wire 层内联;其余任何 workspace 泄漏即构建错误。正是统一的 bundle 形态让这一覆盖不留死角——每个插件都经同一预设构建,没有包能坐在门禁之外。
-- **壳自足。**内核(boot + loading 页)对任何插件包零值 import;其状态 store 为手写。大声失败的呈现不得依赖它所报告失败的那个系统。
+Web 内核保持不依赖框架,也不 import 任何动态包实体。Modules 本身是动态图 row,但 host parser 会在 Vite 主模块前送达其普通 factory。内核调用 `create()` 时,由 HTML 安装的 `__ModuleLoader__` facade 使用该 factory 构造模块系统。Runtime 经同一个 pending queue 到达;React、Cordis 与静态 UI 库的身份由外壳 seed 提供。
 
 ### 一套模块系统,一个插件治理器
 
 浏览器复刻 host 侧的分工。`dsh-client-modules`(`ClientModuleSystem`)坐上 host 侧由 Node 内部 ESM loader 占据的模块系统席位;同一份 vendored `@cordisjs/plugin-loader` 在两侧都坐治理席。二者的分界线一句话说尽:**模块系统拥有模块身份与字节——代码怎么到达、怎么登记、怎么变成导出内容;Loader 拥有插件生命周期——插件何时挂载、等待什么、如何拆除。**
 
-`ClientModuleSystem` 是一张 lazy CJS 表。执行 bundle 只**登记**其工厂——bundle 调用 `window.__ModuleLoader__.load({ id, factory })`,此外什么都不发生。模块体的一切副作用(包括 CSS 注入)都住在工厂闭包里,在物化时运行:物化即该 id 的首次 `require`/import,此后记忆化。工厂若 require 一个已登记未物化的同伴,就递归物化它,因此任何地方都不存在排序。被要求 import 一个 id 时,表按固定分支顺序解析:种子词条 → 记忆化的记录 → 静态登记(壳自有模块,如 app-shell)→ 已登记的工厂 → 图行外部 classic script 加载 → 大声抛错。最后这一抛是构建期纯度门禁在运行时的镜像。系统还保管逐模块簿记——名下 `<style data-plugin>` 标签 id、观测到的 require 边——并暴露 HMR(热模块替换)需要的两个动词:`prefetch(id)`(加载脚本、只登记工厂;并发调用共享同一在途任务)与 `invalidate(id)`(丢弃工厂与记录,下次到达即重新加载)。
+`ClientModuleSystem` 是一张 lazy CJS 表。执行 bundle 只**登记**其 factory——bundle 调用 `window.__ModuleLoader__.load({ id, factory })`,此外什么都不发生。模块体的一切副作用(包括 CSS 注入)都住在 factory 闭包里,在物化时运行:物化即该 id 的首次 `require`/import,此后记忆化。Import 和 prefetch 会先递归登记已声明的动态请求,再登记消费者;随后 factory 会同步物化任何已登记但尚未物化的请求。模块表按固定分支顺序解析:seed word → 记忆化记录 → graph row classic-script 登记 → 已登记 factory 物化 → 大声抛错。Modules factory 是自举例外:HTML facade 先物化它,构造过程再把同一 exports 直接写入记忆化表。最后这一抛是构建期纯度门禁在运行时的镜像。系统还保管逐模块簿记——名下 `<style data-plugin>` 标签 id、观测到的 require 边——并暴露 HMR(热模块替换)需要的两个动词:`prefetch(id)`(登记所请求的动态 factory 和本 row 自身的 factory;并发到达共享一个任务)与 `invalidate(id)`(丢弃非 bootstrap factory 与记录,下次到达即重新加载)。
 
 vendored Loader 经其 `internal` 约定消费模块系统——唯一调用点是 `tree.import`——并拥有一切 entry 形状的事务:entry 创建、fiber 经 cordis 服务等待的激活(注入的服务未就位即保持 PENDING,服务 provide 时级联激活)、update/refresh、拆除。治理代码按 vendor 政策与 host 侧逐字节相同。浏览器化是壳 vite 配置里的编译期映射:一个 `node:module` stub 别名加若干 `process.*` define,使 `ModuleLoader.fromInternal()` 返回 undefined——这正是留给壳来填的空槽。模块系统挂载为 `ctx.modules`。
 
@@ -58,29 +44,29 @@ vendored Loader 经其 `internal` 约定消费模块系统——唯一调用点
 
 共享 tsdown 预设为每个插件产出 `client.js.map`,并把第一方源码路径重写成浏览器可识别的仓库形状 `/packages/<group>/<package>/src/...`。内联进 bundle 的其他 workspace 源码同样回到其 `packages/` 归属,依赖包路径保持原样;`sourcesContent` 承载源码,因此 host 只需在 `/plugins/<id>/client.js.map` 供给 map,无需开放源码路由。Vite 壳也产出 sourcemap,使壳代码与图外插件都能从 stack 和性能 profile 回到 TypeScript/TSX。
 
-`rev` 继续作为脚本 URL 的查询参数和内容一致性锚点,bundle 与 map 都以 `no-cache` 供给。外部脚本的 `error` 事件不给响应状态与正文,因此失败诊断只报告 URL;同源 host 供给与构建期写入的 handoff id 是身份边界,`load` 后的工厂存在性检查负责拒绝未登记预期 id 的产物。
+`rev` 继续作为脚本 URL 的查询参数和内容一致性锚点,bundle 与 map 都以 `no-cache` 供给。外部脚本的 `error` 事件不给响应状态与正文,因此失败诊断只报告 URL;同源 host 供给与构建期写入的 registration id 是身份边界,`load` 后的工厂存在性检查负责拒绝未登记预期 id 的产物。
 
 ### 装载流程,端到端
 
-从 `dsh web` 启动到 UI 出现之间发生了什么?三个阶段:host 组合并供给一张图,壳预取,然后 cordis 编排。
+从 `dsh web` 启动到 UI 出现之间发生了什么?三个阶段:host 组合 graph 并由 parser 预载 bootstrap factory,HTML facade 创建模块系统且外壳执行预取,然后 Cordis 编排。
 
 **host 侧——组合这张图。**
 
 1. 负责组合的 app(`apps/cli`)把名册作为普通行放进它的 `cordis.yml` 配置树——client 插件包与每个 host 插件一样是 entry 行,包括无条件挂载的 `client-hmr` 行。名册行 import 失败由 `assertEntriesLoaded` 捕获;fiber reject 的行则由 `assertEntriesActivated` 报告原始 stack([host boot 决策](2026-07-24-web-config-tree-boot-and-transport-layering.md))。
-2. `dsh-client-modules` 的 node 半(该包是双面的:浏览器半就是模块表)扫描 loader entry 的 package.json `dsh.client` 声明,组合出 `window.__DSH_BOOT__`:`{ rev, entries: [{ id, url, rev, inject?, immediately? }] }`。`inject` 边与 `immediately` 标记都来自 manifest,永不人肉抄写。它会拒绝没有已构建 `./client` bundle 的已声明插件,并把它们的 package/path 行归到一条源码构建要求下;畸形声明字段同样会让激活失败,host 检查会从 FAILED fiber 报告这两类错误。
+2. `dsh-client-modules` 的 node 半(该包是双面的:浏览器半就是模块表)扫描 loader entry 的 package.json `dsh.client` 声明,组合出 `window.__DSH_BOOT__`:`{ rev, entries: [{ id, url, rev, inject?, immediately?, external? }] }`。三个可选字段都来自 manifest,永不人肉抄写。组合会把被请求的动态图 row 排到消费者之前,并拒绝同步请求环。它会拒绝没有已构建 `./client` bundle 的已声明插件,并把它们的 package/path 行归到一条源码构建要求下;畸形声明字段同样会让激活失败,host 检查会从 FAILED fiber 报告这两类错误。
 3. 扫描是单包增量——不存在全量重扫代码路径。每次 cordis `internal/plugin` 发射把该 fiber 的 entry 名标脏(无 entry 的 fiber O(1) 丢弃);微任务 flush 把每个脏名对账 live loader entries,包元数据(含「非 client 包」的否定结论)按名永久缓存,bundle 重哈希只经 `rebuilt(id)` 可达。激活趟从当前 entries 灌同一脏集合并同步 flush,初扫与稳态共享一条实现。每个 bundle 的内容哈希是其 `rev`(缓存失效 + HMR diff 锚点),行集合哈希进 `graph.rev`,每一行都作为脚本资源供给:`/plugins/<id>/client.js?rev=…`,对应 sourcemap 位于同一路径加 `.map`。图类型单源在 modules 包的 `./client` 出口——webserver 对图一无所知(它是朴素路由注册插件;bundle 路由和 index 渲染 tap 都由 modules 自己注册)。
 
 为什么名册是 yml 行而不是扫描?因为哪些插件组合进一次部署是组合决策,不是包属性——一个在仓库中声明了 dsh.client 的包,不代表这次部署要挂载它,扫描发现无从替人做这个决定;node 半只扫描配置树实际挂载了的东西。
 
-**第一阶段——模块面。**壳在图之上建起模块系统,然后并行预取每个 `immediately` 行。预取即加载外部脚本,只登记工厂。单行预取失败在这里被吞下:第二阶段 import 时会重试加载并拥有那次大声失败,因此一个坏行藏不住其他行。`immediately` 是预取标记——不是屏障,不是身份。包声明它,注册表把它带进图行。基础设施插件(connection、runtime、ui-theme、i18n,外加 hmr)声明它;UI 插件则径直按需到达
+**第一阶段——模块面。**注入的 HTML 以 queue 模式安装 `window.__ModuleLoader__`,以阻塞式 classic script 执行 modules 与 runtime graph row,赋值 `window.__DSH_BOOT__`,然后启动 Vite 主模块。内核把原始图和外壳 seed 传给 facade 的 `create()`。Facade 移除 modules registration,用拒绝全部 external 的 bootstrap `require` 将其物化,再调用其 `createClientModuleSystem` 导出。Modules bundle 解析图、构造系统、记忆化自身 exports,并在模块闭包中保留该实例;构造过程先把同一 facade 切换到 live registration,再排空 runtime 的 pending factory。随后内核并行预取每个 `immediately` row;prefetch 会递归登记已声明的动态请求和 row 自身,但不物化任一项。单行预取失败在这里被吞下,因为第二阶段 import 会重试并拥有那次大声失败。`immediately` 仍是到达标记,不是生命周期屏障或包身份
 
 **第二阶段——插件面。**
 
 1. 内核挂载 vendored Loader,在任何 entry 存在之前就把模块系统注入为 `internal`。顺序有讲究:`tree.import` 的裸 import 兜底分支在浏览器里绝不能跑到。
-2. 它为图中每一行创建 entry,外加 app-shell 伪行。装配 entry 是内核自己追加的壳自有代码——向模块系统静态登记,绝不进 host 图——因此与其余一切共乘同一套 entry 生命周期与状态覆盖
-3. 创建顺序不携带任何语义;fiber 经服务等待激活
+2. 它统一创建每个 graph row。Import modules row 会返回记忆化的 bootstrap exports,其 `apply()` 把闭包中的系统提供为 `ctx.modules`;需要该 service 的 row 会保持 PENDING 直至此时,因此 modules row 无需特殊创建位置。渲染组装是由 `dsh-client-ui-renderer` 提供的普通 host graph row;内核不追加组装伪 entry
+3. Graph 顺序治理同步 factory 可用性;Cordis 激活与之独立,仍经服务等待推进
 4. `settled` = 每个 entry 已创建 + `loader.await()` 完全停稳 + 一次全 ACTIVE 扫描。扫描列出每个 import 失败、FAILED 或 PENDING 的 fiber 及其缺失的服务。它存在的理由:cordis 的 inject 等待没有超时——这次扫描就是大声失败的兜底线。
-5. loading 页的启动状态是经 `internal/status` 对真实 fiber 状态的投影。settled 翻转即一次性切换到真实 UI。
+5. 不依赖框架的 loading 页经 `internal/status` 投影真实 fiber 状态。检查完成后,内核调用 `ctx.uiRenderer.mount(container)`,一次切换到真实 UI。
 
 ### 热重载:一个驱动插件,自行监视的 bundle
 
@@ -100,33 +86,19 @@ vendored Loader 经其 `internal` 约定消费模块系统——唯一调用点
 
 每个插件都共享同一套语义;`immediately` 行的重载与 lazy 行分毫不差。依赖级联不花一行 client 代码:fiber 的激活纪元串接着它各服务提供方的 uid,因此换掉提供方的 fiber,每个依赖方都会经 cordis 本身重新装载。重载 connection 或 runtime 会级联整个 UI——正确,虽然重。
 
-支持边界,如实陈述。重载粒度刻意做粗:全新 fiber、全新组件、React 状态丢失、数据层不动——react-refresh 级的状态保留与「重执行 bundle 即重跑工厂」相冲突,属刻意不做。普通包(react 家族、壳内核、尚未升格的库)不是 entry:改它们意味着壳重建加整页刷新。v1 不做回滚:import 失败让 entry 失去 fiber,下一个 rebuilt 帧从头重试;apply 失败留下 FAILED fiber 交给状态投影;两者都大声记录。自我重载可行——在途的重载在旧 bundle 的闭包里跑完,新的 apply 再开一条新 SSE 通道——但空窗期到达的帧会丢失,下次重建会再次通知。一处已知的仅限 dev 竞态:rebuilt 帧与仍在途的 boot 到达重叠时共享那次到达的任务,可能物化重建前的字节;下一帧自愈。
-
-## 包盘点(现状 → 长期)
-
-| 包 | 角色 | 现状 | 长期 |
-|---|---|---|---|
-| react 家族 / cordis | 平台单例 | 打进壳,已播种 | 永为普通包(绝对基座) |
-| vendored `@cordisjs/plugin-loader` | entry 治理(两侧同一份代码) | 编译期浏览器化,内核挂载 | 不动(vendor 政策) |
-| `dsh-client-modules` | client 模块系统 | lazy CJS 模块表;双阶段 boot | 永为普通包(模块先于模块) |
-| `dsh-client-web` | 壳内核 + AppRoot + app-shell 装配 | 自足(手写状态 store,零插件值 import) | 持续缩小 |
-| `dsh-client-ui-slots` | slot 注册表核心 | 普通包,已播种 | 升格为插件;接收 runtime 的 slots 机件 |
-| `dsh-client-web-react` | ctx↔React 胶水 | 普通包,已播种 | 升格为插件;渲染器安装移入其 apply |
-| `dsh-client-ui-primitives` | 基础组件 | 普通包,已播种 | 升格为插件(组件经 slot/服务供给) |
-| `dsh-client-connection` | wire 层 | 插件(dsh.client + bundle),声明 `immediately` | 传输替换(Electron IPC 载体) |
-| `dsh-client-runtime` | 会话对象层 + slots 服务 + store 引擎 | 插件,声明 `immediately` | 持续缩向纯会话对象层 |
-| `dsh-client-ui-theme` | 主题 token/服务 | 插件,声明 `immediately`,外加 `./styles/*` 源码通道 | Theme Registry(另行裁定) |
-| `dsh-client-i18n` | I18nService | 插件,声明 `immediately` | 按部署组合语言包 |
-| `dsh-client-hmr` | 热重载驱动 | 插件,声明 `immediately` | 回滚;重连握手 |
-| ui-layout / ui-sidebar / ui-conversation / ui-trajectory | UI 功能 | 插件,按需到达 | conversation 域拆分;trajectory 真实现 |
+支持边界,如实陈述。重载粒度刻意做粗:全新 fiber、全新组件、React 状态丢失、数据层不动——react-refresh 级的状态保留与「重执行 bundle 即重跑 factory」相冲突,属刻意不做。静态装配包与外壳内核不是 entry:改动它们意味着外壳重建加整页刷新。重载不做回滚:import 失败让 entry 失去 fiber,下一个 rebuilt 帧从头重试;apply 失败留下 FAILED fiber 交给状态投影;两者都大声记录。自我重载可行——在途的重载在旧 bundle 的闭包里跑完,新的 apply 再开一条新 SSE 通道——但空窗期到达的帧会丢失,下次重建会再次通知。一处已知的仅限 dev 竞态:rebuilt 帧与仍在途的 boot 到达重叠时共享那次到达的任务,可能物化重建前的字节;下一帧自愈。
+
+## 包归属
+
+当前包盘点与构建形态位于[client 外壳分层 Note](2026-08-15-client-shells-and-dynamic-packages.md)。本 Note 只保留适用于每个动态图 row 的装载属性:惰性 factory 登记、Cordis entry 治理、外部 script 到达、sourcemap 与 HMR。
 
 ## Consequences
 
-wire 两侧跑着同一份治理实现;浏览器特有层只包含一套模块系统和一个重载插件。插件包只有一种形态,纯度门禁因此覆盖全部插件。依赖边与启动档位都与其所有者——manifest——同住,负责组合的 app 只握名册。各漂移缺陷类被结构性关死:共享清单人肉同步、装载顺序耦合、跨插件 import、名册/档位双重记账。浏览器原生脚本装载使插件网络资源、生成 bundle 与 TypeScript/TSX 源码保持标准映射,模块系统也只保留一个可替换的 `loadBundle` 钩子。
+Wire 两侧运行同一份治理实现;浏览器特有层只包含一套模块系统和一个重载插件。动态包只有一种产物形态,因此纯度检查覆盖全部动态包。Cordis 依赖、模块请求与启动档位都与其所有者——manifest——同住,负责组合的 app 只握名册。Host graph 校验与递归请求到达使同步 factory 依赖保持显式。浏览器原生 script 装载保留插件网络资源、生成 bundle 与 TypeScript/TSX 源码之间的标准映射,模块系统也只保留一个可替换的 `loadBundle` 钩子。
 
-接受的代价:vendored Loader 在浏览器里背着闲置机件(EntryTree 持久化是 no-op,分组/隔离未用);开发期每次修改插件都要付一次 bundle 重建加 fiber 重挂;图中 `inject` 行仅是信息性说明——激活的真相在服务层——因此不匹配会在 settled 扫描时浮出,而不是在图校验时被拦下;三个尚未升格的库在各自的 DI 转换落地之前保持静态 import 导出;每个 bundle 多出一份 sourcemap 产物,外部脚本失败也只能给出粗粒度的 URL 诊断,不能像显式 fetch 那样报告 HTTP 状态。
+接受的代价:vendored Loader 在浏览器里背着闲置机件(EntryTree 持久化是 no-op,分组/隔离未用);开发期每次修改插件都要付一次 bundle 重建加 fiber 重挂;graph `inject` row 仅是信息性说明——激活的真相在服务层——因此不匹配会在 settled 扫描时浮出,而不是在 graph 校验时被拦下;静态 UI 库保留直接实体导出;每个 bundle 多出一份 sourcemap 产物,外部 script 失败也只能给出粗粒度 URL 诊断,不能像显式 fetch 那样报告 HTTP 状态。
 
-名册:住在 web 组合包的配置树里(`packages/bundle/web-app/cordis.patch.yml`);`mountWebPlugins` 与 `CLIENT_PACKAGES` 常量已消失,重组一次部署等于换 yml/overlay。图的组合器从 webserver 侧的注册表迁进 `dsh-client-modules` 的 node 半(该包按本 note 的升级法则升格为双面——其消费方现经 cordis DI 到达),传输拆分同轮落地:webserver 变为朴素路由注册插件,`/api/*` 绑定迁到 connection 的 node 半、走升格后的 `api-gateway` 插件(`dsh-host-apiproxy` 提供 `ctx.apiProxy`),dev 的 bundle 监视与 SSE(Server-Sent Events)通道迁到 hmr 的 node 半。
+名册位于 web 组合包的配置树(`packages/bundle/web-app/cordis.patch.yml`);`mountWebPlugins` 与 `CLIENT_PACKAGES` 常量已消失,重组一次部署等于替换 yml/overlay。Graph 组合器位于 `dsh-client-modules` node 半,由 parser 预载的 client face 则自举浏览器模块表。Webserver 继续作为朴素路由注册插件;`/api/*` 绑定属于 connection node 半,并经 `api-gateway`(由 `dsh-host-apiproxy` 提供 `ctx.apiProxy`);开发期 bundle 监视与 SSE 通道属于 hmr node 半。
 
 ## Alternatives considered
 
@@ -137,7 +109,7 @@ wire 两侧跑着同一份治理实现;浏览器特有层只包含一套模块
 | 在浏览器复用 `@cordisjs/plugin-hmr` | 约 80% 在解决浏览器没有的问题(fs 监听、深度图着色、Node 的双缓存);只按形状抄用其重载骨架 |
 | 模块联邦(module federation) | 独立构建的远端 bundle 恰是 vite 联邦不支持的形态 |
 | import map | 早已排除;DI require 表是终局机制 |
-| 现在就彻底 ctx 化(react 与库全走服务,不设模块表) | 模块轴上的极端形态;搁置——升级法则让包一次一个地走向它 |
-| 冻结表 + 到达即实例化 | 要求按到达时刻排序;lazy CJS 登记让递归 `require` 自行定序,且与朴素拉取器的阶段拆分相合 |
+| 现在就彻底 ctx 化(React 与库全走服务,不设模块表) | 静态 UI 库仍暴露同步实体,因此删除模块表会让这些 import 失去共享身份 |
+| 冻结表 + 到达即实例化 | 会在 script 到达时执行 bundle 副作用;惰性登记把执行推迟到 Cordis import,并由递归 `require` 物化已登记请求 |
 | fetch 响应文本后注入内联 `<script>` | 模块系统必须缓冲整份源码并维护 fetch/execute 两条路径;动态源码执行也切断浏览器网络资源、sourcemap 与 profile 的原生关联 |
 | 构建器推送重建通道(编排器在 `onSuccess` 里 POST `/plugins/rebuilt`) | 把重载耦合到一个钦定的构建器进程和第二套 wire 协议;webserver 本就握有每个 bundle 路径,stat 轮询(每次 stat 变化即重哈希)已兜住当年为推送辩护的撕裂写竞态 |

+ 2 - 2
.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: ce723fc4743640bd83e73ebf6b6fd1b51c8d86a8
-2026-07-25-web-client-session-scope-and-provide-channel.zh.md: f16b0fe8f7daf5f19340179da62980a8ef8ead5b
+2026-07-25-web-client-session-scope-and-provide-channel.md: 81ae4bdfa4f9666efc9eccd05bc4fe95d24b3dba
+2026-07-25-web-client-session-scope-and-provide-channel.zh.md: 22a9e8c9a471493079d7d842d5f5ad6fe736103b

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.md

@@ -85,7 +85,7 @@ A session "materialized but with no first prompt" is governed by the summary-der
 
 ### Per-session provisioning: the `sessions.provide` standard-kit channel
 
-The sole provisioning path by which session slot components fetch their own session data. Plugins declare a fixed key map through the static descriptor `sessions.provide({hooks, props, resolve})` (a duplicate key throws at registration); `resolve(binding)` materializes values for a specific session and tears them down with the scope. Web-react's `standardKit` single loop binds the hooks compartment into `use<Name>` selector hooks (`observableHook`→uSES, anti-tearing) and passes the props compartment through as-is.
+The sole provisioning path by which session slot components fetch their own session data. Plugins declare a fixed key map through the static descriptor `sessions.provide({hooks, props, resolve})` (a duplicate key throws at registration); `resolve(binding)` materializes values for a specific session and tears them down with the scope. ui-renderer's `standardKit` single loop binds the hooks compartment into `use<Name>` selector hooks (`observableHook`→uSES, anti-tearing) and passes the props compartment through as-is.
 
 Slot scope is the closed set `root | session-maybe | session`:
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-25-web-client-session-scope-and-provide-channel.zh.md

@@ -85,7 +85,7 @@ Session 实例与 scope 同生命周期,存活资格 = host listed(一个判
 
 ### 逐会话供数:`sessions.provide` 标准件通道
 
-会话 slot 组件「自己拿会话数据」的唯一供数路径。插件以静态描述符 `sessions.provide({hooks, props, resolve})` 声明固定键表(重名 key 注册时 throw),`resolve(binding)` 在确定会话下物化值并随 scope 拆;web-react `standardKit` 统一循环把 hooks 格绑成 `use<Name>` 选择器钩子(`observableHook`→uSES,防 tearing)、props 格原样透传。
+会话 slot 组件「自己拿会话数据」的唯一供数路径。插件以静态描述符 `sessions.provide({hooks, props, resolve})` 声明固定键表(重名 key 注册时 throw),`resolve(binding)` 在确定会话下物化值并随 scope 拆;ui-renderer `standardKit` 统一循环把 hooks 格绑成 `use<Name>` 选择器钩子(`observableHook`→uSES,防 tearing)、props 格原样透传。
 
 slot scope 是闭集 `root | session-maybe | session`:
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.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-client-locale-full-rollout.md
-2026-07-30-client-locale-full-rollout.md: 0faf4e0424e037b59b24d32f7fa987ac36497691
-2026-07-30-client-locale-full-rollout.zh.md: 5c26c2d5e7b75b89675b0b0d9ca3f147d2152bc8
+2026-07-30-client-locale-full-rollout.md: c6c5a8f2faffd3e03462eaad159ae94c53c735ce
+2026-07-30-client-locale-full-rollout.zh.md: 8d6220784104944f5d07b533e4f107ceffdfcfea

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.md

@@ -21,7 +21,7 @@ After the typed locale standard seat landed (`locale:` on register → framework
 - **Error/failure strings stay English**: client-authored fallbacks (`command failed`, plan-toggle failures), RpcError messages, and wire `error.message (code)` pass-throughs render verbatim.
 - **Design literals stay out of the dictionaries**: tool-row variant titles (Think/Bash/…), SYSTEM/USER-style kind badges, the Plan chip wordmark, the whole StatsLine — identical in both languages.
 - **ui-trajectory is deferred wholesale** (a developer inspection surface, terminology-dense, ruled separately).
-- **Boot copy stays hardcoded** (AppRoot renders before the locale service exists).
+- **Boot copy stays hardcoded** (the framework-free boot page runs before the locale service exists).
 
 **Derivation layers stay pure; localization happens at render.** ui-workspace's `relativeTime` returns structured `{unit, n}` composed with dictionary templates by the renderer; blank sessions and the Ungrouped bucket keep their stored titles, with the renderer substituting localized copy off the `blank` flag / absent `workspaceId`; **blank rows are excluded from search entirely** (a bilingual display title cannot match a single-language query stably). Dates use no Intl: format templates live in the dictionaries (message clock `clock.md`/`clock.ymd`, workspace hover `date.ymd`) and the formatters take `t` as a parameter, staying pure.
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.zh.md

@@ -21,7 +21,7 @@ typed locale 标准席位(`locale:` 注册声明 → 框架注入强类型 `t`
 - **错误/失败类字符串一律英文**:client 自产的兜底串(`command failed`、plan 切换失败)、RpcError 消息、wire 透出的 `error.message (code)` 原样呈现。
 - **设计字面量不进字典**:工具行 variant 标题(Think/Bash/…)、SYSTEM/USER 类 kind 徽标、Plan chip 字标、整个 StatsLine——中英界面显示一致。
 - **ui-trajectory 整包缓做**(开发者检查面,术语密集,单独裁决)。
-- **boot 文案保持硬编码**(AppRoot 渲染早于 locale 服务可用)。
+- **boot 文案保持硬编码**(不依赖框架的启动页运行早于 locale 服务可用)。
 
 **派生层保持纯函数,本地化只在渲染层**:ui-workspace 的 `relativeTime` 返回结构化 `{unit, n}` 由渲染组合字典模板;blank 会话/未分组桶的存储标题不变,渲染按 `blank` 标志/`workspaceId` 缺席替换本地化文案;**搜索态 blank 行一律排除**(双语标题无法与单语查询稳定匹配)。日期不引 Intl:格式模板进字典(消息时钟 `clock.md`/`clock.ymd`,workspace hover `date.ymd`),格式化函数吃 `t` 参数保持纯。
 

+ 2 - 2
.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: 46d773f2b4cd9c50ef1eefb2c32d78dbf8dbd100
-2026-07-30-web-config-plane.zh.md: d035be8533393df011f3047749bfa48e11c558bb
+2026-07-30-web-config-plane.md: 1326f50a792b6c6f791c9515ea03cc362d16fc7e
+2026-07-30-web-config-plane.zh.md: bea4bbc237f44864a5fa9bf677267ca0055c13ed

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
.agents/notes/implemented/architecture/2026-07-30-web-config-plane.md


Разница между файлами не показана из-за своего большого размера
+ 1 - 1
.agents/notes/implemented/architecture/2026-07-30-web-config-plane.zh.md


+ 2 - 2
.agents/notes/implemented/architecture/2026-08-09-cordis-event-walk-backstop.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-08-09-cordis-event-walk-backstop.md
-2026-08-09-cordis-event-walk-backstop.md: 1d833858f9471c98f54b3793edd12e4710b8c0fc
-2026-08-09-cordis-event-walk-backstop.zh.md: 4981016a22f94d710855331bc636c8f3dd9a4801
+2026-08-09-cordis-event-walk-backstop.md: c031b7c444a4d9bdfb0792523ba50e297f53b56a
+2026-08-09-cordis-event-walk-backstop.zh.md: a22be64b0bb2f14697425516c4287f8330670bce

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-09-cordis-event-walk-backstop.md

@@ -20,7 +20,7 @@ A new curated `EVENT_WALK_EXEMPTIONS` map names every declared event the project
 
 The partition judgment moved out of `computeOutputs` into the pure `walkPartitionProblems(input, maps)` so every acceptance path is provable by unit test without running the Typert projection; `computeOutputs` feeds it the rendered model plus the independent scan and keeps aggregating page-splice errors as before.
 
-The audit that motivated this found the host face already complete: 48 rendered services + 10 walk exemptions covered all 58 host-visible Context keys, all 49 host events rendered, and every type name in every rendered signature is classified by the existing fail-closed `LINK_MAP`/`FOUNDATION_TYPE_NAMES`/`TYPE_LINK_EXEMPTIONS` check. The 25 findings (12 events, 13 keys) were all client-face; each now carries a named exemption pointing at its owning README, consistent with the existing `appShell`/`connection` precedent.
+The audit that motivated this found the host face already complete: 48 rendered services + 10 walk exemptions covered all 58 host-visible Context keys, all 49 host events rendered, and every type name in every rendered signature is classified by the existing fail-closed `LINK_MAP`/`FOUNDATION_TYPE_NAMES`/`TYPE_LINK_EXEMPTIONS` check. The 25 findings (12 events, 13 keys) were all client-face; each now carries a named exemption pointing at its owning README, consistent with the existing `uiRenderer`/`connection` precedent.
 
 ## Verification
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-09-cordis-event-walk-backstop.zh.md

@@ -20,7 +20,7 @@ Status: implemented
 
 分区判定从 `computeOutputs` 中提取为纯函数 `walkPartitionProblems(input, maps)`,使每条验收路径都能以单元测试证明而无需运行 Typert 投影;`computeOutputs` 向它馈送渲染模型加独立扫描结果,页面拼接错误的聚合方式保持不变。
 
-促成本决定的审计发现 host face 本已完备:48 个渲染服务 + 10 条 walk 豁免覆盖全部 58 个 host 可见 Context key,49 个 host 事件全部渲染,且每个渲染签名中的每个类型名都已被既有的 fail-closed `LINK_MAP`/`FOUNDATION_TYPE_NAMES`/`TYPE_LINK_EXEMPTIONS` 检查分类。25 条发现(12 事件、13 key)全部在 client face;现在每条都带指向其所属 README 的具名豁免,与既有的 `appShell`/`connection` 先例一致。
+促成本决定的审计发现 host face 本已完备:48 个渲染服务 + 10 条 walk 豁免覆盖全部 58 个 host 可见 Context key,49 个 host 事件全部渲染,且每个渲染签名中的每个类型名都已被既有的 fail-closed `LINK_MAP`/`FOUNDATION_TYPE_NAMES`/`TYPE_LINK_EXEMPTIONS` 检查分类。25 条发现(12 事件、13 key)全部在 client face;现在每条都带指向其所属 README 的具名豁免,与既有的 `uiRenderer`/`connection` 先例一致。
 
 ## 验证
 

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.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-08-10-remote-event-delivery.md
-2026-08-10-remote-event-delivery.md: 2b8c2b03b96c6337d1ec70ce1d91746abb4ef743
-2026-08-10-remote-event-delivery.zh.md: 00e580b7e6a71999fd0f202a1cfca2713df388ae
+2026-08-10-remote-event-delivery.md: 9c2b5087772a5a343514d1766a14e90edb261813
+2026-08-10-remote-event-delivery.zh.md: 213715f5d9efcc11290059e5c5b0c06bbd7e255d

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.md

@@ -56,7 +56,7 @@ $on<Event extends TypertRemoteEvent>(event: Event, listener: Events[Event]): ()
 
 `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 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 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 the implicit `PLATFORM_MODULES` plus `PRELOADED_CLIENT_EXTERNALS` baseline, the package's `dsh.client.external` requests, the `INLINE_SAFE` wire layer, and generated `/remote` contributions. 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:
 
 ```ts ignore-check
 $dispatch(event: string, args: readonly unknown[]): void

+ 1 - 1
.agents/notes/implemented/architecture/2026-08-10-remote-event-delivery.zh.md

@@ -56,7 +56,7 @@ $on<Event extends TypertRemoteEvent>(event: Event, listener: Events[Event]): ()
 
 `Events` 按程序解析:host 程序里是 host 事件全集,client 程序里是 client 编译面看得见的那些——同一个谓词在两侧各自成立,不需要把 host 声明拖进 client。
 
-**契约把消费动词与载体交接分开**:消费方用 `$on` 订阅,持有 host 帧 sink 的一方用 `$dispatch` 把解码后的帧交进来。它**不能**是一个跨插件的模块级函数:client bundle 纯度门禁(`packages/client/tsdown.client.ts`)只放行 `CLIENT_EXTERNALS`、`INLINE_SAFE` 那层 wire 契约与 `/remote` 生成物三类值导入,而靠 inline 绕过会把 `ClientRemoteService` 复制一份进 runtime bundle、令 `instanceof` 恒假。cordis 服务方法正是该门禁指定的协作形态:
+**契约把消费动词与载体交接分开**:消费方用 `$on` 订阅,持有 host 帧 sink 的一方用 `$dispatch` 把解码后的帧交进来。它**不能**是一个跨插件的模块级函数:client bundle 纯度门禁(`packages/client/tsdown.client.ts`)只放行隐式的 `PLATFORM_MODULES` 加 `PRELOADED_CLIENT_EXTERNALS` 基座、包自身的 `dsh.client.external` 请求、`INLINE_SAFE` wire 层与 `/remote` 生成物值导入。靠 inline 绕过会把 `ClientRemoteService` 复制一份进 runtime bundle、令 `instanceof` 恒假。cordis 服务方法正是该门禁指定的协作形态:
 
 ```ts ignore-check
 $dispatch(event: string, args: readonly unknown[]): void

+ 6 - 0
.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.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-15-client-shells-and-dynamic-packages.md
+2026-08-15-client-shells-and-dynamic-packages.md: a92300663bac3bfe04768cf2a4f0c354c4c0c66f
+2026-08-15-client-shells-and-dynamic-packages.zh.md: a0bf695b32c8f2f903555f9f2c75d666eddeabd6

+ 86 - 0
.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.md

@@ -0,0 +1,86 @@
+# Agent Note: Client shell layering and dynamic package boundaries
+
+Status: implemented
+
+English | [中文](2026-08-15-client-shells-and-dynamic-packages.zh.md)
+
+> The [client plugin loading model](2026-07-23-client-plugin-loading-model.md) owns module arrival, Cordis lifecycle, and HMR. This note owns package placement, build faces, shared module requests, and npm dependency declarations; those decisions supersede the older package taxonomy and import-edge rules in the loading note.
+
+## Problem
+
+Client npm dependency sections describe installation and development relationships, but they do not reliably describe bundle contents. Treating `dependencies`, `peerDependencies`, or `devDependencies` as implicit bundler instructions can inline a shared React or workspace identity, or leave a built library carrying unresolved child imports without the host that is meant to assemble them.
+
+The browser application also contains distinct roles: the HTML/Vite compilation entry, the framework-free Cordis startup kernel, static assembly libraries, and Loader-governed plugins. Early execution from HTML is an arrival policy, not a package kind. Runtime and modules need to arrive before the Vite main module while retaining ordinary `lib/client.js` artifacts and dynamic graph rows.
+
+Shared UI libraries still expose synchronous TypeScript and React values to many consumers. Until those values move behind services or slots, making the libraries formal dynamic entries would preserve the value coupling while obscuring which module identity the shell must share.
+
+## Decision
+
+### Layers and build forms
+
+| Layer | Members | Responsibility | Build and load form |
+| --- | --- | --- | --- |
+| Web compilation shell | `apps/web` | Owns `index.html`, Vite configuration, dist chunks, and static assets | Assembles final browser output from built package exports |
+| Startup kernel | `packages/client/web` | Owns the plain-DOM boot page, module-system wiring, Cordis settlement, and renderer handoff | `staticLinked` `lib/index.js`; no `dsh.client` row |
+| Static assembly libraries | Cordis, `ui-primitives`, `ui-slots` | Supply shared module identities and direct value APIs | ESM `lib/index.js`, merged and chunked by Vite; not Loader entries |
+| Module bootstrap | `packages/client/modules` | Supplies the client module table and its Cordis wrapper | Dynamic package with one ordinary `lib/client.js`; the host delivers its factory early |
+| Dynamic client packages | runtime, `ui-renderer`, theme, and feature plugins | Participate through Cordis services, slots, and effects | Declare `dsh.client`, emit self-registering `lib/client.js`, and remain host-graph entries |
+
+`packages/client/web` keeps Cordis as matching peer and development dependencies and uses modules and static UI packages as development compilation inputs. `apps/web` consumes built package exports rather than aliases into workspace source.
+
+The `staticLinked` preset leaves every bare specifier as an external import in `lib/index.js` and emits relative CSS assets beside it. The Vite host resolves and deduplicates those imports and decides final chunk boundaries. A static library therefore does not copy the host's bundling policy into its own artifact.
+
+### Shared module requests
+
+Dynamic browser bundles implicitly externalize the common baseline: `PLATFORM_MODULES` names shell-seeded React, Cordis, and static UI identities, while `PRELOADED_CLIENT_EXTERNALS` names runtime's parser-preloaded dynamic identity. A package uses `dsh.client.external` only for an exact non-baseline value request. Type-only imports are erased and create no request; permitted third-party implementation libraries remain private bundle contents.
+
+A request has exactly two suppliers:
+
+1. The dynamic package row it names; a trailing `/client` aliases that package row.
+2. An exact key in the shell's static module table.
+
+There is no general `dsh.client.provide` alias mechanism. Dynamic rows and static keys exhaust the real suppliers, while Cordis service provision remains independent. Graph composition rejects malformed or missing requests, self-requests, and synchronous request cycles, and orders dynamic suppliers before their consumers. `ClientModuleSystem.import()` and `prefetch()` recursively register those dynamic supplier factories before the consumer can materialize, so network timing cannot violate the synchronous request graph.
+
+### Parser preloading and React handoff
+
+The modules Node half injects the startup protocol into the served HTML in this order:
+
+1. Install `window.__ModuleLoader__` in queue mode with `pendingQueue`, `load()`, and `create()`.
+2. Execute the modules graph row's ordinary `lib/client.js` as a blocking classic script.
+3. Execute runtime's ordinary `lib/client.js` the same way.
+4. Assign `window.__DSH_BOOT__`.
+5. Execute the Vite main module.
+
+Both early scripts only register factories. The startup kernel passes the raw graph and shell seeds to `__ModuleLoader__.create()`. The facade removes the modules registration, materializes it with a `require` function that rejects every external, and invokes its `createClientModuleSystem` export. The modules bundle parses the graph, constructs `ClientModuleSystem`, caches its own exports as the modules row, and retains the system in a module closure. Construction switches the same facade to live mode before draining runtime's pending factory. The modules client face consequently has a zero-runtime-external bootstrap requirement.
+
+After the `immediately` tier has registered its factories, the kernel creates all Loader entries, awaits Cordis quiescence, and requires every fiber to be ACTIVE. It then calls `ctx.uiRenderer.mount(container)`. The dynamic `ui-renderer` package owns React, slot rendering, hydration of the existing boot DOM, and the React root lifecycle; the startup kernel and failure page remain React-free.
+
+### Dependency declarations
+
+Every client package keeps Cordis in matching `peerDependencies` and `devDependencies`. A dynamic package that imports, re-exports, augments, or names an internal dynamic package in `dsh.client.inject` keeps that package as matching peer and development dependencies. Static client inputs and React modules are development-only inputs for a dynamic package because the shell supplies their runtime identities.
+
+Ordinary installed libraries remain `dependencies`: a dynamic build may bundle a private implementation, while a `staticLinked` library retains its bare import for the final host. Each build face decides externality independently from npm sections. Published file lists cover every runtime entry, relative asset, and declaration file reached by the artifact.
+
+`verify-client-packages` enforces these classifications, dependency sections, build forms, parser-preload alignment, shared-module requests, and module-graph acyclicity. The repository publint pass enforces publication closure. The verifier's `--fix` mode repairs only unambiguous manifest drift.
+
+## Alternatives considered
+
+**Convert every client package into a dynamic plugin immediately.** `ui-primitives` and `ui-slots` still provide synchronous values without independent service or slot lifecycles; a manifest declaration alone would not remove those imports.
+
+**Generate a separate `client-static.js` for modules or runtime.** Both packages remain dynamic graph rows and Cordis plugins; only their factory arrival is early. A second artifact would encode host policy in a filename and create two runtime products from one source.
+
+**Compile all shared modules into the Vite entry.** This would remove deployment composition and plugin-level replacement from business plugins, including the renderer and theme.
+
+**Retain a general module-provider declaration.** Package rows and exact static keys already name all suppliers; aliases would add another ownership protocol without a third supply source.
+
+**Hardcode preload URLs in `apps/web/index.html`.** URLs and `rev` values belong to the host's current graph. Rewriting the served HTML keeps the queue, bundle URLs, and manifest on one graph revision.
+
+## Consequences
+
+Bundle contents stay stable when an npm dependency moves between peer and development sections, because each build face declares externality directly. Static libraries remain host-assembled, while dynamic packages retain uniform artifacts and lifecycle governance.
+
+The startup protocol depends on the modules and runtime package ids, and modules must remain self-contained at runtime. A missing bootstrap registration fails before Cordis starts; later plugin import, apply, and service-wait failures remain visible through the boot page's ACTIVE scan.
+
+The shell consumes built `lib/` products, so source and browser artifacts can drift until the relevant build or watcher runs. Typechecking source alone does not prove the served application uses the same code.
+
+The two static UI libraries remain deliberate exceptions. Converting either one to a dynamic package requires moving all value consumers to services or slots and removing its identity from the static seed in the same change.

+ 86 - 0
.agents/notes/implemented/architecture/2026-08-15-client-shells-and-dynamic-packages.zh.md

@@ -0,0 +1,86 @@
+# Agent Note: 客户端壳分层与动态包边界
+
+Status: implemented
+
+[English](2026-08-15-client-shells-and-dynamic-packages.md) | 中文
+
+> [Client 插件装载模型](2026-07-23-client-plugin-loading-model.md)负责模块到达、Cordis 生命周期和 HMR。本 Note 负责包归属、构建 face、共享模块请求及 npm 依赖声明;这些决定取代装载 Note 中较早的包分类和 import 边规则。
+
+## Problem
+
+Client npm 依赖区段描述安装和开发关系,但不能可靠描述 bundle 内容。把 `dependencies`、`peerDependencies` 或 `devDependencies` 当作隐式 bundler 指令,可能内联本应共享的 React 或 workspace 身份,也可能让构建后的库携带未解析子 import,却没有交给预期的宿主组装。
+
+浏览器应用还包含不同角色:HTML/Vite 编译入口、不依赖框架的 Cordis 启动内核、静态装配库,以及由 Loader 治理的插件。HTML 提前执行属于到达策略,不定义包类别。Runtime 和 modules 需要先于 Vite 主模块到达,同时继续使用普通 `lib/client.js` 产物和动态图 row。
+
+共享 UI 库仍向大量消费者暴露同步 TypeScript 与 React 实体。在这些实体进入 service 或 slot 前,形式上把库改为动态 entry 只会保留实体耦合,并模糊外壳必须共享的模块身份。
+
+## Decision
+
+### 分层与构建形态
+
+| 层 | 成员 | 职责 | 构建与加载形态 |
+| --- | --- | --- | --- |
+| Web 编译壳 | `apps/web` | 拥有 `index.html`、Vite 配置、dist chunk 和静态资源 | 从已构建 package export 组装最终浏览器产物 |
+| 启动内核 | `packages/client/web` | 拥有纯 DOM 启动页、模块系统接线、Cordis settle 和 renderer handoff | `staticLinked` `lib/index.js`;无 `dsh.client` row |
+| 静态装配库 | Cordis、`ui-primitives`、`ui-slots` | 提供共享模块身份和直接实体 API | ESM `lib/index.js`,由 Vite 合并拆分;不是 Loader entry |
+| 模块自举包 | `packages/client/modules` | 提供 client 模块表及其 Cordis wrapper | 带一个普通 `lib/client.js` 的动态包;host 提前送达其 factory |
+| 动态 client 包 | runtime、`ui-renderer`、主题和功能插件 | 通过 Cordis service、slot 和 effect 参与应用 | 声明 `dsh.client`,产出自注册 `lib/client.js`,并保留 host graph entry |
+
+`packages/client/web` 把 Cordis 保持为 matching peer 与开发依赖,并把 modules 和静态 UI 包作为开发期编译输入。`apps/web` 消费已构建 package export,不通过 alias 读取 workspace 源码。
+
+`staticLinked` 预设让 `lib/index.js` 中每个 bare specifier 保持 external import,并在旁边输出相对 CSS 资产。Vite 宿主负责解析和去重这些 import,并决定最终 chunk 边界。静态库不会把宿主打包策略复制进自身产物。
+
+### 共享模块请求
+
+动态浏览器 bundle 会隐式 external 统一基座:`PLATFORM_MODULES` 命名由外壳播种的 React、Cordis 和静态 UI 身份,`PRELOADED_CLIENT_EXTERNALS` 命名由 HTML parser 预载的 runtime 动态身份。包只在精确请求基座外实体时使用 `dsh.client.external`。纯类型 import 会被擦除,不产生请求;允许的第三方实现库保留为 bundle 私有内容。
+
+请求只有两种提供方:
+
+1. 请求所命名的 dynamic package row;末尾 `/client` 会别名到该 package row。
+2. 外壳静态模块表中的精确 key。
+
+不存在通用 `dsh.client.provide` 别名机制。动态 row 和静态 key 已穷尽实际提供方,Cordis service provide 与此相互独立。图组合会拒绝畸形或缺失请求、自请求和同步请求环,并把动态提供方排在消费者之前。`ClientModuleSystem.import()` 与 `prefetch()` 会在消费者能够物化前递归登记这些动态提供方的 factory,因此网络时序无法破坏同步请求图。
+
+### Parser 预载与 React 移交
+
+Modules Node 半按以下顺序向实际返回的 HTML 注入启动协议:
+
+1. 以 queue 模式安装 `window.__ModuleLoader__`,包含 `pendingQueue`、`load()` 与 `create()`。
+2. 以阻塞式 classic script 执行 modules graph row 的普通 `lib/client.js`。
+3. 以相同方式执行 runtime 的普通 `lib/client.js`。
+4. 赋值 `window.__DSH_BOOT__`。
+5. 执行 Vite 主模块。
+
+两个提前执行的脚本都只注册 factory。启动内核把原始图与外壳 seed 传给 `__ModuleLoader__.create()`。Facade 移除 modules registration,用拒绝全部 external 的 `require` 函数将其物化,再调用其 `createClientModuleSystem` 导出。Modules bundle 解析图、构造 `ClientModuleSystem`、把自身 exports 缓存为 modules row,并在模块闭包中保留该系统。构造过程先把同一 facade 切换到 live 模式,再排空 runtime 的 pending factory。因此 modules client face 必须满足零 runtime external 的自举要求。
+
+`immediately` 层级完成 factory 注册后,内核创建全部 Loader entry,等待 Cordis 静止,并要求每个 fiber 都进入 ACTIVE。随后调用 `ctx.uiRenderer.mount(container)`。动态 `ui-renderer` 包拥有 React、slot 渲染、已有启动 DOM 的 hydrate 和 React root 生命周期;启动内核与失败页保持 React-free。
+
+### 依赖声明
+
+每个 client 包都把 Cordis 保持为 matching `peerDependencies` 和 `devDependencies`。动态包若 import、re-export、augment 内部动态包,或在 `dsh.client.inject` 中命名它,就把该包保持为 matching peer 与开发依赖。静态 client 输入和 React 模块对动态包只是开发依赖,因为外壳提供其运行期身份。
+
+普通安装库仍放在 `dependencies`:动态构建可以内联私有实现,而 `staticLinked` 库会保留 bare import 交给最终宿主。各构建 face 独立决定 external,不由 npm 区段推导。发布文件列表覆盖产物实际可达的每个运行期入口、相对资产和声明文件。
+
+`verify-client-packages` 会检查这些分类、依赖区段、构建形态、parser preload 对齐、共享模块请求和模块图无环性。仓库 publint pass 负责检查发布闭包。该验证器的 `--fix` 模式只修复无歧义的 manifest 漂移。
+
+## Alternatives considered
+
+**立即把所有 client 包改为动态插件。** `ui-primitives` 与 `ui-slots` 仍提供同步实体,且没有独立 service 或 slot 生命周期;只加 manifest 声明不会移除这些 import。
+
+**为 modules 或 runtime 生成单独的 `client-static.js`。** 两个包仍是动态图 row 和 Cordis 插件,只有 factory 提前到达。第二份产物会把宿主策略编码进文件名,并让同一源码产生两个运行期产品。
+
+**把全部共享模块编进 Vite entry。** 这会让业务插件失去部署组合与插件级替换能力,包括 renderer 和主题。
+
+**保留通用模块 provider 声明。** Package row 和精确静态 key 已命名全部提供方;别名会增加另一套归属协议,却没有第三种供给来源。
+
+**在 `apps/web/index.html` 中硬编码预载 URL。** URL 与 `rev` 属于 host 当前 graph。改写实际返回的 HTML 才能让 queue、bundle URL 和 manifest 使用同一 graph revision。
+
+## Consequences
+
+Npm 依赖在 peer 与开发区段间移动时,bundle 内容保持稳定,因为每个构建 face 都直接声明 external。静态库继续由宿主装配,动态包则保留统一产物与生命周期治理。
+
+启动协议依赖 modules 和 runtime 的 package id,modules 还必须保持运行期自包含。缺少 bootstrap registration 会在 Cordis 启动前失败;后续插件 import、apply 与 service 等待失败仍由启动页的 ACTIVE 扫描呈现。
+
+外壳消费已构建 `lib/` 产品,因此在相关 build 或 watcher 运行前,源码与浏览器产物可能漂移。仅源码 typecheck 通过不能证明实际服务的应用使用同一份代码。
+
+两个静态 UI 库仍是明确例外。把其中任一项转换为动态包时,必须在同一变更中把全部实体消费者迁移到 service 或 slot,并从静态 seed 删除对应身份。

+ 6 - 0
.agents/notes/implemented/architecture/2026-08-17-dynamic-client-render-and-attachment-ownership.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-17-dynamic-client-render-and-attachment-ownership.md
+2026-08-17-dynamic-client-render-and-attachment-ownership.md: a7955156adf76e8655295d4db316ed35d1968585
+2026-08-17-dynamic-client-render-and-attachment-ownership.zh.md: 815d4f92a6db56b1b3c5da16f264e27e952aae5f

+ 43 - 0
.agents/notes/implemented/architecture/2026-08-17-dynamic-client-render-and-attachment-ownership.md

@@ -0,0 +1,43 @@
+# Agent Note: Dynamic client render and attachment ownership
+
+Status: implemented
+
+English | [中文](2026-08-17-dynamic-client-render-and-attachment-ownership.zh.md)
+
+## Problem
+
+The host-authored client graph governs browser plugins, but three presentation paths sat outside that lifecycle. The web kernel created the React root and a shell-owned assembly pseudo-entry, `ui-conversation` imported attachment components as package values, and the shell imported ui-theme's global styles. Disabling, failing, or reloading a plugin therefore did not govern all of the rendering and CSS that belonged to it.
+
+The loading and failure page has the opposite requirement: it must remain usable when any dynamic plugin, including the renderer, fails to activate. It cannot depend on the React tree whose failure it reports.
+
+## Decision
+
+`@deepseek-ai/dsh-client-web` is a framework-free boot kernel. It draws its loading and failure page with DOM operations and local CSS fallbacks, constructs the client module system and Cordis Loader, creates the statically adopted modules bootstrap entry plus every host-graph entry, and waits until every fiber is ACTIVE. Loader state changes retain one spinner node and update only its CSS arc when an entry first becomes active. The arc grows from one fifth to four fifths of the ring, preserving a visible gap throughout rotation. After the roster settles, the kernel resolves `ctx.uiRenderer` and hands the existing container to `mount()`.
+
+`@deepseek-ai/dsh-client-ui-renderer` is an `immediately` dynamic client plugin. It owns the React slot outlets, SessionProvider, and observable-to-uSES binding. After its `slots` and `sessions` injections activate, it installs the slot renderer and provides `ctx.uiRenderer`. `mount()` hydrates the kernel-authored boot DOM, then replaces it with the assembled application in a layout effect before the browser can paint an intermediate frame. The hydrated spinner node retains its animation phase. The assembled tree projects the selected session title and performs the sole context-level `renderSlot('root')` call. The service, renderer installation, and React root all dispose with their owners.
+
+`ui-conversation` declares `conversation.input.attachments` and `conversation.message.images` and supplies attachment data, callbacks, authorized image loading, and its locale seat. `ui-attachment` waits on those declarations through `ctx.slots.inject()` and registers the draft rail/drop target and historical image gallery/lightbox. The React implementations remain internal package values; cross-plugin composition uses slots. This package integration supersedes the direct-import ruling in the [attachment display note](../feature/2026-08-11-web-attachment-display-alignment.md) without changing that note's visual and interaction decisions.
+
+ui-theme imports its five global stylesheets as `?inline` strings. Its client entry calls `installThemeStyles(ctx)`, which installs one style tag per sheet through `ctx.effect()`, so unloading or reloading ui-theme removes or replaces its global CSS with the same lifecycle as its service. The web kernel retains only mount defaults and a self-contained boot-page palette whose fonts and colors match the corresponding theme tokens.
+
+React, React DOM, Cordis, ui-slots, and ui-primitives remain static platform modules with one browser identity. The dynamic ui-renderer bundle consumes those shared modules and owns the rendering effects.
+
+## Verification
+
+Component tests pin the persistent progress spinner, hydration without boot-DOM mutation, document title, application tree, attachment entries, and disposal. The assembled built-bundle boot exercises the real module table and dynamic entries, while the theme style tests prove its tags install and dispose with the plugin fiber. The browser replay lane covers the complete handoff from the framework-free page to the rendered application.
+
+## Alternatives considered
+
+**Keep the shell-owned app assembly pseudo-entry.** Rejected because it remains invisible to the host graph and makes render ownership a special Loader path even though the assembly has ordinary service dependencies and lifecycle effects.
+
+**Keep exported attachment atoms and import them from ui-conversation.** Rejected because a direct component import bypasses independent plugin composition and reload ownership. Owner data still travels directly through typed slot props; only presentation selection is dynamic.
+
+**Keep ui-theme styles in the shell's base stylesheet.** Rejected because theme CSS would remain active when the theme plugin is absent or failed and would not participate in plugin reload cleanup.
+
+**Render the failure page with React.** Rejected because a ui-renderer or React-tree failure must not remove the only diagnostic available in the browser.
+
+## Consequences
+
+The host graph contains every dynamic rendering owner, and HMR replaces attachment presentation, render assembly, and theme CSS through plugin lifecycle. A ui-renderer failure leaves a readable DOM failure page instead of a blank React mount. Omitting ui-attachment deliberately leaves its optional slots empty; the shipped web composition includes it, and a configured entry that fails activation prevents the full-application handoff.
+
+The application still waits for the complete client roster before its first React frame. The shell still statically bundles the platform module identities, and the boot page maintains a small private light/dark palette because ui-theme CSS is unavailable until that plugin materializes.

+ 43 - 0
.agents/notes/implemented/architecture/2026-08-17-dynamic-client-render-and-attachment-ownership.zh.md

@@ -0,0 +1,43 @@
+# Agent Note: 客户端渲染与附件呈现的动态归属
+
+Status: implemented
+
+[English](2026-08-17-dynamic-client-render-and-attachment-ownership.md) | 中文
+
+## 问题
+
+宿主编写的客户端图管理浏览器插件,但三条呈现路径位于其生命周期之外。Web 内核创建 React 根和由外壳持有的组装伪 entry,`ui-conversation` 以包值形式导入附件组件,外壳还导入 ui-theme 的全局样式。因此,禁用、失败或重载某个插件时,并不能同时管理属于该插件的全部渲染与 CSS。
+
+加载与失败页面的要求正好相反:包括渲染器在内的任何动态插件激活失败时,它都必须保持可用。它不能依赖自己正在报告其失败的 React 树。
+
+## 决定
+
+`@deepseek-ai/dsh-client-web` 是不依赖框架的启动内核。它通过 DOM 操作与本地 CSS 回退绘制加载和失败页面,构造客户端模块系统与 Cordis Loader,创建静态接纳的 modules 启动 entry 和宿主图中的每个 entry,并等待所有 fiber 进入 ACTIVE。Loader 状态变化会保留同一个 spinner 节点,只在 entry 首次进入 active 时更新其 CSS 圆弧。圆弧从圆环的五分之一增长至五分之四,在旋转期间始终保留可见缺口。名册稳定后,内核解析 `ctx.uiRenderer`,把现有容器交给 `mount()`。
+
+`@deepseek-ai/dsh-client-ui-renderer` 是带 `immediately` 标记的动态客户端插件。它持有 React slot outlet、SessionProvider 与 observable 到 uSES 的绑定。它注入的 `slots` 与 `sessions` 激活后,便安装 slot 渲染器并提供 `ctx.uiRenderer`。`mount()` hydrate 内核生成的启动 DOM,再通过 layout effect 在浏览器绘制中间帧前将其替换为组装完成的应用。hydrate 后的 spinner 节点会保持动画相位。组装后的树投影当前会话标题,并执行唯一一次上下文级 `renderSlot('root')` 调用。服务、渲染器安装和 React 根都随各自持有方 dispose。
+
+`ui-conversation` 声明 `conversation.input.attachments` 与 `conversation.message.images`,并提供附件数据、回调、经会话授权的图片加载及其 locale seat。`ui-attachment` 通过 `ctx.slots.inject()` 等待这些声明,再注册草稿附件栏/拖放目标和历史图片画廊/灯箱。React 实现仍是包内值;跨插件组合通过 slot 完成。这项包集成决策取代[附件展示 Note](../feature/2026-08-11-web-attachment-display-alignment.md)中的直接导入规则,但不改变该 Note 的视觉与交互决策。
+
+ui-theme 把自己的五份全局样式表作为 `?inline` 字符串导入。客户端 entry 调用 `installThemeStyles(ctx)`,经 `ctx.effect()` 为每份样式安装一个 style 标签,因此卸载或重载 ui-theme 时,其全局 CSS 会随服务的同一生命周期删除或替换。Web 内核只保留挂载默认值,以及字体和颜色与对应主题 token 一致的自给自足启动页配色。
+
+React、React DOM、Cordis、ui-slots 与 ui-primitives 仍是保持单一浏览器身份的静态平台模块。动态 ui-renderer bundle 消费这些共享模块并持有渲染副作用。
+
+## 验证
+
+组件测试固定持久进度 spinner、hydrate 不改变启动 DOM、文档标题、应用树、附件 entry 与 dispose 行为。组装后的构建 bundle 启动测试会运行真实模块表与动态 entry,theme 样式测试则证明这些标签会随插件 fiber 安装和释放。浏览器回放测试覆盖从不依赖框架的页面到渲染应用的完整交接。
+
+## 备选方案
+
+**保留外壳持有的应用组装伪 entry。** 否决:它仍不在宿主图中,而且会把渲染归属变成特殊 Loader 路径,尽管该组装只有普通服务依赖与生命周期副作用。
+
+**保留导出的附件原子组件并由 ui-conversation 导入。** 否决:直接导入组件会绕过独立插件组合与重载归属。持有方数据仍通过带类型的 slot props 直接传递;只有呈现选择是动态的。
+
+**把 ui-theme 样式留在外壳的基础样式表中。** 否决:主题插件缺失或失败时,主题 CSS 仍会生效,而且不会参与插件重载清理。
+
+**用 React 渲染失败页面。** 否决:渲染服务或 React 树失败时,不能连同浏览器中唯一的诊断一起移除。
+
+## 结果
+
+宿主图包含每个动态渲染持有方,HMR 通过插件生命周期替换附件呈现、渲染组装与主题 CSS。渲染服务失败时会留下可读的 DOM 失败页面,而不是空白 React 挂载点。有意省略 ui-attachment 会让其可选 slot 保持为空;随产品交付的 Web 组合包含该插件,而配置中存在但激活失败的 entry 会阻止完整应用交接。
+
+应用首个 React 帧仍会等待完整客户端名册。外壳仍静态打包平台模块身份;由于 ui-theme CSS 要等到该插件物化后才可用,启动页还要维护一小套私有的明暗配色。

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.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-07-28-themed-scrollbars-and-reserved-gutter.md
-2026-07-28-themed-scrollbars-and-reserved-gutter.md: a820a92406ce4054f16922064772d03c6ac3ab83
-2026-07-28-themed-scrollbars-and-reserved-gutter.zh.md: 80104c4aca5986c3a1f49186ac4adf3169f46da5
+2026-07-28-themed-scrollbars-and-reserved-gutter.md: 3dbaf73158d7c5182b0992d1120965b008aff649
+2026-07-28-themed-scrollbars-and-reserved-gutter.zh.md: bcc11952930143ae36841a677627a7e77ef21d32

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md

@@ -12,7 +12,7 @@ The visible symptom that surfaced the gap was elsewhere. The workspace browser's
 
 ## Decision
 
-`packages/client/ui-theme/src/styles/scrollbar.css` is the sole consumer of the four tokens, and the fifth ui-theme sheet in the shell's import chain (`packages/client/web/src/base.css`). It follows `design-platform.css` there because it reads that sheet's tokens.
+`packages/client/ui-theme/src/styles/scrollbar.css` is the sole consumer of the four tokens and the third global sheet imported by ui-theme's dynamic client entry. It follows `design-platform.css` there because it reads that sheet's tokens; both compile into ui-theme's plugin-owned client bundle.
 
 The rules sit on `body`, not `html`. `design-platform.css` declares the `--dsw-alias-*` tokens on `body`, with the dark overrides on `body[data-ds-dark-theme]`, and custom properties inherit only downward; an `html` rule resolves them to the guaranteed-invalid value, at which point `scrollbar-color` computes to `auto` and no theming happens at all.
 

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.zh.md

@@ -12,7 +12,7 @@ Status: implemented
 
 ## 决策
 
-`packages/client/ui-theme/src/styles/scrollbar.css` 是这四个 token 的唯一消费方,也是壳的导入链(`packages/client/web/src/base.css`)中第五张 ui-theme 样式表。它排在 `design-platform.css` 之后,因为它读取那张样式表的 token。
+`packages/client/ui-theme/src/styles/scrollbar.css` 是这四个 token 的唯一消费方,也是 ui-theme 动态客户端 entry 导入的第三张全局样式表。它排在 `design-platform.css` 之后,因为它读取那张样式表的 token;两者都会编译进 ui-theme 持有的客户端 bundle
 
 规则挂在 `body` 上,而非 `html`。`design-platform.css` 在 `body` 上声明 `--dsw-alias-*` token,暗色覆盖挂在 `body[data-ds-dark-theme]` 上,而自定义属性只向下继承;挂在 `html` 上的规则会把它们解析为 guaranteed-invalid 值,此时 `scrollbar-color` 计算为 `auto`,主题完全不起作用。
 

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.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-host-backed-web-preferences.md
-2026-08-06-host-backed-web-preferences.md: cb3222e3edc9a5382b2c54eb9953b6b952b86b11
-2026-08-06-host-backed-web-preferences.zh.md: 08208daa683b7cc67e3ad4efd9a79f15ef82c7a3
+2026-08-06-host-backed-web-preferences.md: 5d90f2be7c8b4030e9bdc00eed2769491ec009e5
+2026-08-06-host-backed-web-preferences.zh.md: c861c45bff299e06841165a2b36d0781e8f54d99

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md

@@ -14,7 +14,7 @@ The first theme implementation moved only Appearance to Host settings but awaite
 
 The owning Host halves register three schemas: optional `locale.preference` (`zh` or `en`, where absence delegates to the browser), `ui-theme.preference` (`light`, `dark`, or `system`, default `system`), and `ui-conversation.busyEnter` (`queue` or `steer`, default `queue`). The local settings provider stores explicit choices in `$DSH_HOME/settings.yaml`, which resolves to `~/.dsh/settings.yaml` under the default home. The API proxy explicitly exposes all three namespaces beside the other Web settings; registration alone never crosses that configuration boundary.
 
-The client runtime provides one `bindSettingsScope` lifecycle per namespace — the browser mirror of the Host-side settings owner seam. It installs `settings/changed` and `connection/reset` listeners before starting a background initial read, so no settings transport can block plugin activation and an invalidation cannot fall into a read-before-subscribe gap, and it publishes a snapshot store (status, section value, revision, writability, host/memory mode) the domain service subscribes to. The default decoder validates each incoming section against the namespace's own serialized wire schema, rehydrated through dsh-client-schema-form, so domains carry no hand-written wire guards. Domain services take the scope as an ordinary constructor collaborator, publish their provisional defaults immediately—browser-derived locale, system theme, and Queue—then adopt an accepted Host section without writing it back; a service constructed without a scope (standalone dictionary or policy fixtures) simply stays process-local.
+`dsh-client-ui-settings` provides `ctx.settingsScope.bind(spec)`, which owns one lifecycle per namespace as the browser mirror of the Host-side settings owner seam. It installs `settings/document-updated` and `connection/reset` listeners before starting a background initial read, so no settings transport can block plugin activation and an invalidation cannot fall into a read-before-subscribe gap, and it publishes a snapshot store (status, section value, revision, writability, host/memory mode) the domain service subscribes to. The default decoder validates each incoming section against the namespace's own serialized wire schema, rehydrated through the colocated `ctx.settingsSchema` service, so domains carry no hand-written wire guards. Domain services take the scope as an ordinary constructor collaborator, publish their provisional defaults immediately—browser-derived locale, system theme, and Queue—then adopt an accepted Host section without writing it back; a service constructed without a scope (standalone dictionary or policy fixtures) simply stays process-local.
 
 User changes update the live service synchronously and queue a `settings.mutate` path operation through `scope.set`. The scope serializes gestures, sends the latest known namespace revision as `expectedRevision`, records every successful revision, and lets only the latest write settlement republish live state. A rejected or failed latest write reloads Host state. Disposal rejects new work, skips queued operations, suppresses publication by the in-flight operation, and waits for that operation to settle before the plugin reaches quiescence.
 

+ 1 - 1
.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.zh.md

@@ -14,7 +14,7 @@ Web 的 Appearance、Language 和繁忙态 Enter 偏好原本存在浏览器 `lo
 
 各领域所属的 Host half 注册三份 schema:可选的 `locale.preference`(`zh` 或 `en`,缺失时交由浏览器决定)、`ui-theme.preference`(`light`、`dark` 或 `system`,默认为 `system`),以及 `ui-conversation.busyEnter`(`queue` 或 `steer`,默认为 `queue`)。本地 settings 提供方将显式选择存入 `$DSH_HOME/settings.yaml`,在使用默认 home 时,该路径解析为 `~/.dsh/settings.yaml`。API 代理会显式暴露这三个 namespace,与其他 Web settings 并列;仅注册它们,绝不会跨越该配置边界。
 
-客户端运行时为每个 namespace 提供一份 `bindSettingsScope` 生命周期——即 Host 侧 settings owner seam 的浏览器镜像。它在开始后台初始读取之前安装 `settings/changed` 和 `connection/reset` 监听器,因此任何 settings 传输都不会阻塞插件激活,失效通知也不会掉入先读取、后订阅的空档;它还会发布一个供领域服务订阅的快照 store(状态、分节值、revision、可写性、host/内存模式)。默认解码器会对照该 namespace 自身的序列化 wire schema(经 dsh-client-schema-form 还原)校验每个传入分节,因此各领域无需携带手写的 wire 校验器。领域服务把 scope 当作普通的构造函数协作者接收,立即发布各自的暂定默认值:由浏览器派生的 locale、系统主题和 Queue;随后采纳已获接受的 Host 分节,但不将其写回;不带 scope 构造的服务——独立词典或政策 fixture(测试前置数据)——则仅停留在进程本地。
+`dsh-client-ui-settings` 提供 `ctx.settingsScope.bind(spec)`,为每个 namespace 持有一份生命周期,作为 Host 侧 settings owner seam 的浏览器镜像。它在开始后台初始读取之前安装 `settings/document-updated` 和 `connection/reset` 监听器,因此任何 settings 传输都不会阻塞插件激活,失效通知也不会掉入先读取、后订阅的空档;它还会发布一个供领域服务订阅的快照 store(状态、分节值、revision、可写性、host/内存模式)。默认解码器会对照该 namespace 自身的序列化 wire schema(经同包的 `ctx.settingsSchema` 服务还原)校验每个传入分节,因此各领域无需携带手写的 wire 校验器。领域服务把 scope 当作普通的构造函数协作者接收,立即发布各自的暂定默认值:由浏览器派生的 locale、系统主题和 Queue;随后采纳已获接受的 Host 分节,但不将其写回;不带 scope 构造的服务——独立词典或政策 fixture(测试前置数据)——则仅停留在进程本地。
 
 用户变更会同步更新实时服务,并经 `scope.set` 将一项 `settings.mutate` 路径操作排入队列。scope 会串行处理手势,以最新已知 namespace revision 作为 `expectedRevision` 发送,记录每次成功写入的 revision,并且只允许最新写入的结算结果重新发布实时状态。最新写入被拒或失败时,scope 会重新加载 Host 状态。插件释放会拒绝新工作、跳过已排队操作、抑制运行中操作发布状态,并等待该操作结算后才让插件达到完全停稳。
 

+ 2 - 2
.agents/notes/implemented/bug-fix/2026-08-10-pre-plugin-theme-bootstrap.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-10-pre-plugin-theme-bootstrap.md
-2026-08-10-pre-plugin-theme-bootstrap.md: 006afffc9a07a3abde4131ec972df3be5949ed09
-2026-08-10-pre-plugin-theme-bootstrap.zh.md: 206390518f3a320e63f699bad6d228010faaf969
+2026-08-10-pre-plugin-theme-bootstrap.md: 7f5c57316d7dd022b08918459282a6f6128eb93b
+2026-08-10-pre-plugin-theme-bootstrap.zh.md: 7152bf8dc2bbfae2a671c4da899cbebda0bd4b39

+ 3 - 3
.agents/notes/implemented/bug-fix/2026-08-10-pre-plugin-theme-bootstrap.md

@@ -6,13 +6,13 @@ English | [中文](2026-08-10-pre-plugin-theme-bootstrap.zh.md)
 
 ## Problem
 
-The web shell renders `Loading plugins…` before the browser-side plugin tree activates. The theme tokens are already loaded with the shell styles, but `color-scheme` and `body[data-ds-dark-theme]` are not written until ui-theme's ThemeRuntime and ui-layout's ThemePresenter activate; with a persisted dark preference, the loading page therefore renders first with the light palette and then switches to dark.
+The web shell renders `Loading plugins…` before the browser-side plugin tree activates. ui-theme's token styles arrive with its dynamic client bundle, so the framework-free loading page uses a private light/dark fallback palette. Without an earlier write to `color-scheme` and `body[data-ds-dark-theme]`, a persisted dark preference would still render that page first with its light fallback and then switch to dark when ui-theme's ThemeRuntime and ui-layout's ThemePresenter activate.
 
 `dshClient.immediately` only includes the bundle in first-stage prefetching; it does not cause the plugin to execute before HTML parsing or the shell's initial render. Changing only the client plugin's loading tier cannot close this window.
 
 ## Decision
 
-ui-theme's host half transforms each index HTML document through `ctx.webServer.tapIndex()`, inserting a synchronous inline script immediately after the opening `<body>` tag. The transform registers under an optional `httpServer` injection, so compositions without that service still activate ui-theme and install no transform. When the HTML parser executes the script, the body exists, but the shell's module script and React root have not yet run.
+ui-theme's host half transforms each index HTML document through `ctx.webServer.tapIndex()`, inserting a synchronous inline script immediately after the opening `<body>` tag. The transform registers under an optional `httpServer` injection, so compositions without that service still activate ui-theme and install no transform. When the HTML parser executes the script, the body exists, but the shell's module script and framework-free boot page have not yet run.
 
 The host half registers the [`ui-theme.preference` settings section](2026-08-06-host-backed-web-preferences.md) when a settings provider exists. For each index response, it embeds that schema-validated built-in preference in the inline script; without a settings provider or active registration, it embeds the `system` default. The browser resolves `system` through `prefers-color-scheme`, falling back to light when `matchMedia` is unavailable. It writes only the two pieces of DOM state that ThemePresenter later owns: `document.documentElement.style.colorScheme` and `body[data-ds-dark-theme]`.
 
@@ -34,4 +34,4 @@ ui-theme's unit tests cover activation without either optional Host service, the
 
 ## Consequences
 
-The loading page's first frame matches the durable built-in preference and defaults to the OS preference when no settings provider is composed. The index transform reads Host settings for every response, while the inline script contains only the selected built-in value and `system` resolution. Changes to the built-in preference semantics or ThemePresenter DOM fields must update both the script and ThemeRuntime. A custom theme still applies fully only after the browser plugins activate; during the loading interval, the page uses the light or dark base palette to which that theme resolves.
+The loading page's first frame matches the durable built-in preference and defaults to the OS preference when no settings provider is composed. The index transform reads Host settings for every response, while the inline script contains only the selected built-in value and `system` resolution. Changes to the built-in preference semantics or ThemePresenter DOM fields must update both the script and ThemeRuntime. A custom theme still applies fully only after the browser plugins activate; during the loading interval, the page uses its private light or dark fallback palette.

+ 3 - 3
.agents/notes/implemented/bug-fix/2026-08-10-pre-plugin-theme-bootstrap.zh.md

@@ -6,13 +6,13 @@ Status: implemented
 
 ## 问题
 
-Web 壳在浏览器侧插件树激活前呈现 `Loading plugins…`。主题 token 已随壳样式加载,但 `color-scheme` 和 `body[data-ds-dark-theme]` 要等 ui-theme 的 ThemeRuntime 与 ui-layout 的 ThemePresenter 激活后才写入;持久化偏好为深色时,加载页因此先按浅色调色板绘制,再切为深色。
+Web 壳在浏览器侧插件树激活前呈现 `Loading plugins…`。ui-theme 的 token 样式随动态客户端 bundle 到达,因此不依赖框架的加载页使用私有的明暗回退配色。如果不提前写入 `color-scheme` 与 `body[data-ds-dark-theme]`,持久化偏好为深色时,该页面仍会先按浅色回退绘制,再在 ui-theme 的 ThemeRuntime 与 ui-layout 的 ThemePresenter 激活后切为深色。
 
 `dshClient.immediately` 只把 bundle 纳入第一阶段预取,不会让插件在 HTML 解析或壳首次渲染前执行。仅调整客户端插件的加载档位无法关闭这段时间窗口。
 
 ## 决策
 
-ui-theme 的主机侧通过 `ctx.webServer.tapIndex()` 转换每份 index HTML,在 `<body>` 起始标签后紧接一段同步内联脚本。该转换通过可选的 `httpServer` 注入注册,因此不含该服务的组合仍会激活 ui-theme,但不会安装转换。HTML 解析器执行该脚本时,body 已存在,而壳的模块脚本与 React 根节点尚未运行。
+ui-theme 的主机侧通过 `ctx.webServer.tapIndex()` 转换每份 index HTML,在 `<body>` 起始标签后紧接一段同步内联脚本。该转换通过可选的 `httpServer` 注入注册,因此不含该服务的组合仍会激活 ui-theme,但不会安装转换。HTML 解析器执行该脚本时,body 已存在,而壳的模块脚本与不依赖框架的启动页尚未运行。
 
 settings provider 存在时,主机侧会注册 [`ui-theme.preference` settings 分节](2026-08-06-host-backed-web-preferences.md)。它为每份 index 响应把经过 schema 校验的内建偏好嵌入内联脚本;不存在 settings provider 或有效注册时则嵌入默认值 `system`。浏览器通过 `prefers-color-scheme` 解析 `system`,不支持 `matchMedia` 时回退为浅色。脚本只写 ThemePresenter 后续拥有的两项 DOM 状态:`document.documentElement.style.colorScheme` 与 `body[data-ds-dark-theme]`。
 
@@ -34,4 +34,4 @@ ui-theme 的单元测试覆盖不含任一可选 Host 服务时的激活、脚
 
 ## 后果
 
-加载页首帧与持久化内建偏好一致;未组合 settings provider 时则默认采用系统偏好。index 转换会为每份响应读取 Host settings,而内联脚本只包含选定的内建值与 `system` 解析逻辑。内建偏好语义或 ThemePresenter DOM 字段变化时,必须同时更新脚本与 ThemeRuntime。自定义主题仍会在浏览器插件激活后才完整应用;加载期间,页面使用该主题解析后的浅色或深色基础调色板
+加载页首帧与持久化内建偏好一致;未组合 settings provider 时则默认采用系统偏好。index 转换会为每份响应读取 Host settings,而内联脚本只包含选定的内建值与 `system` 解析逻辑。内建偏好语义或 ThemePresenter DOM 字段变化时,必须同时更新脚本与 ThemeRuntime。自定义主题仍会在浏览器插件激活后才完整应用;加载期间,页面使用自己的浅色或深色回退配色

+ 2 - 2
.agents/notes/implemented/feature/2026-08-11-web-attachment-display-alignment.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-08-11-web-attachment-display-alignment.md
-2026-08-11-web-attachment-display-alignment.md: 18c732c078ef6efd17e5c83708ba6e68ac663ce6
-2026-08-11-web-attachment-display-alignment.zh.md: 8a4222c31cd1fa2ad75ca314513c057fdb780d07
+2026-08-11-web-attachment-display-alignment.md: dd3816661792b882a2ce77cf8b03cc5e4ad9965e
+2026-08-11-web-attachment-display-alignment.zh.md: e16bd33f9a244f2b86b6beee835b4114dedb0898

+ 3 - 3
.agents/notes/implemented/feature/2026-08-11-web-attachment-display-alignment.md

@@ -14,7 +14,7 @@ All of this UI also lived inside `dsh-client-ui-conversation` — the rail inlin
 
 ## Decision
 
-Attachment display lives in a new zero-cordis atoms package, `@deepseek-ai/dsh-client-ui-attachment` (`packages/client/ui-attachment`), patterned on `dsh-client-ui-primitives`: `AttachmentRail` (64px/16px-radius thumbnails, single-click `onOpen`, inside-the-card remove control revealed on hover or focus and permanent under `pointer: coarse`, hidden scrollbar with circular edge arrows recomputed from scroll geometry, vertical-wheel horizontal pan clamped to 60px/tick, end-reveal on growth), `MessageImage`/`ImageGallery` (single-click preview), and `ImageLightbox`. Strings arrive as label props; `ui-conversation` bridges its `conversation` dictionary through `src/client/image-labels.ts` and keeps the machine wiring (draft ids, preview state, intake callbacks). The cross-package import is sanctioned exactly because the package is an atoms library, not a client plugin: plugin-to-plugin component imports stay forbidden, and the composer's rail is composer-owned rendering, not a slot.
+Attachment display lives in `@deepseek-ai/dsh-client-ui-attachment` (`packages/client/ui-attachment`): `AttachmentRail` (64px/16px-radius thumbnails, single-click `onOpen`, inside-the-card remove control revealed on hover or focus and permanent under `pointer: coarse`, hidden scrollbar with circular edge arrows recomputed from scroll geometry, vertical-wheel horizontal pan clamped to 60px/tick, end-reveal on growth), `MessageImage`/`ImageGallery` (single-click preview), and `ImageLightbox`. These remain internal pure-props components. `ui-conversation` declares the composer-attachment and message-image slots and supplies draft ids, image loading, intake callbacks, and its locale seat; the dynamic ui-attachment client entry waits on those declarations and registers the presentation. The [dynamic render and attachment ownership note](../architecture/2026-08-17-dynamic-client-render-and-attachment-ownership.md) owns this package integration; the visual behavior recorded here is unchanged.
 
 Both overlays body-portal: the lightbox opened from a chat message sits under transformed ancestors that would trap `position: fixed` in their own box (the backdrop covered only the chat column), so `ImageLightbox` and `Toast` render through `createPortal(document.body)` and cover the viewport from every opener. The transient banner is a `ui-primitives` `Toast` atom (120px from the viewport top, horizontally centered over its optional anchor — the composer card, so it sits over the chat column — `role="alert"`, `pointer-events: none`, three-second hold then one-second fade, `onDone` unmount, keyed per show so identical repeated messages re-announce). `InputBar` routes both intake rejections (`addImages`'s returned reason) and `promptError` through it, replacing the inline strips, and `ModelSelect` routes rejected model selections through the same atom while its in-menu strip with Retry stays the catalog-load surface; the machine-notice strip is untouched. DeepSeek Chat's source (a local reference copy) provided the target behaviors: its `ImageThumbnailInInput` (64px cards, opacity-transition delete), `ScrollArrows` (sentinel-driven paging), and `useToast` usage.
 
@@ -22,7 +22,7 @@ Both overlays body-portal: the lightbox opened from a chat message sits under tr
 
 **Keep the components inside `ui-conversation` and only restyle.** Rejected by the user: the attachment surface is expected to grow (file cards, upload progress), and the repo's plugin discipline forbids other plugins importing `ui-conversation` internals, so growth inside the plugin builds an unreusable pile. The atoms package gives the same components a sanctioned import path.
 
-**A `ui-attachment` client plugin registering slots.** Rejected: the rail renders inside the composer the machine owns and the gallery inside chat nodes; neither is a composition hole another plugin should fill, and a plugin would force slot indirection for what are pure presentational components.
+**Export attachment atoms and import them directly from `ui-conversation`.** Rejected by the package integration decision: direct component imports bypass dynamic plugin lifecycle and cross-plugin slot composition. The conversation package still owns the data and render sites, while ui-attachment owns their optional presentation entries.
 
 **Toast inside `ui-conversation`.** Rejected: nothing about a transient banner is conversation-specific, and `ui-primitives` is the established home for zero-cordis atoms other surfaces may reuse.
 
@@ -30,4 +30,4 @@ Both overlays body-portal: the lightbox opened from a chat message sits under tr
 
 ## Consequences
 
-The composer and history image surfaces now match DeepSeek Chat's interaction model, and the label-prop seam means the atoms render under any locale without reaching for one. The cost is a real package boundary: `ui-attachment` carries the standard scaffolding (invariant companion, bilingual README, tsconfig face, per-file 100% coverage) and item strings must be resolved by every future consumer rather than inherited. Error banners are now transient — a user who looks away for four seconds misses the message, the trade DeepSeek Chat itself makes. Non-image attachments remain unsupported; the rail's card model is ready for them but the composer's intake is image-only (tracked in the package README's limitations).
+The composer and history image surfaces match DeepSeek Chat's interaction model, and the pure-props components render under the conversation slot's locale seat without reaching into application state. The cost is a real dynamic package boundary: `ui-attachment` carries the standard plugin scaffolding (client bundle, invariant companion, bilingual README, tsconfig face, per-file 100% coverage), and omitting it leaves the two optional attachment slots empty. Error banners are transient — a user who looks away for four seconds misses the message, the trade DeepSeek Chat itself makes. Non-image attachments remain unsupported; the rail's card model is ready for them but the composer's intake is image-only (tracked in the package README's limitations).

+ 3 - 3
.agents/notes/implemented/feature/2026-08-11-web-attachment-display-alignment.zh.md

@@ -14,7 +14,7 @@ Web 输入框的图片界面缺乏基本可用性(用户反馈,issue #2248
 
 ## 决定
 
-附件展示落位到新的零 cordis 原子组件包 `@deepseek-ai/dsh-client-ui-attachment`(`packages/client/ui-attachment`),模式照 `dsh-client-ui-primitives`:`AttachmentRail`(64px、16px 圆角缩略图,单击 `onOpen`,卡片内部的删除按钮悬停或聚焦显示、`pointer: coarse` 下常显,隐藏滚动条配两端圆形箭头并依滚动几何重算,纵向滚轮转横向平移且单次钳制 60px,新增条目滚到栏尾),`MessageImage`/`ImageGallery`(单击预览),以及 `ImageLightbox`。文案经 label props 传入;`ui-conversation` 通过 `src/client/image-labels.ts` 桥接 `conversation` 词典,并保留状态机接线(草稿 id、预览状态、接收回调)。跨包 import 之所以是被允许的路径,正因为它是原子组件库而非 client 插件:插件之间仍禁止互相 import 组件,且附件栏是输入框自有的渲染,不是插槽
+附件展示位于 `@deepseek-ai/dsh-client-ui-attachment`(`packages/client/ui-attachment`):`AttachmentRail`(64px、16px 圆角缩略图,单击 `onOpen`,卡片内部的删除按钮悬停或聚焦显示、`pointer: coarse` 下常显,隐藏滚动条配两端圆形箭头并依滚动几何重算,纵向滚轮转横向平移且单次钳制 60px,新增条目滚到栏尾),`MessageImage`/`ImageGallery`(单击预览),以及 `ImageLightbox`。这些组件仍是包内的纯 props 组件。`ui-conversation` 声明输入框附件与消息图片 slot,并提供草稿 id、图片加载、接收回调及其 locale seat;动态 ui-attachment 客户端 entry 等待这些声明并注册呈现。[动态渲染与附件归属 Note](../architecture/2026-08-17-dynamic-client-render-and-attachment-ownership.md)负责这项包集成决策;本 Note 记录的视觉行为保持不变
 
 两个浮层都 portal 到 body:从聊天消息打开的灯箱位于带 transform 的祖先之下,`position: fixed` 会被困在祖先的盒子里(遮罩只盖住聊天列),因此 `ImageLightbox` 与 `Toast` 经 `createPortal(document.body)` 渲染,从任何打开位置都覆盖整个视口。短时横幅是 `ui-primitives` 的 `Toast` 原子(距视口顶部 120px,水平中心跟随可选锚点——composer 卡片,因此横幅在聊天列上居中——`role="alert"`、`pointer-events: none`,停留三秒再一秒淡出,`onDone` 卸载,按展示序号作 key 使相同文案重新播报)。`InputBar` 把接收拒绝(`addImages` 返回的原因)和 `promptError` 都改走 toast,替换内联红条,`ModelSelect` 的模型选择被拒也走同一原子,其菜单内带 Retry 的错误条仍是目录加载的呈现面;状态机 notice 条不受影响。DeepSeek Chat 源码(本地参考副本)提供了目标行为:其 `ImageThumbnailInInput`(64px 卡片、透明度过渡的删除钮)、`ScrollArrows`(哨兵驱动的翻页)与 `useToast` 用法。
 
@@ -22,7 +22,7 @@ Web 输入框的图片界面缺乏基本可用性(用户反馈,issue #2248
 
 **组件留在 `ui-conversation` 里只改样式。** 被用户否决:附件面预期还会长(文件卡片、上传进度),而仓库的插件纪律禁止其他插件 import `ui-conversation` 内部实现,在插件里生长只会堆出无法复用的一坨。原子组件包给了同样的组件一条被允许的 import 路径。
 
-**做成注册插槽的 `ui-attachment` client 插件。** 否决:附件栏渲染在状态机持有的输入框里,画廊渲染在聊天节点里,二者都不是该由其他插件填充的组合孔位,插件形态会为纯展示组件强加插槽间接层
+**导出附件原子组件并由 `ui-conversation` 直接导入。** 被包集成决策否决:直接导入组件会绕过动态插件生命周期与跨插件 slot 组合。conversation 包仍持有数据与渲染位置,ui-attachment 则持有其中可选的呈现 entry
 
 **Toast 放在 `ui-conversation`。** 否决:短时横幅没有任何会话特有的东西,`ui-primitives` 是零 cordis 原子组件的既定归属,其他界面也可能复用。
 
@@ -30,4 +30,4 @@ Web 输入框的图片界面缺乏基本可用性(用户反馈,issue #2248
 
 ## 结果
 
-输入框与历史图片界面的交互模型现已与 DeepSeek Chat 一致,label props 接缝让原子组件在任何语言环境下渲染而无需触达 locale。代价是一个真实的包边界:`ui-attachment` 背上标准脚手架(invariant 伴生、双语 README、tsconfig face、逐文件 100% 覆盖率),且每个未来消费者都要自行解析条目文案而非继承。错误横幅变为短时——用户移开视线四秒就会错过消息,这正是 DeepSeek Chat 自己做的取舍。非图片附件仍不支持;附件栏的卡片模型已就绪,但输入框的接收仍只认图片(记录于包 README 的限制一节)。
+输入框与历史图片界面的交互模型与 DeepSeek Chat 一致,纯 props 组件通过 conversation slot 的 locale seat 渲染,无需触达应用状态。代价是一个真实的动态包边界:`ui-attachment` 带有标准插件脚手架(客户端 bundle、invariant 伴生、双语 README、tsconfig face、逐文件 100% 覆盖率),省略该插件会让两个可选附件 slot 保持为空。错误横幅是短时的——用户移开视线四秒就会错过消息,这正是 DeepSeek Chat 自己做的取舍。非图片附件仍不支持;附件栏的卡片模型已就绪,但输入框的接收仍只认图片(记录于包 README 的限制一节)。

+ 2 - 2
.agents/notes/implemented/process/2026-07-26-web-syntax-highlighting-shiki.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/process/2026-07-26-web-syntax-highlighting-shiki.md
-2026-07-26-web-syntax-highlighting-shiki.md: 48a1e4c43f19693f90906f210f0ed85db3f31687
-2026-07-26-web-syntax-highlighting-shiki.zh.md: 7998f7811c8b396538c0d6d06951814e7d4b8e39
+2026-07-26-web-syntax-highlighting-shiki.md: 967df645db3df0a33f9cd1f34e22e912a348c30e
+2026-07-26-web-syntax-highlighting-shiki.zh.md: ff5dd24a403530914d779a2498d0627b90249ef8

+ 1 - 1
.agents/notes/implemented/process/2026-07-26-web-syntax-highlighting-shiki.md

@@ -16,7 +16,7 @@ The client rendered every code surface — markdown fences in assistant prose, t
 
 - **Dependency**: `shiki/core` + `@shikijs/langs`, composed via `createHighlighterCoreSync` with `createJavaScriptRegexEngine({ forgiving: true })` — no oniguruma WASM, no async init, bundle-friendly. Grammar allowlist: `typescript` (embeds JS), `shellscript`, `json` — the languages the harness actually renders; everything else falls back to a geometry-identical plain block, never an error. Prior art: the VitePress site already renders all documentation code through shiki, and TextMate grammars materially beat regex highlighters on TypeScript — the payload that matters here.
 - **Singleton**: `ui-primitives/src/markdown/highlight.ts` creates one `HighlighterCore` per document and exposes `highlightToHtml(code, lang)` (undefined = render plain). Engine + grammar construction is a ~120-175ms long task, so the module pre-warms the singleton in a deferred task at plugin boot (the lazy path stays as the correctness fallback), keeping the cost off the render path where a stream's finalize swap would jank. The alias table is a `Map`, not an object: fence info strings are assistant-authored, so a label like `constructor` must miss instead of resolving an inherited property and crashing shiki. The shared `CodeBlock` component owns both arms; its shiki arm injects the generated span tree via `dangerouslySetInnerHTML` — sanctioned because shiki emits a static span tree computed from the code text (no user HTML passes through, no scripts/handlers), shiki's own documented consumption path.
-- **Theming**: shiki's `createCssVariablesTheme` routes every token color through `--shiki-*` custom properties; the VALUES live in a new `ui-theme/styles/shiki.css` token sheet (light on `:root`, dark on `body[data-ds-dark-theme]` — the same cascade as every other sheet), imported by the shell's `base.css` chain. Component CSS stays tokens-only; no literal color ever enters JS or component sheets. Background/foreground alias the existing markdown code-block tokens so highlighted and plain blocks agree.
+- **Theming**: shiki's `createCssVariablesTheme` routes every token color through `--shiki-*` custom properties; the VALUES live in `ui-theme/styles/shiki.css` (light on `:root`, dark on `body[data-ds-dark-theme]` — the same cascade as every other sheet), imported by ui-theme's dynamic client entry and compiled into its plugin-owned global CSS. Component CSS stays tokens-only; no literal color ever enters JS or component sheets. Background/foreground alias the existing markdown code-block tokens so highlighted and plain blocks agree.
 - **Surfaces**: markdown fences (`MarkdownText`'s `pre` component routes single-string fences through `CodeBlock`), the `run_code` expanded program body (ToolRow's code variant, `lang="typescript"`), and the details panel's Input args (`lang="json"`). Tool output is never syntax-highlighted — it is arbitrary text, and guessing a grammar would mis-highlight more than it helps; a bash card's output carries only the color its own ANSI sequences declare, through [the terminal card](../feature/2026-07-28-web-terminal-card.md).
 
 ## Alternatives considered

+ 1 - 1
.agents/notes/implemented/process/2026-07-26-web-syntax-highlighting-shiki.zh.md

@@ -16,7 +16,7 @@ client 过去把每一处代码表面——assistant 正文里的 markdown 围
 
 - **依赖**:`shiki/core` + `@shikijs/langs`,经 `createHighlighterCoreSync` 搭配 `createJavaScriptRegexEngine({ forgiving: true })` 组装——不带 oniguruma WASM、没有异步初始化、对 bundle 友好。语法(grammar)白名单:`typescript`(内嵌 JS)、`shellscript`、`json`——即 harness 实际会渲染的那几种语言;其余一律回退到几何完全一致的纯文本块,绝不报错。先例:VitePress 站点已经通过 shiki 渲染全部文档代码;而在 TypeScript(正是此处要紧的载荷)上,TextMate 语法实质性优于正则高亮器。
 - **单例**:`ui-primitives/src/markdown/highlight.ts` 为每个 document 创建一个 `HighlighterCore`,并公开 `highlightToHtml(code, lang)`(undefined 即渲染为纯文本)。引擎加语法的构建是一次约 120-175ms 的长任务,因此模块在插件启动时用延迟任务预热单例(惰性路径保留为正确性兜底),把这笔开销挪出渲染路径——否则流式 finalize 交换的那一刻会卡顿。别名表用 `Map` 而非对象:fence 信息字符串由 assistant 撰写,诸如 `constructor` 这样的标签必须落空,而不是解析到继承属性并让 shiki 崩溃。共享的 `CodeBlock` 组件同时拥有两条分支;其 shiki 分支经 `dangerouslySetInnerHTML` 注入生成的 span 树——此用法获准,因为 shiki 输出的是从代码文本计算出的静态 span 树(不流经任何用户 HTML,没有脚本或事件处理器),这正是 shiki 自身文档载明的消费路径。
-- **主题化**:shiki 的 `createCssVariablesTheme` 让每一种 token 颜色都经由 `--shiki-*` 自定义属性路由;取值本身住在新增的 `ui-theme/styles/shiki.css` token 表里(亮色在 `:root`、暗色在 `body[data-ds-dark-theme]`——层叠方式与其余每张样式表相同),由壳的 `base.css` 导入链引入。组件 CSS 保持只用 token;任何颜色字面量都不进入 JS 或组件样式表。背景/前景以别名指向既有的 markdown 代码块 token,使高亮块与纯文本块彼此一致。
+- **主题化**:shiki 的 `createCssVariablesTheme` 让每一种 token 颜色都经由 `--shiki-*` 自定义属性路由;取值本身住在 `ui-theme/styles/shiki.css` token 表里(亮色在 `:root`、暗色在 `body[data-ds-dark-theme]`——层叠方式与其余每张样式表相同),由 ui-theme 的动态客户端 entry 导入并编译进该插件持有的全局 CSS。组件 CSS 保持只用 token;任何颜色字面量都不进入 JS 或组件样式表。背景/前景以别名指向既有的 markdown 代码块 token,使高亮块与纯文本块彼此一致。
 - **表面**:markdown 围栏代码块(`MarkdownText` 的 `pre` 组件把单字符串围栏路由到 `CodeBlock`)、`run_code` 展开后的程序正文(ToolRow 的 code 变体,`lang="typescript"`),以及 details 面板的 Input 参数(`lang="json"`)。工具输出从不做语法高亮——它是任意文本,硬猜一种语法,带来的误高亮会多于帮助;bash 卡片的输出只承载其自身 ANSI 序列声明的颜色,经由[终端卡片](../feature/2026-07-28-web-terminal-card.md)渲染。
 
 ## 曾考虑的替代方案

+ 2 - 2
.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.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/proposed/architecture/2026-07-27-session-projection-and-command-log.md
-2026-07-27-session-projection-and-command-log.md: 7681ef8462ef194a2dd3dcbcd2cb84d354f98aef
-2026-07-27-session-projection-and-command-log.zh.md: 6f6cc75994222025c412bec9f0b25f6d2dd982c3
+2026-07-27-session-projection-and-command-log.md: aa83ae4d0e96bc1681aec22da854ba258b9e19a9
+2026-07-27-session-projection-and-command-log.zh.md: 00349ac46dc300c60f725b257e0fff9bcdf3a586

+ 1 - 1
.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md

@@ -99,7 +99,7 @@ A domain's input event set is its own choice — that is the general rule this e
 
 ### React: `useProjection`, the fifth framework hook seat
 
-The existing four seats cannot host this state (store discipline bans business objects; inject bans hooks; `ConversationSnapshot` is being evacuated). `useProjection` becomes a framework seat, minted in web-react (the one hook constructor), delivered through the same standard-kit channel as `useSession` (`provideInfo` → SessionProvider → props):
+The existing four seats cannot host this state (store discipline bans business objects; inject bans hooks; `ConversationSnapshot` is being evacuated). `useProjection` becomes a framework seat, minted in ui-renderer (the one hook constructor), delivered through the same standard-kit channel as `useSession` (`provideInfo` → SessionProvider → props):
 
 ```ts ignore-check
 type UseProjection = {

+ 1 - 1
.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.zh.md

@@ -99,7 +99,7 @@ plan mode 完整演示了这套模式——触发路径、运行面、回放面
 
 ### React:`useProjection`,第五个框架钩子席位
 
-既有四个席位都装不下这份状态(store 纪律禁止业务对象;inject 禁止钩子;`ConversationSnapshot` 正在被清退)。`useProjection` 成为一个框架席位,在 web-react(唯一的钩子铸造点)铸造,经与 `useSession` 相同的标准套件通道(`provideInfo` → SessionProvider → props)送达:
+既有四个席位都装不下这份状态(store 纪律禁止业务对象;inject 禁止钩子;`ConversationSnapshot` 正在被清退)。`useProjection` 成为一个框架席位,在 ui-renderer(唯一的钩子铸造点)铸造,经与 `useSession` 相同的标准套件通道(`provideInfo` → SessionProvider → props)送达:
 
 ```ts ignore-check
 type UseProjection = {

+ 4 - 7
apps/web/package.json

@@ -22,20 +22,15 @@
   "scripts": {
     "build": "vite build",
     "dev": "vite",
-    "watch": "vite build --watch"
+    "watch": "vite build --watch --no-emptyOutDir"
   },
   "license": "MIT",
-  "dependencies": {
-    "@deepseek-ai/dsh-client-web": "workspace:^",
-    "react": "^18.2.0",
-    "react-dom": "^18.2.0"
-  },
   "devDependencies": {
     "@deepseek-ai/cordis-plugin-group": "workspace:^",
     "@deepseek-ai/dsh-client-modules": "workspace:^",
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
-    "@deepseek-ai/dsh-client-web-react": "workspace:^",
+    "@deepseek-ai/dsh-client-web": "workspace:^",
     "@deepseek-ai/dsh-cmdline": "workspace:^",
     "@deepseek-ai/dsh-pwsh-local": "workspace:^",
     "@types/node": "^22.0.0",
@@ -43,6 +38,8 @@
     "@types/react-dom": "~18.3.0",
     "@vitejs/plugin-react": "^4.0.0",
     "playwright": "^1.49.0",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
     "typescript": "^6.0.3",
     "vite": "^6.0.0",
     "vitest": "^4.1.8",

+ 1 - 1
apps/web/src/main.ts

@@ -1,6 +1,6 @@
 /**
  * Web application entry: thin bootstrap over the shell library. Everything —
- * loader holding, module-table seeding, AppRoot gate, plugin assembly — lives
+ * module-table seeding, the boot page, and the UI-renderer handoff — lives
  * in @deepseek-ai/dsh-client-web; this file only finds the mount point.
  */
 import { AppWebEntry } from '@deepseek-ai/dsh-client-web'

+ 125 - 38
apps/web/tests/assembled-boot.ts

@@ -8,53 +8,122 @@
 // Keyless and deterministic: the fixture is the fake server, so nothing here
 // reaches a model or the network.
 import { readFileSync } from 'node:fs'
-import { join } from 'node:path'
+import { createRequire } from 'node:module'
+import { dirname, join, resolve } from 'node:path'
+import { pathToFileURL } from 'node:url'
 import { act, cleanup } from '@testing-library/react'
 import { afterEach, beforeEach, vi } from 'vitest'
-import type { WebBootEntry } from '@deepseek-ai/dsh-client-modules/client'
+import { injectBootManifest, orderByModuleGraph } from '@deepseek-ai/dsh-client-modules'
+import type { ClientModuleLoaderTarget, WebBootEntry } from '@deepseek-ai/dsh-client-modules/client'
 import { AppWebEntry } from '@deepseek-ai/dsh-client-web'
 
-/** Boot entries for the minimal assembled graph, each carrying the workspace bundle it loads. */
-const PLUGINS: readonly (WebBootEntry & { bundlePath: string })[] = [
-  { id: '@deepseek-ai/dsh-typert-registry', bundlePath: 'packages/typert/registry/lib/client.js', url: '/plugins/typert-registry.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-connection', bundlePath: 'packages/client/connection/lib/client.js', url: '/plugins/connection.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-api-gateway', bundlePath: 'packages/api/gateway/lib/client.js', url: '/plugins/api-gateway.js', rev: 'fx', inject: ['@deepseek-ai/dsh-typert-registry', '@deepseek-ai/dsh-client-connection'], immediately: true },
-  { id: '@deepseek-ai/dsh-api-remotes', bundlePath: 'packages/api/remotes/lib/client.js', url: '/plugins/api-remotes.js', rev: 'fx', inject: ['@deepseek-ai/dsh-api-gateway'], immediately: true },
-  // The settings domain base: the only provider of ctx.settingsScope, which the
-  // locale and ui-theme rows below inject for their preference rows. Without it
-  // both stay pending and ui-layout never activates, so nothing renders.
-  { id: '@deepseek-ai/dsh-client-ui-settings', bundlePath: 'packages/client/ui-settings/lib/client.js', url: '/plugins/ui-settings.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection', '@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-api-remotes'], immediately: true },
-  { id: '@deepseek-ai/dsh-client-runtime', bundlePath: 'packages/client/runtime/lib/client.js', url: '/plugins/runtime.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection', '@deepseek-ai/dsh-typert-registry', '@deepseek-ai/dsh-api-gateway'], immediately: true },
-  { id: '@deepseek-ai/dsh-client-ui-theme', bundlePath: 'packages/client/ui-theme/lib/client.js', url: '/plugins/ui-theme.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection', '@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-locale', '@deepseek-ai/dsh-client-ui-settings', '@deepseek-ai/dsh-api-remotes'], immediately: true },
-  { id: '@deepseek-ai/dsh-client-locale', bundlePath: 'packages/client/locale/lib/client.js', url: '/plugins/locale.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection', '@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-settings', '@deepseek-ai/dsh-api-remotes'], immediately: true },
-  { id: '@deepseek-ai/dsh-client-ui-layout', bundlePath: 'packages/client/ui-layout/lib/client.js', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
-  { id: '@deepseek-ai/dsh-client-ui-sidebar', bundlePath: 'packages/client/ui-sidebar/lib/client.js', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
-  { id: '@deepseek-ai/dsh-client-ui-conversation', bundlePath: 'packages/client/ui-conversation/lib/client.js', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
-  { id: '@deepseek-ai/dsh-client-ui-tool', bundlePath: 'packages/client/ui-tool/lib/client.js', url: '/plugins/ui-tool.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-locale', '@deepseek-ai/dsh-client-ui-conversation'] },
-  { id: '@deepseek-ai/dsh-client-ui-workflow-run', bundlePath: 'packages/client/ui-workflow-run/lib/client.js', url: '/plugins/ui-workflow-run.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-locale', '@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-conversation'] },
+interface AssembledPlugin extends WebBootEntry {
+  /** Absolute path to the built client artifact declared by this package. */
+  bundlePath: string
+}
+
+interface ClientPackageManifest {
+  name?: string
+  exports?: Record<string, string | { default?: string }>
+  dsh?: {
+    client?: {
+      platform?: string
+      inject?: string[]
+      external?: string[]
+      immediately?: boolean
+    }
+  }
+}
+
+interface ComposedEntry {
+  name?: unknown
+  disabled?: unknown
+}
+
+interface BootComposition {
+  loadOverlayPatches(binName: string, file: string): unknown[]
+  composeEntries(layers: readonly unknown[][]): ComposedEntry[]
+}
+
+const REPO_ROOT = process.cwd()
+const BUNDLE_LAYERS = [
   {
-    id: '@deepseek-ai/dsh-client-ui-workspace',
-    bundlePath: 'packages/client/ui-workspace/lib/client.js',
-    url: '/plugins/ui-workspace.js',
-    rev: 'fx',
-    inject: [
-      '@deepseek-ai/dsh-client-runtime',
-      '@deepseek-ai/dsh-client-ui-conversation',
-      '@deepseek-ai/dsh-client-ui-sidebar',
-    ],
+    manifest: join(REPO_ROOT, 'packages/bundle/base/package.json'),
+    patch: join(REPO_ROOT, 'packages/bundle/base/cordis.patch.yml'),
   },
-  { id: '@deepseek-ai/dsh-session-log-export', bundlePath: 'packages/session-query/session-log-export/lib/client.js', url: '/plugins/session-log-download.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-commands', '@deepseek-ai/dsh-client-ui-conversation'] },
-  { id: '@deepseek-ai/dsh-client-ui-trajectory', bundlePath: 'packages/client/ui-trajectory/lib/client.js', url: '/plugins/ui-trajectory.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
-]
+  {
+    manifest: join(REPO_ROOT, 'packages/bundle/web-app/package.json'),
+    patch: join(REPO_ROOT, 'packages/bundle/web-app/cordis.patch.yml'),
+  },
+] as const
+const bundleResolvers = BUNDLE_LAYERS.map(layer => createRequire(layer.manifest))
+const webBundleResolver = bundleResolvers[1]
+if (webBundleResolver === undefined) throw new Error('assembled boot: web bundle resolver missing')
+const appBoot = await import(pathToFileURL(webBundleResolver.resolve('@deepseek-ai/dsh-app-boot')).href) as unknown as BootComposition
+
+function resolvePackageManifest(specifier: string): string | undefined {
+  for (const require of bundleResolvers) {
+    try {
+      return require.resolve(`${specifier}/package.json`)
+    } catch {
+      continue
+    }
+  }
+  return undefined
+}
+
+function resolveClientExport(packagePath: string, pkg: ClientPackageManifest): string {
+  const declared = pkg.exports?.['./client']
+  const relative = typeof declared === 'string' ? declared : declared?.default
+  if (relative === undefined) {
+    throw new Error(`assembled boot: ${pkg.name ?? packagePath} declares dsh.client without a ./client export`)
+  }
+  return resolve(dirname(packagePath), relative)
+}
+
+/** Derive the assembled browser graph from the same bundle patches and package declarations as `dsh web`. */
+function loadAssembledPlugins(): readonly AssembledPlugin[] {
+  const entries = appBoot.composeEntries(BUNDLE_LAYERS.map(layer =>
+    appBoot.loadOverlayPatches('assembled boot', layer.patch)))
+  const plugins = new Map<string, AssembledPlugin>()
+  for (const entry of entries) {
+    if (entry.disabled === true || typeof entry.name !== 'string') continue
+    const packagePath = resolvePackageManifest(entry.name)
+    if (packagePath === undefined) continue
+    const pkg = JSON.parse(readFileSync(packagePath, 'utf8')) as ClientPackageManifest
+    const declaration = pkg.dsh?.client
+    if (declaration?.platform !== 'web') continue
+    if (pkg.name !== entry.name) {
+      throw new Error(`assembled boot: ${entry.name} resolved package ${pkg.name ?? '<unnamed>'}`)
+    }
+    plugins.set(entry.name, {
+      id: entry.name,
+      bundlePath: resolveClientExport(packagePath, pkg),
+      url: `/plugins/${entry.name}/client.js?rev=fx`,
+      rev: 'fx',
+      ...(declaration.inject === undefined ? {} : { inject: declaration.inject }),
+      ...(declaration.external === undefined ? {} : { external: declaration.external }),
+      ...(declaration.immediately === true ? { immediately: true } : {}),
+    })
+  }
+  return orderByModuleGraph([...plugins.values()]).map(({ id }) => {
+    const plugin = plugins.get(id)
+    /* v8 ignore next -- orderByModuleGraph returns the input row identities */
+    if (plugin === undefined) throw new Error(`assembled boot: ordered unknown client package ${id}`)
+    return plugin
+  })
+}
+
+const PLUGINS = loadAssembledPlugins()
 
 const bundles = new Map(PLUGINS.map(plugin => [
   plugin.url,
-  readFileSync(join(process.cwd(), plugin.bundlePath), 'utf8'),
+  readFileSync(plugin.bundlePath, 'utf8'),
 ]))
 
 interface FixtureWindow extends Window {
   __DSH_BOOT__?: { rev: string; entries: WebBootEntry[] }
-  __ModuleLoader__?: unknown
+  __ModuleLoader__?: ClientModuleLoaderTarget
 }
 
 class ResizeObserverStub {
@@ -63,8 +132,13 @@ class ResizeObserverStub {
   unobserve(): void {}
 }
 
+class EventSourceStub {
+  addEventListener(): void {}
+  close(): void {}
+}
+
 const win = window as FixtureWindow
-let unmount: (() => void) | undefined
+let unmount: (() => Promise<void>) | undefined
 
 /**
  * Register the per-test jsdom setup and teardown the assembled boot needs:
@@ -84,13 +158,14 @@ export function installAssembledBootEnv(): void {
     Object.defineProperty(navigator, 'language', { value: 'en-US', configurable: true })
     document.title = 'DeepSeek Harness'
     vi.stubGlobal('ResizeObserver', ResizeObserverStub)
+    vi.stubGlobal('EventSource', EventSourceStub)
     vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
       setTimeout(() => { callback(0) }, 0) as unknown as number)
     vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) })
   })
 
-  afterEach(() => {
-    act(() => { unmount?.() })
+  afterEach(async () => {
+    await act(async () => { await unmount?.() })
     unmount = undefined
     cleanup()
     delete win.__DSH_BOOT__
@@ -118,6 +193,18 @@ export function mountAssembledApp(): void {
   root.id = 'root'
   document.body.appendChild(root)
   win.__DSH_BOOT__ = { rev: 'fx', entries: PLUGINS.map(({ bundlePath: _bundlePath, ...plugin }) => plugin) }
+  const html = injectBootManifest('<head></head>', win.__DSH_BOOT__)
+  const facadeSource = /<head><script>([\s\S]*?)<\/script>/.exec(html)?.[1]
+  if (facadeSource === undefined) throw new Error('missing injected ModuleLoader facade')
+  ;(0, eval)(facadeSource)
+  // Mirror the blocking Host-injected scripts before the Vite entry calls create().
+  for (const id of ['@deepseek-ai/dsh-client-modules', '@deepseek-ai/dsh-client-runtime']) {
+    const plugin = PLUGINS.find(candidate => candidate.id === id)
+    if (plugin === undefined) throw new Error(`missing parser-preloaded fixture row ${id}`)
+    const code = bundles.get(plugin.url)
+    if (code === undefined) throw new Error(`missing built bundle ${plugin.url}`)
+    ;(0, eval)(code)
+  }
   act(() => {
     const entry = new AppWebEntry(root, {
       loadBundle: async (url) => {
@@ -127,7 +214,7 @@ export function mountAssembledApp(): void {
       },
     })
     void entry.run()
-    unmount = () => { entry.dispose() }
+    unmount = () => entry.dispose()
   })
 }
 

+ 6 - 3
apps/web/tests/built-boot.snapshot.ts

@@ -43,9 +43,12 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn
   await waitFor(() => {
     expect(document.querySelector('[data-sample="bash"]')).not.toBeNull()
   }, { timeout: 10_000 })
-  // Resolve the resident approval so the ordinary composer bar (which owns
-  // ContextMeter) resumes without replacing the session shell. This minimal
-  // boot graph intentionally does not mount the separate question UI plugin.
+  // The generated bundle roster mounts the question UI before the approval UI.
+  // Skip the resident fixture's three questions, then resolve its approval so
+  // the ordinary composer bar (which owns ContextMeter) resumes.
+  for (let index = 0; index < 3; index += 1) {
+    fireEvent.click(await screen.findByRole('button', { name: 'Skip this question' }))
+  }
   fireEvent.click(await screen.findByRole('button', { name: 'Allow once' }))
 
   // The fixture mirrors all three token-meter projections, so the assembled

+ 8 - 0
apps/web/tests/cordis-tool-round.e2e.ts

@@ -108,8 +108,10 @@ describe('web e2e: Cordis tools use their owned cards', () => {
     // NOT the plugin running. Until a person answers, the browser half has not
     // been fetched, evaluated, or mounted anywhere on this page.
     expect(await page.locator('[data-snapshot-probe]').count()).toBe(0)
+    const approvalTurnSettled = scaffold.whenTurnSettled()
     await approve.click()
     await expect.poll(() => page.locator('[data-snapshot-probe]').count(), { timeout: 30_000 }).toBe(1)
+    await approvalTurnSettled
 
     const sessionId = await runTurnSettled
     const stopTurnSettled = scaffold.whenTurnSettled()
@@ -162,6 +164,12 @@ describe('web e2e: Cordis tools use their owned cards', () => {
 
   it.skipIf(MODE === 'record')('matches the conversation aria golden', async () => {
     onTestFailed(() => saveFailureShot(page, 'web-e2e-cordis-aria'))
+    await page.locator('[data-conversation-scroll]').evaluate((host) => { host.scrollTop = host.scrollHeight })
+    await expect.poll(
+      async () => page.getByRole('button', { name: 'Back to bottom', exact: true }).count(),
+      { timeout: 10_000 },
+    ).toBe(0)
+    await page.mouse.move(0, 0)
     const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
     await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
   })

+ 7 - 5
apps/web/tests/image-display.snapshot.ts

@@ -142,10 +142,11 @@ it('accepts pasted images into the composer rail in order and removes them', asy
       getData: () => '',
     },
   })
-  const toast = await screen.findByRole('alert')
-  expect(toast.textContent).toContain('Only PNG, JPG, WebP, and GIF images are supported')
+  const unsupportedMessage = 'Only PNG, JPG, WebP, and GIF images are supported'
+  const toast = await screen.findByText(unsupportedMessage)
+  expect(toast.closest('[role="alert"]')).not.toBeNull()
   await waitFor(() => {
-    expect(screen.queryByRole('alert')).toBeNull()
+    expect(screen.queryByText(unsupportedMessage)).toBeNull()
   }, { timeout: 6_000 })
 })
 
@@ -190,8 +191,9 @@ it('accepts a whole-page drop under the limits-labeled overlay and refuses an ov
       getData: () => '',
     },
   })
-  const banner = await screen.findByRole('alert')
-  expect(banner.textContent).toContain('A message can include up to 20 images')
+  const limitMessage = 'A message can include up to 20 images'
+  const banner = await screen.findByText(limitMessage)
+  expect(banner.closest('[role="alert"]')).not.toBeNull()
   const rail = document.querySelector('[role="group"][aria-label="Pending images"]')
   expect([...(rail?.querySelectorAll('img') ?? [])]).toHaveLength(1)
 })

+ 1 - 1
apps/web/tests/settings-chrome.e2e.ts

@@ -191,7 +191,7 @@ describe('web e2e: settings modal and General preferences', () => {
     await page.keyboard.press('Escape')
 
     // Hold real plugin bundles so the shell-owned loading page remains observable.
-    const pluginPattern = '**/plugins/**'
+    const pluginPattern = /\/plugins\/@deepseek-ai\/dsh-client-ui-theme\/client\.js(?:\?.*)?$/
     let releaseBundles = (): void => {}
     const bundlesReleased = new Promise<void>((resolve) => { releaseBundles = resolve })
     await page.route(pluginPattern, async (route) => {

+ 11 - 7
apps/web/tests/snapshots/cordis-tool-round/session.jsonl

@@ -47,11 +47,15 @@
 {"type":"step/end","seq":248,"time":1785157569554,"data":{"turn":1,"step":4}}
 {"type":"turn/end","seq":249,"time":1785157569554,"data":{"turn":1,"reason":{"kind":"completed"}}}
 {"type":"user/message","seq":250,"time":1785157569555,"data":{"content":[{"type":"text","text":"Use only Cordis tools. Call cordis_stop with pluginId \"snap-1\". After it succeeds, reply exactly CORDIS_UI_DONE and stop."}],"source":{"kind":"user","rpcId":"{{rpcId}}"}},"surfaceOp":"append"}
-{"type":"assistant/chunk","seq":251,"time":1785157569556,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
-{"type":"assistant/chunk","seq":252,"time":1785157569557,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"cordis-stop","name":"cordis_stop","arguments":"{\"pluginId\":\"snap-1\"}"}}}}
+{"type":"assistant/chunk","seq":251,"time":1785157569556,"data":{"turn":2,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":252,"time":1785157569557,"data":{"turn":2,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"The Cordis Plugin is running."}}}}
 {"type":"assistant/chunk","seq":253,"time":1785157569558,"data":{"turn":2,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
-{"type":"assistant/chunk","seq":254,"time":1785157569559,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
-{"type":"assistant/chunk","seq":255,"time":1785157569560,"data":{"turn":2,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
-{"type":"assistant/chunk","seq":256,"time":1785157569561,"data":{"turn":2,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CORDIS_UI_DONE"}}}}
-{"type":"assistant/chunk","seq":257,"time":1785157569562,"data":{"turn":2,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
-{"type":"assistant/chunk","seq":258,"time":1785157569563,"data":{"turn":2,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/chunk","seq":254,"time":1785157569559,"data":{"turn":2,"step":1,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
+{"type":"assistant/chunk","seq":255,"time":1785157569560,"data":{"turn":3,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"tool-call"}}}
+{"type":"assistant/chunk","seq":256,"time":1785157569561,"data":{"turn":3,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"tool-call","id":"cordis-stop","name":"cordis_stop","arguments":"{\"pluginId\":\"snap-1\"}"}}}}
+{"type":"assistant/chunk","seq":257,"time":1785157569562,"data":{"turn":3,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
+{"type":"assistant/chunk","seq":258,"time":1785157569563,"data":{"turn":3,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
+{"type":"assistant/chunk","seq":259,"time":1785157569564,"data":{"turn":3,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
+{"type":"assistant/chunk","seq":260,"time":1785157569565,"data":{"turn":3,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"text","text":"CORDIS_UI_DONE"}}}}
+{"type":"assistant/chunk","seq":261,"time":1785157569566,"data":{"turn":3,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":3,"outputTokens":3}}}}
+{"type":"assistant/chunk","seq":262,"time":1785157569567,"data":{"turn":3,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}

+ 14 - 9
apps/web/tests/snapshots/cordis-tool-round/ui.expected.md

@@ -66,6 +66,18 @@
   - img
   - img
   - text: Context injection cordis-host-runner
+- paragraph: The Cordis Plugin is running.
+- button "Copy":
+  - img
+- button "Good response":
+  - img
+- button "Bad response":
+  - img
+- button "Branch into a new conversation":
+  - img
+- text: {{clock}} Ran for {{duration}} Use only Cordis tools. Call cordis_stop with pluginId "snap-1". After it succeeds, reply exactly CORDIS_UI_DONE and stop. {{clock}}
+- button "Copy":
+  - img
 - img
 - text: Stop Cordis Plugin snap-1
 - button "Inspect"
@@ -79,14 +91,7 @@
   - img
 - button "Branch into a new conversation":
   - img
-- text: {{clock}} Ran for {{duration}} Use only Cordis tools. Call cordis_stop with pluginId "snap-1". After it succeeds, reply exactly CORDIS_UI_DONE and stop. {{clock}}
-- button "Copy":
-  - img
-- status:
-  - text: "This turn failedllm-replay: script exhausted — session requested model call #7 but its script has only 6; re-record the scenario"
-  - code: UNKNOWN
-- button "Back to bottom":
-  - img
+- text: {{clock}} Ran for {{duration}}
 - textbox "Message the agent"
 - button "Commands":
   - img
@@ -96,4 +101,4 @@
   - img
 - button "0% of context used"
 - button "Send message" [disabled]
-- text: 3 turns · 7 steps LLM {{duration}} · Tool call {{duration}} Cache hit 77% Input 66.5K tok · Output 318 tok
+- text: 3 turns · 7 steps LLM {{duration}} · Tool call {{duration}} Cache hit 77% Input 66.5K tok · Output 321 tok

+ 14 - 17
apps/web/vite.config.ts

@@ -127,25 +127,22 @@ export default defineConfig({
     },
   },
   resolve: {
-    // Workspace packages resolve to SOURCE: package.json exports point at lib
-    // for Node/type consumers, but the browser bundle must compile src directly
-    // so CSS rides vite's pipeline instead of the CSS-externalized lib bundle.
-    // Only the shell's normal package entry is aliased — plugin packages are
-    // NEVER bundled here (shell self-sufficiency — see
-    // packages/client/web/README.md); they arrive as runtime
-    // bundles through the client module system. Order matters — subpath
-    // aliases must win over bare-name prefixes.
+    // One instance per shared npm identity: a bare specifier otherwise resolves
+    // from the importer's directory, so a diverging range ships a second React
+    // and splits hook and element identity. Entries are package ids — they cover
+    // react/jsx-runtime and react-dom/client — and resolve from this package's
+    // node_modules, so react must stay a devDependency here and any watcher must
+    // run vite from this directory (scripts/dev-web.ts). Workspace packages need
+    // no entry: pnpm links each of them to a single directory.
+    dedupe: ['react', 'react-dom'],
+    // Workspace packages are consumed as built lib products: each resolves
+    // through its own package.json exports from the importer's directory, and
+    // CSS still rides Vite's pipeline because the client build preset emits it
+    // beside the bundle. Plugin packages never enter this graph; they arrive as
+    // runtime bundles through the client module system. The remaining alias
+    // browserizes the vendored Cordis Loader's only Node import.
     alias: [
-      // Browserization of the vendored cordis Loader: its only node-only
-      // import; the two process probes are mapped by `define` below.
       { find: /^node:module$/, replacement: src('./src/node-module-stub.ts') },
-      { find: /^@deepseek-ai\/dsh-client-web$/, replacement: src('../../packages/client/web/src/boot.tsx') },
-      { find: /^@deepseek-ai\/dsh-client-web-react$/, replacement: src('../../packages/client/web-react/src/index.ts') },
-      { find: /^@deepseek-ai\/dsh-client-ui-slots$/, replacement: src('../../packages/client/ui-slots/src/index.ts') },
-      { find: /^@deepseek-ai\/dsh-client-ui-primitives$/, replacement: src('../../packages/client/ui-primitives/src/index.ts') },
-      { find: /^@deepseek-ai\/dsh-client-ui-attachment$/, replacement: src('../../packages/client/ui-attachment/src/index.ts') },
-      { find: /^@deepseek-ai\/dsh-client-schema-form$/, replacement: src('../../packages/client/schema-form/src/index.ts') },
-      { find: /^@deepseek-ai\/dsh-client-modules\/client$/, replacement: src('../../packages/client/modules/src/client/index.ts') },
     ],
   },
   define: {

+ 2 - 2
docs/config-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/config-catalog.md
-config-catalog.md: 4da2774eb94eae3216c2cf89b7864a075872f9ee
-config-catalog.zh.md: 4f9e2cf3ecb454cbc34da565bdc23877c4b54401
+config-catalog.md: 9e3dc2b14a04877c191227703752d0b1dbb2b92b
+config-catalog.zh.md: 56c1fd075b680c79c7730b9bdcb1275e83bb3f64

+ 2 - 3
docs/config-catalog.md

@@ -3032,6 +3032,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
 - `@deepseek-ai/dsh-client-modules` — requires `webServer` · `loader` ([`packages/client/modules/src/index.ts`](../packages/client/modules/src/index.ts))
 - `@deepseek-ai/dsh-client-runtime` ([`packages/client/runtime/src/index.ts`](../packages/client/runtime/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-agent-preset` ([`packages/client/ui-agent-preset/src/index.ts`](../packages/client/ui-agent-preset/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-attachment` ([`packages/client/ui-attachment/src/index.ts`](../packages/client/ui-attachment/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-commands` ([`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-conversation` ([`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-cordis` ([`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts))
@@ -3046,6 +3047,7 @@ These load from a `cordis.yml` entry with no `config:` block; they declare no co
 - `@deepseek-ai/dsh-client-ui-model-selection` ([`packages/client/ui-model-selection/src/index.ts`](../packages/client/ui-model-selection/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-permission-presets` ([`packages/client/ui-permission-presets/src/index.ts`](../packages/client/ui-permission-presets/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-plan` ([`packages/client/ui-plan/src/index.ts`](../packages/client/ui-plan/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-renderer` ([`packages/client/ui-renderer/src/index.ts`](../packages/client/ui-renderer/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-settings` ([`packages/client/ui-settings/src/index.ts`](../packages/client/ui-settings/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-settings-general` ([`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-settings-models` ([`packages/client/ui-settings-models/src/index.ts`](../packages/client/ui-settings-models/src/index.ts))
@@ -3122,13 +3124,10 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them.
 - `@deepseek-ai/dsh-atomic-write` ([`packages/util/atomic-write/src/index.ts`](../packages/util/atomic-write/src/index.ts))
 - `@deepseek-ai/dsh-base` ([`packages/bundle/base/src/index.ts`](../packages/bundle/base/src/index.ts))
 - `@deepseek-ai/dsh-brand` ([`packages/util/brand/src/index.ts`](../packages/util/brand/src/index.ts))
-- `@deepseek-ai/dsh-client-schema-form` ([`packages/client/schema-form/src/index.ts`](../packages/client/schema-form/src/index.ts))
 - `@deepseek-ai/dsh-client-test-runtime` ([`packages/test-support/client-runtime/src/index.ts`](../packages/test-support/client-runtime/src/index.ts))
-- `@deepseek-ai/dsh-client-ui-attachment` ([`packages/client/ui-attachment/src/index.ts`](../packages/client/ui-attachment/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-primitives` ([`packages/client/ui-primitives/src/index.ts`](../packages/client/ui-primitives/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-slots` ([`packages/client/ui-slots/src/index.ts`](../packages/client/ui-slots/src/index.ts))
 - `@deepseek-ai/dsh-client-web` ([`packages/client/web/src/index.ts`](../packages/client/web/src/index.ts))
-- `@deepseek-ai/dsh-client-web-react` ([`packages/client/web-react/src/index.ts`](../packages/client/web-react/src/index.ts))
 - `@deepseek-ai/dsh-cmdline` ([`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts))
 - `@deepseek-ai/dsh-code-runtime-python` ([`packages/code-runtime/code-runtime-python/src/index.ts`](../packages/code-runtime/code-runtime-python/src/index.ts))
 - `@deepseek-ai/dsh-home-paths` ([`packages/util/home-paths/src/index.ts`](../packages/util/home-paths/src/index.ts))

+ 2 - 3
docs/config-catalog.zh.md

@@ -3034,6 +3034,7 @@ export interface Config {
 - `@deepseek-ai/dsh-client-modules` — 需要 `webServer` · `loader`([`packages/client/modules/src/index.ts`](../packages/client/modules/src/index.ts))
 - `@deepseek-ai/dsh-client-runtime`([`packages/client/runtime/src/index.ts`](../packages/client/runtime/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-agent-preset`([`packages/client/ui-agent-preset/src/index.ts`](../packages/client/ui-agent-preset/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-attachment`([`packages/client/ui-attachment/src/index.ts`](../packages/client/ui-attachment/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-commands`([`packages/client/ui-commands/src/index.ts`](../packages/client/ui-commands/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-conversation`([`packages/client/ui-conversation/src/index.ts`](../packages/client/ui-conversation/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-cordis`([`packages/extensions/ui-cordis/src/index.ts`](../packages/extensions/ui-cordis/src/index.ts))
@@ -3048,6 +3049,7 @@ export interface Config {
 - `@deepseek-ai/dsh-client-ui-model-selection`([`packages/client/ui-model-selection/src/index.ts`](../packages/client/ui-model-selection/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-permission-presets`([`packages/client/ui-permission-presets/src/index.ts`](../packages/client/ui-permission-presets/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-plan`([`packages/client/ui-plan/src/index.ts`](../packages/client/ui-plan/src/index.ts))
+- `@deepseek-ai/dsh-client-ui-renderer`([`packages/client/ui-renderer/src/index.ts`](../packages/client/ui-renderer/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-settings`([`packages/client/ui-settings/src/index.ts`](../packages/client/ui-settings/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-settings-general`([`packages/client/ui-settings-general/src/index.ts`](../packages/client/ui-settings-general/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-settings-models`([`packages/client/ui-settings-models/src/index.ts`](../packages/client/ui-settings-models/src/index.ts))
@@ -3123,13 +3125,10 @@ export interface Config {
 - `@deepseek-ai/dsh-atomic-write`([`packages/util/atomic-write/src/index.ts`](../packages/util/atomic-write/src/index.ts))
 - `@deepseek-ai/dsh-base`([`packages/bundle/base/src/index.ts`](../packages/bundle/base/src/index.ts))
 - `@deepseek-ai/dsh-brand`([`packages/util/brand/src/index.ts`](../packages/util/brand/src/index.ts))
-- `@deepseek-ai/dsh-client-schema-form`([`packages/client/schema-form/src/index.ts`](../packages/client/schema-form/src/index.ts))
 - `@deepseek-ai/dsh-client-test-runtime`([`packages/test-support/client-runtime/src/index.ts`](../packages/test-support/client-runtime/src/index.ts))
-- `@deepseek-ai/dsh-client-ui-attachment`([`packages/client/ui-attachment/src/index.ts`](../packages/client/ui-attachment/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-primitives`([`packages/client/ui-primitives/src/index.ts`](../packages/client/ui-primitives/src/index.ts))
 - `@deepseek-ai/dsh-client-ui-slots`([`packages/client/ui-slots/src/index.ts`](../packages/client/ui-slots/src/index.ts))
 - `@deepseek-ai/dsh-client-web`([`packages/client/web/src/index.ts`](../packages/client/web/src/index.ts))
-- `@deepseek-ai/dsh-client-web-react`([`packages/client/web-react/src/index.ts`](../packages/client/web-react/src/index.ts))
 - `@deepseek-ai/dsh-cmdline`([`packages/boot/cmdline/src/index.ts`](../packages/boot/cmdline/src/index.ts))
 - `@deepseek-ai/dsh-code-runtime-python`([`packages/code-runtime/code-runtime-python/src/index.ts`](../packages/code-runtime/code-runtime-python/src/index.ts))
 - `@deepseek-ai/dsh-home-paths`([`packages/util/home-paths/src/index.ts`](../packages/util/home-paths/src/index.ts))

+ 2 - 2
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: 2eb7c748ee0bcf6eb63d200841e35f2606958a24
-module-graph.zh.md: 35a5615914711da1f52e2ecfb938c2e134f6afcb
+module-graph.md: 285caa985be5f2cf3cb363651d0e0a250c232873
+module-graph.zh.md: a571f53da1726637b88b95cdf07ab9e3fe952168

+ 171 - 197
docs/module-graph.md

@@ -123,7 +123,6 @@ flowchart TD
     pkg_client_locale["client-locale"]
     pkg_client_modules["client-modules"]
     pkg_client_runtime["client-runtime"]
-    pkg_client_schema_form["client-schema-form"]
     pkg_client_ui_agent_preset["client-ui-agent-preset"]
     pkg_client_ui_attachment["client-ui-attachment"]
     pkg_client_ui_commands["client-ui-commands"]
@@ -140,6 +139,7 @@ flowchart TD
     pkg_client_ui_permission_presets["client-ui-permission-presets"]
     pkg_client_ui_plan["client-ui-plan"]
     pkg_client_ui_primitives["client-ui-primitives"]
+    pkg_client_ui_renderer["client-ui-renderer"]
     pkg_client_ui_settings["client-ui-settings"]
     pkg_client_ui_settings_general["client-ui-settings-general"]
     pkg_client_ui_settings_models["client-ui-settings-models"]
@@ -156,7 +156,6 @@ flowchart TD
     pkg_client_ui_workflow_run["client-ui-workflow-run"]
     pkg_client_ui_workspace["client-ui-workspace"]
     pkg_client_web["client-web"]
-    pkg_client_web_react["client-web-react"]
   end
   subgraph group_code_runtime["packages/code-runtime"]
     pkg_code_runtime["code-runtime"]
@@ -335,13 +334,9 @@ flowchart TD
   pkg_scope --> pkg_invariants
   pkg_cmdline --> pkg_invariants
   pkg_base --> pkg_invariants
-  pkg_client_modules --> pkg_invariants
-  pkg_client_schema_form --> pkg_invariants
-  pkg_client_ui_attachment --> pkg_invariants
   pkg_client_ui_primitives --> pkg_invariants
   pkg_client_ui_slots --> pkg_invariants
   pkg_client_web --> pkg_invariants
-  pkg_client_web_react --> pkg_invariants
   pkg_code_runtime --> pkg_invariants
   pkg_code_runtime_python --> pkg_invariants
   pkg_e2b --> pkg_invariants
@@ -359,11 +354,8 @@ flowchart TD
   pkg_typert_registry --> pkg_invariants
   pkg_attachment --> pkg_brand
   pkg_attachment --> pkg_invariants
-  pkg_client_connection --> pkg_host_webserver
-  pkg_client_connection --> pkg_invariants
-  pkg_client_hmr --> pkg_client_modules
-  pkg_client_hmr --> pkg_host_webserver
-  pkg_client_hmr --> pkg_invariants
+  pkg_client_modules --> pkg_host_webserver
+  pkg_client_modules --> pkg_invariants
   pkg_credentials --> pkg_brand
   pkg_credentials --> pkg_invariants
   pkg_subprocess_e2b --> pkg_e2b
@@ -395,12 +387,12 @@ flowchart TD
   pkg_llm --> pkg_brand
   pkg_llm --> pkg_invariants
   pkg_llm --> pkg_timeout
-  pkg_api_gateway --> pkg_client_connection
-  pkg_api_gateway --> pkg_invariants
-  pkg_api_gateway --> pkg_typert_registry
   pkg_attachment_local --> pkg_attachment
   pkg_attachment_local --> pkg_home_paths
   pkg_attachment_local --> pkg_invariants
+  pkg_client_hmr --> pkg_client_modules
+  pkg_client_hmr --> pkg_host_webserver
+  pkg_client_hmr --> pkg_invariants
   pkg_credentials_local --> pkg_atomic_write
   pkg_credentials_local --> pkg_credentials
   pkg_credentials_local --> pkg_home_paths
@@ -998,21 +990,6 @@ flowchart TD
   pkg_hooks_claude_code --> pkg_session_persistence
   pkg_hooks_claude_code --> pkg_subagent
   pkg_hooks_claude_code --> pkg_tools
-  pkg_api_remotes --> pkg_agent
-  pkg_api_remotes --> pkg_agent_presets
-  pkg_api_remotes --> pkg_api_gateway
-  pkg_api_remotes --> pkg_commands
-  pkg_api_remotes --> pkg_cordis_host_runner
-  pkg_api_remotes --> pkg_credentials
-  pkg_api_remotes --> pkg_goal
-  pkg_api_remotes --> pkg_host_plugin_inventory
-  pkg_api_remotes --> pkg_invariants
-  pkg_api_remotes --> pkg_llm
-  pkg_api_remotes --> pkg_message_feedback
-  pkg_api_remotes --> pkg_session
-  pkg_api_remotes --> pkg_session_persistence
-  pkg_api_remotes --> pkg_settings
-  pkg_api_remotes --> pkg_typert_registry
   pkg_web_app --> pkg_invariants
   pkg_web_app --> pkg_shell_env
   pkg_web_app --> pkg_system_prompt
@@ -1088,10 +1065,14 @@ flowchart TD
   pkg_subagent_spawn_in_process --> pkg_invariants
   pkg_subagent_spawn_in_process --> pkg_subagent
   pkg_subagent_spawn_in_process --> pkg_subagent_in_process_driver
-  pkg_client_runtime --> pkg_api_remotes
-  pkg_client_runtime --> pkg_invariants
-  pkg_client_runtime --> pkg_typert_protocol
-  pkg_client_runtime --> pkg_typert_registry
+  pkg_client_connection --> pkg_attachment
+  pkg_client_connection --> pkg_commands
+  pkg_client_connection --> pkg_host_apiproxy
+  pkg_client_connection --> pkg_host_webserver
+  pkg_client_connection --> pkg_invariants
+  pkg_client_connection --> pkg_llm
+  pkg_client_connection --> pkg_session
+  pkg_client_connection --> pkg_tools
   pkg_compaction_basic --> pkg_agent
   pkg_compaction_basic --> pkg_commands
   pkg_compaction_basic --> pkg_compaction
@@ -1141,21 +1122,9 @@ flowchart TD
   pkg_subagent_dsh_sdk --> pkg_session
   pkg_subagent_dsh_sdk --> pkg_subagent
   pkg_subagent_dsh_sdk --> pkg_subprocess
-  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_client_ui_settings_models --> pkg_api_remotes
-  pkg_client_ui_settings_models --> pkg_client_connection
-  pkg_client_ui_settings_models --> pkg_client_runtime
-  pkg_client_ui_settings_models --> pkg_client_schema_form
-  pkg_client_ui_settings_models --> pkg_client_ui_primitives
-  pkg_client_ui_settings_models --> pkg_client_ui_slots
-  pkg_client_ui_settings_models --> pkg_client_web_react
-  pkg_client_ui_settings_models --> pkg_invariants
+  pkg_api_gateway --> pkg_client_connection
+  pkg_api_gateway --> pkg_invariants
+  pkg_api_gateway --> pkg_typert_registry
   pkg_acp_demo --> pkg_acp
   pkg_acp_demo --> pkg_agent_instructions
   pkg_acp_demo --> pkg_agent_spine_demo
@@ -1166,68 +1135,91 @@ flowchart TD
   pkg_acp_demo --> pkg_session_query
   pkg_acp_demo --> pkg_session_query_sqlite
   pkg_acp_demo --> pkg_tools
-  pkg_client_test_runtime --> pkg_client_runtime
-  pkg_client_test_runtime --> pkg_client_ui_slots
-  pkg_client_test_runtime --> pkg_client_web_react
-  pkg_client_test_runtime --> pkg_host_apiproxy
-  pkg_client_test_runtime --> pkg_invariants
+  pkg_api_remotes --> pkg_agent
+  pkg_api_remotes --> pkg_agent_presets
+  pkg_api_remotes --> pkg_api_gateway
+  pkg_api_remotes --> pkg_commands
+  pkg_api_remotes --> pkg_cordis_host_runner
+  pkg_api_remotes --> pkg_credentials
+  pkg_api_remotes --> pkg_goal
+  pkg_api_remotes --> pkg_host_plugin_inventory
+  pkg_api_remotes --> pkg_invariants
+  pkg_api_remotes --> pkg_llm
+  pkg_api_remotes --> pkg_message_feedback
+  pkg_api_remotes --> pkg_session
+  pkg_api_remotes --> pkg_session_persistence
+  pkg_api_remotes --> pkg_settings
+  pkg_api_remotes --> pkg_typert_registry
+  pkg_client_runtime --> pkg_agent
+  pkg_client_runtime --> pkg_api_remotes
+  pkg_client_runtime --> pkg_attachment
+  pkg_client_runtime --> pkg_client_connection
+  pkg_client_runtime --> pkg_commands
+  pkg_client_runtime --> pkg_host_apiproxy
+  pkg_client_runtime --> pkg_invariants
+  pkg_client_runtime --> pkg_llm
+  pkg_client_runtime --> pkg_llm_retry
+  pkg_client_runtime --> pkg_session
+  pkg_client_runtime --> pkg_session_projection
+  pkg_client_runtime --> pkg_session_title
+  pkg_client_runtime --> pkg_tools
+  pkg_client_runtime --> pkg_typert_protocol
+  pkg_client_runtime --> pkg_typert_registry
+  pkg_client_ui_renderer --> pkg_client_runtime
+  pkg_client_ui_renderer --> pkg_invariants
+  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_invariants
+  pkg_client_ui_settings --> pkg_settings
   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_client_locale --> pkg_settings
+  pkg_client_test_runtime --> pkg_client_runtime
+  pkg_client_test_runtime --> pkg_client_ui_renderer
+  pkg_client_test_runtime --> pkg_client_ui_slots
+  pkg_client_test_runtime --> pkg_host_apiproxy
+  pkg_client_test_runtime --> pkg_invariants
   pkg_client_ui_input_trigger --> pkg_client_locale
   pkg_client_ui_input_trigger --> pkg_client_runtime
-  pkg_client_ui_input_trigger --> pkg_client_ui_primitives
-  pkg_client_ui_input_trigger --> pkg_client_ui_slots
   pkg_client_ui_input_trigger --> pkg_invariants
+  pkg_client_ui_settings_models --> pkg_api_remotes
+  pkg_client_ui_settings_models --> pkg_client_connection
+  pkg_client_ui_settings_models --> pkg_client_locale
+  pkg_client_ui_settings_models --> pkg_client_runtime
+  pkg_client_ui_settings_models --> pkg_client_ui_settings
+  pkg_client_ui_settings_models --> pkg_invariants
   pkg_client_ui_settings_plugin_inventory --> pkg_api_remotes
   pkg_client_ui_settings_plugin_inventory --> pkg_client_locale
   pkg_client_ui_settings_plugin_inventory --> pkg_client_runtime
-  pkg_client_ui_settings_plugin_inventory --> pkg_client_ui_primitives
   pkg_client_ui_settings_plugin_inventory --> pkg_client_ui_settings
-  pkg_client_ui_settings_plugin_inventory --> pkg_client_ui_slots
   pkg_client_ui_settings_plugin_inventory --> pkg_invariants
   pkg_client_ui_settings_plugins --> pkg_api_remotes
   pkg_client_ui_settings_plugins --> pkg_client_connection
   pkg_client_ui_settings_plugins --> pkg_client_locale
   pkg_client_ui_settings_plugins --> pkg_client_runtime
-  pkg_client_ui_settings_plugins --> pkg_client_ui_primitives
   pkg_client_ui_settings_plugins --> pkg_client_ui_settings
-  pkg_client_ui_settings_plugins --> pkg_client_ui_slots
-  pkg_client_ui_settings_plugins --> pkg_client_web_react
   pkg_client_ui_settings_plugins --> 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_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_compaction
-  pkg_client_ui_trajectory --> pkg_invariants
-  pkg_client_ui_trajectory --> pkg_tools
-  pkg_client_ui_user_questions --> pkg_api_remotes
-  pkg_client_ui_user_questions --> pkg_client_locale
-  pkg_client_ui_user_questions --> 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_ui_theme --> pkg_settings
+  pkg_client_ui_layout --> pkg_client_runtime
+  pkg_client_ui_layout --> pkg_client_ui_theme
+  pkg_client_ui_layout --> pkg_invariants
+  pkg_cordis_client_runner --> pkg_api_remotes
+  pkg_cordis_client_runner --> pkg_client_connection
+  pkg_cordis_client_runner --> pkg_client_modules
+  pkg_cordis_client_runner --> pkg_client_runtime
+  pkg_cordis_client_runner --> pkg_client_ui_theme
+  pkg_cordis_client_runner --> pkg_invariants
   pkg_client_ui_conversation --> pkg_agent
   pkg_client_ui_conversation --> pkg_api_remotes
   pkg_client_ui_conversation --> pkg_attachment
@@ -1235,97 +1227,67 @@ flowchart TD
   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_attachment
   pkg_client_ui_conversation --> pkg_client_ui_input_trigger
-  pkg_client_ui_conversation --> pkg_client_ui_primitives
+  pkg_client_ui_conversation --> pkg_client_ui_layout
   pkg_client_ui_conversation --> pkg_client_ui_settings
-  pkg_client_ui_conversation --> pkg_client_ui_slots
   pkg_client_ui_conversation --> pkg_commands
   pkg_client_ui_conversation --> pkg_compaction
+  pkg_client_ui_conversation --> pkg_goal
   pkg_client_ui_conversation --> pkg_invariants
   pkg_client_ui_conversation --> pkg_llm_retry
+  pkg_client_ui_conversation --> pkg_permission_presets
+  pkg_client_ui_conversation --> pkg_plan_mode
   pkg_client_ui_conversation --> pkg_session_stats
+  pkg_client_ui_conversation --> pkg_settings
   pkg_client_ui_conversation --> pkg_token_meter
+  pkg_client_ui_conversation --> pkg_tool_todo
   pkg_client_ui_conversation --> pkg_tools
-  pkg_client_ui_directory_picker_browse --> pkg_client_locale
-  pkg_client_ui_directory_picker_browse --> pkg_client_runtime
-  pkg_client_ui_directory_picker_browse --> pkg_client_ui_primitives
-  pkg_client_ui_directory_picker_browse --> pkg_client_ui_slots
-  pkg_client_ui_directory_picker_browse --> pkg_client_ui_workspace
-  pkg_client_ui_directory_picker_browse --> pkg_invariants
-  pkg_client_ui_directory_picker_native --> pkg_client_runtime
-  pkg_client_ui_directory_picker_native --> pkg_client_ui_slots
-  pkg_client_ui_directory_picker_native --> pkg_client_ui_workspace
-  pkg_client_ui_directory_picker_native --> pkg_invariants
-  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_cordis_client_runner --> pkg_api_remotes
-  pkg_cordis_client_runner --> pkg_client_connection
-  pkg_cordis_client_runner --> pkg_client_modules
-  pkg_cordis_client_runner --> pkg_client_runtime
-  pkg_cordis_client_runner --> pkg_client_ui_slots
-  pkg_cordis_client_runner --> pkg_client_ui_theme
-  pkg_cordis_client_runner --> pkg_invariants
+  pkg_client_ui_sidebar --> pkg_client_locale
+  pkg_client_ui_sidebar --> pkg_client_runtime
+  pkg_client_ui_sidebar --> pkg_client_ui_layout
+  pkg_client_ui_sidebar --> 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
   pkg_client_ui_agent_preset --> pkg_client_ui_conversation
-  pkg_client_ui_agent_preset --> pkg_client_ui_primitives
   pkg_client_ui_agent_preset --> pkg_client_ui_settings
-  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_attachment --> pkg_attachment
+  pkg_client_ui_attachment --> pkg_client_runtime
+  pkg_client_ui_attachment --> pkg_client_ui_conversation
+  pkg_client_ui_attachment --> pkg_invariants
   pkg_client_ui_commands --> pkg_api_remotes
   pkg_client_ui_commands --> pkg_client_locale
   pkg_client_ui_commands --> pkg_client_runtime
   pkg_client_ui_commands --> pkg_client_ui_conversation
   pkg_client_ui_commands --> pkg_client_ui_input_trigger
-  pkg_client_ui_commands --> pkg_client_ui_primitives
-  pkg_client_ui_commands --> pkg_client_ui_slots
   pkg_client_ui_commands --> pkg_commands
   pkg_client_ui_commands --> pkg_invariants
   pkg_client_ui_deliverables --> pkg_client_connection
   pkg_client_ui_deliverables --> pkg_client_locale
   pkg_client_ui_deliverables --> pkg_client_runtime
   pkg_client_ui_deliverables --> pkg_client_ui_conversation
-  pkg_client_ui_deliverables --> pkg_client_ui_slots
   pkg_client_ui_deliverables --> pkg_invariants
   pkg_client_ui_deliverables --> pkg_system_prompt
   pkg_client_ui_goal --> pkg_api_remotes
   pkg_client_ui_goal --> pkg_client_locale
   pkg_client_ui_goal --> pkg_client_runtime
   pkg_client_ui_goal --> pkg_client_ui_conversation
-  pkg_client_ui_goal --> pkg_client_ui_primitives
-  pkg_client_ui_goal --> pkg_client_ui_slots
   pkg_client_ui_goal --> pkg_commands
   pkg_client_ui_goal --> pkg_goal
   pkg_client_ui_goal --> pkg_invariants
+  pkg_client_ui_goal --> pkg_session
+  pkg_client_ui_goal --> pkg_typert_protocol
   pkg_client_ui_jobs --> pkg_client_locale
   pkg_client_ui_jobs --> pkg_client_runtime
   pkg_client_ui_jobs --> pkg_client_ui_conversation
-  pkg_client_ui_jobs --> pkg_client_ui_primitives
-  pkg_client_ui_jobs --> pkg_client_ui_slots
   pkg_client_ui_jobs --> pkg_invariants
   pkg_client_ui_message_feedback --> pkg_api_remotes
   pkg_client_ui_message_feedback --> pkg_client_connection
   pkg_client_ui_message_feedback --> pkg_client_locale
   pkg_client_ui_message_feedback --> pkg_client_runtime
   pkg_client_ui_message_feedback --> pkg_client_ui_conversation
-  pkg_client_ui_message_feedback --> pkg_client_ui_primitives
-  pkg_client_ui_message_feedback --> pkg_client_ui_slots
   pkg_client_ui_message_feedback --> pkg_invariants
   pkg_client_ui_message_feedback --> pkg_message_feedback
   pkg_client_ui_message_feedback --> pkg_typert_protocol
@@ -1333,16 +1295,20 @@ flowchart TD
   pkg_client_ui_plan --> pkg_client_locale
   pkg_client_ui_plan --> pkg_client_runtime
   pkg_client_ui_plan --> pkg_client_ui_conversation
-  pkg_client_ui_plan --> pkg_client_ui_primitives
-  pkg_client_ui_plan --> pkg_client_ui_slots
   pkg_client_ui_plan --> pkg_invariants
   pkg_client_ui_plan --> pkg_plan_mode
+  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_settings
+  pkg_client_ui_settings_general --> pkg_client_ui_sidebar
+  pkg_client_ui_settings_general --> pkg_invariants
+  pkg_client_ui_settings_general --> pkg_settings
   pkg_client_ui_subagent --> pkg_client_locale
   pkg_client_ui_subagent --> pkg_client_runtime
   pkg_client_ui_subagent --> pkg_client_ui_conversation
   pkg_client_ui_subagent --> pkg_client_ui_input_trigger
-  pkg_client_ui_subagent --> pkg_client_ui_primitives
-  pkg_client_ui_subagent --> pkg_client_ui_slots
   pkg_client_ui_subagent --> pkg_invariants
   pkg_client_ui_subagent --> pkg_subagent
   pkg_client_ui_subagent --> pkg_token_meter
@@ -1350,32 +1316,44 @@ flowchart TD
   pkg_client_ui_tool --> pkg_client_locale
   pkg_client_ui_tool --> pkg_client_runtime
   pkg_client_ui_tool --> pkg_client_ui_conversation
-  pkg_client_ui_tool --> pkg_client_ui_primitives
-  pkg_client_ui_tool --> pkg_client_ui_slots
   pkg_client_ui_tool --> 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_conversation
+  pkg_client_ui_trajectory --> pkg_compaction
+  pkg_client_ui_trajectory --> pkg_invariants
+  pkg_client_ui_trajectory --> pkg_tools
+  pkg_client_ui_user_questions --> pkg_api_remotes
+  pkg_client_ui_user_questions --> pkg_client_locale
+  pkg_client_ui_user_questions --> pkg_client_runtime
+  pkg_client_ui_user_questions --> pkg_client_ui_conversation
+  pkg_client_ui_user_questions --> pkg_invariants
   pkg_client_ui_workflow_run --> pkg_client_locale
   pkg_client_ui_workflow_run --> pkg_client_runtime
   pkg_client_ui_workflow_run --> pkg_client_ui_conversation
-  pkg_client_ui_workflow_run --> pkg_client_ui_primitives
-  pkg_client_ui_workflow_run --> pkg_client_ui_slots
   pkg_client_ui_workflow_run --> pkg_invariants
   pkg_client_ui_workflow_run --> pkg_session
   pkg_client_ui_workflow_run --> pkg_tool_workflow
   pkg_client_ui_workflow_run --> pkg_workflow
-  pkg_host_directory_picker_auto --> pkg_client_ui_directory_picker_browse
-  pkg_host_directory_picker_auto --> pkg_client_ui_directory_picker_native
-  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_workspace --> pkg_client_locale
+  pkg_client_ui_workspace --> pkg_client_runtime
+  pkg_client_ui_workspace --> pkg_client_ui_conversation
+  pkg_client_ui_workspace --> pkg_client_ui_sidebar
+  pkg_client_ui_workspace --> pkg_invariants
   pkg_session_log_export --> pkg_client_locale
   pkg_session_log_export --> pkg_client_runtime
   pkg_session_log_export --> pkg_client_ui_commands
   pkg_session_log_export --> pkg_client_ui_conversation
-  pkg_session_log_export --> pkg_client_ui_primitives
-  pkg_session_log_export --> pkg_client_ui_slots
   pkg_session_log_export --> pkg_commands
   pkg_session_log_export --> pkg_invariants
+  pkg_client_ui_directory_picker_browse --> pkg_client_locale
+  pkg_client_ui_directory_picker_browse --> pkg_client_runtime
+  pkg_client_ui_directory_picker_browse --> pkg_client_ui_workspace
+  pkg_client_ui_directory_picker_browse --> pkg_invariants
+  pkg_client_ui_directory_picker_native --> pkg_client_runtime
+  pkg_client_ui_directory_picker_native --> pkg_client_ui_workspace
+  pkg_client_ui_directory_picker_native --> pkg_invariants
   pkg_client_ui_model_selection --> pkg_api_remotes
   pkg_client_ui_model_selection --> pkg_client_connection
   pkg_client_ui_model_selection --> pkg_client_locale
@@ -1383,19 +1361,14 @@ flowchart TD
   pkg_client_ui_model_selection --> pkg_client_ui_commands
   pkg_client_ui_model_selection --> pkg_client_ui_conversation
   pkg_client_ui_model_selection --> pkg_client_ui_input_trigger
-  pkg_client_ui_model_selection --> pkg_client_ui_primitives
-  pkg_client_ui_model_selection --> pkg_client_ui_slots
   pkg_client_ui_model_selection --> pkg_invariants
   pkg_client_ui_permission_presets --> pkg_api_remotes
   pkg_client_ui_permission_presets --> pkg_client_connection
   pkg_client_ui_permission_presets --> pkg_client_locale
   pkg_client_ui_permission_presets --> pkg_client_runtime
-  pkg_client_ui_permission_presets --> pkg_client_schema_form
   pkg_client_ui_permission_presets --> pkg_client_ui_commands
   pkg_client_ui_permission_presets --> pkg_client_ui_input_trigger
-  pkg_client_ui_permission_presets --> pkg_client_ui_primitives
   pkg_client_ui_permission_presets --> pkg_client_ui_settings
-  pkg_client_ui_permission_presets --> pkg_client_ui_slots
   pkg_client_ui_permission_presets --> pkg_invariants
   pkg_client_ui_permission_presets --> pkg_permission_presets
   pkg_client_ui_skill --> pkg_api_remotes
@@ -1403,8 +1376,6 @@ flowchart TD
   pkg_client_ui_skill --> pkg_client_locale
   pkg_client_ui_skill --> pkg_client_runtime
   pkg_client_ui_skill --> pkg_client_ui_input_trigger
-  pkg_client_ui_skill --> pkg_client_ui_primitives
-  pkg_client_ui_skill --> pkg_client_ui_slots
   pkg_client_ui_skill --> pkg_client_ui_tool
   pkg_client_ui_skill --> pkg_invariants
   pkg_client_ui_cordis --> pkg_api_remotes
@@ -1412,12 +1383,16 @@ flowchart TD
   pkg_client_ui_cordis --> pkg_client_locale
   pkg_client_ui_cordis --> pkg_client_runtime
   pkg_client_ui_cordis --> pkg_client_ui_input_trigger
-  pkg_client_ui_cordis --> pkg_client_ui_primitives
   pkg_client_ui_cordis --> pkg_client_ui_sidebar
-  pkg_client_ui_cordis --> pkg_client_ui_slots
   pkg_client_ui_cordis --> pkg_client_ui_tool
   pkg_client_ui_cordis --> pkg_cordis_client_runner
   pkg_client_ui_cordis --> pkg_invariants
+  pkg_host_directory_picker_auto --> pkg_client_ui_directory_picker_browse
+  pkg_host_directory_picker_auto --> pkg_client_ui_directory_picker_native
+  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
 ```
 
 | Package | Group | Depends on |
@@ -1433,13 +1408,9 @@ flowchart TD
 | [`scope`](../packages/core/scope) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`cmdline`](../packages/boot/cmdline) | `boot` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`base`](../packages/bundle/base) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-modules`](../packages/client/modules) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-schema-form`](../packages/client/schema-form) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-attachment`](../packages/client/ui-attachment) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-primitives`](../packages/client/ui-primitives) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-slots`](../packages/client/ui-slots) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-web`](../packages/client/web) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-web-react`](../packages/client/web-react) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`code-runtime-python`](../packages/code-runtime/code-runtime-python) | `code-runtime` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`e2b`](../packages/e2b/e2b) | `e2b` | [`invariants`](../packages/runtime-diagnostics/invariants) |
@@ -1456,8 +1427,7 @@ flowchart TD
 | [`typert-protocol`](../packages/typert/protocol) | `typert` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`typert-registry`](../packages/typert/registry) | `typert` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-connection`](../packages/client/connection) | `client` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-modules`](../packages/client/modules) | `client` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`credentials`](../packages/credentials/credentials) | `credentials` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`invariants`](../packages/runtime-diagnostics/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
@@ -1470,8 +1440,8 @@ flowchart TD
 | [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`typert-loader`](../packages/typert/loader) | `typert` | [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) |
 | [`llm`](../packages/llm/llm) | `llm` | [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`timeout`](../packages/util/timeout) |
-| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) |
 | [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment) |
 | [`settings-file`](../packages/settings/settings-file) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
 | [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
@@ -1588,7 +1558,6 @@ flowchart TD
 | [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
 | [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
-| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
 | [`web-app`](../packages/bundle/web-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) |
 | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
@@ -1601,44 +1570,49 @@ flowchart TD
 | [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/runtime-diagnostics/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-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
 | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
-| [`client-runtime`](../packages/client/runtime) | `client` | [`api-remotes`](../packages/api/remotes), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry) |
+| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
 | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
 | [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
 | [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/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) |
 | [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
-| [`client-ui-settings`](../packages/client/ui-settings) | `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-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
-| [`client-ui-settings-models`](../packages/client/ui-settings-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/runtime-diagnostics/invariants) |
+| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) |
 | [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/runtime-diagnostics/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) |
-| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`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/runtime-diagnostics/invariants) |
-| [`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/runtime-diagnostics/invariants) |
-| [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `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/runtime-diagnostics/invariants) |
-| [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`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), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `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), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/runtime-diagnostics/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/runtime-diagnostics/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/runtime-diagnostics/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), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`tools`](../packages/core/tools) |
-| [`client-ui-user-questions`](../packages/client/ui-user-questions) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`invariants`](../packages/runtime-diagnostics/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/runtime-diagnostics/invariants) |
-| [`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-attachment`](../packages/client/ui-attachment), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm-retry`](../packages/llm/llm-retry), [`session-stats`](../packages/session/session-stats), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
-| [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse) | `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), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`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/runtime-diagnostics/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/runtime-diagnostics/invariants) |
-| [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/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/runtime-diagnostics/invariants) |
-| [`client-ui-commands`](../packages/client/ui-commands) | `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-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-deliverables`](../packages/client/ui-deliverables) | `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-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt) |
-| [`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/runtime-diagnostics/invariants) |
-| [`client-ui-jobs`](../packages/client/ui-jobs) | `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/runtime-diagnostics/invariants) |
-| [`client-ui-message-feedback`](../packages/client/ui-message-feedback) | `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-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback), [`typert-protocol`](../packages/typert/protocol) |
-| [`client-ui-plan`](../packages/client/ui-plan) | `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), [`invariants`](../packages/runtime-diagnostics/invariants), [`plan-mode`](../packages/plan/plan-mode) |
-| [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter) |
-| [`client-ui-tool`](../packages/client/ui-tool) | `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), [`invariants`](../packages/runtime-diagnostics/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/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
+| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
+| [`client-runtime`](../packages/client/runtime) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-title`](../packages/session/session-title), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry) |
+| [`client-ui-renderer`](../packages/client/ui-renderer) | `client` | [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
+| [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
+| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`client-runtime`](../packages/client/runtime), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-slots`](../packages/client/ui-slots), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/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-settings`](../packages/client/ui-settings), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
+| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`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-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-settings`](../packages/client/ui-settings), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools) |
+| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-layout`](../packages/client/ui-layout), [`invariants`](../packages/runtime-diagnostics/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-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-attachment`](../packages/client/ui-attachment) | `client` | [`attachment`](../packages/attachment/attachment), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-commands`](../packages/client/ui-commands) | `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-input-trigger`](../packages/client/ui-input-trigger), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-deliverables`](../packages/client/ui-deliverables) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt) |
+| [`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), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
+| [`client-ui-jobs`](../packages/client/ui-jobs) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-message-feedback`](../packages/client/ui-message-feedback) | `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), [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback), [`typert-protocol`](../packages/typert/protocol) |
+| [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/invariants), [`plan-mode`](../packages/plan/plan-mode) |
+| [`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-settings`](../packages/client/ui-settings), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
+| [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter) |
+| [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/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-conversation`](../packages/client/ui-conversation), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`tools`](../packages/core/tools) |
+| [`client-ui-user-questions`](../packages/client/ui-user-questions) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/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), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
+| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`session-log-export`](../packages/session-query/session-log-export) | `session-query` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-conversation`](../packages/client/ui-conversation), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-model-selection`](../packages/client/ui-model-selection) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-permission-presets`](../packages/client/ui-permission-presets) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) |
+| [`client-ui-skill`](../packages/client/ui-skill) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-tool`](../packages/client/ui-tool), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-cordis`](../packages/extensions/ui-cordis) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`client-ui-tool`](../packages/client/ui-tool), [`cordis-client-runner`](../packages/extensions/cordis-client-runner), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse), [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native), [`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/runtime-diagnostics/invariants) |
-| [`session-log-export`](../packages/session-query/session-log-export) | `session-query` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-commands`](../packages/client/ui-commands), [`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), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-model-selection`](../packages/client/ui-model-selection) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-permission-presets`](../packages/client/ui-permission-presets) | `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-commands`](../packages/client/ui-commands), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) |
-| [`client-ui-skill`](../packages/client/ui-skill) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-tool`](../packages/client/ui-tool), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-cordis`](../packages/extensions/ui-cordis) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-tool`](../packages/client/ui-tool), [`cordis-client-runner`](../packages/extensions/cordis-client-runner), [`invariants`](../packages/runtime-diagnostics/invariants) |

+ 171 - 197
docs/module-graph.zh.md

@@ -125,7 +125,6 @@ flowchart TD
     pkg_client_locale["client-locale"]
     pkg_client_modules["client-modules"]
     pkg_client_runtime["client-runtime"]
-    pkg_client_schema_form["client-schema-form"]
     pkg_client_ui_agent_preset["client-ui-agent-preset"]
     pkg_client_ui_attachment["client-ui-attachment"]
     pkg_client_ui_commands["client-ui-commands"]
@@ -142,6 +141,7 @@ flowchart TD
     pkg_client_ui_permission_presets["client-ui-permission-presets"]
     pkg_client_ui_plan["client-ui-plan"]
     pkg_client_ui_primitives["client-ui-primitives"]
+    pkg_client_ui_renderer["client-ui-renderer"]
     pkg_client_ui_settings["client-ui-settings"]
     pkg_client_ui_settings_general["client-ui-settings-general"]
     pkg_client_ui_settings_models["client-ui-settings-models"]
@@ -158,7 +158,6 @@ flowchart TD
     pkg_client_ui_workflow_run["client-ui-workflow-run"]
     pkg_client_ui_workspace["client-ui-workspace"]
     pkg_client_web["client-web"]
-    pkg_client_web_react["client-web-react"]
   end
   subgraph group_code_runtime["packages/code-runtime"]
     pkg_code_runtime["code-runtime"]
@@ -337,13 +336,9 @@ flowchart TD
   pkg_scope --> pkg_invariants
   pkg_cmdline --> pkg_invariants
   pkg_base --> pkg_invariants
-  pkg_client_modules --> pkg_invariants
-  pkg_client_schema_form --> pkg_invariants
-  pkg_client_ui_attachment --> pkg_invariants
   pkg_client_ui_primitives --> pkg_invariants
   pkg_client_ui_slots --> pkg_invariants
   pkg_client_web --> pkg_invariants
-  pkg_client_web_react --> pkg_invariants
   pkg_code_runtime --> pkg_invariants
   pkg_code_runtime_python --> pkg_invariants
   pkg_e2b --> pkg_invariants
@@ -361,11 +356,8 @@ flowchart TD
   pkg_typert_registry --> pkg_invariants
   pkg_attachment --> pkg_brand
   pkg_attachment --> pkg_invariants
-  pkg_client_connection --> pkg_host_webserver
-  pkg_client_connection --> pkg_invariants
-  pkg_client_hmr --> pkg_client_modules
-  pkg_client_hmr --> pkg_host_webserver
-  pkg_client_hmr --> pkg_invariants
+  pkg_client_modules --> pkg_host_webserver
+  pkg_client_modules --> pkg_invariants
   pkg_credentials --> pkg_brand
   pkg_credentials --> pkg_invariants
   pkg_subprocess_e2b --> pkg_e2b
@@ -397,12 +389,12 @@ flowchart TD
   pkg_llm --> pkg_brand
   pkg_llm --> pkg_invariants
   pkg_llm --> pkg_timeout
-  pkg_api_gateway --> pkg_client_connection
-  pkg_api_gateway --> pkg_invariants
-  pkg_api_gateway --> pkg_typert_registry
   pkg_attachment_local --> pkg_attachment
   pkg_attachment_local --> pkg_home_paths
   pkg_attachment_local --> pkg_invariants
+  pkg_client_hmr --> pkg_client_modules
+  pkg_client_hmr --> pkg_host_webserver
+  pkg_client_hmr --> pkg_invariants
   pkg_credentials_local --> pkg_atomic_write
   pkg_credentials_local --> pkg_credentials
   pkg_credentials_local --> pkg_home_paths
@@ -1000,21 +992,6 @@ flowchart TD
   pkg_hooks_claude_code --> pkg_session_persistence
   pkg_hooks_claude_code --> pkg_subagent
   pkg_hooks_claude_code --> pkg_tools
-  pkg_api_remotes --> pkg_agent
-  pkg_api_remotes --> pkg_agent_presets
-  pkg_api_remotes --> pkg_api_gateway
-  pkg_api_remotes --> pkg_commands
-  pkg_api_remotes --> pkg_cordis_host_runner
-  pkg_api_remotes --> pkg_credentials
-  pkg_api_remotes --> pkg_goal
-  pkg_api_remotes --> pkg_host_plugin_inventory
-  pkg_api_remotes --> pkg_invariants
-  pkg_api_remotes --> pkg_llm
-  pkg_api_remotes --> pkg_message_feedback
-  pkg_api_remotes --> pkg_session
-  pkg_api_remotes --> pkg_session_persistence
-  pkg_api_remotes --> pkg_settings
-  pkg_api_remotes --> pkg_typert_registry
   pkg_web_app --> pkg_invariants
   pkg_web_app --> pkg_shell_env
   pkg_web_app --> pkg_system_prompt
@@ -1090,10 +1067,14 @@ flowchart TD
   pkg_subagent_spawn_in_process --> pkg_invariants
   pkg_subagent_spawn_in_process --> pkg_subagent
   pkg_subagent_spawn_in_process --> pkg_subagent_in_process_driver
-  pkg_client_runtime --> pkg_api_remotes
-  pkg_client_runtime --> pkg_invariants
-  pkg_client_runtime --> pkg_typert_protocol
-  pkg_client_runtime --> pkg_typert_registry
+  pkg_client_connection --> pkg_attachment
+  pkg_client_connection --> pkg_commands
+  pkg_client_connection --> pkg_host_apiproxy
+  pkg_client_connection --> pkg_host_webserver
+  pkg_client_connection --> pkg_invariants
+  pkg_client_connection --> pkg_llm
+  pkg_client_connection --> pkg_session
+  pkg_client_connection --> pkg_tools
   pkg_compaction_basic --> pkg_agent
   pkg_compaction_basic --> pkg_commands
   pkg_compaction_basic --> pkg_compaction
@@ -1143,21 +1124,9 @@ flowchart TD
   pkg_subagent_dsh_sdk --> pkg_session
   pkg_subagent_dsh_sdk --> pkg_subagent
   pkg_subagent_dsh_sdk --> pkg_subprocess
-  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_client_ui_settings_models --> pkg_api_remotes
-  pkg_client_ui_settings_models --> pkg_client_connection
-  pkg_client_ui_settings_models --> pkg_client_runtime
-  pkg_client_ui_settings_models --> pkg_client_schema_form
-  pkg_client_ui_settings_models --> pkg_client_ui_primitives
-  pkg_client_ui_settings_models --> pkg_client_ui_slots
-  pkg_client_ui_settings_models --> pkg_client_web_react
-  pkg_client_ui_settings_models --> pkg_invariants
+  pkg_api_gateway --> pkg_client_connection
+  pkg_api_gateway --> pkg_invariants
+  pkg_api_gateway --> pkg_typert_registry
   pkg_acp_demo --> pkg_acp
   pkg_acp_demo --> pkg_agent_instructions
   pkg_acp_demo --> pkg_agent_spine_demo
@@ -1168,68 +1137,91 @@ flowchart TD
   pkg_acp_demo --> pkg_session_query
   pkg_acp_demo --> pkg_session_query_sqlite
   pkg_acp_demo --> pkg_tools
-  pkg_client_test_runtime --> pkg_client_runtime
-  pkg_client_test_runtime --> pkg_client_ui_slots
-  pkg_client_test_runtime --> pkg_client_web_react
-  pkg_client_test_runtime --> pkg_host_apiproxy
-  pkg_client_test_runtime --> pkg_invariants
+  pkg_api_remotes --> pkg_agent
+  pkg_api_remotes --> pkg_agent_presets
+  pkg_api_remotes --> pkg_api_gateway
+  pkg_api_remotes --> pkg_commands
+  pkg_api_remotes --> pkg_cordis_host_runner
+  pkg_api_remotes --> pkg_credentials
+  pkg_api_remotes --> pkg_goal
+  pkg_api_remotes --> pkg_host_plugin_inventory
+  pkg_api_remotes --> pkg_invariants
+  pkg_api_remotes --> pkg_llm
+  pkg_api_remotes --> pkg_message_feedback
+  pkg_api_remotes --> pkg_session
+  pkg_api_remotes --> pkg_session_persistence
+  pkg_api_remotes --> pkg_settings
+  pkg_api_remotes --> pkg_typert_registry
+  pkg_client_runtime --> pkg_agent
+  pkg_client_runtime --> pkg_api_remotes
+  pkg_client_runtime --> pkg_attachment
+  pkg_client_runtime --> pkg_client_connection
+  pkg_client_runtime --> pkg_commands
+  pkg_client_runtime --> pkg_host_apiproxy
+  pkg_client_runtime --> pkg_invariants
+  pkg_client_runtime --> pkg_llm
+  pkg_client_runtime --> pkg_llm_retry
+  pkg_client_runtime --> pkg_session
+  pkg_client_runtime --> pkg_session_projection
+  pkg_client_runtime --> pkg_session_title
+  pkg_client_runtime --> pkg_tools
+  pkg_client_runtime --> pkg_typert_protocol
+  pkg_client_runtime --> pkg_typert_registry
+  pkg_client_ui_renderer --> pkg_client_runtime
+  pkg_client_ui_renderer --> pkg_invariants
+  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_invariants
+  pkg_client_ui_settings --> pkg_settings
   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_client_locale --> pkg_settings
+  pkg_client_test_runtime --> pkg_client_runtime
+  pkg_client_test_runtime --> pkg_client_ui_renderer
+  pkg_client_test_runtime --> pkg_client_ui_slots
+  pkg_client_test_runtime --> pkg_host_apiproxy
+  pkg_client_test_runtime --> pkg_invariants
   pkg_client_ui_input_trigger --> pkg_client_locale
   pkg_client_ui_input_trigger --> pkg_client_runtime
-  pkg_client_ui_input_trigger --> pkg_client_ui_primitives
-  pkg_client_ui_input_trigger --> pkg_client_ui_slots
   pkg_client_ui_input_trigger --> pkg_invariants
+  pkg_client_ui_settings_models --> pkg_api_remotes
+  pkg_client_ui_settings_models --> pkg_client_connection
+  pkg_client_ui_settings_models --> pkg_client_locale
+  pkg_client_ui_settings_models --> pkg_client_runtime
+  pkg_client_ui_settings_models --> pkg_client_ui_settings
+  pkg_client_ui_settings_models --> pkg_invariants
   pkg_client_ui_settings_plugin_inventory --> pkg_api_remotes
   pkg_client_ui_settings_plugin_inventory --> pkg_client_locale
   pkg_client_ui_settings_plugin_inventory --> pkg_client_runtime
-  pkg_client_ui_settings_plugin_inventory --> pkg_client_ui_primitives
   pkg_client_ui_settings_plugin_inventory --> pkg_client_ui_settings
-  pkg_client_ui_settings_plugin_inventory --> pkg_client_ui_slots
   pkg_client_ui_settings_plugin_inventory --> pkg_invariants
   pkg_client_ui_settings_plugins --> pkg_api_remotes
   pkg_client_ui_settings_plugins --> pkg_client_connection
   pkg_client_ui_settings_plugins --> pkg_client_locale
   pkg_client_ui_settings_plugins --> pkg_client_runtime
-  pkg_client_ui_settings_plugins --> pkg_client_ui_primitives
   pkg_client_ui_settings_plugins --> pkg_client_ui_settings
-  pkg_client_ui_settings_plugins --> pkg_client_ui_slots
-  pkg_client_ui_settings_plugins --> pkg_client_web_react
   pkg_client_ui_settings_plugins --> 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_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_compaction
-  pkg_client_ui_trajectory --> pkg_invariants
-  pkg_client_ui_trajectory --> pkg_tools
-  pkg_client_ui_user_questions --> pkg_api_remotes
-  pkg_client_ui_user_questions --> pkg_client_locale
-  pkg_client_ui_user_questions --> 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_ui_theme --> pkg_settings
+  pkg_client_ui_layout --> pkg_client_runtime
+  pkg_client_ui_layout --> pkg_client_ui_theme
+  pkg_client_ui_layout --> pkg_invariants
+  pkg_cordis_client_runner --> pkg_api_remotes
+  pkg_cordis_client_runner --> pkg_client_connection
+  pkg_cordis_client_runner --> pkg_client_modules
+  pkg_cordis_client_runner --> pkg_client_runtime
+  pkg_cordis_client_runner --> pkg_client_ui_theme
+  pkg_cordis_client_runner --> pkg_invariants
   pkg_client_ui_conversation --> pkg_agent
   pkg_client_ui_conversation --> pkg_api_remotes
   pkg_client_ui_conversation --> pkg_attachment
@@ -1237,97 +1229,67 @@ flowchart TD
   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_attachment
   pkg_client_ui_conversation --> pkg_client_ui_input_trigger
-  pkg_client_ui_conversation --> pkg_client_ui_primitives
+  pkg_client_ui_conversation --> pkg_client_ui_layout
   pkg_client_ui_conversation --> pkg_client_ui_settings
-  pkg_client_ui_conversation --> pkg_client_ui_slots
   pkg_client_ui_conversation --> pkg_commands
   pkg_client_ui_conversation --> pkg_compaction
+  pkg_client_ui_conversation --> pkg_goal
   pkg_client_ui_conversation --> pkg_invariants
   pkg_client_ui_conversation --> pkg_llm_retry
+  pkg_client_ui_conversation --> pkg_permission_presets
+  pkg_client_ui_conversation --> pkg_plan_mode
   pkg_client_ui_conversation --> pkg_session_stats
+  pkg_client_ui_conversation --> pkg_settings
   pkg_client_ui_conversation --> pkg_token_meter
+  pkg_client_ui_conversation --> pkg_tool_todo
   pkg_client_ui_conversation --> pkg_tools
-  pkg_client_ui_directory_picker_browse --> pkg_client_locale
-  pkg_client_ui_directory_picker_browse --> pkg_client_runtime
-  pkg_client_ui_directory_picker_browse --> pkg_client_ui_primitives
-  pkg_client_ui_directory_picker_browse --> pkg_client_ui_slots
-  pkg_client_ui_directory_picker_browse --> pkg_client_ui_workspace
-  pkg_client_ui_directory_picker_browse --> pkg_invariants
-  pkg_client_ui_directory_picker_native --> pkg_client_runtime
-  pkg_client_ui_directory_picker_native --> pkg_client_ui_slots
-  pkg_client_ui_directory_picker_native --> pkg_client_ui_workspace
-  pkg_client_ui_directory_picker_native --> pkg_invariants
-  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_cordis_client_runner --> pkg_api_remotes
-  pkg_cordis_client_runner --> pkg_client_connection
-  pkg_cordis_client_runner --> pkg_client_modules
-  pkg_cordis_client_runner --> pkg_client_runtime
-  pkg_cordis_client_runner --> pkg_client_ui_slots
-  pkg_cordis_client_runner --> pkg_client_ui_theme
-  pkg_cordis_client_runner --> pkg_invariants
+  pkg_client_ui_sidebar --> pkg_client_locale
+  pkg_client_ui_sidebar --> pkg_client_runtime
+  pkg_client_ui_sidebar --> pkg_client_ui_layout
+  pkg_client_ui_sidebar --> 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
   pkg_client_ui_agent_preset --> pkg_client_ui_conversation
-  pkg_client_ui_agent_preset --> pkg_client_ui_primitives
   pkg_client_ui_agent_preset --> pkg_client_ui_settings
-  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_attachment --> pkg_attachment
+  pkg_client_ui_attachment --> pkg_client_runtime
+  pkg_client_ui_attachment --> pkg_client_ui_conversation
+  pkg_client_ui_attachment --> pkg_invariants
   pkg_client_ui_commands --> pkg_api_remotes
   pkg_client_ui_commands --> pkg_client_locale
   pkg_client_ui_commands --> pkg_client_runtime
   pkg_client_ui_commands --> pkg_client_ui_conversation
   pkg_client_ui_commands --> pkg_client_ui_input_trigger
-  pkg_client_ui_commands --> pkg_client_ui_primitives
-  pkg_client_ui_commands --> pkg_client_ui_slots
   pkg_client_ui_commands --> pkg_commands
   pkg_client_ui_commands --> pkg_invariants
   pkg_client_ui_deliverables --> pkg_client_connection
   pkg_client_ui_deliverables --> pkg_client_locale
   pkg_client_ui_deliverables --> pkg_client_runtime
   pkg_client_ui_deliverables --> pkg_client_ui_conversation
-  pkg_client_ui_deliverables --> pkg_client_ui_slots
   pkg_client_ui_deliverables --> pkg_invariants
   pkg_client_ui_deliverables --> pkg_system_prompt
   pkg_client_ui_goal --> pkg_api_remotes
   pkg_client_ui_goal --> pkg_client_locale
   pkg_client_ui_goal --> pkg_client_runtime
   pkg_client_ui_goal --> pkg_client_ui_conversation
-  pkg_client_ui_goal --> pkg_client_ui_primitives
-  pkg_client_ui_goal --> pkg_client_ui_slots
   pkg_client_ui_goal --> pkg_commands
   pkg_client_ui_goal --> pkg_goal
   pkg_client_ui_goal --> pkg_invariants
+  pkg_client_ui_goal --> pkg_session
+  pkg_client_ui_goal --> pkg_typert_protocol
   pkg_client_ui_jobs --> pkg_client_locale
   pkg_client_ui_jobs --> pkg_client_runtime
   pkg_client_ui_jobs --> pkg_client_ui_conversation
-  pkg_client_ui_jobs --> pkg_client_ui_primitives
-  pkg_client_ui_jobs --> pkg_client_ui_slots
   pkg_client_ui_jobs --> pkg_invariants
   pkg_client_ui_message_feedback --> pkg_api_remotes
   pkg_client_ui_message_feedback --> pkg_client_connection
   pkg_client_ui_message_feedback --> pkg_client_locale
   pkg_client_ui_message_feedback --> pkg_client_runtime
   pkg_client_ui_message_feedback --> pkg_client_ui_conversation
-  pkg_client_ui_message_feedback --> pkg_client_ui_primitives
-  pkg_client_ui_message_feedback --> pkg_client_ui_slots
   pkg_client_ui_message_feedback --> pkg_invariants
   pkg_client_ui_message_feedback --> pkg_message_feedback
   pkg_client_ui_message_feedback --> pkg_typert_protocol
@@ -1335,16 +1297,20 @@ flowchart TD
   pkg_client_ui_plan --> pkg_client_locale
   pkg_client_ui_plan --> pkg_client_runtime
   pkg_client_ui_plan --> pkg_client_ui_conversation
-  pkg_client_ui_plan --> pkg_client_ui_primitives
-  pkg_client_ui_plan --> pkg_client_ui_slots
   pkg_client_ui_plan --> pkg_invariants
   pkg_client_ui_plan --> pkg_plan_mode
+  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_settings
+  pkg_client_ui_settings_general --> pkg_client_ui_sidebar
+  pkg_client_ui_settings_general --> pkg_invariants
+  pkg_client_ui_settings_general --> pkg_settings
   pkg_client_ui_subagent --> pkg_client_locale
   pkg_client_ui_subagent --> pkg_client_runtime
   pkg_client_ui_subagent --> pkg_client_ui_conversation
   pkg_client_ui_subagent --> pkg_client_ui_input_trigger
-  pkg_client_ui_subagent --> pkg_client_ui_primitives
-  pkg_client_ui_subagent --> pkg_client_ui_slots
   pkg_client_ui_subagent --> pkg_invariants
   pkg_client_ui_subagent --> pkg_subagent
   pkg_client_ui_subagent --> pkg_token_meter
@@ -1352,32 +1318,44 @@ flowchart TD
   pkg_client_ui_tool --> pkg_client_locale
   pkg_client_ui_tool --> pkg_client_runtime
   pkg_client_ui_tool --> pkg_client_ui_conversation
-  pkg_client_ui_tool --> pkg_client_ui_primitives
-  pkg_client_ui_tool --> pkg_client_ui_slots
   pkg_client_ui_tool --> 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_conversation
+  pkg_client_ui_trajectory --> pkg_compaction
+  pkg_client_ui_trajectory --> pkg_invariants
+  pkg_client_ui_trajectory --> pkg_tools
+  pkg_client_ui_user_questions --> pkg_api_remotes
+  pkg_client_ui_user_questions --> pkg_client_locale
+  pkg_client_ui_user_questions --> pkg_client_runtime
+  pkg_client_ui_user_questions --> pkg_client_ui_conversation
+  pkg_client_ui_user_questions --> pkg_invariants
   pkg_client_ui_workflow_run --> pkg_client_locale
   pkg_client_ui_workflow_run --> pkg_client_runtime
   pkg_client_ui_workflow_run --> pkg_client_ui_conversation
-  pkg_client_ui_workflow_run --> pkg_client_ui_primitives
-  pkg_client_ui_workflow_run --> pkg_client_ui_slots
   pkg_client_ui_workflow_run --> pkg_invariants
   pkg_client_ui_workflow_run --> pkg_session
   pkg_client_ui_workflow_run --> pkg_tool_workflow
   pkg_client_ui_workflow_run --> pkg_workflow
-  pkg_host_directory_picker_auto --> pkg_client_ui_directory_picker_browse
-  pkg_host_directory_picker_auto --> pkg_client_ui_directory_picker_native
-  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_workspace --> pkg_client_locale
+  pkg_client_ui_workspace --> pkg_client_runtime
+  pkg_client_ui_workspace --> pkg_client_ui_conversation
+  pkg_client_ui_workspace --> pkg_client_ui_sidebar
+  pkg_client_ui_workspace --> pkg_invariants
   pkg_session_log_export --> pkg_client_locale
   pkg_session_log_export --> pkg_client_runtime
   pkg_session_log_export --> pkg_client_ui_commands
   pkg_session_log_export --> pkg_client_ui_conversation
-  pkg_session_log_export --> pkg_client_ui_primitives
-  pkg_session_log_export --> pkg_client_ui_slots
   pkg_session_log_export --> pkg_commands
   pkg_session_log_export --> pkg_invariants
+  pkg_client_ui_directory_picker_browse --> pkg_client_locale
+  pkg_client_ui_directory_picker_browse --> pkg_client_runtime
+  pkg_client_ui_directory_picker_browse --> pkg_client_ui_workspace
+  pkg_client_ui_directory_picker_browse --> pkg_invariants
+  pkg_client_ui_directory_picker_native --> pkg_client_runtime
+  pkg_client_ui_directory_picker_native --> pkg_client_ui_workspace
+  pkg_client_ui_directory_picker_native --> pkg_invariants
   pkg_client_ui_model_selection --> pkg_api_remotes
   pkg_client_ui_model_selection --> pkg_client_connection
   pkg_client_ui_model_selection --> pkg_client_locale
@@ -1385,19 +1363,14 @@ flowchart TD
   pkg_client_ui_model_selection --> pkg_client_ui_commands
   pkg_client_ui_model_selection --> pkg_client_ui_conversation
   pkg_client_ui_model_selection --> pkg_client_ui_input_trigger
-  pkg_client_ui_model_selection --> pkg_client_ui_primitives
-  pkg_client_ui_model_selection --> pkg_client_ui_slots
   pkg_client_ui_model_selection --> pkg_invariants
   pkg_client_ui_permission_presets --> pkg_api_remotes
   pkg_client_ui_permission_presets --> pkg_client_connection
   pkg_client_ui_permission_presets --> pkg_client_locale
   pkg_client_ui_permission_presets --> pkg_client_runtime
-  pkg_client_ui_permission_presets --> pkg_client_schema_form
   pkg_client_ui_permission_presets --> pkg_client_ui_commands
   pkg_client_ui_permission_presets --> pkg_client_ui_input_trigger
-  pkg_client_ui_permission_presets --> pkg_client_ui_primitives
   pkg_client_ui_permission_presets --> pkg_client_ui_settings
-  pkg_client_ui_permission_presets --> pkg_client_ui_slots
   pkg_client_ui_permission_presets --> pkg_invariants
   pkg_client_ui_permission_presets --> pkg_permission_presets
   pkg_client_ui_skill --> pkg_api_remotes
@@ -1405,8 +1378,6 @@ flowchart TD
   pkg_client_ui_skill --> pkg_client_locale
   pkg_client_ui_skill --> pkg_client_runtime
   pkg_client_ui_skill --> pkg_client_ui_input_trigger
-  pkg_client_ui_skill --> pkg_client_ui_primitives
-  pkg_client_ui_skill --> pkg_client_ui_slots
   pkg_client_ui_skill --> pkg_client_ui_tool
   pkg_client_ui_skill --> pkg_invariants
   pkg_client_ui_cordis --> pkg_api_remotes
@@ -1414,12 +1385,16 @@ flowchart TD
   pkg_client_ui_cordis --> pkg_client_locale
   pkg_client_ui_cordis --> pkg_client_runtime
   pkg_client_ui_cordis --> pkg_client_ui_input_trigger
-  pkg_client_ui_cordis --> pkg_client_ui_primitives
   pkg_client_ui_cordis --> pkg_client_ui_sidebar
-  pkg_client_ui_cordis --> pkg_client_ui_slots
   pkg_client_ui_cordis --> pkg_client_ui_tool
   pkg_client_ui_cordis --> pkg_cordis_client_runner
   pkg_client_ui_cordis --> pkg_invariants
+  pkg_host_directory_picker_auto --> pkg_client_ui_directory_picker_browse
+  pkg_host_directory_picker_auto --> pkg_client_ui_directory_picker_native
+  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
 ```
 
 | Package | Group | Depends on |
@@ -1435,13 +1410,9 @@ flowchart TD
 | [`scope`](../packages/core/scope) | `core` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`cmdline`](../packages/boot/cmdline) | `boot` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`base`](../packages/bundle/base) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-modules`](../packages/client/modules) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-schema-form`](../packages/client/schema-form) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-attachment`](../packages/client/ui-attachment) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-primitives`](../packages/client/ui-primitives) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-ui-slots`](../packages/client/ui-slots) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`client-web`](../packages/client/web) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-web-react`](../packages/client/web-react) | `client` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`code-runtime`](../packages/code-runtime/code-runtime) | `code-runtime` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`code-runtime-python`](../packages/code-runtime/code-runtime-python) | `code-runtime` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`e2b`](../packages/e2b/e2b) | `e2b` | [`invariants`](../packages/runtime-diagnostics/invariants) |
@@ -1458,8 +1429,7 @@ flowchart TD
 | [`typert-protocol`](../packages/typert/protocol) | `typert` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`typert-registry`](../packages/typert/registry) | `typert` | [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`attachment`](../packages/attachment/attachment) | `attachment` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-connection`](../packages/client/connection) | `client` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-modules`](../packages/client/modules) | `client` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`credentials`](../packages/credentials/credentials) | `credentials` | [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`subprocess-e2b`](../packages/e2b/subprocess-e2b) | `e2b` | [`e2b`](../packages/e2b/e2b), [`invariants`](../packages/runtime-diagnostics/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`host-frontend-static`](../packages/host/frontend-static) | `host` | [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
@@ -1472,8 +1442,8 @@ flowchart TD
 | [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`typert-loader`](../packages/typert/loader) | `typert` | [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) |
 | [`llm`](../packages/llm/llm) | `llm` | [`attachment`](../packages/attachment/attachment), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`timeout`](../packages/util/timeout) |
-| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) |
 | [`attachment-local`](../packages/attachment/attachment-local) | `attachment` | [`attachment`](../packages/attachment/attachment), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-hmr`](../packages/client/hmr) | `client` | [`client-modules`](../packages/client/modules), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment) |
 | [`settings-file`](../packages/settings/settings-file) | `settings` | [`atomic-write`](../packages/util/atomic-write), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
 | [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
@@ -1590,7 +1560,6 @@ flowchart TD
 | [`tool-subagent-control`](../packages/subagent/tool-subagent-control) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
 | [`tool-subagent-report`](../packages/subagent/tool-subagent-report) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`hooks-claude-code`](../packages/hooks/hooks-claude-code) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) |
-| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
 | [`web-app`](../packages/bundle/web-app) | `bundle` | [`invariants`](../packages/runtime-diagnostics/invariants), [`shell-env`](../packages/shell/shell-env), [`system-prompt`](../packages/core/system-prompt) |
 | [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner) | `compaction` | [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`tool-cordis`](../packages/extensions/tool-cordis) | `extensions` | [`agent`](../packages/core/agent), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
@@ -1603,44 +1572,49 @@ flowchart TD
 | [`subagent-codex`](../packages/subagent/subagent-codex) | `subagent` | [`invariants`](../packages/runtime-diagnostics/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-in-process`](../packages/subagent/subagent-fork-in-process) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
 | [`subagent-spawn-in-process`](../packages/subagent/subagent-spawn-in-process) | `subagent` | [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver) |
-| [`client-runtime`](../packages/client/runtime) | `client` | [`api-remotes`](../packages/api/remotes), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry) |
+| [`client-connection`](../packages/client/connection) | `client` | [`attachment`](../packages/attachment/attachment), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`tools`](../packages/core/tools) |
 | [`compaction-basic`](../packages/compaction/compaction-basic) | `compaction` | [`agent`](../packages/core/agent), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`compaction-tool-result-pruner`](../packages/compaction/compaction-tool-result-pruner), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) |
 | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-instructions`](../packages/context/agent-instructions), [`agent-loop`](../packages/core/agent-loop), [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`home-paths`](../packages/util/home-paths), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs-local`](../packages/jobs/jobs-local), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`shell-env`](../packages/shell/shell-env), [`skill`](../packages/skill/skill), [`skill-filesystem`](../packages/skill/skill-filesystem), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/shell/tool-bash), [`tool-goal`](../packages/goal/tool-goal), [`tool-jobs`](../packages/jobs/tool-jobs), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) |
 | [`sdk-client`](../packages/sdk/client) | `sdk` | [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-protocol`](../packages/sdk/protocol), [`session`](../packages/core/session) |
 | [`sdk-jsonrpc-server`](../packages/sdk/server) | `sdk` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/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) |
 | [`subagent-dsh-sdk`](../packages/subagent/subagent-dsh-sdk) | `subagent` | [`agent`](../packages/core/agent), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`sdk-client`](../packages/sdk/client), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subprocess`](../packages/subprocess/subprocess) |
-| [`client-ui-settings`](../packages/client/ui-settings) | `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-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
-| [`client-ui-settings-models`](../packages/client/ui-settings-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/runtime-diagnostics/invariants) |
+| [`api-gateway`](../packages/api/gateway) | `api` | [`client-connection`](../packages/client/connection), [`invariants`](../packages/runtime-diagnostics/invariants), [`typert-registry`](../packages/typert/registry) |
 | [`acp-demo`](../packages/examples/acp-demo) | `examples` | [`acp`](../packages/acp/acp), [`agent-instructions`](../packages/context/agent-instructions), [`agent-spine-demo`](../packages/examples/agent-spine-demo), [`app-boot`](../packages/boot/app-boot), [`invariants`](../packages/runtime-diagnostics/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) |
-| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`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/runtime-diagnostics/invariants) |
-| [`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/runtime-diagnostics/invariants) |
-| [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `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/runtime-diagnostics/invariants) |
-| [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`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), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `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), [`client-web-react`](../packages/client/web-react), [`invariants`](../packages/runtime-diagnostics/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/runtime-diagnostics/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/runtime-diagnostics/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), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`tools`](../packages/core/tools) |
-| [`client-ui-user-questions`](../packages/client/ui-user-questions) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`invariants`](../packages/runtime-diagnostics/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/runtime-diagnostics/invariants) |
-| [`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-attachment`](../packages/client/ui-attachment), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm-retry`](../packages/llm/llm-retry), [`session-stats`](../packages/session/session-stats), [`token-meter`](../packages/llm/token-meter), [`tools`](../packages/core/tools) |
-| [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse) | `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), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`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/runtime-diagnostics/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/runtime-diagnostics/invariants) |
-| [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-runtime`](../packages/client/runtime), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/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/runtime-diagnostics/invariants) |
-| [`client-ui-commands`](../packages/client/ui-commands) | `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-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-deliverables`](../packages/client/ui-deliverables) | `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-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt) |
-| [`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/runtime-diagnostics/invariants) |
-| [`client-ui-jobs`](../packages/client/ui-jobs) | `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/runtime-diagnostics/invariants) |
-| [`client-ui-message-feedback`](../packages/client/ui-message-feedback) | `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-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback), [`typert-protocol`](../packages/typert/protocol) |
-| [`client-ui-plan`](../packages/client/ui-plan) | `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), [`invariants`](../packages/runtime-diagnostics/invariants), [`plan-mode`](../packages/plan/plan-mode) |
-| [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter) |
-| [`client-ui-tool`](../packages/client/ui-tool) | `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), [`invariants`](../packages/runtime-diagnostics/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/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
+| [`api-remotes`](../packages/api/remotes) | `api` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`api-gateway`](../packages/api/gateway), [`commands`](../packages/interaction/commands), [`cordis-host-runner`](../packages/extensions/cordis-host-runner), [`credentials`](../packages/credentials/credentials), [`goal`](../packages/goal/goal), [`host-plugin-inventory`](../packages/host/plugin-inventory), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`message-feedback`](../packages/feedback/message-feedback), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`settings`](../packages/settings/settings), [`typert-registry`](../packages/typert/registry) |
+| [`client-runtime`](../packages/client/runtime) | `client` | [`agent`](../packages/core/agent), [`api-remotes`](../packages/api/remotes), [`attachment`](../packages/attachment/attachment), [`client-connection`](../packages/client/connection), [`commands`](../packages/interaction/commands), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`llm-retry`](../packages/llm/llm-retry), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-title`](../packages/session/session-title), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`typert-registry`](../packages/typert/registry) |
+| [`client-ui-renderer`](../packages/client/ui-renderer) | `client` | [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-settings`](../packages/client/ui-settings) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
+| [`client-locale`](../packages/client/locale) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
+| [`client-test-runtime`](../packages/test-support/client-runtime) | `test-support` | [`client-runtime`](../packages/client/runtime), [`client-ui-renderer`](../packages/client/ui-renderer), [`client-ui-slots`](../packages/client/ui-slots), [`host-apiproxy`](../packages/host/apiproxy), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-input-trigger`](../packages/client/ui-input-trigger) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-settings-models`](../packages/client/ui-settings-models) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-settings-plugin-inventory`](../packages/client/ui-settings-plugin-inventory) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-settings-plugins`](../packages/client/ui-settings-plugins) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/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-settings`](../packages/client/ui-settings), [`host-webserver`](../packages/host/webserver), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
+| [`client-ui-layout`](../packages/client/ui-layout) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`cordis-client-runner`](../packages/extensions/cordis-client-runner) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-modules`](../packages/client/modules), [`client-runtime`](../packages/client/runtime), [`client-ui-theme`](../packages/client/ui-theme), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`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-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-layout`](../packages/client/ui-layout), [`client-ui-settings`](../packages/client/ui-settings), [`commands`](../packages/interaction/commands), [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`session-stats`](../packages/session/session-stats), [`settings`](../packages/settings/settings), [`token-meter`](../packages/llm/token-meter), [`tool-todo`](../packages/todo/tool-todo), [`tools`](../packages/core/tools) |
+| [`client-ui-sidebar`](../packages/client/ui-sidebar) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-layout`](../packages/client/ui-layout), [`invariants`](../packages/runtime-diagnostics/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-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-attachment`](../packages/client/ui-attachment) | `client` | [`attachment`](../packages/attachment/attachment), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-commands`](../packages/client/ui-commands) | `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-input-trigger`](../packages/client/ui-input-trigger), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-deliverables`](../packages/client/ui-deliverables) | `client` | [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/invariants), [`system-prompt`](../packages/core/system-prompt) |
+| [`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), [`commands`](../packages/interaction/commands), [`goal`](../packages/goal/goal), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`typert-protocol`](../packages/typert/protocol) |
+| [`client-ui-jobs`](../packages/client/ui-jobs) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-message-feedback`](../packages/client/ui-message-feedback) | `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), [`invariants`](../packages/runtime-diagnostics/invariants), [`message-feedback`](../packages/feedback/message-feedback), [`typert-protocol`](../packages/typert/protocol) |
+| [`client-ui-plan`](../packages/client/ui-plan) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/invariants), [`plan-mode`](../packages/plan/plan-mode) |
+| [`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-settings`](../packages/client/ui-settings), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants), [`settings`](../packages/settings/settings) |
+| [`client-ui-subagent`](../packages/client/ui-subagent) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`invariants`](../packages/runtime-diagnostics/invariants), [`subagent`](../packages/subagent/subagent), [`token-meter`](../packages/llm/token-meter) |
+| [`client-ui-tool`](../packages/client/ui-tool) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/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-conversation`](../packages/client/ui-conversation), [`compaction`](../packages/compaction/compaction), [`invariants`](../packages/runtime-diagnostics/invariants), [`tools`](../packages/core/tools) |
+| [`client-ui-user-questions`](../packages/client/ui-user-questions) | `client` | [`api-remotes`](../packages/api/remotes), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`invariants`](../packages/runtime-diagnostics/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), [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`tool-workflow`](../packages/workflow/tool-workflow), [`workflow`](../packages/workflow/workflow) |
+| [`client-ui-workspace`](../packages/client/ui-workspace) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`session-log-export`](../packages/session-query/session-log-export) | `session-query` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-conversation`](../packages/client/ui-conversation), [`commands`](../packages/interaction/commands), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse) | `client` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native) | `client` | [`client-runtime`](../packages/client/runtime), [`client-ui-workspace`](../packages/client/ui-workspace), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-model-selection`](../packages/client/ui-model-selection) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-permission-presets`](../packages/client/ui-permission-presets) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-settings`](../packages/client/ui-settings), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) |
+| [`client-ui-skill`](../packages/client/ui-skill) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-tool`](../packages/client/ui-tool), [`invariants`](../packages/runtime-diagnostics/invariants) |
+| [`client-ui-cordis`](../packages/extensions/ui-cordis) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`client-ui-tool`](../packages/client/ui-tool), [`cordis-client-runner`](../packages/extensions/cordis-client-runner), [`invariants`](../packages/runtime-diagnostics/invariants) |
 | [`host-directory-picker-auto`](../packages/host/directory-picker-auto) | `host` | [`client-ui-directory-picker-browse`](../packages/client/ui-directory-picker-browse), [`client-ui-directory-picker-native`](../packages/client/ui-directory-picker-native), [`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/runtime-diagnostics/invariants) |
-| [`session-log-export`](../packages/session-query/session-log-export) | `session-query` | [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-commands`](../packages/client/ui-commands), [`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), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-model-selection`](../packages/client/ui-model-selection) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-commands`](../packages/client/ui-commands), [`client-ui-conversation`](../packages/client/ui-conversation), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-permission-presets`](../packages/client/ui-permission-presets) | `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-commands`](../packages/client/ui-commands), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-settings`](../packages/client/ui-settings), [`client-ui-slots`](../packages/client/ui-slots), [`invariants`](../packages/runtime-diagnostics/invariants), [`permission-presets`](../packages/interaction/permission-presets) |
-| [`client-ui-skill`](../packages/client/ui-skill) | `client` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-tool`](../packages/client/ui-tool), [`invariants`](../packages/runtime-diagnostics/invariants) |
-| [`client-ui-cordis`](../packages/extensions/ui-cordis) | `extensions` | [`api-remotes`](../packages/api/remotes), [`client-connection`](../packages/client/connection), [`client-locale`](../packages/client/locale), [`client-runtime`](../packages/client/runtime), [`client-ui-input-trigger`](../packages/client/ui-input-trigger), [`client-ui-primitives`](../packages/client/ui-primitives), [`client-ui-sidebar`](../packages/client/ui-sidebar), [`client-ui-slots`](../packages/client/ui-slots), [`client-ui-tool`](../packages/client/ui-tool), [`cordis-client-runner`](../packages/extensions/cordis-client-runner), [`invariants`](../packages/runtime-diagnostics/invariants) |

+ 2 - 2
docs/subsystems/client-modules.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/client-modules.md
-client-modules.md: c2002b847cd15560ca4493ce6e8623667c395360
-client-modules.zh.md: 88be252a7fdff6bca7e2d8eb4e37b9e1a75dda98
+client-modules.md: 52fa4ef73766e8e7e7601e604f78aded9940328f
+client-modules.zh.md: e830349be77000759684a76b2834478a803e3e11

+ 11 - 3
docs/subsystems/client-modules.md

@@ -16,7 +16,9 @@ The graph is the wire single source between the Node and browser halves: the hos
  * single source: the host node half (package root) produces this same shape.
  * `immediately` marks stage-one prefetch; `inject` is informational graph
  * metadata (the authoritative edges live in each package's `dsh.client`
- * declaration and reach fibers through entry creation).
+ * declaration and reach fibers through entry creation). `external` carries
+ * module-graph edges: unlike `inject`, they constrain code arrival because
+ * `require` is synchronous (see {@link WebBootGraph.entries}).
  */
 interface WebBootEntry {
   /** Entry name == package name. */
@@ -29,6 +31,8 @@ interface WebBootEntry {
   inject?: string[]
   /** Stage-one prefetch mark: load the script for factory registration during module-face boot. */
   immediately?: boolean
+  /** Non-baseline module specifiers this row requests; omitted when it requests none. */
+  external?: string[]
 }
 ```
 
@@ -37,7 +41,11 @@ interface WebBootEntry {
 interface WebBootGraph {
   /** Consistency anchor over the whole graph (content + bundle hashes). */
   rev: string
-  /** Composed entries; order carries no semantics (activation order is fiber inject waiting). */
+  /**
+   * Composed entries in module-graph order — a dynamic package row precedes
+   * rows whose `external` requests that package. Cordis activation order is
+   * unrelated and remains owned by fiber service waiting.
+   */
   entries: WebBootEntry[]
 }
 ```
@@ -114,5 +122,5 @@ onRebuilt(listener: (id: string, rev: string) => void): () => void
 onGraphChanged(listener: () => void): () => void
 ```
 
-Source: [`packages/client/modules/src/index.ts:184`](../../packages/client/modules/src/index.ts)
+Source: [`packages/client/modules/src/index.ts:295`](../../packages/client/modules/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 11 - 3
docs/subsystems/client-modules.zh.md

@@ -16,7 +16,9 @@ Web 插件表:[dsh-client-modules](../../packages/client/modules) 中 client 
  * single source: the host node half (package root) produces this same shape.
  * `immediately` marks stage-one prefetch; `inject` is informational graph
  * metadata (the authoritative edges live in each package's `dsh.client`
- * declaration and reach fibers through entry creation).
+ * declaration and reach fibers through entry creation). `external` carries
+ * module-graph edges: unlike `inject`, they constrain code arrival because
+ * `require` is synchronous (see {@link WebBootGraph.entries}).
  */
 interface WebBootEntry {
   /** Entry name == package name. */
@@ -29,6 +31,8 @@ interface WebBootEntry {
   inject?: string[]
   /** Stage-one prefetch mark: load the script for factory registration during module-face boot. */
   immediately?: boolean
+  /** Non-baseline module specifiers this row requests; omitted when it requests none. */
+  external?: string[]
 }
 ```
 
@@ -37,7 +41,11 @@ interface WebBootEntry {
 interface WebBootGraph {
   /** Consistency anchor over the whole graph (content + bundle hashes). */
   rev: string
-  /** Composed entries; order carries no semantics (activation order is fiber inject waiting). */
+  /**
+   * Composed entries in module-graph order — a dynamic package row precedes
+   * rows whose `external` requests that package. Cordis activation order is
+   * unrelated and remains owned by fiber service waiting.
+   */
   entries: WebBootEntry[]
 }
 ```
@@ -114,5 +122,5 @@ onRebuilt(listener: (id: string, rev: string) => void): () => void
 onGraphChanged(listener: () => void): () => void
 ```
 
-Source: [`packages/client/modules/src/index.ts:184`](../../packages/client/modules/src/index.ts)
+Source: [`packages/client/modules/src/index.ts:295`](../../packages/client/modules/src/index.ts)
 <!-- END GENERATED cordis-surface -->

+ 9 - 14
knip.json

@@ -209,16 +209,6 @@
         "tests/**/*.tsx"
       ]
     },
-    "packages/client/web-react": {
-      "entry": [
-        "tests/**/*.spec.tsx"
-      ],
-      "project": [
-        "src/**/*.ts",
-        "src/**/*.tsx",
-        "tests/**/*.tsx"
-      ]
-    },
     "packages/client/ui-layout": {
       "entry": [
         "tests/**/*.spec.ts",
@@ -696,13 +686,19 @@
       ]
     },
     "packages/client/web": {
+      "project": [
+        "src/**/*.ts",
+        "tests/**/*.ts"
+      ]
+    },
+    "packages/client/ui-renderer": {
+      "entry": [
+        "tests/**/*.spec.tsx"
+      ],
       "project": [
         "src/**/*.ts",
         "src/**/*.tsx",
         "tests/**/*.tsx"
-      ],
-      "ignoreDependencies": [
-        "@deepseek-ai/dsh-client-ui-theme"
       ]
     },
     "packages/client/ui-settings": {
@@ -729,7 +725,6 @@
       "ignoreDependencies": [
         "@deepseek-ai/dsh-client-ui-primitives",
         "@deepseek-ai/dsh-client-ui-slots",
-        "@deepseek-ai/dsh-client-web-react",
         "@types/react",
         "@types/react-dom",
         "react",

+ 2 - 1
package.json

@@ -97,6 +97,7 @@
     "verify-node-next-types": "tsx scripts/verify-node-next-types.ts",
     "verify-optional-dependency-imports": "tsx scripts/verify-optional-dependency-imports.ts",
     "verify-runtime-closure": "tsx scripts/verify-runtime-closure.ts",
+    "verify-client-packages": "tsx scripts/verify-client-packages.ts",
     "verify-vendored-links": "tsx scripts/verify-vendored-links.ts",
     "verify-cordis-config": "tsx scripts/verify-cordis-config.ts",
     "rescope-vendor": "tsx scripts/rescope-vendor.ts",
@@ -126,7 +127,7 @@
     "verify-module-graph": "tsx scripts/gen-module-graph.ts --check",
     "constraints": "tsx scripts/check-workspace-constraints.ts",
     "doc-sync": "tsx scripts/run-gates.ts doc-sync",
-    "hygiene": "pnpm run rescope-vendor:check && pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-dsh-package-licenses && pnpm run verify-package-invariants && pnpm run verify-built-package-invariants && pnpm run verify-cordis-config && pnpm run verify-node-next-types && pnpm run verify-optional-dependency-imports && pnpm run verify-runtime-closure && pnpm run verify-vendored-links",
+    "hygiene": "pnpm run rescope-vendor:check && pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-dsh-package-licenses && pnpm run verify-package-invariants && pnpm run verify-built-package-invariants && pnpm run verify-cordis-config && pnpm run verify-node-next-types && pnpm run verify-optional-dependency-imports && pnpm run verify-runtime-closure && pnpm run verify-client-packages && pnpm run verify-vendored-links",
     "publish:npm-baseline": "tsx scripts/publish-npm-baseline.ts",
     "release:dsh": "tsx scripts/release/bump.ts --family dsh",
     "release:vendor": "tsx scripts/release/bump.ts --family vendor",

+ 6 - 0
packages/bundle/web-app/cordis.patch.yml

@@ -180,6 +180,9 @@
     - id: ui-layout
       name: '@deepseek-ai/dsh-client-ui-layout'
 
+    - id: ui-renderer
+      name: '@deepseek-ai/dsh-client-ui-renderer'
+
     - id: ui-sidebar
       name: '@deepseek-ai/dsh-client-ui-sidebar'
 
@@ -198,6 +201,9 @@
     - id: ui-conversation
       name: '@deepseek-ai/dsh-client-ui-conversation'
 
+    - id: ui-attachment
+      name: '@deepseek-ai/dsh-client-ui-attachment'
+
     # Tool call tree, generic fallback, and keyed business Tool views.
     - id: ui-tool
       name: '@deepseek-ai/dsh-client-ui-tool'

+ 2 - 0
packages/bundle/web-app/package.json

@@ -51,8 +51,10 @@
     "@deepseek-ai/dsh-client-hmr": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-modules": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
     "@deepseek-ai/dsh-client-runtime": "workspace:^",
     "@deepseek-ai/dsh-client-ui-agent-preset": "workspace:^",
+    "@deepseek-ai/dsh-client-ui-attachment": "workspace:^",
     "@deepseek-ai/dsh-client-ui-commands": "workspace:^",
     "@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
     "@deepseek-ai/dsh-client-ui-cordis": "workspace:^",

+ 41 - 2
packages/client/AGENTS.md

@@ -44,7 +44,7 @@ The `/client` entrypoint of a UI plugin package is its public browser API, not a
 The stack has one-way knowledge, settled in the [web client architecture note](../../.agents/notes/implemented/architecture/2026-07-19-gui-web-client-architecture.md):
 
 1. **Data object layer** (`runtime`, React-free): `ConnectionController` → `SessionManager` → `Session` own all business state (event windows, streaming accumulation, reconnect machine), and the snapshot-store engine (zustand/immer, `defineStore`, `shallowEqual`) lives here too — store products are bare observable sources with no hook members. Zero React imports — grep-assertable.
-2. **Render machinery** (`web-react`, shell-only glue): all ctx-to-React integration — slot renderer/outlets, `SessionProvider`, and the uSES adapter. Every hook is composed here at the binding site from bare sources; business plugin packages carry no web-react dependency at all.
+2. **Render machinery** (`ui-renderer`, dynamic plugin): all ctx-to-React integration — slot renderer/outlets, `SessionProvider`, and the uSES adapter. Every hook is composed here at the binding site from bare sources; production business code carries no ui-renderer value dependency.
 3. **Presentation components** (plugin packages' `src/client/`, pure props): consumables, expected to be rewritten wholesale. Business logic must not leak into them; everything arrives through the four props shares.
 
 Non-negotiables across the layers:
@@ -54,6 +54,44 @@ Non-negotiables across the layers:
 - **Notifier publication discipline**: `notifyNow` is only the direct echo of a user gesture; structural updates use microtask-batched `markDirty`, while visible streaming chunks use cumulative `markFrameDirty`. See `runtime/src/client/sessions/notifier.ts`.
 - **The web layer is pure presentation.** Nothing that is "how to draw" (tool-card views, queue states) enters the session log; the host computes such data per frame or pushes it live, and replay recomputes it — falling back to the generic form when it can't. A new *model-visible* input still requires a session event (repo-wide rule).
 
+## Dependency declaration
+
+Npm sections describe installation and development relationships; each build face independently decides what its artifact contains. [`verify-client-packages`](../../scripts/verify-client-packages.ts) checks the client-specific rules and can repair unambiguous manifest drift with `--fix`.
+
+1. **Every client package keeps Cordis in matching `peerDependencies` and `devDependencies`.** This includes the static packages because their Node face participates in the same Cordis plugin contract.
+2. **A dynamic package declares internal dynamic relationships as peer plus dev.** Production source imports, re-exports, module augmentations, and type-only references to an `@deepseek-ai/dsh-*` package count, as does a package named by `dsh.client.inject`. A test-only internal dependency stays dev-only.
+3. **Static client inputs are dev-only for a dynamic consumer.** A package without `dsh.client`, plus the React modules seeded by the web shell, belongs only in the consumer's `devDependencies`; it never belongs in that dynamic package's `dependencies` or `peerDependencies`. `packages/client/web` likewise keeps Loader, modules, and static UI inputs as development inputs; Cordis remains peer plus dev.
+4. **Ordinary installed libraries stay in `dependencies`.** This includes private implementation libraries bundled into `lib/client.js` and bare imports left in a statically linked `lib/index.js`; the final Vite host, not the library build, merges and splits the latter. A dynamic package never puts an `@deepseek-ai/dsh-*` package in `dependencies`.
+5. **Every peer has a matching development range.** npm dependency and peer cycles are allowed; only the synchronous module-request graph has the separate acyclicity rule below.
+6. **Browser and Node build faces declare externality independently.** A dynamic browser half uses the baseline plus `dsh.client.external`; a statically linked face externalizes every bare specifier; a Node face externalizes its production dependencies ([`tsdown.client.ts`](tsdown.client.ts)). Moving a name between npm sections must not silently change bundle contents.
+7. **Keep the published payload closed.** Every relative runtime import and emitted asset must be covered by `files`; the repository publint pass checks the exact publication view.
+
+## Shared modules and the module graph
+
+A dynamic browser half either carries a module privately or requests the shared module-table identity. The client baseline is centralized in [`web/src/platform.ts`](web/src/platform.ts): `PLATFORM_MODULES` names shell-seeded React, Cordis, and static UI libraries; `PRELOADED_CLIENT_EXTERNALS` names dynamic rows, currently runtime, whose ordinary `lib/client.js` factory arrives before shell boot.
+
+1. **Baseline externals are implicit for every dynamic bundle.** Do not repeat React, Cordis, runtime, `ui-primitives`, or `ui-slots` in package manifests.
+2. **`dsh.client.external` adds a package-specific request.** Use it only for a non-baseline value import whose dynamic row must be materialized through the module table. Declare the exact import specifier; only a trailing `/client` aliases the package row.
+3. **Silence means a private copy.** Ordinary third-party implementation libraries may be bundled independently. A value reached only through `import type` is erased and creates no request.
+4. **A request has two possible suppliers.** A dynamic package supplies its own row; `PLATFORM_MODULES` supplies an exact static-table key. There is no `dsh.client.provide` alias protocol.
+5. **Validate both sides.** The dynamic build preset externalizes the baseline and rejects undeclared workspace value imports; [`verify-client-packages`](../../scripts/verify-client-packages.ts) rejects malformed or redundant requests, missing suppliers, and synchronous request cycles.
+
+### The module graph sits below cordis DI
+
+Three declarations read like dependency edges and none is interchangeable: Cordis service `inject`, module-graph `external`, and `dsh.client.inject` — the informational package-name edges of the [new-package checklist](#new-plugin-package-checklist).
+
+| | Cordis service `inject` | module graph `external` |
+|---|---|---|
+| Unit | service name | module specifier |
+| Timing | runtime; the fiber waits | materialization; the `require` handed to a factory is synchronous and cannot wait |
+| Unsatisfied | stays PENDING, with no timeout | throws on the spot |
+| Who may satisfy it | any plugin providing that service, replaceable | the single module identity, not replaceable |
+| Cycles | allowed | rejected |
+
+The seam is `loader.internal = modules`: cordis reaches plugin code through `EntryTree.import`, so every module request must be satisfiable before cordis can order activation above it. The modules node half emits rows in topological order, and `ClientModuleSystem.import`/`prefetch` recursively registers dynamic provider factories before their consumers materialize. This module order is independent from Cordis activation: a provider that injects services can register first and activate last.
+
+`packages/client/web` is not a Loader entry. Its static imports seed `PLATFORM_MODULES`; parser-preloaded dynamic rows remain ordinary Loader entries and ordinary `lib/client.js` artifacts.
+
 ## Conversation Node discipline
 
 - A Chat business feature registers one `ConversationNodeDefinition` and its keyed `conversation.chat.node` renderer; do not add its event switch or fold to `Session`, `SessionManager`, or a central built-in dispatcher. Follow the [Conversation Node cookbook](../../docs/cookbook/adding-a-conversation-node.md).
@@ -93,9 +131,10 @@ Bringing up a new `packages/client/<name>` plugin package (ui-workspace is a com
 
 1. **Package skeleton**: `package.json` (`@deepseek-ai/dsh-client-<name>`, exports `.`/`./invariant`/`./client`/`./src/*`/`./package.json`, `dsh.client` manifest, `files` list), `tsconfig.json` (extends `tsconfig.base.client.json`, one `references` entry per workspace dependency plus `runtime-diagnostics/invariants`), `tsdown.config.ts` (`clientBundle(id, ['lib/types/index.js', 'lib/types/invariant.js'])`), `src/index.ts` (empty node-half apply), `src/invariant.ts` (companion with a real reason), `src/css-modules.d.ts` when using CSS Modules, `README.md` with the Model Experience section.
 2. **Three registration surfaces, all required** (missing any one fails at a different, later point): the `tsconfig.client.json` aggregate `references` entry; a `dsh.client` row in `packages/bundle/web-app/cordis.patch.yml`; a `packages/bundle/web-app/package.json` dependency (profile boots resolve bare row names through the healed `$DSH_HOME/profiles/node_modules` fallback, which mirrors the app's and each bundle's declared dependencies — a row whose package no manifest declares fails to import). `pnpm-workspace.yaml` already globs `packages/*/*`.
-3. **dsh.client manifest semantics**: `platform: 'web'` always; `immediately: true` only for stage-one-prefetch infrastructure rows. `inject` lists package-name dependency edges — they are **informational only** (preflight display, HMR diffing); they do not sequence entry activation or apply order. Activation order is cordis fiber inject waiting on *services*, nothing else.
+3. **dsh.client manifest semantics**: `platform: 'web'` always, and the declaration requires a `./client` export (the scan throws without one); `immediately: true` only for stage-one-prefetch infrastructure rows. `inject` lists package-name dependency edges — they are **informational only** (preflight display, HMR diffing); they do not sequence entry activation or apply order. Activation order is Cordis fiber inject waiting on *services*, nothing else. A non-baseline `external` request sequences its dynamic supplier ahead of the consumer — see [shared modules](#shared-modules-and-the-module-graph).
 4. **Registering into another package's slot**: apply order is unconstrained, and a business service is not a declaration barrier. Use `ctx.slots.inject(name, () => ctx.slots.register(...))`; it waits on the actual declaration, removes the contribution when that declaration collapses, reruns after redeclaration, and leaves with the caller's plugin fiber. Return a generator yielding each registration when several contributions must install and roll back atomically. A bare `slots.register` into an undeclared slot remains an error; keep service edges only for services the contribution actually reads.
 5. Rebuild the bundle (`pnpm --filter <pkg> bundle`) before probing a live `dsh web` server — the registry serves `lib/client.js`, not sources.
+6. **Declaration decisions**, each settled by [dependency declaration](#dependency-declaration) and [shared modules](#shared-modules-and-the-module-graph): does the package ship a `./client` export; which non-baseline value imports require `dsh.client.external`; which dynamic value dependencies are peer plus dev; which static compile inputs are dev-only; and whether `files` covers every relative runtime import and emitted asset.
 
 ## New component checklist
 

+ 2 - 2
packages/client/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/README.md
-README.md: f22ae99113f7f313c60304ac4c5fc53c8e3172ab
-README.zh.md: c4aa20c9f5f317bbe5dbb6f6731f6eb6d5d7ac7c
+README.md: cff74ddb048df8b37643f4c44f01e2bda4160a77
+README.zh.md: 5e5d623509b9152fb7b59ea228cfa42720b737af

+ 2 - 3
packages/client/README.md

@@ -7,18 +7,17 @@ The browser side of the dsh web GUI: shell boot, browser-host communication, sha
 | Package | Purpose |
 |---|---|
 | [`web/`](web/README.md) | Boots the browser shell from the client entry graph. |
+| [`ui-renderer/`](ui-renderer/README.md) | Binds slot data to React and mounts the assembled application after client boot settles. |
 | [`modules/`](modules/README.md) | Loads browser-side client modules. |
-| [`web-react/`](web-react/README.md) | Connects the shell runtime to React rendering. |
 | [`connection/`](connection/README.md) | Maintains browser-host RPC communication and event delivery. |
 | [`runtime/`](runtime/README.md) | Provides shared client services for sessions, workspaces, and UI composition. |
 | [`hmr/`](hmr/README.md) | Refreshes client plugins during development. |
 | [`locale/`](locale/README.md) | Provides localization preferences and message dictionaries. |
-| [`schema-form/`](schema-form/README.md) | Provides schema-backed draft handling for settings editors. |
 | [`test-runtime/`](../test-support/client-runtime/README.md) | Provides shared repository test support for client feature packages. |
 | [`ui-slots/`](ui-slots/README.md) | Defines how UI features register and compose extension slots. |
 | [`ui-theme/`](ui-theme/README.md) | Applies the selected color theme. |
 | [`ui-primitives/`](ui-primitives/README.md) | Provides shared React controls, icons, and content renderers. |
-| [`ui-attachment/`](ui-attachment/README.md) | Provides attachment display atoms: draft-image rail, message gallery, and lightbox. |
+| [`ui-attachment/`](ui-attachment/README.md) | Registers composer and message-image attachment presentation. |
 | [`ui-layout/`](ui-layout/README.md) | Arranges the main application regions. |
 | [`ui-sidebar/`](ui-sidebar/README.md) | Presents workspace and session navigation. |
 | [`ui-workspace/`](ui-workspace/README.md) | Provides workspace selection and creation surfaces. |

+ 2 - 3
packages/client/README.zh.md

@@ -7,18 +7,17 @@ dsh web GUI 的浏览器侧:shell 启动、浏览器与宿主通信、共享 U
 | 包 | 目的 |
 |---|---|
 | [`web/`](web/README.md) | 从客户端条目图启动浏览器 shell。 |
+| [`ui-renderer/`](ui-renderer/README.md) | 将 slot 数据绑定到 React,并在客户端启动稳定后挂载组装完成的应用。 |
 | [`modules/`](modules/README.md) | 加载浏览器侧客户端模块。 |
-| [`web-react/`](web-react/README.md) | 连接 shell 运行时与 React 渲染。 |
 | [`connection/`](connection/README.md) | 维护浏览器与宿主之间的 RPC 通信和事件传递。 |
 | [`runtime/`](runtime/README.md) | 为会话、工作区和 UI 组合提供共享客户端服务。 |
 | [`hmr/`](hmr/README.md) | 在开发期间刷新客户端插件。 |
 | [`locale/`](locale/README.md) | 提供本地化偏好与消息词典。 |
-| [`schema-form/`](schema-form/README.md) | 为设置编辑器提供 schema 驱动的草稿处理。 |
 | [`test-runtime/`](../test-support/client-runtime/README.md) | 为客户端功能包提供共享的仓库测试支持。 |
 | [`ui-slots/`](ui-slots/README.md) | 定义 UI 功能注册和组合扩展 slot 的方式。 |
 | [`ui-theme/`](ui-theme/README.md) | 应用所选颜色主题。 |
 | [`ui-primitives/`](ui-primitives/README.md) | 提供共享 React 控件、图标和内容渲染器。 |
-| [`ui-attachment/`](ui-attachment/README.md) | 提供附件展示原子组件:草稿图片栏、消息画廊与灯箱。 |
+| [`ui-attachment/`](ui-attachment/README.md) | 注册输入框与消息图片的附件呈现。 |
 | [`ui-layout/`](ui-layout/README.md) | 排列应用的主要区域。 |
 | [`ui-sidebar/`](ui-sidebar/README.md) | 展示工作区与会话导航。 |
 | [`ui-workspace/`](ui-workspace/README.md) | 提供工作区选择与创建界面。 |

+ 14 - 8
packages/client/connection/package.json

@@ -38,12 +38,6 @@
   },
   "license": "MIT",
   "dependencies": {
-    "@deepseek-ai/dsh-attachment": "workspace:^",
-    "@deepseek-ai/dsh-host-apiproxy": "workspace:^",
-    "@deepseek-ai/dsh-commands": "workspace:^",
-    "@deepseek-ai/dsh-llm": "workspace:^",
-    "@deepseek-ai/dsh-session": "workspace:^",
-    "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/schemastery": "workspace:^",
     "ws": "^8.21.0"
   },
@@ -56,12 +50,24 @@
   "peerDependencies": {
     "@deepseek-ai/dsh-host-webserver": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^"
+    "@deepseek-ai/cordis": "workspace:^",
+    "@deepseek-ai/dsh-attachment": "workspace:^",
+    "@deepseek-ai/dsh-host-apiproxy": "workspace:^",
+    "@deepseek-ai/dsh-commands": "workspace:^",
+    "@deepseek-ai/dsh-llm": "workspace:^",
+    "@deepseek-ai/dsh-session": "workspace:^",
+    "@deepseek-ai/dsh-tools": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-host-webserver": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@types/ws": "^8.18.1",
-    "@deepseek-ai/cordis": "workspace:^"
+    "@deepseek-ai/cordis": "workspace:^",
+    "@deepseek-ai/dsh-attachment": "workspace:^",
+    "@deepseek-ai/dsh-host-apiproxy": "workspace:^",
+    "@deepseek-ai/dsh-commands": "workspace:^",
+    "@deepseek-ai/dsh-llm": "workspace:^",
+    "@deepseek-ai/dsh-session": "workspace:^",
+    "@deepseek-ai/dsh-tools": "workspace:^"
   }
 }

+ 5 - 5
packages/client/locale/package.json

@@ -47,25 +47,25 @@
     "@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:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/dsh-settings": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/dsh-api-remotes": "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-invariants": "workspace:^",
     "@types/react": "~18.3.1",
-    "react": "^18.2.0"
+    "react": "^18.2.0",
+    "@deepseek-ai/dsh-client-connection": "workspace:^",
+    "@deepseek-ai/dsh-settings": "workspace:^"
   },
   "dependencies": {
-    "@deepseek-ai/dsh-settings": "workspace:^",
     "@deepseek-ai/schemastery": "workspace:^"
   },
   "files": [

+ 3 - 2
packages/client/locale/tests/apply.client.spec.ts

@@ -4,7 +4,8 @@
 import { Context } from '@deepseek-ai/cordis'
 import { afterEach, describe, expect, it, vi } from 'vitest'
 import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
-import { SettingsScopeBinder } from '@deepseek-ai/dsh-client-ui-settings/client'
+import { SettingsSchemaService } from '@deepseek-ai/dsh-client-ui-settings/src/client/schema.ts'
+import { SettingsScopeBinder } from '@deepseek-ai/dsh-client-ui-settings/src/client/settings-scope.ts'
 import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
 import {
   apply, inject, SETTINGS_NS,
@@ -47,7 +48,7 @@ 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(SettingsScopeBinder).await()
+  await ctx.plugin(SettingsScopeBinder, new SettingsSchemaService(ctx)).await()
   return {
     ctx, slots: ctx.get('slots') as SlotRegistry, describe, mutate,
     setHostPreference: (next: string | undefined) => { preference = next; revision += 1 },

+ 93 - 0
packages/client/locale/tests/document-language.client.spec.ts

@@ -0,0 +1,93 @@
+// @vitest-environment jsdom
+/**
+ * `<html lang>` tracks the active locale.
+ *
+ * The served markup declares one language, but the resolved locale may differ
+ * (browser detection, or a stored Host preference adopted after activation),
+ * and it changes again whenever the user switches. Assistive technology and
+ * browser features read this attribute, so a stale value misreports the
+ * document language rather than merely looking untidy.
+ */
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { Context } from '@deepseek-ai/cordis'
+import { SlotRegistry } from '@deepseek-ai/dsh-client-runtime/client'
+import { SettingsScopeBinder } from '@deepseek-ai/dsh-client-ui-settings/client'
+import { TestRemote } from '@deepseek-ai/dsh-client-test-runtime'
+import { apply, inject } from '@deepseek-ai/dsh-client-locale/client'
+import type { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
+import { LOCALE_SETTINGS_NAMESPACE, LocaleSettingsSchema } from '../src/locale-settings.ts'
+
+/** Boot the plugin over a stub Host settings document. */
+async function bench(preference?: string) {
+  const ctx = new Context()
+  await ctx.plugin(SlotRegistry).await()
+  let stored = preference
+  let revision = 0
+  const namespace = () => ({
+    ns: LOCALE_SETTINGS_NAMESPACE,
+    schema: LocaleSettingsSchema.toJSON(),
+    value: stored === undefined ? {} : { preference: stored },
+    applies: 'live' as const,
+    secrets: [],
+    revision,
+  })
+  const describeRpc = vi.fn(async () => ({
+    rpcId: 'locale-describe' as never,
+    result: { ok: true as const, value: { writable: true, hasDocument: true, namespaces: [namespace()] } },
+  }))
+  const mutate = vi.fn(async (request: { ops: { value: string }[] }) => {
+    stored = request.ops[0]!.value
+    revision += 1
+    return { rpcId: 'locale-mutate' as never, result: { ok: true as const, value: namespace() } }
+  })
+  ctx.provide('connection', { api: { settings: { describe: describeRpc, mutate } }, isLoopback: true } as never)
+  // The settings transport and the forwarded-event port the plugin injects.
+  new TestRemote(ctx)
+  await ctx.plugin(SettingsScopeBinder).await()
+  await ctx.plugin({ inject: [...inject], apply }).await()
+  return { ctx, locale: ctx.get('locale') as LocaleRuntime }
+}
+
+const langOf = (): string => document.documentElement.lang
+
+describe('document language', () => {
+  beforeEach(() => {
+    // The served markup declares the product default; the plugin must not
+    // depend on that value already being correct.
+    document.documentElement.lang = 'en'
+    Object.defineProperty(navigator, 'languages', { value: ['zh-CN'], configurable: true })
+    Object.defineProperty(navigator, 'language', { value: 'zh-CN', configurable: true })
+  })
+
+  afterEach(() => {
+    vi.unstubAllGlobals()
+    const own = navigator as unknown as Record<string, unknown>
+    delete own.languages
+    delete own.language
+  })
+
+  it('states the resolved locale at activation, not the value the markup shipped', async () => {
+    // A Chinese browser resolves zh even though the markup said en.
+    const { locale } = await bench()
+    expect(locale.getLocale().active).toBe('zh')
+    expect(langOf()).toBe('zh-CN')
+  })
+
+  it('follows a locale switch in both directions with BCP 47 tags', async () => {
+    const { locale } = await bench()
+    expect(langOf()).toBe('zh-CN')
+    locale.setLocale('en')
+    // `en` needs no region; `zh` names its script variant, which bare `zh`
+    // leaves ambiguous for pronunciation and font selection.
+    expect(langOf()).toBe('en')
+    locale.setLocale('zh')
+    expect(langOf()).toBe('zh-CN')
+  })
+
+  it('follows an explicit Host preference that overrides browser detection', async () => {
+    // Stored preference wins over the zh browser pinned above.
+    const { locale } = await bench('en')
+    await vi.waitFor(() => { expect(locale.getLocale().active).toBe('en') })
+    await vi.waitFor(() => { expect(langOf()).toBe('en') })
+  })
+})

+ 1 - 1
packages/client/locale/tests/language-row.client.spec.tsx

@@ -4,7 +4,7 @@
 import { afterEach, describe, expect, it, vi } from 'vitest'
 import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
 import { createSnapshotStore, type SessionListState, type WorkspaceListState } from '@deepseek-ai/dsh-client-runtime/client'
-import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
+import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-test-runtime'
 import { LanguageRow } from '../src/client/LanguageRow.tsx'
 import type { LanguageRowComponentProps } from '../src/client/LanguageRow.tsx'
 import { createLanguageRowStore } from '../src/client/settings-store.ts'

+ 2 - 2
packages/client/modules/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/modules/README.md
-README.md: efaff699839b977cc45f89f3c164402241b90dc2
-README.zh.md: 6ab3c8494d7ea902e8b891008f236aec41301a35
+README.md: eaf64599bc5fc5d9663d00c3b341764a4ccd38c8
+README.zh.md: ea051338bb7837cb49f7a5ecd4cdad5b6b3ad71d

+ 7 - 3
packages/client/modules/README.md

@@ -4,11 +4,15 @@ English | [中文](README.zh.md)
 
 Client module system: the browser peer of Node's internal ESM loader, built as a lazy CJS table. The web shell mounts the vendored cordis Loader for entry governance (fiber lifecycle, inject waiting, update/refresh) and injects this package's `ClientModuleLoader` through its `internal` contract — the vendored side's only consumption point is `EntryTree.import`, so replacing `internal` replaces exactly "how plugin code arrives" and nothing else.
 
-Lazy CJS model (web2): executing a plugin bundle only REGISTERS its factory (`window.__ModuleLoader__.load({id, factory})`); every module body side effect — CSS injection included — lives in the factory closure and runs at materialization (`factory(require)` → exports, memoized in `loadCache`), not at script execution. A factory that requires another registered-but-unmaterialized module materializes it recursively, so load order needs no external sequencing; require cycles throw (factory-form CJS cannot deliver partial exports). `<id>/client` and the bare id resolve to the same exports (a plugin bundle IS its package's client half).
+Lazy CJS model (web2): executing a plugin bundle only REGISTERS its factory (`window.__ModuleLoader__.load({id, factory})`); every module body side effect — CSS injection included — lives in the factory closure and runs at materialization (`factory(require)` → exports, memoized in `loadCache`), not at script execution. A factory that requires another registered-but-unmaterialized module materializes it recursively; graph composition places declared dynamic requests before their consumers, and require cycles throw because factory-form CJS cannot deliver partial exports. `<id>/client` and the bare id resolve to the same exports (a plugin bundle IS its package's client half).
 
-Resolution branch order (`import(specifier)`): platform seed word → shell instance; memoized record → surface; shell-own static registry (`registerStatic`, app-shell) → module; registered factory → materialize; graph row (`window.__DSH_BOOT__`) → load its external classic script + materialize; anything else throws — the runtime mirror of the build-time bundle purity gate. The synchronous `require` handed to factories walks the same order minus the asynchronous load branch and records observed edges into the module record. `prefetch` is the stage-one arrival hook (script load and factory registration only; concurrent calls share one in-flight task); `invalidate` drops the factory and materialized record so the next prefetch/import reloads the script (the HMR hook).
+The Host installs `window.__ModuleLoader__` before parser preloads run. Its queue-mode `load()` retains early registrations; `create()` materializes this package's factory with an external-rejecting bootstrap require and calls its `createClientModuleSystem` export. Construction caches those same exports as the modules row, switches the same facade to live registration, and drains the remaining queue. The bundle retains the resulting system in a module closure, so its later Cordis `apply()` provides the identical instance as `ctx.modules` without another page global.
 
-The Node half scans enabled Loader entries for web `dsh.client` packages, resolves each `exports["./client"]`, hashes the built bundle into the boot graph, and serves it with its source map under `/plugins`. Source launch maps host imports to TypeScript source but still consumes this built client export; missing files share one build instruction followed by a package/path list, while unrelated filesystem errors remain separate failures.
+Resolution branch order (`import(specifier)`): platform seed word → shell instance; memoized record → exports; graph row (`window.__DSH_BOOT__`) → register its classic-script factory; registered factory → materialize; anything else throws — the runtime mirror of the build-time bundle purity gate. The synchronous `require` handed to factories walks the same order minus the asynchronous graph-row load and records observed edges into the module record. `prefetch` is the stage-one arrival hook (script load and factory registration only; concurrent calls share one in-flight task); `invalidate` drops a non-bootstrap factory and materialized record so the next prefetch/import reloads the script (the HMR hook).
+
+The Node half scans enabled Loader entries for web `dsh.client` packages, resolves each `exports["./client"]`, hashes the built bundle into the boot graph, carries package-specific `dsh.client.external` requests, orders dynamic providers before consumers, and serves each bundle with its source map under `/plugins`. Source launch maps host imports to TypeScript source but still consumes this built client export; missing files share one build instruction followed by a package/path list, while unrelated filesystem errors remain separate failures.
+
+`dsh.client.external` is an optional exact-specifier request list beyond the implicit baseline: shell-seeded React, Cordis, and static UI libraries plus parser-preloaded runtime. A request is answered by the dynamic package row it names or an exact static-table key; only a trailing `/client` aliases a package row, and there is no provider-alias declaration. Type-only imports are erased and create no request. Composition rejects malformed requests, missing suppliers, self-requests, and synchronous request cycles; import and prefetch recursively register dynamic suppliers before their consumers materialize. See [shared modules and the module graph](../AGENTS.md#shared-modules-and-the-module-graph).
 
 ## Model Experience
 

+ 7 - 3
packages/client/modules/README.zh.md

@@ -4,11 +4,15 @@
 
 客户端模块系统:Node 内部 ESM loader 的浏览器端对等实现,以惰性 CJS 表实现。web 外壳挂载 vendored cordis Loader 来治理配置项(fiber 生命周期、inject 等待、update/refresh),并通过其 `internal` 约定注入该包的 `ClientModuleLoader`;vendored 一侧唯一的消费点是 `EntryTree.import`,因此替换 `internal` 恰好只会替换「插件代码如何到达」,不会改变其他内容。
 
-惰性 CJS 模型(web2):执行插件 bundle 只会注册其 factory(`window.__ModuleLoader__.load({id, factory})`);每个模块主体的副作用(包括 CSS 注入)都位于 factory 闭包中,在物化时运行(`factory(require)` → 导出表层,并在 `loadCache` 中记忆化),不会在脚本执行时运行。如果 factory 依赖另一个已注册但尚未物化的模块,系统会递归物化它,因此加载顺序无需外部编排;require 循环会抛出异常(factory 形式的 CJS 无法提供部分导出)。`<id>/client` 与裸 id 指向同一表层(一个插件 bundle 就是其包的客户端侧)。
+惰性 CJS 模型(web2):执行插件 bundle 只会注册其 factory(`window.__ModuleLoader__.load({id, factory})`);每个模块主体的副作用(包括 CSS 注入)都位于 factory 闭包中,在物化时运行(`factory(require)` → 导出表层,并在 `loadCache` 中记忆化),不会在脚本执行时运行。如果 factory 依赖另一个已注册但尚未物化的模块,系统会递归物化它;图组合会把声明的动态请求提供方放在消费者之前,而 require 循环会抛出异常,因为 factory 形式的 CJS 无法提供部分导出。`<id>/client` 与裸 id 指向同一表层(一个插件 bundle 就是其包的客户端侧)。
 
-解析分支顺序(`import(specifier)`):平台种子词 → 外壳实例;记忆化记录 → 表层;外壳自身的静态注册表(`registerStatic`,app-shell)→ 模块;已注册 factory → 物化;模块图记录(`window.__DSH_BOOT__`)→ 加载外部 classic script + 物化;其他情况一律抛出异常。这是构建时 bundle 纯度门禁的运行时镜像。交给 factory 的同步 `require` 采用相同顺序,但不含异步加载分支,并把观察到的边记录到模块记录中。`prefetch` 是第一阶段到达钩子(只加载脚本并注册 factory;并发调用共享一个进行中的任务);`invalidate` 会丢弃 factory 与物化记录,使下一次 prefetch/import 重新加载脚本;它是 HMR(热模块替换)钩子
+Host 会在 parser preload 运行前安装 `window.__ModuleLoader__`。其 queue 模式的 `load()` 保存提前到达的 registration;`create()` 使用拒绝 external 的 bootstrap require 物化本包 factory,并调用其 `createClientModuleSystem` 导出。构造过程把同一组导出缓存为 modules row,把同一个 facade 切换到 live registration,再排空余下 queue。Bundle 通过模块闭包保留生成的系统,因此随后 Cordis `apply()` 能把同一实例提供为 `ctx.modules`,无需另一个页面全局变量
 
-Node 侧会扫描已启用的 Loader 配置项以发现 web `dsh.client` 包,解析每个 `exports["./client"]`,把构建后的 bundle 哈希写入启动图,并通过 `/plugins` 提供该文件及其 sourcemap。源码启动会把宿主侧导入映射到 TypeScript 源码,但仍消费这一构建后的客户端导出;缺失文件共享一条构建说明,随后以包/路径列表列出各项,而无关的文件系统错误仍是独立故障。
+解析分支顺序(`import(specifier)`):平台种子词 → 外壳实例;记忆化记录 → 导出;模块图记录(`window.__DSH_BOOT__`)→ 登记其 classic-script factory;已登记 factory → 物化;其他情况一律抛出异常。这是构建时 bundle 纯度门禁的运行时镜像。交给 factory 的同步 `require` 采用相同顺序,但不含异步 graph-row 加载分支,并把观察到的边记录到模块记录中。`prefetch` 是第一阶段到达钩子(只加载脚本并登记 factory;并发调用共享一个进行中的任务);`invalidate` 会丢弃非 bootstrap factory 与物化记录,使下一次 prefetch/import 重新加载脚本;它是 HMR(热模块替换)钩子。
+
+Node 侧会扫描已启用的 Loader 配置项以发现 web `dsh.client` 包,解析每个 `exports["./client"]`,把构建后的 bundle 哈希和包专属 `dsh.client.external` 请求写入启动图,把动态提供方排在消费者之前,并通过 `/plugins` 提供该文件及其 sourcemap。源码启动会把宿主侧导入映射到 TypeScript 源码,但仍消费这一构建后的客户端导出;缺失文件共享一条构建说明,随后以包/路径列表列出各项,而无关的文件系统错误仍是独立故障。
+
+`dsh.client.external` 是统一基座之外的可选精确 specifier 请求列表。统一基座包括外壳播种的 React、Cordis 和静态 UI 库,以及由 HTML parser 预载的 runtime。请求由其命名的动态 package row 或精确静态表键回答;只有末尾 `/client` 会别名到 package row,并且不存在 provider 别名声明。纯类型 import 会被擦除,不产生请求。组合阶段会拒绝畸形请求、缺失提供方、自请求和同步请求环;import 与 prefetch 会在消费者物化前递归登记动态提供方。参见[共享模块与模块图](../AGENTS.md#shared-modules-and-the-module-graph)。
 
 ## 模型体验
 

+ 3 - 1
packages/client/modules/package.json

@@ -55,6 +55,8 @@
   ],
   "peerDependencies": {
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^"
+    "@deepseek-ai/cordis": "workspace:^",
+    "@deepseek-ai/cordis-plugin-loader": "workspace:^",
+    "@deepseek-ai/dsh-host-webserver": "workspace:^"
   }
 }

+ 44 - 16
packages/client/modules/src/client/index.ts

@@ -2,33 +2,61 @@
  * Browser half (the standard `./client` export): the module-system class and
  * wire contract, plus the enrollment plugin face. The module system itself is
  * built by the shell kernel BEFORE cordis exists (the bootstrap exception —
- * the mechanism that loads plugins cannot arrive through
- * itself); the plugin face only enrolls that pre-existing instance by
- * providing it as `ctx.modules`. The kernel statically registers this module,
- * so the graph row for this package never triggers a real fetch — arrival is
- * a no-op against the already-registered entry.
+ * the mechanism that loads plugins cannot arrive through itself). The host
+ * parser-preloads this ordinary client bundle into the pending registration
+ * queue. The HTML-installed loader facade materializes this bundle and calls
+ * its bootstrap export, which constructs the system and retains the same
+ * exports for this package's graph row. The plugin face only enrolls that
+ * pre-existing instance by providing it as `ctx.modules`.
  * @module @deepseek-ai/dsh-client-modules/client
  */
 import type { Context } from '@deepseek-ai/cordis'
-import type { DshWindow } from './manifest.ts'
+import { ClientModuleSystem } from './system.ts'
+import { parseBootManifest } from './manifest.ts'
+import type {
+  ClientBootstrapModule, ClientModuleCreateOptions, ClientModuleLoaderTarget,
+} from './manifest.ts'
 
-export { ClientModuleSystem } from './system.ts'
-export { parseBootManifest } from './manifest.ts'
+export { ClientModuleSystem }
+export { parseBootManifest, stripClientSuffix } from './manifest.ts'
 export type {
-  BootManifest, BootModuleRow, BootPluginRow, ClientModuleLoader, ClientModuleRecord,
-  ClientModuleSystemOptions, ClientPluginHandoff, DshWindow, WebBootEntry, WebBootGraph,
+  BootManifest, BootModuleRow, BootPluginRow, ClientBootstrapModule, ClientBundleRegistration,
+  ClientModuleCreateOptions, ClientModuleLoader, ClientModuleLoaderTarget, ClientModuleRecord,
+  ClientModuleSystemOptions, DshWindow,
+  WebBootEntry, WebBootGraph,
 } from './manifest.ts'
 
+let moduleSystem: ClientModuleSystem | undefined
+
+/**
+ * Build the live module system from the HTML facade's materialized modules bundle.
+ * @param target - Stable registration facade whose pending queue becomes the live sink.
+ * @param bootstrapModule - This bundle's id and already-materialized exports.
+ * @param options - Raw boot graph, platform seed, and optional bundle transport.
+ * @returns The created module system, also published for this package's Cordis plugin face.
+ */
+export function createClientModuleSystem(
+  target: ClientModuleLoaderTarget,
+  bootstrapModule: ClientBootstrapModule,
+  options: ClientModuleCreateOptions,
+): ClientModuleSystem {
+  moduleSystem = new ClientModuleSystem({
+    manifest: parseBootManifest(options.boot),
+    staticModules: options.staticModules,
+    registrationTarget: target,
+    bootstrapModule,
+    ...(options.loadBundle === undefined ? {} : { loadBundle: options.loadBundle }),
+  })
+  return moduleSystem
+}
+
 /**
  * Enroll the kernel-built module system as `ctx.modules`.
  * @param ctx - client root context.
  */
 export function apply(ctx: Context): void {
-  const modules = (globalThis as DshWindow).__DSH_MODULES__
-  // The kernel writes the slot right after constructing the instance, before
-  // any cordis entry exists — a missing slot means the kernel sequencing broke.
-  if (modules === undefined) {
-    throw new Error('client-modules: window.__DSH_MODULES__ missing — the shell kernel must construct the module system before plugin boot')
+  if (moduleSystem === undefined) {
+    throw new Error('client-modules: createClientModuleSystem must run before plugin boot')
   }
-  ctx.reflect.provide('modules', modules)
+  ctx.reflect.provide('modules', moduleSystem)
 }

+ 113 - 44
packages/client/modules/src/client/manifest.ts

@@ -16,13 +16,12 @@
  * so load order needs no external sequencing.
  *
  * Resolution branch order (import): seed word → shell instance; memoized
- * record → exports; static registry (shell-own modules, e.g. app-shell) →
- * module; registered factory → materialize; graph row → load + materialize;
- * anything else → throw (loud — the runtime mirror of the
- * build-time bundle purity gate). The synchronous `require` handed to
- * factories walks the same order minus the load branch: loading is async,
- * so only already-registered bundles can be required — and cross-plugin value
- * imports are a build error anyway.
+ * record → exports; graph row → register its dependency factories and own
+ * factory; registered factory → materialize; anything else → throw (loud —
+ * the runtime mirror of the build-time bundle purity gate).
+ * The synchronous `require` handed to factories walks the same order minus
+ * the load branch. Loading is async, so a requested dynamic package must have
+ * registered its factory before a consumer materializes.
  *
  * This file is the browser-safe contract face (zero node imports): the
  * `__DSH_BOOT__` wire types, the boot-manifest parser, and the boundaries around
@@ -45,7 +44,9 @@ declare module '@deepseek-ai/cordis' {
  * single source: the host node half (package root) produces this same shape.
  * `immediately` marks stage-one prefetch; `inject` is informational graph
  * metadata (the authoritative edges live in each package's `dsh.client`
- * declaration and reach fibers through entry creation).
+ * declaration and reach fibers through entry creation). `external` carries
+ * module-graph edges: unlike `inject`, they constrain code arrival because
+ * `require` is synchronous (see {@link WebBootGraph.entries}).
  */
 export interface WebBootEntry {
   /** Entry name == package name. */
@@ -58,13 +59,19 @@ export interface WebBootEntry {
   inject?: string[]
   /** Stage-one prefetch mark: load the script for factory registration during module-face boot. */
   immediately?: boolean
+  /** Non-baseline module specifiers this row requests; omitted when it requests none. */
+  external?: string[]
 }
 
 /** The composed client entry graph the host injects as `window.__DSH_BOOT__`. */
 export interface WebBootGraph {
   /** Consistency anchor over the whole graph (content + bundle hashes). */
   rev: string
-  /** Composed entries; order carries no semantics (activation order is fiber inject waiting). */
+  /**
+   * Composed entries in module-graph order — a dynamic package row precedes
+   * rows whose `external` requests that package. Cordis activation order is
+   * unrelated and remains owned by fiber service waiting.
+   */
   entries: WebBootEntry[]
 }
 
@@ -76,6 +83,8 @@ export interface BootModuleRow {
   url: string
   /** Bundle content hash. */
   rev: string
+  /** Module specifiers this row requests from the module table ([] when the wire omits them). */
+  external: string[]
 }
 
 /** The cordis-plugin view of one boot row: what entry composition needs (optional wire fields normalized). */
@@ -98,6 +107,36 @@ export interface BootManifest {
   plugins: BootPluginRow[]
 }
 
+/**
+ * Validate an optional string-array field read from a `dsh.client` declaration
+ * or from the boot wire.
+ * @param subject - diagnostic prefix naming the package or the wire row.
+ * @param field - field name as it appears in the diagnostic.
+ * @param value - the raw field value.
+ * @returns the validated array, or undefined when the field is absent.
+ * @throws {Error} when the value is present but is not an array of strings.
+ */
+export function optionalStringArray(subject: string, field: string, value: unknown): string[] | undefined {
+  if (value === undefined) return undefined
+  if (!Array.isArray(value) || value.some(item => typeof item !== 'string')) {
+    throw new Error(`client-modules: ${subject} ${field} must be a string array`)
+  }
+  return value as string[]
+}
+
+/**
+ * Normalize a module specifier onto the graph row that owns it: a plugin bundle
+ * IS its package's client half, so `<id>/client` (the exports subpath external
+ * bundles emit) and the bare package name resolve to the same exports. Both the
+ * require path and graph composition normalize here, which is what lets each
+ * importing package request the subpath its own code imports.
+ * @param spec - module specifier as a bundle requires it or a declaration spells it.
+ * @returns the specifier with a trailing `/client` removed.
+ */
+export function stripClientSuffix(spec: string): string {
+  return spec.endsWith('/client') ? spec.slice(0, -'/client'.length) : spec
+}
+
 /**
  * Parse `window.__DSH_BOOT__` into the two consumer views. Wire boundary:
  * a missing or malformed graph throws (the shell shows the loud failure —
@@ -127,24 +166,29 @@ export function parseBootManifest(wire: unknown): BootManifest {
     if (typeof row.id !== 'string' || typeof row.url !== 'string' || typeof row.rev !== 'string') {
       throw new Error(`client-modules: boot manifest entry ${where} must carry string id/url/rev`)
     }
-    if (row.inject !== undefined && (!Array.isArray(row.inject) || row.inject.some(i => typeof i !== 'string'))) {
-      throw new Error(`client-modules: boot manifest entry ${where} inject must be a string array`)
-    }
+    const subject = `boot manifest entry ${where}`
+    const inject = optionalStringArray(subject, 'inject', row.inject)
+    const external = optionalStringArray(subject, 'external', row.external)
     if (row.immediately !== undefined && typeof row.immediately !== 'boolean') {
       throw new Error(`client-modules: boot manifest entry ${where} immediately must be a boolean`)
     }
-    modules.push({ id: row.id, url: row.url, rev: row.rev })
+    modules.push({
+      id: row.id,
+      url: row.url,
+      rev: row.rev,
+      external: external === undefined ? [] : [...external],
+    })
     plugins.push({
       id: row.id,
-      inject: row.inject === undefined ? [] : [...row.inject as string[]],
+      inject: inject === undefined ? [] : [...inject],
       immediately: row.immediately === true,
     })
   }
   return { rev: graph.rev, modules, plugins }
 }
 
-/** The shape a client bundle hands to `window.__ModuleLoader__.load` (registration handoff). */
-export interface ClientPluginHandoff {
+/** One client bundle's factory registration submitted through `window.__ModuleLoader__.load`. */
+export interface ClientBundleRegistration {
   /** Plugin id (package name) — the registration key; must match the graph row being executed. */
   id: string
   /**
@@ -155,26 +199,49 @@ export interface ClientPluginHandoff {
   factory: (require: (spec: string) => unknown) => Record<string, unknown>
 }
 
-/** Window API of the web boot protocol: the host-injected graph, registration sink, and kernel handoff slot. */
+/** Inputs passed by the web entry when it creates the client module system. */
+export interface ClientModuleCreateOptions {
+  /** Raw Host-injected boot graph; the modules bundle owns validation and projection. */
+  boot: unknown
+  /** Module-table seed: platform-singleton specifier → shell instance. */
+  staticModules: Record<string, unknown>
+  /** Bundle-load hook. Defaults to a same-origin classic `<script src>` element. */
+  loadBundle?: (url: string) => Promise<void>
+}
+
+/** The modules bundle after its factory has been materialized by the HTML bootstrap facade. */
+export interface ClientBootstrapModule {
+  /** Graph/module id carried by the modules bundle registration. */
+  id: string
+  /** Materialized exports reused when Cordis later activates the modules entry. */
+  exports: Record<string, unknown>
+}
+
+/** Stable page-global facade: queues early bundle registrations, then registers them live. */
+export interface ClientModuleLoaderTarget {
+  /** Queue before {@link create}; live registration after it returns. */
+  mode: 'queue' | 'live'
+  /** Registrations submitted by parser-preloaded scripts before the module system exists. */
+  pendingQueue: ClientBundleRegistration[]
+  /** Queue or immediately register one bundle factory according to {@link mode}. */
+  load(registration: ClientBundleRegistration): void
+  /** Create the module system exactly once from the parser-preloaded modules bundle. */
+  create(options: ClientModuleCreateOptions): ClientModuleSystem
+}
+
+/** Window API of the web boot protocol: the host-injected graph and registration facade. */
 export interface DshWindow {
   /** Host-composed entry graph, injected before the shell bundle runs; wire-boundary raw until {@link parseBootManifest}. */
   __DSH_BOOT__?: unknown
-  /** Bundle registration sink; installed once per page by the {@link ClientModuleSystem} constructor. */
-  __ModuleLoader__?: { load(handoff: ClientPluginHandoff): void }
-  /**
-   * Kernel handoff slot: the shell kernel stores the instance here right
-   * after construction (before cordis exists) so the `./client` wrapper
-   * plugin can provide it as `ctx.modules`. Missing slot at wrapper apply
-   * time = kernel sequencing bug, thrown loud.
-   */
-  __DSH_MODULES__?: ClientModuleSystem
+  /** HTML-installed facade: a pending registration queue, then the live module-system target. */
+  __ModuleLoader__?: ClientModuleLoaderTarget
 }
 
 /** Per-module bookkeeping in {@link ClientModuleLoader.loadCache} (module-graph boundary, flat today). */
 export interface ClientModuleRecord {
   /** Module id (entry name / package name). */
   id: string
-  /** Materialized exports (`module.exports` from a factory, or a statically registered shell module). */
+  /** Materialized exports (`module.exports` from a factory or bootstrap registration). */
   exports: unknown
   /** Owned `<style data-plugin>` tag ids (`data-plugin-css` values) injected during materialization. */
   styles: string[]
@@ -190,6 +257,8 @@ export interface ClientModuleRecord {
 export interface ClientModuleLoader {
   /** Discriminant against Node's internal loader shapes ('v1'/'v2'). */
   version: 'client'
+  /** Parsed Host boot graph shared with the web entry after module-system creation. */
+  manifest: BootManifest
   /** Materialized-module registry: id → record. The governance-side read API for entry exports. */
   loadCache: Map<string, ClientModuleRecord>
   /**
@@ -203,35 +272,35 @@ export interface ClientModuleLoader {
    */
   import(specifier: string, parentURL: string, attrs: Record<string, unknown>): Promise<unknown>
   /**
-   * Register a shell-own module (app-shell — code that ships inside the shell
-   * bundle and never arrives as a plugin bundle).
-   * @param id - entry name (shell-owned pseudo id).
-   * @param module - the statically imported module namespace.
-   */
-  registerStatic(id: string, module: unknown): void
-  /**
-   * Stage-one arrival: load the entry's script to register its factory (no
-   * materialization — module side effects wait for import).
-   * No-op for static-registered ids and ids whose factory is already
-   * registered; concurrent calls share one in-flight task. To force a fresh
-   * load (HMR), {@link invalidate} first.
+   * Stage-one arrival: load the entry's declared dynamic requests, then its
+   * own script, to register their factories (no materialization — module side
+   * effects wait for import).
+   * No-op for materialized bootstrap ids. A registered graph row still
+   * registers any unresolved declared requests before skipping its own script;
+   * concurrent arrivals share one in-flight task. To force a fresh load (HMR),
+   * {@link invalidate} first.
    * @param id - graph entry name.
    */
   prefetch(id: string): Promise<void>
   /**
-   * Full reset of one module: drop its registered factory and materialized
-   * record so the next prefetch/import reloads it (the HMR invalidation hook).
+   * Full reset of one non-bootstrap module: drop its registered factory and
+   * materialized record so the next prefetch/import reloads it (the HMR
+   * invalidation hook). The bootstrap module remains materialized.
    * @param id - entry name to invalidate.
    */
   invalidate(id: string): void
 }
 
-/** Options for {@link ClientModuleSystem} (assembled by the web shell kernel at boot). */
+/** Internal construction inputs assembled by the modules bundle's bootstrap export. */
 export interface ClientModuleSystemOptions {
-  /** Boot rows in the module-table view (from {@link parseBootManifest}). */
-  modules: BootModuleRow[]
+  /** Parsed boot graph owned by the resulting module system. */
+  manifest: BootManifest
   /** Module-table seed: platform-singleton specifier → shell instance. */
   staticModules: Record<string, unknown>
+  /** Stable HTML-installed registration facade to switch from queue to live mode. */
+  registrationTarget: ClientModuleLoaderTarget
+  /** Already-materialized modules bundle consumed while creating the system. */
+  bootstrapModule: ClientBootstrapModule
   /** Bundle-load hook. Defaults to a same-origin classic `<script src>` element. */
   loadBundle?: (url: string) => Promise<void>
 }

+ 84 - 60
packages/client/modules/src/client/system.ts

@@ -4,9 +4,10 @@
  * documented on the public interfaces in `./manifest.ts`; this file owns the
  * state tables and the load/materialize machinery.
  */
+import { stripClientSuffix } from './manifest.ts'
 import type {
-  BootModuleRow, ClientModuleLoader, ClientModuleRecord,
-  ClientModuleSystemOptions, ClientPluginHandoff, DshWindow,
+  BootManifest, BootModuleRow, ClientBundleRegistration, ClientModuleLoader, ClientModuleRecord,
+  ClientModuleSystemOptions,
 } from './manifest.ts'
 
 /** Default bundle-load hook: same-origin external classic script. */
@@ -25,14 +26,6 @@ const defaultLoadBundle = (url: string): Promise<void> => new Promise((resolve,
   document.head.append(el)
 })
 
-/**
- * A plugin bundle IS its package's client half: `<id>/client` (the exports
- * subpath external bundles emit) and the bare graph id name the same
- * exports, so table lookups normalize the suffix away.
- */
-const stripClientSuffix = (spec: string): string =>
-  spec.endsWith('/client') ? spec.slice(0, -'/client'.length) : spec
-
 /**
  * Claim and inventory the <style> tags a factory injected during
  * materialization: preset-emitted tags arrive pre-tagged with data-plugin;
@@ -53,16 +46,18 @@ const claimStyles = (id: string): string[] => {
 /**
  * The client module system: state tables plus the arrival/materialization
  * machinery implementing {@link ClientModuleLoader} (whose members carry the
- * contract documentation). Construction indexes the boot rows and installs the
- * `window.__ModuleLoader__` registration sink — once per page.
+ * contract documentation). Construction indexes the boot rows, retains the
+ * already-materialized bootstrap module, and switches the HTML-installed
+ * loader facade from its pending queue to live registration.
  */
 export class ClientModuleSystem implements ClientModuleLoader {
   readonly version = 'client'
+  readonly manifest: BootManifest
   readonly loadCache = new Map<string, ClientModuleRecord>()
 
   private readonly seed: Map<string, unknown>
-  private readonly statics = new Map<string, unknown>()
-  private readonly factories = new Map<string, ClientPluginHandoff['factory']>()
+  private readonly factories = new Map<string, ClientBundleRegistration['factory']>()
+  private readonly bootstrapIds = new Set<string>()
   /** In-flight prefetch (script load) per id; concurrent callers share it. */
   private readonly pendingArrival = new Map<string, Promise<void>>()
   /** Materialization re-entrancy guard: factory-form CJS cannot deliver partial exports, so a cycle is fatal. */
@@ -72,27 +67,46 @@ export class ClientModuleSystem implements ClientModuleLoader {
 
   /**
    * Build the module system over the parsed boot rows.
-   * @param options - Module rows, module-table staticModules, and bundle-load hook.
+   * @param options - Parsed graph, platform seed, bootstrap module, registration facade, and transport.
    */
   constructor(options: ClientModuleSystemOptions) {
+    this.manifest = options.manifest
     this.seed = new Map(Object.entries(options.staticModules))
     this.loadBundle = options.loadBundle ?? defaultLoadBundle
 
-    for (const row of options.modules) {
+    for (const row of options.manifest.modules) {
       if (this.graphRows.has(row.id)) throw new Error(`client-modules: duplicate graph entry "${row.id}"`)
       this.graphRows.set(row.id, row)
     }
 
-    const win = globalThis as DshWindow
-    if (win.__ModuleLoader__ !== undefined) throw new Error('client-modules: window.__ModuleLoader__ already installed (double boot?)')
-    win.__ModuleLoader__ = {
-      load: (handoff: ClientPluginHandoff): void => {
-        // Registration is keyed by the handoff id; a duplicate means a bundle
-        // executed twice without an invalidate — always a bug, always loud.
-        if (this.factories.has(handoff.id)) throw new Error(`client-modules: duplicate factory registration for "${handoff.id}" (bundle executed twice without invalidate?)`)
-        this.factories.set(handoff.id, handoff.factory)
-      },
+    const bootstrapId = stripClientSuffix(options.bootstrapModule.id)
+    this.bootstrapIds.add(bootstrapId)
+    this.loadCache.set(bootstrapId, {
+      id: bootstrapId,
+      exports: options.bootstrapModule.exports,
+      styles: [],
+      edges: new Set(),
+    })
+
+    const target = options.registrationTarget
+    if (target.mode !== 'queue') {
+      throw new Error('client-modules: window.__ModuleLoader__.create called after module-system boot')
+    }
+    const pending = target.pendingQueue.splice(0)
+    // Switch first: a bundle that executes while pending registrations drain
+    // must register live rather than append behind the drain.
+    target.mode = 'live'
+    target.load = (registration) => { this.register(registration) }
+    for (const registration of pending) target.load(registration)
+  }
+
+  /** Register one bundle factory, rejecting a script that executes twice without invalidation. */
+  private register(registration: ClientBundleRegistration): void {
+    const id = stripClientSuffix(registration.id)
+    if (this.bootstrapIds.has(id) || this.factories.has(id)) {
+      throw new Error(`client-modules: duplicate factory registration for "${registration.id}" (bundle executed twice without invalidate?)`)
     }
+    this.factories.set(id, registration.factory)
   }
 
   /** Load one graph row so its factory is registered (idempotent per in-flight arrival). */
@@ -100,7 +114,7 @@ export class ClientModuleSystem implements ClientModuleLoader {
     const { id, url } = row
     const pending = this.pendingArrival.get(id)
     if (pending !== undefined) return pending
-    if (this.factories.has(id)) return Promise.resolve()
+    if (this.loadCache.has(id) || this.factories.has(id)) return Promise.resolve()
     const task = this.loadBundle(url).then(() => {
       if (!this.factories.has(id)) {
         throw new Error(`client-modules: bundle ${url} loaded without registering "${id}" via __ModuleLoader__.load`)
@@ -110,6 +124,25 @@ export class ClientModuleSystem implements ClientModuleLoader {
     return task
   }
 
+  /** Register each unresolved dynamic request before registering its consumer. */
+  private async arriveGraphRow(row: BootModuleRow, open: readonly string[] = []): Promise<void> {
+    const cycleStart = open.indexOf(row.id)
+    if (cycleStart !== -1) {
+      throw new Error(
+        `client-modules: module arrival cycle ${[...open.slice(cycleStart), row.id].join(' -> ')} `
+        + '(the host must reject this graph before serving it)',
+      )
+    }
+    const next = [...open, row.id]
+    for (const request of row.external) {
+      const id = stripClientSuffix(request)
+      if (this.seed.has(request) || this.loadCache.has(id)) continue
+      const dependency = this.graphRows.get(id)
+      if (dependency !== undefined) await this.arriveGraphRow(dependency, next)
+    }
+    await this.arrive(row)
+  }
+
   /** Materialize a registered factory (synchronous; memoized in loadCache). */
   private materialize(id: string): ClientModuleRecord {
     const existing = this.loadCache.get(id)
@@ -133,64 +166,55 @@ export class ClientModuleSystem implements ClientModuleLoader {
   }
 
   /**
-   * The synchronous require answered to factories: seed → static → memoized
-   * record → registered factory (recursive materialization — this is what
-   * makes load order self-resolving). Fetching is async and therefore
-   * unreachable from here; an unregistered plugin specifier is loud (and a
-   * cross-plugin value import is already a build error upstream).
+   * The synchronous require answered to factories: seed → memoized record →
+   * registered factory. Fetching is async and therefore unreachable
+   * from here; an external dynamic package must have arrived before its
+   * consumer materializes.
    */
   private makeRequire(edges: Set<string>): (spec: string) => unknown {
     return (spec: string): unknown => {
       edges.add(spec)
       if (this.seed.has(spec)) return this.seed.get(spec)
-      if (this.statics.has(spec)) return this.statics.get(spec)
       const id = stripClientSuffix(spec)
       const record = this.loadCache.get(id)
       if (record !== undefined) return record.exports
       if (this.factories.has(id)) return this.materialize(id).exports
       throw new Error(
-        `client-modules: require("${spec}") missed the module table — not a platform seed word, not a shell-own module, `
-        + 'and no registered factory (a build-time externals drift, or a forbidden cross-plugin value import)',
+        `client-modules: require("${spec}") missed the module table — not a platform seed word, not a materialized module, `
+        + 'and no registered package factory (a build-time externals drift, or a dynamic dependency that did not arrive)',
       )
     }
   }
 
   async import(specifier: string): Promise<unknown> {
     if (this.seed.has(specifier)) return this.seed.get(specifier)
-    const existing = this.loadCache.get(specifier)
+    const id = stripClientSuffix(specifier)
+    const existing = this.loadCache.get(id)
     if (existing !== undefined) return existing.exports
-    if (this.statics.has(specifier)) {
-      const exports = this.statics.get(specifier)
-      this.loadCache.set(specifier, { id: specifier, exports, styles: [], edges: new Set() })
-      return exports
-    }
-    if (!this.factories.has(specifier)) {
-      const row = this.graphRows.get(specifier)
-      if (row === undefined) {
-        throw new Error(
-          `client-modules: cannot resolve "${specifier}" — not a seed word, not a shell-own module, `
-          + 'and not a row in the boot graph (the runtime mirror of the bundle purity gate)',
-        )
-      }
-      await this.arrive(row)
+    const row = this.graphRows.get(id)
+    if (row !== undefined) {
+      await this.arriveGraphRow(row)
+    } else if (!this.factories.has(id)) {
+      throw new Error(
+        `client-modules: cannot resolve "${specifier}" — not a seed word, not a materialized module, `
+        + 'and not a row in the boot graph (the runtime mirror of the bundle purity gate)',
+      )
     }
-    return this.materialize(specifier).exports
-  }
-
-  registerStatic(id: string, module: unknown): void {
-    if (this.statics.has(id)) throw new Error(`client-modules: shell-own module "${id}" registered twice`)
-    this.statics.set(id, module)
+    return this.materialize(id).exports
   }
 
   async prefetch(id: string): Promise<void> {
-    if (this.statics.has(id)) return
-    const row = this.graphRows.get(id)
+    const normalized = stripClientSuffix(id)
+    if (this.loadCache.has(normalized)) return
+    const row = this.graphRows.get(normalized)
     if (row === undefined) throw new Error(`client-modules: prefetch("${id}") — not a graph entry`)
-    await this.arrive(row)
+    await this.arriveGraphRow(row)
   }
 
   invalidate(id: string): void {
-    this.factories.delete(id)
-    this.loadCache.delete(id)
+    const normalized = stripClientSuffix(id)
+    if (this.bootstrapIds.has(normalized)) return
+    this.factories.delete(normalized)
+    this.loadCache.delete(normalized)
   }
 }

+ 150 - 28
packages/client/modules/src/index.ts

@@ -2,10 +2,11 @@
  * Node half of the client module system (`dsh.client` dual-face package): scans
  * the host Loader's entries for packages declaring `dsh.client`, composes the
  * `window.__DSH_BOOT__` entry graph (wire single source: {@link WebBootEntry}
- * in `./client/manifest.ts`), serves `/plugins/<id>/client.js` and its source
- * map, taps the index render to inject the boot manifest, and provides the
- * `clientModuleHost` service (the HMR node half's registration/notification
- * face).
+ * in `./client/manifest.ts`) in module-graph order, serves
+ * `/plugins/<id>/client.js` and its source map, taps the index render to
+ * inject the boot manifest plus the parser-blocking bootstrap preloads, and
+ * provides the `clientModuleHost` service (the HMR node half's
+ * registration/notification face).
  *
  * Scanning is incremental per package — there is no full-rescan code path.
  * Every cordis `internal/plugin` emission (fiber construction/disposal) marks
@@ -30,8 +31,10 @@ import { Service } from '@deepseek-ai/cordis'
 import type { Context } from '@deepseek-ai/cordis'
 import type {} from '@deepseek-ai/cordis-plugin-loader'
 import type {} from '@deepseek-ai/dsh-host-webserver'
+import { optionalStringArray, stripClientSuffix } from './client/manifest.ts'
 import type { WebBootEntry, WebBootGraph } from './client/manifest.ts'
 
+export { stripClientSuffix } from './client/manifest.ts'
 export type {
   BootManifest, BootModuleRow, BootPluginRow, WebBootEntry, WebBootGraph,
 } from './client/manifest.ts'
@@ -49,15 +52,29 @@ interface DshClientDeclaration {
   platform: string
   /** Boot phase-one prefetch mark; absent means lazy (fetched on demand). */
   immediately?: boolean
+  /**
+   * Exact module-table requests beyond the implicit client baseline. Any
+   * specifier is valid, including subpaths such as `<pkg>/client`; each
+   * importing package declares its own exceptional requests. A type-only
+   * import is not a request because the transform erases it before resolution.
+   * Absent means the package uses only the baseline externals.
+   */
+  external?: string[]
 }
 
-/** Resolved package metadata for one `dsh.client` package (cached per name, never expires). */
-interface PkgMeta {
-  clientPath: string
+/** The declared fields a graph row carries, normalized (absent array declarations become empty). */
+interface WebBootRowFields {
   inject?: string[]
+  /** Module specifiers the package requests from the module table. */
+  external: string[]
   immediately: boolean
 }
 
+/** Resolved package metadata for one `dsh.client` package (cached per name, never expires). */
+interface PkgMeta extends WebBootRowFields {
+  clientPath: string
+}
+
 /** Recovery instruction shared by grouped startup and steady-state bundle diagnostics. */
 const CLIENT_BUNDLE_BUILD_INSTRUCTION = 'run `pnpm run build` before launch'
 
@@ -99,10 +116,10 @@ class ClientPackageCompositionError extends AggregateError {
   }
 }
 
-/** One composed table row: the wire entry plus its bundle path. */
+/** One composed table row: the wire entry plus the resolved package metadata behind it. */
 interface WebPluginRecord {
   entry: WebBootEntry
-  clientPath: string
+  meta: PkgMeta
 }
 
 /** Narrow an unknown parsed JSON value to the `dsh.client` declaration, throwing on malformed fields. */
@@ -115,15 +132,15 @@ function parseDshClient(pkgName: string, value: unknown): DshClientDeclaration |
   if (typeof decl.platform !== 'string') {
     throw new Error(`client-modules: ${pkgName} dsh.client.platform must be a string`)
   }
-  if (decl.inject !== undefined && (!Array.isArray(decl.inject) || decl.inject.some(i => typeof i !== 'string'))) {
-    throw new Error(`client-modules: ${pkgName} dsh.client.inject must be a string array`)
-  }
+  const inject = optionalStringArray(pkgName, 'dsh.client.inject', decl.inject)
+  const external = optionalStringArray(pkgName, 'dsh.client.external', decl.external)
   if (decl.immediately !== undefined && typeof decl.immediately !== 'boolean') {
     throw new Error(`client-modules: ${pkgName} dsh.client.immediately must be a boolean`)
   }
   return {
     platform: decl.platform,
-    ...(decl.inject !== undefined ? { inject: decl.inject as string[] } : {}),
+    ...(inject !== undefined ? { inject } : {}),
+    ...(external !== undefined ? { external } : {}),
     ...(decl.immediately !== undefined ? { immediately: decl.immediately } : {}),
   }
 }
@@ -147,27 +164,121 @@ function shortHash(input: string | Buffer): string {
 }
 
 /** Graph row for one bundle rev (url carries the rev as its cache-busting query). */
-function graphRow(id: string, rev: string, injectEdges: string[] | undefined, immediately: boolean): WebBootEntry {
+function graphRow(id: string, rev: string, fields: WebBootRowFields): WebBootEntry {
   return {
     id,
     url: `/plugins/${id}/client.js?rev=${rev}`,
     rev,
-    ...(injectEdges !== undefined ? { inject: injectEdges } : {}),
-    ...(immediately ? { immediately: true } : {}),
+    ...(fields.inject !== undefined ? { inject: fields.inject } : {}),
+    ...(fields.immediately ? { immediately: true } : {}),
+    ...(fields.external.length > 0 ? { external: fields.external } : {}),
   }
 }
 
 /**
- * Inject the boot entry graph into index.html: `window.__DSH_BOOT__` as the
- * first script in <head> (before the shell bundle reads it). `<` is escaped in
- * the JSON so plugin-controlled strings cannot break out of the script element.
+ * Order composed rows so every requested dynamic package precedes its
+ * consumers. An `external` specifier is either the package row it names
+ * (`<pkg>/client` aliases the bare package) or a static-table name that adds no
+ * graph edge.
+ * @param entries - composed rows in scan order.
+ * @returns the same rows reordered; scan order breaks every tie.
+ * @throws {Error} when a row requests itself or when the module graph has a
+ * cycle; the message lists the packages on it.
+ */
+export function orderByModuleGraph(entries: readonly WebBootEntry[]): WebBootEntry[] {
+  const rowsById = new Map<string, WebBootEntry>()
+  for (const entry of entries) rowsById.set(entry.id, entry)
+  const ordered: WebBootEntry[] = []
+  const placed = new Set<string>()
+  const open: string[] = []
+  const visit = (entry: WebBootEntry): void => {
+    if (placed.has(entry.id)) return
+    const cycleStart = open.indexOf(entry.id)
+    if (cycleStart !== -1) {
+      throw new Error(
+        `client-modules: module graph cycle ${[...open.slice(cycleStart), entry.id].join(' -> ')} `
+        + '— a requested package row must precede its consumers, and factory-form CJS cannot deliver partial exports',
+      )
+    }
+    open.push(entry.id)
+    for (const name of entry.external ?? []) {
+      const dependency = rowsById.get(name) ?? rowsById.get(stripClientSuffix(name))
+      if (dependency === entry) {
+        throw new Error(
+          `client-modules: "${entry.id}" requests module "${name}" that it answers itself `
+          + '— a row must not declare its own package in dsh.client.external',
+        )
+      }
+      if (dependency !== undefined) visit(dependency)
+    }
+    open.pop()
+    placed.add(entry.id)
+    ordered.push(entry)
+  }
+  for (const entry of entries) visit(entry)
+  return ordered
+}
+
+/** Bootstrap package whose ordinary client bundle supplies the module-system implementation. */
+const CLIENT_MODULES_ID = '@deepseek-ai/dsh-client-modules'
+
+/** Dynamic package whose ordinary client bundle must be registered before plugin boot starts. */
+const CLIENT_RUNTIME_ID = '@deepseek-ai/dsh-client-runtime'
+
+/** Ordinary dynamic bundles the HTML parser executes before the Vite shell. */
+const PARSER_PRELOAD_IDS = [CLIENT_MODULES_ID, CLIENT_RUNTIME_ID] as const
+
+/** Escape a graph URL before placing it in a quoted HTML attribute. */
+function escapeHtmlAttribute(value: string): string {
+  return value
+    .replaceAll('&', '&amp;')
+    .replaceAll('"', '&quot;')
+    .replaceAll('<', '&lt;')
+    .replaceAll('>', '&gt;')
+}
+
+/**
+ * Inject the boot protocol into index.html. The inline registration queue precedes
+ * blocking classic scripts for modules' and runtime's ordinary
+ * `lib/client.js` artifacts. Its `create()` method materializes the modules
+ * bundle, delegates construction to that bundle, and leaves the same facade
+ * in live-registration mode. The graph script follows before the shell reads
+ * it. `<` is escaped in JSON so a plugin-controlled string cannot break out
+ * of the script element.
  * @param html - the index.html source.
  * @param graph - the composed entry graph.
  * @returns the html with the graph script injected.
  */
 export function injectBootManifest(html: string, graph: WebBootGraph): string {
   const json = JSON.stringify(graph).replaceAll('<', '\\u003c')
-  const script = `<script>window.__DSH_BOOT__ = ${json}</script>`
+  const bootstrapId = JSON.stringify(CLIENT_MODULES_ID)
+  const queue = `<script>(()=>{
+const pendingQueue=[]
+window.__ModuleLoader__={
+  mode:"queue",
+  pendingQueue,
+  load(registration){pendingQueue.push(registration)},
+  create(options){
+    if(this.mode!=="queue")throw new Error("client-modules: window.__ModuleLoader__.create called after module-system boot")
+    const index=pendingQueue.findIndex(registration=>registration.id===${bootstrapId})
+    const registration=pendingQueue[index]
+    if(registration===undefined)throw new Error("client-modules: HTML did not preload ${CLIENT_MODULES_ID}/client.js")
+    pendingQueue.splice(index,1)
+    const exports=registration.factory(specifier=>{
+      throw new Error('client-modules: ${CLIENT_MODULES_ID}/client.js requested external "'+specifier+'" before the module system existed')
+    })
+    if(typeof exports!=="object"||exports===null||typeof exports.createClientModuleSystem!=="function"||typeof exports.apply!=="function"){
+      throw new Error("client-modules: ${CLIENT_MODULES_ID}/client.js did not export the bootstrap module face")
+    }
+    return exports.createClientModuleSystem(this,{id:registration.id,exports},options)
+  }
+}
+})()</script>`
+  const preload = PARSER_PRELOAD_IDS.map(id => graph.entries.find(entry => entry.id === id))
+    .filter((entry): entry is WebBootEntry => entry !== undefined)
+    .map(entry => `<script src="${escapeHtmlAttribute(entry.url)}"></script>`)
+    .join('')
+  const script = `${queue}${preload}<script>window.__DSH_BOOT__ = ${json}</script>`
   const head = html.indexOf('<head>')
   if (head !== -1) return `${html.slice(0, head + 6)}${script}${html.slice(head + 6)}`
   // Headless fixture pages may lack <head>; prepending keeps the read-before-shell ordering.
@@ -262,7 +373,7 @@ export class ClientModuleRegistry extends Service {
    * @returns the path, or undefined for an unknown id.
    */
   clientPath(id: string): string | undefined {
-    return this.table.get(id)?.clientPath
+    return this.table.get(id)?.meta.clientPath
   }
 
   /**
@@ -274,9 +385,9 @@ export class ClientModuleRegistry extends Service {
   rebuilt(id: string): string | undefined {
     const record = this.table.get(id)
     if (record === undefined) return undefined
-    const rev = shortHash(readFileSync(record.clientPath))
+    const rev = shortHash(readFileSync(record.meta.clientPath))
     if (rev === record.entry.rev) return rev
-    record.entry = graphRow(id, rev, record.entry.inject, record.entry.immediately === true)
+    record.entry = graphRow(id, rev, record.meta)
     this.composed = this.compose()
     for (const notify of this.rebuildListeners) {
       // Containment: rebuilt() runs inside the HMR watch callback — a
@@ -313,7 +424,7 @@ export class ClientModuleRegistry extends Service {
   }
 
   private compose(): WebBootGraph {
-    const entries = [...this.table.values()].map(record => record.entry)
+    const entries = orderByModuleGraph([...this.table.values()].map(record => record.entry))
     return { rev: shortHash(JSON.stringify(entries)), entries }
   }
 
@@ -358,6 +469,7 @@ export class ClientModuleRegistry extends Service {
     const meta: PkgMeta = {
       clientPath: join(dirname(pkgPath), clientRel),
       ...(decl.inject !== undefined ? { inject: decl.inject } : {}),
+      external: decl.external ?? [],
       immediately: decl.immediately === true,
     }
     this.pkgMeta.set(pkgName, meta)
@@ -396,7 +508,7 @@ export class ClientModuleRegistry extends Service {
     // The rev rides the row from here on: a fiber restart reuses the row (and
     // its rev) untouched; only rebuilt() re-reads the bundle.
     const rev = this.initialBundleRevision(entryName, meta.clientPath)
-    this.table.set(entryName, { entry: graphRow(entryName, rev, meta.inject, meta.immediately), clientPath: meta.clientPath })
+    this.table.set(entryName, { entry: graphRow(entryName, rev, meta), meta })
     return true
   }
 
@@ -412,10 +524,20 @@ export class ClientModuleRegistry extends Service {
         onError(error instanceof Error ? error : new Error(String(error)))
       }
     }
-    if (changed) {
-      this.composed = this.compose()
-      this.notifyGraphChanged()
+    if (!changed) return
+    let composed: WebBootGraph
+    try {
+      composed = this.compose()
+    } catch (error) {
+      // An unorderable module graph is a property of the whole table, not of
+      // the arriving package, so it surfaces here: aggregated into the
+      // activation throw, or warned in steady state while the last orderable
+      // graph stays served.
+      onError(error as Error)
+      return
     }
+    this.composed = composed
+    this.notifyGraphChanged()
   }
 
   private readonly serveBundle = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {

+ 149 - 40
packages/client/modules/tests/loader.client.spec.ts

@@ -7,15 +7,19 @@
  * default transport hook, and the loud failure modes (duplicate
  * registration, cycles, table misses, double boot).
  */
+import { Context } from '@deepseek-ai/cordis'
 import { afterEach, describe, expect, it, vi } from 'vitest'
 import {
-  ClientModuleSystem,
-  type BootModuleRow, type ClientModuleLoader, type ClientPluginHandoff, type DshWindow,
+  apply, createClientModuleSystem, parseBootManifest,
+  type BootModuleRow, type ClientBundleRegistration, type ClientModuleCreateOptions,
+  type ClientModuleLoader, type ClientModuleLoaderTarget, type DshWindow,
 } from '../src/client/index.ts'
 
+const MODULES_ID = '@deepseek-ai/dsh-client-modules'
 const win = globalThis as DshWindow
+const bootstrapExports = { apply, createClientModuleSystem }
 
-type Factory = ClientPluginHandoff['factory']
+type Factory = ClientBundleRegistration['factory']
 
 afterEach(() => {
   vi.unstubAllGlobals()
@@ -23,14 +27,31 @@ afterEach(() => {
   for (const el of document.querySelectorAll('style, script')) el.remove()
 })
 
-const row = (id: string): BootModuleRow => ({ id, url: `/plugins/${id}/client.js?rev=0`, rev: '0' })
+const row = (id: string, fields: Partial<BootModuleRow> = {}): BootModuleRow =>
+  ({ id, url: `/plugins/${id}/client.js?rev=0`, rev: '0', external: [], ...fields })
 
 interface Bench {
   loader: ClientModuleLoader
+  target: ClientModuleLoaderTarget
   fetched: string[]
   gates: Map<string, () => void>
 }
 
+/** Build the page-global facade shape consumed by the module system. */
+function registrationTarget(pending: ClientBundleRegistration[] = []): ClientModuleLoaderTarget {
+  const pendingQueue = [...pending]
+  const target: ClientModuleLoaderTarget = {
+    mode: 'queue',
+    pendingQueue,
+    load: (registration) => { pendingQueue.push(registration) },
+    create: options => createClientModuleSystem(target, {
+      id: MODULES_ID,
+      exports: bootstrapExports,
+    }, options),
+  }
+  return target
+}
+
 /**
  * Loader over scripted bundles: load records the row URL, optionally waits on
  * a release callback, then registers the scripted factory through the window
@@ -39,35 +60,61 @@ interface Bench {
 function bench(
   entries: BootModuleRow[],
   bundles: Record<string, Factory | null> = {},
-  opts: { seed?: Record<string, unknown>; gated?: string[] } = {},
+  opts: {
+    seed?: Record<string, unknown>
+    gated?: string[]
+    pending?: ClientBundleRegistration[]
+    defaultTransport?: boolean
+  } = {},
 ): Bench {
   const fetched: string[] = []
   const gates = new Map<string, () => void>()
-  const loader = new ClientModuleSystem({
-    modules: entries,
+  const target = registrationTarget(opts.pending)
+  win.__ModuleLoader__ = target
+  const loadBundle = async (url: string): Promise<void> => {
+    fetched.push(url)
+    if (opts.gated?.includes(url) === true) {
+      await new Promise<void>((resolve) => { gates.set(url, resolve) })
+    }
+    const id = /\/plugins\/(.+)\/client\.js/.exec(url)?.[1]
+    const factory = id === undefined ? undefined : bundles[id]
+    if (factory == null || id === undefined) return
+    win.__ModuleLoader__?.load({ id, factory })
+  }
+  const loader = target.create({
+    boot: { rev: 'graph', entries },
     staticModules: opts.seed ?? {},
-    loadBundle: async (url) => {
-      fetched.push(url)
-      if (opts.gated?.includes(url) === true) {
-        await new Promise<void>((resolve) => { gates.set(url, resolve) })
-      }
-      const id = /\/plugins\/(.+)\/client\.js/.exec(url)?.[1]
-      const factory = id === undefined ? undefined : bundles[id]
-      if (factory == null || id === undefined) return
-      win.__ModuleLoader__?.load({ id, factory })
-    },
+    ...(opts.defaultTransport === true ? {} : { loadBundle }),
   })
-  return { loader, fetched, gates }
+  return { loader, target, fetched, gates }
 }
 
+describe('Cordis plugin face', () => {
+  it('rejects activation before the HTML facade creates the module system', () => {
+    expect(() => { apply(new Context()) }).toThrow('createClientModuleSystem must run before plugin boot')
+  })
+})
+
 describe('lazy CJS arrival', () => {
+  it('drains registrations queued by parser-blocking preload scripts into the same live facade', async () => {
+    const b = bench([row('runtime')], {}, {
+      pending: [{ id: 'runtime', factory: () => ({ marker: 'preloaded' }) }],
+    })
+    const exports = await b.loader.import('runtime', '', {})
+    expect((exports as { marker: string }).marker).toBe('preloaded')
+    expect(b.target.pendingQueue).toEqual([])
+    expect(b.fetched).toEqual([])
+    expect(win.__ModuleLoader__).toBe(b.target)
+    expect(b.target.mode).toBe('live')
+  })
+
   it('prefetch loads and registers but does not run the factory', async () => {
     const ran: string[] = []
     const b = bench([row('a')], { a: () => { ran.push('a'); return {} } })
     await b.loader.prefetch('a')
     expect(b.fetched).toEqual(['/plugins/a/client.js?rev=0'])
     expect(ran).toEqual([])
-    expect(b.loader.loadCache.size).toBe(0)
+    expect(b.loader.loadCache.has('a')).toBe(false)
   })
 
   it('import materializes once and memoizes the exports', async () => {
@@ -88,6 +135,26 @@ describe('lazy CJS arrival', () => {
     expect(b.fetched).toHaveLength(1)
   })
 
+  it('registers declared dynamic requests before materializing their consumer', async () => {
+    const b = bench([
+      row('consumer', { external: ['provider/client', 'react'] }),
+      row('provider'),
+    ], {
+      consumer: req => ({ provider: req('provider/client'), react: req('react') }),
+      provider: () => ({ marker: 'provider' }),
+    }, { seed: { react: { marker: 'react' } } })
+    const exports = await b.loader.import('consumer', '', {}) as {
+      provider: { marker: string }
+      react: { marker: string }
+    }
+    expect(b.fetched).toEqual([
+      '/plugins/provider/client.js?rev=0',
+      '/plugins/consumer/client.js?rev=0',
+    ])
+    expect(exports.provider.marker).toBe('provider')
+    expect(exports.react.marker).toBe('react')
+  })
+
   it('concurrent callers share one in-flight arrival and materialize once', async () => {
     const ran: string[] = []
     const url = '/plugins/a/client.js?rev=0'
@@ -168,24 +235,32 @@ describe('require resolution', () => {
   })
 })
 
-describe('static registry', () => {
-  it('serves shell-own modules to import and require without any fetch', async () => {
-    const shell = { marker: 'app-shell' }
-    const b = bench([row('a')], {
-      a: req => ({ dep: req('app-shell') }),
+describe('bootstrap module', () => {
+  it('caches the materialized modules exports under the package id and /client alias', async () => {
+    const b = bench([
+      row('consumer', { external: [`${MODULES_ID}/client`] }),
+      row(MODULES_ID),
+    ], {
+      consumer: req => ({ dep: req(`${MODULES_ID}/client`) }),
     })
-    b.loader.registerStatic('app-shell', shell)
-    await b.loader.prefetch('app-shell')
-    expect(await b.loader.import('app-shell', '', {})).toBe(shell)
-    expect(b.loader.loadCache.get('app-shell')?.styles).toEqual([])
-    expect((await b.loader.import('a', '', {}) as { dep: unknown }).dep).toBe(shell)
-    expect(b.fetched).toEqual(['/plugins/a/client.js?rev=0'])
+    await b.loader.prefetch(MODULES_ID)
+    const exports = await b.loader.import('consumer', '', {}) as { dep: unknown }
+    expect(exports.dep).toBe(bootstrapExports)
+    expect(await b.loader.import(`${MODULES_ID}/client`, '', {})).toBe(bootstrapExports)
+    expect(b.fetched).toEqual(['/plugins/consumer/client.js?rev=0'])
   })
 
-  it('duplicate static registration is loud', () => {
+  it('publishes the same closed-over system when the modules Cordis plugin activates', () => {
     const b = bench([])
-    b.loader.registerStatic('app-shell', {})
-    expect(() => { b.loader.registerStatic('app-shell', {}) }).toThrow('registered twice')
+    const ctx = new Context()
+    apply(ctx)
+    expect(ctx.modules).toBe(b.loader)
+  })
+
+  it('rejects a second queued registration for the bootstrap id', () => {
+    expect(() => bench([], {}, {
+      pending: [{ id: `${MODULES_ID}/client`, factory: () => ({}) }],
+    })).toThrow(`duplicate factory registration for "${MODULES_ID}/client"`)
   })
 })
 
@@ -216,10 +291,44 @@ describe('failure modes', () => {
     expect(() => bench([row('a'), row('a')])).toThrow('duplicate graph entry "a"')
   })
 
+  it('a module arrival cycle is loud even if a malformed host graph reaches the browser', async () => {
+    const b = bench([
+      row('a', { external: ['b'] }),
+      row('b', { external: ['a'] }),
+    ])
+    await expect(b.loader.prefetch('a')).rejects.toThrow('module arrival cycle a -> b -> a')
+  })
+
   it('double boot is loud', () => {
-    bench([])
-    expect(() => new ClientModuleSystem({ modules: [], staticModules: {} }))
-      .toThrow('already installed (double boot?)')
+    const b = bench([])
+    const options: ClientModuleCreateOptions = {
+      boot: { rev: 'graph', entries: [] },
+      staticModules: {},
+    }
+    expect(() => b.target.create(options)).toThrow('create called after module-system boot')
+  })
+})
+
+describe('boot manifest wire', () => {
+  it('normalizes absent shared-module fields and carries the declared ones', () => {
+    const manifest = parseBootManifest({
+      rev: 'graph',
+      entries: [
+        { id: 'a', url: '/plugins/a/client.js', rev: '1' },
+        { id: 'b', url: '/plugins/b/client.js', rev: '2', external: ['react'] },
+      ],
+    })
+    expect(manifest.modules).toEqual([
+      { id: 'a', url: '/plugins/a/client.js', rev: '1', external: [] },
+      { id: 'b', url: '/plugins/b/client.js', rev: '2', external: ['react'] },
+    ])
+  })
+
+  it('rejects a non-array external', () => {
+    expect(() => parseBootManifest({
+      rev: 'graph',
+      entries: [{ id: 'a', url: '/a', rev: '1', external: 'react' }],
+    })).toThrow('client-modules: boot manifest entry "a" external must be a string array')
   })
 })
 
@@ -283,8 +392,8 @@ describe('default transport seam', () => {
         script.dispatchEvent(new Event('load'))
       })
     })
-    const loader: ClientModuleLoader = new ClientModuleSystem({ modules: [row('dee')], staticModules: {} })
-    const exports = await loader.import('dee', '', {})
+    const b = bench([row('dee')], {}, { defaultTransport: true })
+    const exports = await b.loader.import('dee', '', {})
     expect((exports as { marker: string }).marker).toBe('via-script')
     expect(append).toHaveBeenCalledOnce()
     expect([...document.querySelectorAll('script')]).toEqual([])
@@ -296,8 +405,8 @@ describe('default transport seam', () => {
       if (!(script instanceof HTMLScriptElement)) throw new Error('expected script node')
       queueMicrotask(() => { script.dispatchEvent(new Event('error')) })
     })
-    const loader = new ClientModuleSystem({ modules: [row('dee')], staticModules: {} })
-    await expect(loader.prefetch('dee')).rejects.toThrow(
+    const b = bench([row('dee')], {}, { defaultTransport: true })
+    await expect(b.loader.prefetch('dee')).rejects.toThrow(
       'bundle script /plugins/dee/client.js?rev=0 failed to load',
     )
     expect([...document.querySelectorAll('script')]).toEqual([])

+ 185 - 1
packages/client/modules/tests/node-half.client.spec.ts

@@ -5,10 +5,16 @@ import type { IncomingMessage, ServerResponse } from 'node:http'
 import { tmpdir } from 'node:os'
 import { dirname, join } from 'node:path'
 import { pathToFileURL } from 'node:url'
+import { runInNewContext } from 'node:vm'
 import { Context } from '@deepseek-ai/cordis'
 import { afterEach, describe, expect, it } from 'vitest'
 import type { WebServer, WebRoute } from '@deepseek-ai/dsh-host-webserver'
-import { ClientModuleRegistry } from '../src/index.ts'
+import * as modulesClient from '../src/client/index.ts'
+import { ClientModuleRegistry, injectBootManifest, orderByModuleGraph } from '../src/index.ts'
+import type { ClientModuleLoaderTarget, WebBootEntry, WebBootGraph } from '../src/client/index.ts'
+
+const MODULES_ID = '@deepseek-ai/dsh-client-modules'
+const RUNTIME_ID = '@deepseek-ai/dsh-client-runtime'
 
 let root: string | undefined
 
@@ -37,6 +43,13 @@ function writePackage(
   return clientPath
 }
 
+/** Create a built package with the supplied client declaration. */
+function writeBuiltPackage(packageName: string, client: Record<string, unknown>): void {
+  const clientPath = writePackage(packageName, { dsh: { client: { platform: 'web', ...client } } })
+  mkdirSync(dirname(clientPath), { recursive: true })
+  writeFileSync(clientPath, 'module.exports = {}\n')
+}
+
 /** Construct the node-half service and capture its plugin-bundle route. */
 function constructWithRoute(packageNames: string[]): { service: ClientModuleRegistry; route: WebRoute } {
   const ctx = new Context()
@@ -68,6 +81,85 @@ function construct(packageNames: string[]): ClientModuleRegistry {
   return constructWithRoute(packageNames).service
 }
 
+/** Execute the exact first inline script emitted by the Host HTML transform. */
+function injectedFacade(graph: WebBootGraph): { html: string; target: ClientModuleLoaderTarget } {
+  const html = injectBootManifest('<html><head></head><body><script type="module" src="/index.js"></script></body></html>', graph)
+  const source = /<head><script>([\s\S]*?)<\/script>/.exec(html)?.[1]
+  if (source === undefined) throw new Error('missing injected ModuleLoader facade script')
+  const window: { __ModuleLoader__?: ClientModuleLoaderTarget } = {}
+  runInNewContext(source, { window })
+  if (window.__ModuleLoader__ === undefined) throw new Error('facade script did not install __ModuleLoader__')
+  return { html, target: window.__ModuleLoader__ }
+}
+
+const bootGraph = (): WebBootGraph => ({
+  rev: 'graph',
+  entries: [
+    { id: MODULES_ID, url: '/plugins/modules.js?rev=m', rev: 'm' },
+    { id: RUNTIME_ID, url: '/plugins/runtime.js?rev=r', rev: 'r' },
+  ],
+})
+
+describe('HTML bootstrap facade', () => {
+  it('precedes blocking preloads and the boot graph, then becomes the live registration target', async () => {
+    const graph = bootGraph()
+    const { html, target } = injectedFacade(graph)
+    const facadeAt = html.indexOf('window.__ModuleLoader__=')
+    const modulesAt = html.indexOf('<script src="/plugins/modules.js?rev=m"></script>')
+    const runtimeAt = html.indexOf('<script src="/plugins/runtime.js?rev=r"></script>')
+    const graphAt = html.indexOf('window.__DSH_BOOT__ = ')
+    const entryAt = html.indexOf('<script type="module" src="/index.js"></script>')
+    expect([facadeAt, modulesAt, runtimeAt, graphAt, entryAt]).toEqual([...new Set([
+      facadeAt, modulesAt, runtimeAt, graphAt, entryAt,
+    ])].sort((a, b) => a - b))
+
+    target.load({ id: MODULES_ID, factory: () => modulesClient })
+    target.load({ id: RUNTIME_ID, factory: () => ({ marker: 'runtime' }) })
+    const system = target.create({ boot: graph, staticModules: {} })
+
+    expect(target.mode).toBe('live')
+    expect(target.pendingQueue).toEqual([])
+    expect(system.manifest.rev).toBe('graph')
+    expect(await system.import(MODULES_ID)).toBe(modulesClient)
+    expect(await system.import(`${RUNTIME_ID}/client`)).toEqual({ marker: 'runtime' })
+    expect(() => target.create({ boot: graph, staticModules: {} }))
+      .toThrow('create called after module-system boot')
+  })
+
+  it('rejects a page that did not preload the modules bundle', () => {
+    const graph = bootGraph()
+    const { target } = injectedFacade(graph)
+    expect(() => target.create({ boot: graph, staticModules: {} }))
+      .toThrow(`HTML did not preload ${MODULES_ID}/client.js`)
+  })
+
+  it('rejects a bootstrap bundle with a runtime external', () => {
+    const graph = bootGraph()
+    const { target } = injectedFacade(graph)
+    target.load({
+      id: MODULES_ID,
+      factory: (require) => {
+        require('react')
+        return modulesClient
+      },
+    })
+    expect(() => target.create({ boot: graph, staticModules: {} }))
+      .toThrow(`${MODULES_ID}/client.js requested external "react"`)
+  })
+
+  it.each([
+    null,
+    { ...modulesClient, createClientModuleSystem: undefined },
+    { ...modulesClient, apply: undefined },
+  ])('rejects a bootstrap bundle without the complete module face', (exports) => {
+    const graph = bootGraph()
+    const { target } = injectedFacade(graph)
+    target.load({ id: MODULES_ID, factory: () => exports as unknown as Record<string, unknown> })
+    expect(() => target.create({ boot: graph, staticModules: {} }))
+      .toThrow(`${MODULES_ID}/client.js did not export the bootstrap module face`)
+  })
+})
+
 describe('client bundle activation', () => {
   it('allows sibling dsh roles', () => {
     const currentName = '@fixture/current-client-field'
@@ -150,3 +242,95 @@ describe('client bundle activation', () => {
     expect(body).toBe(map)
   })
 })
+
+describe('shared module declarations', () => {
+  it('accepts external requests and carries them onto the graph row', () => {
+    const packageName = '@fixture/shared-declared'
+    writeBuiltPackage(packageName, { external: ['react'] })
+    expect(construct([packageName]).graph().entries).toEqual([{
+      id: packageName,
+      url: expect.stringContaining(`/plugins/${packageName}/client.js?rev=`) as unknown as string,
+      rev: expect.any(String) as unknown as string,
+      external: ['react'],
+    }])
+  })
+
+  it('omits external when the package declares no requests', () => {
+    const packageName = '@fixture/shared-absent'
+    writeBuiltPackage(packageName, {})
+    const [row] = construct([packageName]).graph().entries
+    expect(row).not.toHaveProperty('external')
+  })
+
+  it('rejects a non-array external', () => {
+    const packageName = '@fixture/external-not-array'
+    writeBuiltPackage(packageName, { external: 'react' })
+    expect(() => construct([packageName]))
+      .toThrow(`client-modules: ${packageName} dsh.client.external must be a string array`)
+  })
+})
+
+describe('module graph order', () => {
+  const entry = (id: string, fields: Partial<WebBootEntry> = {}): WebBootEntry =>
+    ({ id, url: `/plugins/${id}/client.js?rev=0`, rev: '0', ...fields })
+  const ids = (entries: readonly WebBootEntry[]): string[] => entries.map(row => row.id)
+
+  it('places every requested package row before its consumers along a chain', () => {
+    expect(ids(orderByModuleGraph([
+      entry('ui', { external: ['slots'] }),
+      entry('slots', { external: ['render'] }),
+      entry('render'),
+    ]))).toEqual(['render', 'slots', 'ui'])
+  })
+
+  it('places a shared package row before both arms of a diamond', () => {
+    expect(ids(orderByModuleGraph([
+      entry('app', { external: ['left', 'right'] }),
+      entry('left', { external: ['vendor'] }),
+      entry('right', { external: ['vendor'] }),
+      entry('vendor'),
+    ]))).toEqual(['vendor', 'left', 'right', 'app'])
+  })
+
+  it('resolves a /client request onto the requested package row', () => {
+    expect(ids(orderByModuleGraph([
+      entry('ui', { external: ['runtime/client'] }),
+      entry('runtime'),
+    ]))).toEqual(['runtime', 'ui'])
+  })
+
+  it('leaves a request no row answers to the static assembly channel', () => {
+    expect(ids(orderByModuleGraph([
+      entry('consumer', { external: ['@deepseek-ai/cordis'] }),
+      entry('other'),
+    ]))).toEqual(['consumer', 'other'])
+  })
+
+  it('rejects a cycle and names the packages on it', () => {
+    expect(() => orderByModuleGraph([
+      entry('a', { external: ['b'] }),
+      entry('b', { external: ['a'] }),
+    ])).toThrow('client-modules: module graph cycle a -> b -> a')
+  })
+
+  it('rejects a row requesting its own package name', () => {
+    expect(() => orderByModuleGraph([entry('solo', { external: ['solo'] })]))
+      .toThrow('client-modules: "solo" requests module "solo" that it answers itself')
+  })
+
+  it('composes the served graph in module-graph order', () => {
+    const consumerName = '@fixture/order-consumer'
+    const dependencyName = '@fixture/order-dependency'
+    writeBuiltPackage(consumerName, { external: [dependencyName] })
+    writeBuiltPackage(dependencyName, {})
+    expect(ids(construct([consumerName, dependencyName]).graph().entries))
+      .toEqual([dependencyName, consumerName])
+  })
+
+  it('fails activation loud when scanned packages form a module cycle', () => {
+    writeBuiltPackage('@fixture/cycle-a', { external: ['@fixture/cycle-b'] })
+    writeBuiltPackage('@fixture/cycle-b', { external: ['@fixture/cycle-a'] })
+    expect(() => construct(['@fixture/cycle-a', '@fixture/cycle-b']))
+      .toThrow('module graph cycle @fixture/cycle-a -> @fixture/cycle-b -> @fixture/cycle-a')
+  })
+})

+ 4 - 1
packages/client/modules/tsdown.config.ts

@@ -1,3 +1,6 @@
 import { clientBundle } from '../tsdown.client.ts'
 
-export default clientBundle('@deepseek-ai/dsh-client-modules', ['lib/types/index.js', 'lib/types/invariant.js'])
+export default clientBundle(
+  '@deepseek-ai/dsh-client-modules',
+  ['lib/types/index.js', 'lib/types/invariant.js'],
+)

+ 2 - 2
packages/client/runtime/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/runtime/README.md
-README.md: 1fb91bc8ca1bf9beae0ea12632acd9572db58670
-README.zh.md: 294a77d7081f5b475aca8b50f6c6d8322370375d
+README.md: cad8a08f015a7115ebff76cfce4d3ab5eb7a175e
+README.zh.md: 262ab1eeb8ce2d09e801e2af790046d8cdc11e55

+ 2 - 2
packages/client/runtime/README.md

@@ -6,7 +6,7 @@ Client cordis boot and React-free object services: SlotRegistry wraps SlotCore a
 
 For each prompt that can reach a local root or continuable child Agent, the runtime samples the browser's current `Intl.DateTimeFormat().resolvedOptions().timeZone` and attaches it to that one Session or subagent prompt RPC. It is neither cached nor included in Session creation or fork state, so travel and concurrent tabs keep message-local provenance. A browser that cannot provide a non-empty zone fails the prompt locally instead of silently substituting deployment state.
 
-`bindSettingsScope` is the browser mirror of the Host-side settings owner seam for one domain-owned namespace. It subscribes before starting a nonblocking initial read, publishes a uSES snapshot (status, section value, the composition `base` and raw `user` layers, revision, writability, host/memory mode), serializes `set` and `unset` writes with the latest known namespace revision, suppresses stale publications, recovers a rejected latest write from Host state, and reaches quiescence on plugin disposal. The default decoder validates each section against the namespace's own serialized wire schema (rehydrated through dsh-client-schema-form), so a domain adds a decoder only to narrow beyond that schema. Loopback pages use the Host settings API; remote pages stay in memory mode. A field is overridden when it is PRESENT in `user` — an override equal to the composition default is still an override, which comparing values could not see — and `unset` is how a form clears one back to `base`. Domain packages own the namespace schema, default, and live service rather than putting product policy in runtime.
+Settings owners share the React-free `SettingsScopeSpec`, `SettingsScope`, and snapshot types defined here. ui-settings owns `ctx.settingsScope.bind(spec)`, its Host transport, schema validation, and lifecycle; see [its package contract](../ui-settings/README.md).
 
 ## Slot declaration injection
 
@@ -24,7 +24,7 @@ Workspace and Session lists have independent monotone `pending` → `ready` base
 
 `WorkspaceListState.archivedSessionIds` mirrors the Host's registry-global archive set (a `readonly SessionId[]` in Host order, replaced only when membership changes; consumers needing O(1) lookups build a transient Set). It is full-snapshot state: the `workspace.list` baseline, the `archiveSession` unary echo, and the `host/archived-sessions-changed` frame each install the complete set. `WorkspaceRuntime.archiveSession(sessionId)` archives over the wire; the projection sweep clears the current selection into the New Session view state whenever it lands in the archive set — one rule covering the local echo, another tab's frame, and a reconnect baseline restoring a selection archived while this client was away. A set installed while a `workspace.list` request is in flight also supersedes that stale baseline's set. Grouping surfaces hide members everywhere while the session rows stay in the list store.
 
-SlotRegistry gives the renderer separate bare observables for `useSessions` and `useWorkspaces`; web-react creates the hooks. Workspace business state does not enter `SessionListState` or an entry store.
+SlotRegistry gives the renderer separate bare observables for `useSessions` and `useWorkspaces`; ui-renderer creates the hooks. Workspace business state does not enter `SessionListState` or an entry store.
 
 `indexSubagentDescendants()` derives per-parent total and running descendant counts from the retained list mirror. It follows only uninterrupted `origin: 'subagent'` ancestry, so an ordinary fork starts a separate ownership subtree; cycles stop without throwing, and a missing parent remains a harmless key until its summary arrives.
 

+ 2 - 2
packages/client/runtime/README.zh.md

@@ -6,7 +6,7 @@
 
 对于每条可到达本地根 Agent 或可继续子 Agent 的提示词,运行时都会采样浏览器当前的 `Intl.DateTimeFormat().resolvedOptions().timeZone`,并只把该值附加到这一次 Session 或 subagent 提示词 RPC。该值既不缓存,也不包含在 Session 创建或 fork 状态中,因此旅行与并发标签页都能保留消息本地的来源信息。浏览器若无法提供非空时区,会在本地拒绝该提示词,而不会悄然使用部署状态代替。
 
-`bindSettingsScope` 面向单个由领域持有的 namespace,是 Host 侧 settings owner seam 的浏览器镜像。它在开始非阻塞初始读取前建立订阅,发布 uSES 快照(状态、分节值、组装 `base` 层与原始 `user` 层、revision、可写性、host/内存模式),使用已知最新 namespace revision 串行执行 `set` 与 `unset` 写入,抑制陈旧发布,并在最新写入被拒时从 Host 状态恢复;插件释放时,它会达到完全停稳。默认解码器会对照该 namespace 自身的序列化 wire schema(经 dsh-client-schema-form 还原)校验每个分节,因此领域只有在需要比该 schema 进一步收窄时才添加解码器。回环页面使用 Host settings API,远程页面则停留在内存模式。字段是否被覆盖,取决于它是否**出现**在 `user` 中——与组装默认值相同的覆盖仍然是覆盖,比较值是看不出来的——而 `unset` 就是表单把某个字段清回 `base` 的方式。namespace schema、默认值与实时服务归领域包所有,而非把产品政策放入运行时
+设置所有者共用本包定义的不依赖 React 的 `SettingsScopeSpec`、`SettingsScope` 与快照类型。ui-settings 拥有 `ctx.settingsScope.bind(spec)`、对应的 Host 传输、schema 校验与生命周期;详见[该包的约定](../ui-settings/README.md)
 
 ## Slot 声明注入
 
@@ -24,7 +24,7 @@ Workspace 和 Session 列表各自具有单调的 `pending` → `ready` 基线
 
 `WorkspaceListState.archivedSessionIds` 镜像 Host 的注册表级全局归档集合(一个按 Host 顺序的 `readonly SessionId[]`,仅在成员变化时才替换;需要 O(1) 查询的消费方自建临时 Set)。它是全快照状态:`workspace.list` 基线、`archiveSession` 一元回声和 `host/archived-sessions-changed` 帧各自安装完整集合。`WorkspaceRuntime.archiveSession(sessionId)` 通过 wire 归档;投影层在当前 selection 落入归档集合时统一清空为 New Session 视图状态——一条规则同时覆盖本地回声、其他标签页的帧、以及重连基线恢复出一个离线期间被归档的 selection。在 `workspace.list` 请求进行中安装的集合还会取代该过期基线携带的集合。各分组视图在所有位置隐藏集合成员,而会话行本身仍留在列表 store 中。
 
-SlotRegistry 分别为 renderer 提供 `useSessions` 与 `useWorkspaces` 的裸 observable;web-react 创建钩子。Workspace 业务状态不会进入 `SessionListState` 或条目 store。
+SlotRegistry 分别为 renderer 提供 `useSessions` 与 `useWorkspaces` 的裸 observable;ui-renderer 创建钩子。Workspace 业务状态不会进入 `SessionListState` 或条目 store。
 
 `indexSubagentDescendants()` 从保留的列表镜像中派生每个 parent 的后代总数与运行中后代数。它只沿不间断的 `origin: 'subagent'` 祖先链追踪,因此普通 fork 会开启独立的归属子树;遇到环时,追踪会停止但不会抛出异常,缺失的 parent 则会保留为无害的键,直至其摘要到达。
 

+ 24 - 13
packages/client/runtime/package.json

@@ -42,28 +42,26 @@
   },
   "license": "MIT",
   "dependencies": {
+    "immer": "^10.1.1",
+    "zustand": "~4.4.7"
+  },
+  "peerDependencies": {
+    "@deepseek-ai/cordis": "workspace:^",
+    "@deepseek-ai/dsh-api-remotes": "workspace:^",
+    "@deepseek-ai/dsh-invariants": "workspace:^",
+    "@deepseek-ai/dsh-typert-protocol": "workspace:^",
+    "@deepseek-ai/dsh-typert-registry": "workspace:^",
     "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-attachment": "workspace:^",
     "@deepseek-ai/dsh-client-connection": "workspace:^",
     "@deepseek-ai/dsh-commands": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-host-apiproxy": "workspace:^",
     "@deepseek-ai/dsh-llm": "workspace:^",
     "@deepseek-ai/dsh-llm-retry": "workspace:^",
     "@deepseek-ai/dsh-session": "workspace:^",
     "@deepseek-ai/dsh-session-projection": "workspace:^",
     "@deepseek-ai/dsh-session-title": "workspace:^",
-    "@deepseek-ai/dsh-tools": "workspace:^",
-    "immer": "^10.1.1",
-    "react": "^18.2.0",
-    "zustand": "~4.4.7"
-  },
-  "peerDependencies": {
-    "@deepseek-ai/cordis": "workspace:^",
-    "@deepseek-ai/dsh-api-remotes": "workspace:^",
-    "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/dsh-typert-protocol": "workspace:^",
-    "@deepseek-ai/dsh-typert-registry": "workspace:^"
+    "@deepseek-ai/dsh-tools": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
@@ -72,7 +70,20 @@
     "@deepseek-ai/dsh-timeout": "workspace:^",
     "@deepseek-ai/dsh-typert-protocol": "workspace:^",
     "@deepseek-ai/dsh-typert-registry": "workspace:^",
-    "@types/react": "~18.3.1"
+    "@types/react": "~18.3.1",
+    "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
+    "react": "^18.2.0",
+    "@deepseek-ai/dsh-agent": "workspace:^",
+    "@deepseek-ai/dsh-attachment": "workspace:^",
+    "@deepseek-ai/dsh-client-connection": "workspace:^",
+    "@deepseek-ai/dsh-commands": "workspace:^",
+    "@deepseek-ai/dsh-host-apiproxy": "workspace:^",
+    "@deepseek-ai/dsh-llm": "workspace:^",
+    "@deepseek-ai/dsh-llm-retry": "workspace:^",
+    "@deepseek-ai/dsh-session": "workspace:^",
+    "@deepseek-ai/dsh-session-projection": "workspace:^",
+    "@deepseek-ai/dsh-session-title": "workspace:^",
+    "@deepseek-ai/dsh-tools": "workspace:^"
   },
   "files": [
     "lib/index.js",

+ 3 - 3
packages/client/runtime/src/client/contract/store.ts

@@ -4,9 +4,9 @@
  * shell over it: {@link defineStore} bakes an init/persist/actions literal
  * into a {@link StoreHandle}, the registration-side store seat of slot
  * terminals. Lives in the React-free runtime (the data layer owns its
- * engine; web-react is shell-only React
+ * engine; ui-renderer is shell-only React
  * glue): engine products are bare observables — subscribe/getSnapshot/
- * update/set, NO selector hook. Hook synthesis is web-react's (the one
+ * update/set, NO selector hook. Hook synthesis is ui-renderer's (the one
  * uSES bridge, cached per source at the binding site).
  */
 import { createStore, type StoreApi } from 'zustand/vanilla'
@@ -26,7 +26,7 @@ export type {
 /** Minimal observable snapshot source: Session objects and snapshot stores both satisfy it. */
 export interface ObservableSnapshot<T> { getSnapshot(): T; subscribe(fn: () => void): () => void }
 
-/** Writable snapshot store (bare data face; React selector hooks are synthesized in web-react). */
+/** Writable snapshot store (bare data face; React selector hooks are synthesized in ui-renderer). */
 export interface SnapshotStore<T> extends ObservableSnapshot<T> {
   /**
    * Mutate the state through an immer draft.

+ 1 - 1
packages/client/runtime/src/client/index.ts

@@ -66,7 +66,7 @@ export type { WorkspaceListState } from './workspaces/service.ts'
 export type {
   DirectoryEntry, DirectoryListing, WorkspaceId, WorkspaceView,
 } from '@deepseek-ai/dsh-client-connection/client'
-// Runtime owns the snapshot store; web-react only binds it to React.
+// Runtime owns the snapshot store; ui-renderer only binds it to React.
 export { createSnapshotStore, defineStore, shallowEqual } from './contract/store.ts'
 export type {
   EngineStoreHandle, EngineStoreInstance, ObservableSnapshot, SnapshotStore,

Некоторые файлы не были показаны из-за большого количества измененных файлов