瀏覽代碼

Merge pull request #2571 from deepseek-harness/worktree-reducepkg

fix(client): declare browser-only externals as devDependencies
imccyu 2 月之前
父節點
當前提交
6c18b04a6b
共有 54 個文件被更改,包括 1090 次插入 和 291 次删除
  1. 2 2
      .agents/notes/implemented/process/2026-07-30-generated-third-party-notices.i18n.yaml
  2. 0 0
      .agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md
  3. 0 0
      .agents/notes/implemented/process/2026-07-30-generated-third-party-notices.zh.md
  4. 6 0
      .agents/notes/proposed/process/2026-08-14-client-build-time-deps.i18n.yaml
  5. 100 0
      .agents/notes/proposed/process/2026-08-14-client-build-time-deps.md
  6. 100 0
      .agents/notes/proposed/process/2026-08-14-client-build-time-deps.zh.md
  7. 4 4
      THIRD_PARTY_NOTICES.md
  8. 4 4
      apps/web/package.json
  9. 1 0
      apps/web/vite.config.ts
  10. 1 1
      lefthook.yml
  11. 2 1
      package.json
  12. 10 1
      packages/client/AGENTS.md
  13. 1 2
      packages/client/locale/package.json
  14. 5 5
      packages/client/runtime/package.json
  15. 1 2
      packages/client/ui-agent-preset/package.json
  16. 5 5
      packages/client/ui-attachment/package.json
  17. 3 6
      packages/client/ui-commands/package.json
  18. 3 4
      packages/client/ui-conversation/package.json
  19. 2 4
      packages/client/ui-deliverables/package.json
  20. 3 6
      packages/client/ui-directory-picker-browse/package.json
  21. 1 2
      packages/client/ui-directory-picker-native/package.json
  22. 1 2
      packages/client/ui-goal/package.json
  23. 3 6
      packages/client/ui-input-trigger/package.json
  24. 2 4
      packages/client/ui-jobs/package.json
  25. 1 2
      packages/client/ui-layout/package.json
  26. 1 2
      packages/client/ui-message-feedback/package.json
  27. 1 3
      packages/client/ui-model-selection/package.json
  28. 1 2
      packages/client/ui-permission-presets/package.json
  29. 1 2
      packages/client/ui-plan/package.json
  30. 5 7
      packages/client/ui-primitives/package.json
  31. 4 5
      packages/client/ui-settings-general/package.json
  32. 1 2
      packages/client/ui-settings-models/package.json
  33. 1 2
      packages/client/ui-settings-plugin-inventory/package.json
  34. 3 6
      packages/client/ui-settings-plugins/package.json
  35. 1 2
      packages/client/ui-settings/package.json
  36. 3 6
      packages/client/ui-sidebar/package.json
  37. 1 2
      packages/client/ui-skill/package.json
  38. 2 4
      packages/client/ui-subagent/package.json
  39. 2 3
      packages/client/ui-theme/package.json
  40. 2 5
      packages/client/ui-tool/package.json
  41. 4 8
      packages/client/ui-trajectory/package.json
  42. 4 4
      packages/client/ui-user-questions/package.json
  43. 2 4
      packages/client/ui-workflow-run/package.json
  44. 3 6
      packages/client/ui-workspace/package.json
  45. 4 4
      packages/client/web-react/package.json
  46. 4 4
      packages/client/web/package.json
  47. 1 2
      packages/extensions/cordis-client-runner/package.json
  48. 1 2
      packages/extensions/ui-cordis/package.json
  49. 26 8
      packages/session-query/session-log-export/package.json
  50. 101 114
      pnpm-lock.yaml
  51. 230 0
      scripts/browser-bundled-externals.ts
  52. 21 3
      scripts/gen-third-party-notices.spec.ts
  53. 31 16
      scripts/gen-third-party-notices.ts
  54. 368 0
      scripts/verify-client-runtime-deps.ts

+ 2 - 2
.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.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-30-generated-third-party-notices.md
-2026-07-30-generated-third-party-notices.md: 9ca24c9634b0410f9b7cc255902344cd6014bb90
-2026-07-30-generated-third-party-notices.zh.md: b49e60565bbae62cf17026db17babc1ba7a408dc
+2026-07-30-generated-third-party-notices.md: 2128537b09f9d5d68cb1175b1ecb195fa50e1865
+2026-07-30-generated-third-party-notices.zh.md: 906f5ed5b9a11f3ddd68491686b460c5d9f4a682

文件差異過大導致無法顯示
+ 0 - 0
.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.md


文件差異過大導致無法顯示
+ 0 - 0
.agents/notes/implemented/process/2026-07-30-generated-third-party-notices.zh.md


+ 6 - 0
.agents/notes/proposed/process/2026-08-14-client-build-time-deps.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/proposed/process/2026-08-14-client-build-time-deps.md
+2026-08-14-client-build-time-deps.md: 0605225a70a16fed9004acfe26adba9b6166f201
+2026-08-14-client-build-time-deps.zh.md: 5caed3422288711532c40cd767f72c97693e68b4

+ 100 - 0
.agents/notes/proposed/process/2026-08-14-client-build-time-deps.md

@@ -0,0 +1,100 @@
+# Agent Note: Client build-time dependencies stay out of the install face
+
+Status: proposed
+
+English | [中文](2026-08-14-client-build-time-deps.zh.md)
+
+## Problem
+
+A browser artifact resolves nothing on the user's machine:
+
+- A `ui-*` plugin package's browser artifact is `lib/client.js`, where tsdown inlines every non-platform specifier (`noExternal` in `packages/client/tsdown.client.ts`). The specifiers that survive are answered by the loader's frozen module table, because `require` inside that bundle is a parameter the loader injects, not Node's.
+- Platform modules (`PLATFORM_MODULES`) come from the shell `dist`, never from Node resolution.
+- The shell's own imports are inlined by Vite into `@deepseek-ai/dsh-web-frontend`'s published `dist`; that package ships `dist` alone and has no `.` export.
+
+Every browser code path is therefore a build product, served as an asset or baked into `dist`. Yet the packages those artifacts are built from — react, react-dom, shiki, katex, clsx, the micromark and mdast families — sit in `dependencies` and non-optional `peerDependencies`, which npm installs for every consumer of the published package. Across the repository that is 79 such external declarations in 38 packages, downloaded by users who never load them.
+
+## Proposal
+
+### The rule
+
+**An external package only a browser artifact reaches belongs in `devDependencies`.** Two deliberate omissions are as much part of the rule:
+
+- **External packages only.** A `@deepseek-ai/*` name stays where its manifest puts it. Such a declaration also states which package supplies an injected service, which Remote contribution an assembly mounts, or which Loader row must resolve; [verify-runtime-closure](../../../../scripts/verify-runtime-closure.ts) and the Loader read it, and the app installs the package regardless — so moving one removes meaning without removing a download.
+- **Anything the node half reaches stays**, an erased type import included.
+
+Faces are walked from the entries a manifest publishes, not by a directory rule, so a module under `src/` that only the browser entry reaches counts as browser source:
+
+| kind | test | host face entries |
+| --- | --- | --- |
+| `bundle-half` | has a `./client` export | every export target except `./client` |
+| `browser-library` | under `packages/client/` with no `./client` export | `src/invariant.ts` alone — the companion the host mounts; `.` is browser code |
+| `prebuilt-dist` | no `.` export, ships a `dist` | none: the package offers Node no entry |
+
+### The gate: `scripts/verify-client-runtime-deps.ts`
+
+Wired into `pnpm run hygiene`, about 35 seconds — the cost of two bound Programs, the same order as `verify-optional-dependency-imports` in that lane. It reuses the repository's tooling rather than growing its own: `TypeScriptProject` (`scripts/ts-project.ts`) binds the host and client compiler faces separately (that file states why the two cannot share one program — the cordis Context merges collide), `ts.resolveModuleName` resolves relative specifiers, and the walk stops at the package boundary.
+
+Three findings decided the mechanism, after a first pass that scanned string literals:
+
+1. A package name must match as a name: the `react` substring inside `'@deepseek-ai/dsh-client-web-react'` silently swallowed react.
+2. Whether `./client` is the tsdown browser bundle is keyed on the **artifact path** (`./lib/client.js`), not the subpath name — `dsh-goal` publishes `./client` as `./lib/types/client.js`, a plain tsc-emitted browser-shared module.
+3. `require`, `require.resolve`, and dynamic `import()` on a literal each reach a package; `require.resolve('@deepseek-ai/dsh-web-frontend/dist/index.html')` is a real host resolution path.
+
+Two classes, both reported per entry:
+
+| class | count | test |
+| --- | --- | --- |
+| `browser` | 74 | only a browser artifact reaches it |
+| `nothing` | 5 | no reference names it: `client-runtime`'s `react` (which contradicts its own React-free layering red line), the peer `react` of `ui-settings` and `ui-theme`, `ui-trajectory`'s peer `react-dom`, and `ui-primitives`' `@types/mdast` |
+
+Each conservative rule below answers a false report or a semantic loss observed while building it:
+
+- **A type reference from the node half keeps its declaration.** `import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'` in `src/invariant.ts` is erased at run time, yet it states which package supplies the service that companion registers — `verify-runtime-closure`'s relation.
+- **A package publishing a Node entry with no source counterpart is skipped whole, and named in the output.** `dsh-goal`'s `./typert -> ./lib/typert.host.js` is emitted by the typert generator and carries its own `import { z } from 'zod'`, which no source states. Getting this test right removed four false reports, among them `api-gateway`'s `typert-registry`.
+- A `cordis*.yml` the package owns counts as host face: a Loader row names its plugin instead of importing it.
+- `@deepseek-ai/cordis` is exempt — check-workspace-constraints requires it as both peer and dev everywhere.
+
+`--json` output feeds the bulk edit and the install measurement.
+
+### What leaves an install
+
+Measured against a real `npm install` of the published CLI, with tarball bytes read from an isolated cache: 103 external tarballs stop being downloaded, 6.05 MB in total.
+
+| group | packages | saved |
+| --- | --- | --- |
+| syntax highlighting and math (shiki family, oniguruma family, katex) | 16 | 3.93 MB |
+| react and view libraries (react, react-dom, scheduler, immer, zustand, `@tanstack/*`, clsx, use-sync-external-store) | 11 | 1.47 MB |
+| markdown and ansi pipeline plus odds and ends (micromark, mdast, hast families, anser, a few `@types/*`) | 76 | 0.65 MB |
+
+Our own six browser-library packages (ui-primitives, ui-slots, web-react, ui-attachment, schema-form, client-web — 0.20 MB together) stay installed: code names them, and the rule above leaves those declarations alone.
+
+### How it lands, split by nature
+
+1. **Documentation first**: a declaration section in `packages/client/AGENTS.md`, and one clause in the new-plugin-package checklist.
+2. **The gate**: `scripts/verify-client-runtime-deps.ts`, its `package.json` script, its place in `hygiene`, and a counterexample spec.
+3. **The manifests**: 79 entries in 38 packages. 50 need a new `devDependencies` entry; the rest already carry one, so the change is a deleted line.
+4. **Re-measure after the next release** with the same method, confirming the 103 tarballs stay gone.
+
+## Alternatives considered
+
+- **Scanning string literals**: the first implementation, rejected by the three findings above — the react-inside-web-react substring had already produced a silent miss.
+- **Reading built artifacts (`lib/**/*.js`) instead of source**: that is Node's own view, but the gate would then depend on `pnpm run build`, and it still cannot judge a browser-library's `lib/index.js` (node platform, browser content), so the face test stays either way.
+- **Asking the checker whether a binding is used in a value position** (what `verify-optional-dependency-imports` does): tried, and it also judged 83 node-face type-only declarations movable — no download saved for a real loss of meaning, 53 of them `dsh-invariants`. This gate needs to know whether a reference exists, not whether it is a value.
+- **Also clearing our own six browser-library packages from the install face**, on the test that no install loads one: another 0.20 MB, at the price of deleting 74 workspace declarations that code genuinely names. Ruled out (2026-08-14): keep what the code names. The cleaner end state is to stop publishing those six packages, which is its own proposal.
+- **`peerDependenciesMeta.optional` instead of `devDependencies`**: npm does skip an optional peer, but the meaning is "a consumer may supply this", and there is no run-time consumer at all. The repository must install it to build, which is what `devDependencies` says.
+- **Leaving it to knip**: out of scope for knip, which reports a declared package nothing imports. These specifiers are imported; a bundler inlines them. The evidence is that they persisted on master with knip green. Only the five `nothing` entries overlap.
+- **`optionalDependencies`**: wrong meaning — it says "skip this if it cannot be installed".
+
+## Acceptance criteria
+
+- `pnpm run hygiene` includes `verify-client-runtime-deps` and passes; a counterexample spec proves one `dependencies.react` is rejected.
+- `pnpm run build`, `pnpm run test:gui`, and `DSH_SNAPSHOT=replay pnpm run test:web` pass — the move changes no build input, so artifacts stay byte-identical.
+- A real install after the next release no longer downloads the 103 tarballs above.
+
+## Risks
+
+- **The six browser-library packages that stay installed carry bare imports nothing resolves**: `ui-primitives/lib/index.js` is a rolldown artifact and still reads `from "anser"`, while anser is now dev-only. It is inert — only our Vite build reads that file, and no loader exists for it on a user's machine (verified: only browser code imports those packages, never the host). Retiring their publication is the way to erase it; see Alternatives.
+- **`@types/*` go unreported**: source never names them, so the rule cannot see them. `@types/mdast` was caught only because nothing referenced it either. They belong in dev regardless, and closing that gap is follow-up work.
+- **A skipped package is unprotected**: `dsh-goal` is skipped whole for its generated entry, so its browser-side declarations are now nobody's business. Reading a generated artifact's own run-time imports is what would let the exemption be withdrawn.
+- **A false report would delete a declaration something needs at run time**: three defenses hold that line — literal arguments to `require`, `require.resolve`, and dynamic `import()` count as references; a package's own `cordis*.yml` counts as host face; and no `@deepseek-ai/*` name is subject at all.

+ 100 - 0
.agents/notes/proposed/process/2026-08-14-client-build-time-deps.zh.md

@@ -0,0 +1,100 @@
+# Agent Note: 客户端构建期依赖不进安装面
+
+Status: proposed
+
+[English](2026-08-14-client-build-time-deps.md) | 中文
+
+## Problem
+
+浏览器产物不在用户机上解析任何 specifier:
+
+- `ui-*` 插件包的浏览器产物是 `lib/client.js`,tsdown 把每个非平台 specifier 直接内联(`packages/client/tsdown.client.ts` 的 `noExternal`)。留下来的 specifier 由 loader 的冻结模块表应答——那个 bundle 里的 `require` 是 loader 注入的形参,不是 Node 的。
+- 平台模块(`PLATFORM_MODULES`)由 shell `dist` 提供,不走 Node 解析。
+- shell 自身的 import 由 Vite 内联进 `@deepseek-ai/dsh-web-frontend` 已发布的 `dist`;该包只发 `dist`,连 `.` 导出都没有。
+
+所以浏览器的每条代码路径都是构建产物,或作为静态资源下发,或烤进 `dist`。但这些产物的构建输入——react、react-dom、shiki、katex、clsx、micromark 与 mdast 全族——现在写在 `dependencies` 和非 optional `peerDependencies` 里,而 npm 对每个消费者都会安装这两个区段。全仓 38 个包共 79 处这样的外部依赖声明,装给了永远不会加载它们的用户。
+
+## Proposal
+
+### 规则
+
+**只被浏览器产物触及的外部包落 `devDependencies`。** 两条留白同样是规则的一部分:
+
+- **只管外部依赖**。`@deepseek-ai/*` 一律留在原处:那些声明还表达「谁提供我注入的服务」「这个 assembly 挂载了谁的 Remote」「哪个 Loader 行必须能解析」,[verify-runtime-closure](../../../../scripts/verify-runtime-closure.ts) 与 Loader 都读它,而 app 无论如何都会装那个包——移走只是删掉语义,并没有减少下载。
+- **node 面触及的一律不动**,包括被擦除的类型引用。
+
+face 从 manifest 真正发布的入口走图,不用目录规则,所以 `src/` 下只被浏览器入口触及的模块就算浏览器代码:
+
+| kind | 判据 | host face 入口 |
+| --- | --- | --- |
+| `bundle-half` | 有 `./client` 导出 | 除 `./client` 外的每个导出目标 |
+| `browser-library` | `packages/client/` 下且无 `./client` 导出 | 只有 `src/invariant.ts`——宿主唯一能挂载的伴生模块;`.` 面是浏览器代码 |
+| `prebuilt-dist` | 无 `.` 导出、发布 `dist` | 没有:这个包不给 Node 提供任何入口 |
+
+### 门禁:`scripts/verify-client-runtime-deps.ts`
+
+接入 `pnpm run hygiene`,约 35 秒——两个绑定 Program 的开销,与同 lane 的 `verify-optional-dependency-imports` 同量级。复用仓内既有工具而不自造一套:`TypeScriptProject`(`scripts/ts-project.ts`)分别绑定 host 与 client 两个编译面(该文件写明两者不能合进一个 program——cordis Context merge 会撞),相对 specifier 交给 `ts.resolveModuleName` 解析,遍历到包边界即停。
+
+判据要害有三条,都是起手那版扫字符串字面量踩出来的:
+
+1. 包名必须按名匹配:`'@deepseek-ai/dsh-client-web-react'` 里的 `react` 子串会静默吞掉 react。
+2. `./client` 是不是 tsdown 浏览器 bundle,看的是**产物路径**(`./lib/client.js`)而不是子路径名——`dsh-goal` 的 `./client` 是 `./lib/types/client.js`,一个 tsc 直出的浏览器共享模块。
+3. `require`、`require.resolve`、动态 `import()` 的字面量实参都能触及一个包;`require.resolve('@deepseek-ai/dsh-web-frontend/dist/index.html')` 就是真实存在的宿主解析路径。
+
+两类判定,逐条报告:
+
+| 类 | 数量 | 判据 |
+| --- | --- | --- |
+| `browser` | 74 | 只有浏览器产物触及 |
+| `nothing` | 5 | 没有任何引用具名它:`client-runtime` 的 `react`(与它自己「零 React 引用」的分层红线矛盾)、`ui-settings` 与 `ui-theme` 的 peer `react`、`ui-trajectory` 的 peer `react-dom`、`ui-primitives` 的 `@types/mdast` |
+
+下面每条保守规则都对应一次实测到的误报或语义损失:
+
+- **node 面的类型引用保留声明。** `src/invariant.ts` 里的 `import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'` 运行期被擦除,但它声明了谁提供这个伴生插件要注册的服务——归 `verify-runtime-closure` 管的关系。
+- **发布了没有源码对应文件的 Node 入口的包整包跳过,并在输出里点名。** `dsh-goal` 的 `./typert -> ./lib/typert.host.js` 由 typert 生成器直出,自带 `import { z } from 'zod'`,没有任何源码陈述这件事。把这条判据修对,消掉了四处误报,其中包括 `api-gateway` 的 `typert-registry`。
+- 包自带的 `cordis*.yml` 算 host face:Loader 行是具名它的插件而不是 import 它。
+- `@deepseek-ai/cordis` 豁免——check-workspace-constraints 要求它在每个包里同时是 peer 和 dev。
+
+`--json` 输出供批量改写与安装体积实测复用。
+
+### 安装面少掉什么
+
+对已发布 CLI 真装一遍实测,tarball 字节从独立 cache 读出:103 个外部 tarball 不再下载,合计 6.05 MB。
+
+| 组 | 包数 | 省 |
+| --- | --- | --- |
+| 语法高亮与数学(shiki 族、oniguruma 族、katex) | 16 | 3.93 MB |
+| react 与视图库(react、react-dom、scheduler、immer、zustand、`@tanstack/*`、clsx、use-sync-external-store) | 11 | 1.47 MB |
+| markdown 与 ansi 管线及零碎(micromark、mdast、hast 全族、anser、若干 `@types/*`) | 76 | 0.65 MB |
+
+我们自己的 6 个浏览器库包(ui-primitives、ui-slots、web-react、ui-attachment、schema-form、client-web,合计 0.20 MB)仍留在安装面:代码确实具名它们,上面的规则不动那些声明。
+
+### 分刀落地
+
+1. **文档住顶刀**:`packages/client/AGENTS.md` 的依赖声明节,加新插件包 checklist 里的一句。
+2. **门禁**:`scripts/verify-client-runtime-deps.ts`、它的 `package.json` 脚本、它在 `hygiene` 里的位置,以及一条反例 spec。
+3. **manifest**:38 个包 79 处。其中 50 处需要新增 `devDependencies` 条目,其余包已有同名条目,改动就是删掉一行。
+4. **发版后按同一方法复测**,确认这 103 个 tarball 没有回来。
+
+## Alternatives considered
+
+- **扫字符串字面量**:起手就是这么实现的,被上面三条要害否掉——react-in-web-react 的子串已经造成过一次静默漏报。
+- **读构建产物(`lib/**/*.js`)而不是源码**:那是 Node 自己的视角,但门禁从此依赖 `pnpm run build`,而且它照样判不了浏览器库包的 `lib/index.js`(platform 是 node、内容是浏览器代码),face 判据两种走法都得有。
+- **用 checker 判绑定是否用在值位置**(`verify-optional-dependency-imports` 就是这么做的):试过,它把 83 处 node 面纯类型声明也判成可移出——没省下任何下载,却实打实损失语义,其中 53 处是 `dsh-invariants`。本门禁要知道的是引用是否存在,而不是它是值还是类型。
+- **顺带把我们自己的 6 个浏览器库包也清出安装面**,判据是任何安装都不加载它们:再省 0.20 MB,代价是删掉 74 处代码确实具名的 workspace 声明。已否(2026-08-14):代码用到的就保留。更干净的终态是这 6 个包不再发布,那是另一个提案。
+- **用 `peerDependenciesMeta.optional` 而不是 `devDependencies`**:npm 确实会跳过 optional peer,但那个语义是「消费者可以自行提供」,而这里根本没有运行期消费者。仓内必须装一份才能构建,这正是 `devDependencies` 的意思。
+- **交给 knip**:不属于 knip 的范畴,它报的是「声明了但没人 import」。这些 specifier 确实被 import,只是被打包器内联了。实证就是它们在 master 上长期存在而 knip 全绿。只有 `nothing` 那 5 条与它重叠。
+- **用 `optionalDependencies`**:语义错,它说的是「装不上就跳过」。
+
+## Acceptance criteria
+
+- `pnpm run hygiene` 包含 `verify-client-runtime-deps` 并通过;一条反例 spec 证明一处 `dependencies.react` 会被拒。
+- `pnpm run build`、`pnpm run test:gui`、`DSH_SNAPSHOT=replay pnpm run test:web` 通过——这次迁移不改任何构建输入,产物应逐字节等价。
+- 发版后真装一遍,上面那 103 个 tarball 不再被下载。
+
+## Risks
+
+- **留在安装面的 6 个浏览器库包会带着解析不了的 bare import**:`ui-primitives/lib/index.js` 是 rolldown 产物,仍写着 `from "anser"`,而 anser 已经只在 dev。它是惰性的——只有我们的 Vite 构建会读这个文件,用户机上没有任何加载者(已实证:只有浏览器代码 import 它们,宿主从不)。要彻底消掉就让这些包不再发布,见 Alternatives。
+- **`@types/*` 报不出来**:源码从不具名它们,规则看不见。`@types/mdast` 被抓到只是因为恰好也没有任何引用。它们本来就该在 dev,补这个缺口是后续的事。
+- **被跳过的包没人管**:`dsh-goal` 因生成入口整包跳过,它浏览器侧的声明现在没有门禁看着。能读到生成产物自身的运行期 import,这条豁免才能收回。
+- **误报会删掉运行期真需要的声明**:三层兜底守住这条线——`require`、`require.resolve`、动态 `import()` 的字面量实参都算引用;包自带的 `cordis*.yml` 算 host face;`@deepseek-ai/*` 整体不在判据范围内。

+ 4 - 4
THIRD_PARTY_NOTICES.md

@@ -27,7 +27,7 @@ The Cordis framework and its foundation libraries are source-vendored into this
 
 ## Runtime npm dependencies
 
-External packages that a workspace package resolves at runtime. The tier covers every plugin a user can mount from `cordis.yml` — not only what the `dsh` CLI, Web UI, and Python SDK runtime load by default.
+External packages that reach a user: a workspace package resolves them at runtime, or a published browser artifact carries a copy of their code. The tier covers every plugin a user can mount from `cordis.yml` — not only what the `dsh` CLI, Web UI, and Python SDK runtime load by default — and the packages the client build inlines into a plugin bundle or the shell `dist`, which are declared as `devDependencies` because nothing on a user's machine resolves their specifiers.
 
 | Package | License |
 | --- | --- |
@@ -48,7 +48,6 @@ External packages that a workspace package resolves at runtime. The tier covers
 | [`@shikijs/langs`](https://github.com/shikijs/shiki) | MIT |
 | [`@standard-schema/spec`](https://github.com/standard-schema/standard-schema) | MIT |
 | [`@tanstack/react-virtual`](https://github.com/TanStack/virtual) | MIT |
-| [`@types/mdast`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
 | [`@vscode/ripgrep`](https://github.com/microsoft/vscode-ripgrep) | MIT |
 | [`anser`](https://github.com/IonicaBizau/anser) | MIT |
 | [`chokidar`](https://github.com/paulmillr/chokidar) | MIT |
@@ -73,7 +72,6 @@ External packages that a workspace package resolves at runtime. The tier covers
 | [`micromark-util-classify-character`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-classify-character) | MIT |
 | [`micromark-util-sanitize-uri`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-sanitize-uri) | MIT |
 | [`micromark-util-symbol`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-symbol) | MIT |
-| [`micromark-util-types`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-types) | MIT |
 | [`node-addon-require-builtin`](https://www.npmjs.com/package/node-addon-require-builtin) | MIT |
 | [`node-pty`](https://github.com/microsoft/node-pty) | MIT |
 | [`picomatch`](https://github.com/micromatch/picomatch) | MIT |
@@ -115,7 +113,7 @@ The installed SDK 0.3.220 declares the following optional platform packages. Eac
 
 ## Development-only npm dependencies
 
-External packages **directly declared** only by repository tooling, test infrastructure, the documentation site, the demo leaves, or the native launcher's build workspace. No shipped surface names them itself. A package here may still be pulled in transitively by a runtime dependency — `pnpm-lock.yaml` is the authority on the full closure — so this tier records who declares a package, not what a build ultimately bundles.
+External packages **directly declared** only by repository tooling, test infrastructure, the documentation site, the demo leaves, or the native launcher's build workspace, and carried by no published artifact. No shipped surface names them itself. A package here may still be pulled in transitively by a runtime dependency — `pnpm-lock.yaml` is the authority on the full closure — so this tier records who declares a package, not what a build ultimately bundles.
 
 | Package | License |
 | --- | --- |
@@ -129,6 +127,7 @@ External packages **directly declared** only by repository tooling, test infrast
 | [`@types/babel__code-frame`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
 | [`@types/js-yaml`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
 | [`@types/jsdom`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
+| [`@types/mdast`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
 | [`@types/node`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
 | [`@types/picomatch`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
 | [`@types/react`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
@@ -154,6 +153,7 @@ External packages **directly declared** only by repository tooling, test infrast
 | [`lefthook`](https://github.com/evilmartians/lefthook) | MIT |
 | [`lightningcss`](https://github.com/parcel-bundler/lightningcss) | MPL-2.0 |
 | [`mermaid`](https://github.com/mermaid-js/mermaid) | MIT |
+| [`micromark-util-types`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-types) | MIT |
 | [`oxlint`](https://github.com/oxc-project/oxc) | MIT |
 | [`oxlint-tsgolint`](https://github.com/oxc-project/tsgolint) | MIT |
 | [`playwright`](https://github.com/microsoft/playwright) | Apache-2.0 |

+ 4 - 4
apps/web/package.json

@@ -26,9 +26,7 @@
   },
   "license": "MIT",
   "dependencies": {
-    "@deepseek-ai/dsh-client-web": "workspace:^",
-    "react": "^18.2.0",
-    "react-dom": "^18.2.0"
+    "@deepseek-ai/dsh-client-web": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/cordis-plugin-group": "workspace:^",
@@ -46,6 +44,8 @@
     "typescript": "^6.0.3",
     "vite": "^6.0.0",
     "vitest": "^4.1.8",
-    "fflate": "^0.8.2"
+    "fflate": "^0.8.2",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0"
   }
 }

+ 1 - 0
apps/web/vite.config.ts

@@ -93,6 +93,7 @@ export default defineConfig({
   plugins: [rejectStandaloneServe(), react()],
   build: {
     sourcemap: true,
+    modulePreload: { polyfill: false },
     rollupOptions: {
       output: {
         // Output layout: the two main chunks stay at assets/ root; lazy

+ 1 - 1
lefthook.yml

@@ -28,7 +28,7 @@ pre-commit:
     # lefthook only inspects files present on disk — so that one case still
     # falls through to the freshness assertion in the test lane.
     - name: third-party notices (staged)
-      glob: '{package.json,*/package.json,*/*/package.json,*/*/*/package.json,*/*/*/*/package.json,pnpm-workspace.yaml,*/*/pnpm-workspace.yaml,pnpm-lock.yaml,vendor/README.md,python/*/pyproject.toml,scripts/gen-third-party-notices.ts,scripts/build-exe-for-python-sdk.ts}'
+      glob: '{package.json,*/package.json,*/*/package.json,*/*/*/package.json,*/*/*/*/package.json,pnpm-workspace.yaml,*/*/pnpm-workspace.yaml,pnpm-lock.yaml,vendor/README.md,python/*/pyproject.toml,scripts/gen-third-party-notices.ts,scripts/browser-bundled-externals.ts,packages/client/tsdown.client.ts,scripts/build-exe-for-python-sdk.ts}'
       run: node_modules/.bin/tsx scripts/gen-third-party-notices.ts && git add THIRD_PARTY_NOTICES.md
 
     - name: whitespace (staged)

+ 2 - 1
package.json

@@ -102,6 +102,7 @@
     "rescope-vendor": "tsx scripts/rescope-vendor.ts",
     "rescope-vendor:check": "tsx scripts/rescope-vendor.ts --check",
     "verify-client-domain-graph": "tsx scripts/verify-client-domain-graph.ts",
+    "verify-client-runtime-deps": "tsx scripts/verify-client-runtime-deps.ts",
     "gen-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts",
     "verify-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts --check",
     "gen-cordis-api": "tsx scripts/gen-cordis-api.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-runtime-deps && 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",

+ 10 - 1
packages/client/AGENTS.md

@@ -35,6 +35,15 @@ The `/client` entrypoint of a UI plugin package is its public browser API, not a
 2. **Same-package tests import internals directly** — relative `../src/client/xxx.ts` from package tests, or the `./src/*` subpath where a spec lives outside the package. Never widen the public API to make a test compile.
 3. **Cross-package imports of another plugin's symbols are in principle forbidden.** The sanctioned routes are the slot system (register/renderSlot) and ctx services. If neither fits, stop and escalate — do not add an export to unblock yourself.
 
+## Dependency declaration
+
+A browser artifact resolves nothing on the user's machine: tsdown inlines every non-platform specifier into `lib/client.js`, the shell `dist` answers `PLATFORM_MODULES` from its frozen module table, and Vite inlines the shell's own imports into the published `dist`.
+
+- **An external package only browser code reaches belongs in `devDependencies`.** npm installs `dependencies` and non-optional `peerDependencies` for every consumer, so react, shiki, katex, or clsx declared there ships to users who never load it.
+- **A workspace name stays where it is.** Such a declaration also states which package supplies an injected service or a mounted Remote contribution — [verify-runtime-closure](../../scripts/verify-runtime-closure.ts) and the Loader read it, and the app installs the package regardless.
+- **The node half decides.** Anything its published entries reach — an erased type import, a `require.resolve`, and a Loader row in the package's own `cordis*.yml` included — stays declared as it is.
+- `pnpm run verify-client-runtime-deps` (inside `hygiene`) names each offending entry; knip owns whether the surviving declaration is used at all.
+
 ## ctx discipline (components never see ctx)
 
 `ctx` belongs to the apply world only: the plugin body and the inject factories closed over it. Components — every `.tsx` under a feature domain — receive all data and callbacks **through the four props shares**; they never call a hook that reaches ctx, never import a service class to poke it, never read a React context (business components see zero contexts — `BindingContext` and its kin are renderer-internal). If a component needs something new, the answer is a prop threaded from its share's source (owner site, store declaration, or inject face), not a hook.
@@ -91,7 +100,7 @@ If `test:gui` is red on code you did not touch, neither silently fix nor ignore
 
 Bringing up a new `packages/client/<name>` plugin package (ui-workspace is a complete example; ui-sidebar/ui-user-questions are minimal skeletons):
 
-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.
+1. **Package skeleton**: `package.json` (`@deepseek-ai/dsh-client-<name>`, exports `.`/`./invariant`/`./client`/`./src/*`/`./package.json`, `dsh.client` manifest, `files` list, and every browser-side external package under `devDependencies` per the [declaration rules](#dependency-declaration)), `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.
 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.

+ 1 - 2
packages/client/locale/package.json

@@ -50,8 +50,7 @@
     "@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-invariants": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",

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

@@ -53,10 +53,7 @@
     "@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"
+    "@deepseek-ai/dsh-tools": "workspace:^"
   },
   "peerDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
@@ -72,7 +69,10 @@
     "@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",
+    "immer": "^10.1.1",
+    "react": "^18.2.0",
+    "zustand": "~4.4.7"
   },
   "files": [
     "lib/index.js",

+ 1 - 2
packages/client/ui-agent-preset/package.json

@@ -58,8 +58,7 @@
     "@deepseek-ai/dsh-client-ui-settings": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-client-web-react": "workspace:^",
-    "@deepseek-ai/dsh-invariants": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/dsh-invariants": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-api-remotes": "workspace:^",

+ 5 - 5
packages/client/ui-attachment/package.json

@@ -28,16 +28,16 @@
   "license": "MIT",
   "dependencies": {
     "@deepseek-ai/dsh-attachment": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
-    "clsx": "^2.0.0",
-    "react": "^18.2.0",
-    "react-dom": "^18.2.0"
+    "@deepseek-ai/dsh-client-ui-primitives": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@types/react": "~18.3.1",
-    "@types/react-dom": "~18.3.0"
+    "@types/react-dom": "~18.3.0",
+    "clsx": "^2.0.0",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0"
   },
   "files": [
     "lib/index.js",

+ 3 - 6
packages/client/ui-commands/package.json

@@ -46,9 +46,6 @@
     "watch": "tsdown --watch"
   },
   "license": "MIT",
-  "dependencies": {
-    "clsx": "^2.0.0"
-  },
   "peerDependencies": {
     "@deepseek-ai/dsh-api-remotes": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
@@ -59,8 +56,7 @@
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-commands": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-api-remotes": "workspace:^",
@@ -76,7 +72,8 @@
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@types/react": "~18.3.1",
     "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "react": "^18.2.0",
+    "clsx": "^2.0.0"
   },
   "files": [
     "lib/index.js",

+ 3 - 4
packages/client/ui-conversation/package.json

@@ -49,7 +49,6 @@
   "license": "MIT",
   "dependencies": {
     "@deepseek-ai/dsh-settings": "workspace:^",
-    "clsx": "^2.0.0",
     "@deepseek-ai/schemastery": "workspace:^"
   },
   "peerDependencies": {
@@ -72,8 +71,7 @@
     "@deepseek-ai/dsh-llm-retry": "workspace:^",
     "@deepseek-ai/dsh-session-stats": "workspace:^",
     "@deepseek-ai/dsh-token-meter": "workspace:^",
-    "@deepseek-ai/dsh-tools": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/dsh-tools": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
@@ -104,7 +102,8 @@
     "@deepseek-ai/dsh-tool-todo": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@types/react": "~18.3.1",
-    "react": "^18.2.0"
+    "react": "^18.2.0",
+    "clsx": "^2.0.0"
   },
   "files": [
     "lib/index.js",

+ 2 - 4
packages/client/ui-deliverables/package.json

@@ -45,9 +45,6 @@
     "watch": "tsdown --watch"
   },
   "license": "MIT",
-  "dependencies": {
-    "react": "^18.2.0"
-  },
   "peerDependencies": {
     "@deepseek-ai/dsh-client-connection": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
@@ -68,7 +65,8 @@
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
     "@types/react": "~18.3.1",
-    "@deepseek-ai/cordis": "workspace:^"
+    "@deepseek-ai/cordis": "workspace:^",
+    "react": "^18.2.0"
   },
   "files": [
     "lib/index.js",

+ 3 - 6
packages/client/ui-directory-picker-browse/package.json

@@ -44,9 +44,6 @@
     "watch": "tsdown --watch"
   },
   "license": "MIT",
-  "dependencies": {
-    "clsx": "^2.0.0"
-  },
   "peerDependencies": {
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-runtime": "workspace:^",
@@ -54,8 +51,7 @@
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-client-ui-workspace": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-client-locale": "workspace:^",
@@ -69,7 +65,8 @@
     "@types/react": "~18.3.1",
     "@deepseek-ai/cordis": "workspace:^",
     "react": "^18.2.0",
-    "react-dom": "^18.2.0"
+    "react-dom": "^18.2.0",
+    "clsx": "^2.0.0"
   },
   "files": [
     "lib/index.js",

+ 1 - 2
packages/client/ui-directory-picker-native/package.json

@@ -48,8 +48,7 @@
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-client-ui-workspace": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-client-runtime": "workspace:^",

+ 1 - 2
packages/client/ui-goal/package.json

@@ -55,8 +55,7 @@
     "@deepseek-ai/dsh-commands": "workspace:^",
     "@deepseek-ai/dsh-goal": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-client-locale": "workspace:^",

+ 3 - 6
packages/client/ui-input-trigger/package.json

@@ -43,17 +43,13 @@
     "watch": "tsdown --watch"
   },
   "license": "MIT",
-  "dependencies": {
-    "clsx": "^2.0.0"
-  },
   "peerDependencies": {
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-runtime": "workspace:^",
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-client-locale": "workspace:^",
@@ -64,7 +60,8 @@
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@types/react": "~18.3.1",
     "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "react": "^18.2.0",
+    "clsx": "^2.0.0"
   },
   "files": [
     "lib/index.js",

+ 2 - 4
packages/client/ui-jobs/package.json

@@ -45,9 +45,6 @@
   "publishConfig": {
     "access": "public"
   },
-  "dependencies": {
-    "react": "^18.2.0"
-  },
   "peerDependencies": {
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-runtime": "workspace:^",
@@ -66,7 +63,8 @@
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@types/react": "~18.3.1",
-    "@deepseek-ai/cordis": "workspace:^"
+    "@deepseek-ai/cordis": "workspace:^",
+    "react": "^18.2.0"
   },
   "files": [
     "lib/index.js",

+ 1 - 2
packages/client/ui-layout/package.json

@@ -48,8 +48,7 @@
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-client-ui-theme": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-client-locale": "workspace:^",

+ 1 - 2
packages/client/ui-message-feedback/package.json

@@ -56,8 +56,7 @@
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-message-feedback": "workspace:^",
     "@deepseek-ai/dsh-typert-protocol": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-api-remotes": "workspace:^",

+ 1 - 3
packages/client/ui-model-selection/package.json

@@ -56,9 +56,7 @@
     "@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "clsx": "^2.1.1",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-api-remotes": "workspace:^",

+ 1 - 2
packages/client/ui-permission-presets/package.json

@@ -60,8 +60,7 @@
     "@deepseek-ai/dsh-client-ui-input-trigger": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/dsh-permission-presets": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/dsh-permission-presets": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",

+ 1 - 2
packages/client/ui-plan/package.json

@@ -53,8 +53,7 @@
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/dsh-plan-mode": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-api-remotes": "workspace:^",

+ 5 - 7
packages/client/ui-primitives/package.json

@@ -26,7 +26,11 @@
     "./package.json": "./package.json"
   },
   "license": "MIT",
-  "dependencies": {
+  "devDependencies": {
+    "@deepseek-ai/dsh-invariants": "workspace:^",
+    "@types/react": "~18.3.1",
+    "@types/react-dom": "~18.3.0",
+    "@deepseek-ai/cordis": "workspace:^",
     "@shikijs/langs": "^4.3.1",
     "@types/mdast": "^4.0.4",
     "anser": "^2.3.5",
@@ -48,12 +52,6 @@
     "react-dom": "^18.2.0",
     "shiki": "^4.3.1"
   },
-  "devDependencies": {
-    "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@types/react": "~18.3.1",
-    "@types/react-dom": "~18.3.0",
-    "@deepseek-ai/cordis": "workspace:^"
-  },
   "files": [
     "lib/index.js",
     "lib/invariant.js",

+ 4 - 5
packages/client/ui-settings-general/package.json

@@ -49,8 +49,7 @@
   "license": "MIT",
   "dependencies": {
     "@deepseek-ai/dsh-settings": "workspace:^",
-    "@deepseek-ai/schemastery": "workspace:^",
-    "clsx": "^2.0.0"
+    "@deepseek-ai/schemastery": "workspace:^"
   },
   "peerDependencies": {
     "@deepseek-ai/dsh-api-remotes": "workspace:^",
@@ -63,8 +62,7 @@
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-client-web-react": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-api-remotes": "workspace:^",
@@ -80,7 +78,8 @@
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@deepseek-ai/cordis": "workspace:^",
     "@types/react": "~18.3.1",
-    "react": "^18.2.0"
+    "react": "^18.2.0",
+    "clsx": "^2.0.0"
   },
   "files": [
     "lib/index.js",

+ 1 - 2
packages/client/ui-settings-models/package.json

@@ -54,8 +54,7 @@
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-client-web-react": "workspace:^",
-    "@deepseek-ai/dsh-invariants": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/dsh-invariants": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-api-remotes": "workspace:^",

+ 1 - 2
packages/client/ui-settings-plugin-inventory/package.json

@@ -53,8 +53,7 @@
     "@deepseek-ai/dsh-client-ui-settings": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-api-remotes": "workspace:^",

+ 3 - 6
packages/client/ui-settings-plugins/package.json

@@ -56,8 +56,7 @@
     "@deepseek-ai/dsh-client-ui-settings": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-client-web-react": "workspace:^",
-    "@deepseek-ai/dsh-invariants": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/dsh-invariants": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
@@ -72,6 +71,7 @@
     "@deepseek-ai/dsh-client-web-react": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@types/react": "~18.3.1",
+    "clsx": "^2.0.0",
     "react": "^18.2.0"
   },
   "files": [
@@ -79,8 +79,5 @@
     "lib/invariant.js",
     "lib/client.js",
     "lib/types/**/*.d.ts"
-  ],
-  "dependencies": {
-    "clsx": "^2.0.0"
-  }
+  ]
 }

+ 1 - 2
packages/client/ui-settings/package.json

@@ -52,8 +52,7 @@
     "@deepseek-ai/dsh-client-schema-form": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/dsh-settings": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/dsh-settings": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",

+ 3 - 6
packages/client/ui-sidebar/package.json

@@ -44,17 +44,13 @@
     "watch": "tsdown --watch"
   },
   "license": "MIT",
-  "dependencies": {
-    "clsx": "^2.0.0"
-  },
   "peerDependencies": {
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-runtime": "workspace:^",
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-client-locale": "workspace:^",
@@ -66,7 +62,8 @@
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@types/react": "~18.3.1",
     "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "react": "^18.2.0",
+    "clsx": "^2.0.0"
   },
   "files": [
     "lib/index.js",

+ 1 - 2
packages/client/ui-skill/package.json

@@ -56,8 +56,7 @@
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-client-ui-tool": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-api-remotes": "workspace:^",

+ 2 - 4
packages/client/ui-subagent/package.json

@@ -46,9 +46,6 @@
     "watch": "tsdown --watch"
   },
   "license": "MIT",
-  "dependencies": {
-    "react": "^18.2.0"
-  },
   "peerDependencies": {
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-runtime": "workspace:^",
@@ -73,7 +70,8 @@
     "@deepseek-ai/dsh-subagent": "workspace:^",
     "@deepseek-ai/dsh-token-meter": "workspace:^",
     "@types/react": "~18.3.1",
-    "@deepseek-ai/cordis": "workspace:^"
+    "@deepseek-ai/cordis": "workspace:^",
+    "react": "^18.2.0"
   },
   "files": [
     "lib/index.js",

+ 2 - 3
packages/client/ui-theme/package.json

@@ -54,8 +54,7 @@
     "@deepseek-ai/dsh-client-ui-settings": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-host-webserver": "workspace:^",
-    "@deepseek-ai/dsh-invariants": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/dsh-invariants": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
@@ -69,6 +68,7 @@
     "@deepseek-ai/dsh-host-webserver": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@types/react": "~18.3.1",
+    "clsx": "^2.0.0",
     "react": "^18.2.0"
   },
   "files": [
@@ -84,7 +84,6 @@
   },
   "dependencies": {
     "@deepseek-ai/dsh-settings": "workspace:^",
-    "clsx": "^2.0.0",
     "@deepseek-ai/schemastery": "workspace:^"
   }
 }

+ 2 - 5
packages/client/ui-tool/package.json

@@ -44,9 +44,6 @@
     "watch": "tsdown --watch"
   },
   "license": "MIT",
-  "dependencies": {
-    "clsx": "^2.0.0"
-  },
   "peerDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/dsh-api-remotes": "workspace:^",
@@ -55,8 +52,7 @@
     "@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
-    "@deepseek-ai/dsh-invariants": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/dsh-invariants": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
@@ -72,6 +68,7 @@
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@testing-library/react": "^16.1.0",
     "@types/react": "~18.3.1",
+    "clsx": "^2.0.0",
     "react": "^18.2.0",
     "react-dom": "^18.2.0"
   },

+ 4 - 8
packages/client/ui-trajectory/package.json

@@ -44,10 +44,6 @@
     "watch": "tsdown --watch"
   },
   "license": "MIT",
-  "dependencies": {
-    "@tanstack/react-virtual": "^3.14.9",
-    "diff": "^9.0.0"
-  },
   "peerDependencies": {
     "@deepseek-ai/dsh-agent": "workspace:^",
     "@deepseek-ai/dsh-client-locale": "workspace:^",
@@ -56,9 +52,7 @@
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
     "@deepseek-ai/cordis": "workspace:^",
     "@deepseek-ai/dsh-compaction": "workspace:^",
-    "@deepseek-ai/dsh-tools": "workspace:^",
-    "react": "^18.2.0",
-    "react-dom": "^18.2.0"
+    "@deepseek-ai/dsh-tools": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-agent": "workspace:^",
@@ -75,7 +69,9 @@
     "@types/react-dom": "~18.3.0",
     "@deepseek-ai/cordis": "workspace:^",
     "react": "^18.2.0",
-    "react-dom": "^18.2.0"
+    "react-dom": "^18.2.0",
+    "@tanstack/react-virtual": "^3.14.9",
+    "diff": "^9.0.0"
   },
   "files": [
     "lib/index.js",

+ 4 - 4
packages/client/ui-user-questions/package.json

@@ -48,9 +48,7 @@
     "@deepseek-ai/dsh-client-runtime": "workspace:^",
     "@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
-    "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
-    "clsx": "^2.0.0",
-    "react": "^18.2.0"
+    "@deepseek-ai/dsh-client-ui-slots": "workspace:^"
   },
   "peerDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
@@ -67,7 +65,9 @@
     "@deepseek-ai/dsh-system-prompt": "workspace:^",
     "@deepseek-ai/dsh-tools": "workspace:^",
     "@deepseek-ai/dsh-user-questions": "workspace:^",
-    "@types/react": "~18.3.1"
+    "@types/react": "~18.3.1",
+    "clsx": "^2.0.0",
+    "react": "^18.2.0"
   },
   "files": [
     "lib/index.js",

+ 2 - 4
packages/client/ui-workflow-run/package.json

@@ -50,9 +50,6 @@
     "lib/types/**/*.d.ts"
   ],
   "license": "MIT",
-  "dependencies": {
-    "react": "^18.2.0"
-  },
   "peerDependencies": {
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-runtime": "workspace:^",
@@ -77,6 +74,7 @@
     "@deepseek-ai/dsh-tool-workflow": "workspace:^",
     "@deepseek-ai/dsh-workflow": "workspace:^",
     "@types/react": "~18.3.1",
-    "@deepseek-ai/cordis": "workspace:^"
+    "@deepseek-ai/cordis": "workspace:^",
+    "react": "^18.2.0"
   }
 }

+ 3 - 6
packages/client/ui-workspace/package.json

@@ -45,17 +45,13 @@
     "watch": "tsdown --watch"
   },
   "license": "MIT",
-  "dependencies": {
-    "clsx": "^2.0.0"
-  },
   "peerDependencies": {
     "@deepseek-ai/dsh-client-locale": "workspace:^",
     "@deepseek-ai/dsh-client-runtime": "workspace:^",
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-client-locale": "workspace:^",
@@ -68,7 +64,8 @@
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@types/react": "~18.3.1",
     "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "react": "^18.2.0",
+    "clsx": "^2.0.0"
   },
   "files": [
     "lib/index.js",

+ 4 - 4
packages/client/web-react/package.json

@@ -27,9 +27,7 @@
   },
   "license": "MIT",
   "dependencies": {
-    "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
-    "react": "^18.2.0",
-    "use-sync-external-store": "1.2.0"
+    "@deepseek-ai/dsh-client-ui-slots": "workspace:^"
   },
   "peerDependencies": {
     "@deepseek-ai/dsh-invariants": "workspace:^",
@@ -38,7 +36,9 @@
   "devDependencies": {
     "@deepseek-ai/dsh-invariants": "workspace:^",
     "@types/react": "~18.3.1",
-    "@deepseek-ai/cordis": "workspace:^"
+    "@deepseek-ai/cordis": "workspace:^",
+    "react": "^18.2.0",
+    "use-sync-external-store": "1.2.0"
   },
   "files": [
     "lib/index.js",

+ 4 - 4
packages/client/web/package.json

@@ -33,9 +33,7 @@
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-client-ui-theme": "workspace:^",
-    "@deepseek-ai/dsh-client-web-react": "workspace:^",
-    "react": "^18.2.0",
-    "react-dom": "^18.2.0"
+    "@deepseek-ai/dsh-client-web-react": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",
@@ -45,7 +43,9 @@
     "@types/react": "~18.3.1",
     "@types/react-dom": "~18.3.0",
     "@deepseek-ai/cordis": "workspace:^",
-    "typescript": "^6.0.3"
+    "typescript": "^6.0.3",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0"
   },
   "peerDependencies": {
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",

+ 1 - 2
packages/extensions/cordis-client-runner/package.json

@@ -54,8 +54,7 @@
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-client-ui-theme": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/cordis-plugin-loader": "workspace:^",

+ 1 - 2
packages/extensions/ui-cordis/package.json

@@ -61,8 +61,7 @@
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-client-ui-tool": "workspace:^",
     "@deepseek-ai/dsh-invariants": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/cordis": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/dsh-api-remotes": "workspace:^",

+ 26 - 8
packages/session-query/session-log-export/package.json

@@ -2,7 +2,9 @@
   "name": "@deepseek-ai/dsh-session-log-export",
   "description": "Web Session-log export command and shared download dialog",
   "version": "0.1.0-rc.6",
-  "publishConfig": { "access": "public" },
+  "publishConfig": {
+    "access": "public"
+  },
   "repository": {
     "type": "git",
     "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
@@ -12,14 +14,31 @@
   "main": "lib/index.js",
   "types": "lib/types/index.d.ts",
   "exports": {
-    ".": { "types": "./lib/types/index.d.ts", "default": "./lib/index.js" },
-    "./invariant": { "types": "./lib/types/invariant.d.ts", "default": "./lib/invariant.js" },
-    "./client": { "types": "./lib/types/client/index.d.ts", "default": "./lib/client.js" },
+    ".": {
+      "types": "./lib/types/index.d.ts",
+      "default": "./lib/index.js"
+    },
+    "./invariant": {
+      "types": "./lib/types/invariant.d.ts",
+      "default": "./lib/invariant.js"
+    },
+    "./client": {
+      "types": "./lib/types/client/index.d.ts",
+      "default": "./lib/client.js"
+    },
     "./src/*": "./src/*",
     "./package.json": "./package.json"
   },
-  "files": ["lib/index.js", "lib/invariant.js", "lib/client.js", "lib/types/**/*.d.ts"],
-  "scripts": { "bundle": "tsdown", "watch": "tsdown --watch" },
+  "files": [
+    "lib/index.js",
+    "lib/invariant.js",
+    "lib/client.js",
+    "lib/types/**/*.d.ts"
+  ],
+  "scripts": {
+    "bundle": "tsdown",
+    "watch": "tsdown --watch"
+  },
   "license": "MIT",
   "peerDependencies": {
     "@deepseek-ai/cordis": "workspace:^",
@@ -30,8 +49,7 @@
     "@deepseek-ai/dsh-client-ui-primitives": "workspace:^",
     "@deepseek-ai/dsh-client-ui-slots": "workspace:^",
     "@deepseek-ai/dsh-commands": "workspace:^",
-    "@deepseek-ai/dsh-invariants": "workspace:^",
-    "react": "^18.2.0"
+    "@deepseek-ai/dsh-invariants": "workspace:^"
   },
   "devDependencies": {
     "@deepseek-ai/cordis": "workspace:^",

+ 101 - 114
pnpm-lock.yaml

@@ -358,12 +358,6 @@ importers:
       '@deepseek-ai/dsh-client-web':
         specifier: workspace:^
         version: link:../../packages/client/web
-      react:
-        specifier: ^18.2.0
-        version: 18.3.1
-      react-dom:
-        specifier: ^18.2.0
-        version: 18.3.1(react@18.3.1)
     devDependencies:
       '@deepseek-ai/cordis-plugin-group':
         specifier: workspace:^
@@ -404,6 +398,12 @@ importers:
       playwright:
         specifier: ^1.49.0
         version: 1.61.1
+      react:
+        specifier: ^18.2.0
+        version: 18.3.1
+      react-dom:
+        specifier: ^18.2.0
+        version: 18.3.1(react@18.3.1)
       typescript:
         specifier: ^6.0.3
         version: 6.0.3
@@ -1607,15 +1607,6 @@ importers:
       '@deepseek-ai/dsh-tools':
         specifier: workspace:^
         version: link:../../core/tools
-      immer:
-        specifier: ^10.1.1
-        version: 10.2.0
-      react:
-        specifier: ^18.2.0
-        version: 18.3.1
-      zustand:
-        specifier: ~4.4.7
-        version: 4.4.7(@types/react@18.3.31)(immer@10.2.0)(react@18.3.1)
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -1638,6 +1629,15 @@ importers:
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
+      immer:
+        specifier: ^10.1.1
+        version: 10.2.0
+      react:
+        specifier: ^18.2.0
+        version: 18.3.1
+      zustand:
+        specifier: ~4.4.7
+        version: 4.4.7(@types/react@18.3.31)(immer@10.2.0)(react@18.3.1)
 
   packages/client/schema-form:
     dependencies:
@@ -1705,15 +1705,6 @@ importers:
       '@deepseek-ai/dsh-client-ui-primitives':
         specifier: workspace:^
         version: link:../ui-primitives
-      clsx:
-        specifier: ^2.0.0
-        version: 2.1.1
-      react:
-        specifier: ^18.2.0
-        version: 18.3.1
-      react-dom:
-        specifier: ^18.2.0
-        version: 18.3.1(react@18.3.1)
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -1727,12 +1718,17 @@ importers:
       '@types/react-dom':
         specifier: ~18.3.0
         version: 18.3.7(@types/react@18.3.31)
-
-  packages/client/ui-commands:
-    dependencies:
       clsx:
         specifier: ^2.0.0
         version: 2.1.1
+      react:
+        specifier: ^18.2.0
+        version: 18.3.1
+      react-dom:
+        specifier: ^18.2.0
+        version: 18.3.1(react@18.3.1)
+
+  packages/client/ui-commands:
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -1773,6 +1769,9 @@ importers:
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
+      clsx:
+        specifier: ^2.0.0
+        version: 2.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -1785,9 +1784,6 @@ importers:
       '@deepseek-ai/schemastery':
         specifier: link:../../../vendor/schemastery
         version: link:../../../vendor/schemastery
-      clsx:
-        specifier: ^2.0.0
-        version: 2.1.1
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -1873,15 +1869,14 @@ importers:
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
+      clsx:
+        specifier: ^2.0.0
+        version: 2.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
 
   packages/client/ui-deliverables:
-    dependencies:
-      react:
-        specifier: ^18.2.0
-        version: 18.3.1
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -1913,12 +1908,11 @@ importers:
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
+      react:
+        specifier: ^18.2.0
+        version: 18.3.1
 
   packages/client/ui-directory-picker-browse:
-    dependencies:
-      clsx:
-        specifier: ^2.0.0
-        version: 2.1.1
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -1950,6 +1944,9 @@ importers:
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
+      clsx:
+        specifier: ^2.0.0
+        version: 2.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -2036,10 +2033,6 @@ importers:
         version: 18.3.1(react@18.3.1)
 
   packages/client/ui-input-trigger:
-    dependencies:
-      clsx:
-        specifier: ^2.0.0
-        version: 2.1.1
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -2065,15 +2058,14 @@ importers:
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
+      clsx:
+        specifier: ^2.0.0
+        version: 2.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
 
   packages/client/ui-jobs:
-    dependencies:
-      react:
-        specifier: ^18.2.0
-        version: 18.3.1
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -2102,6 +2094,9 @@ importers:
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
+      react:
+        specifier: ^18.2.0
+        version: 18.3.1
 
   packages/client/ui-layout:
     devDependencies:
@@ -2326,13 +2321,25 @@ importers:
         version: 18.3.1
 
   packages/client/ui-primitives:
-    dependencies:
+    devDependencies:
+      '@deepseek-ai/cordis':
+        specifier: workspace:^
+        version: link:../../../vendor/cordis
+      '@deepseek-ai/dsh-invariants':
+        specifier: workspace:^
+        version: link:../../runtime-diagnostics/invariants
       '@shikijs/langs':
         specifier: ^4.3.1
         version: 4.3.1
       '@types/mdast':
         specifier: ^4.0.4
         version: 4.0.4
+      '@types/react':
+        specifier: ~18.3.1
+        version: 18.3.31
+      '@types/react-dom':
+        specifier: ~18.3.0
+        version: 18.3.7(@types/react@18.3.31)
       anser:
         specifier: ^2.3.5
         version: 2.3.5
@@ -2387,19 +2394,6 @@ importers:
       shiki:
         specifier: ^4.3.1
         version: 4.3.1
-    devDependencies:
-      '@deepseek-ai/cordis':
-        specifier: workspace:^
-        version: link:../../../vendor/cordis
-      '@deepseek-ai/dsh-invariants':
-        specifier: workspace:^
-        version: link:../../runtime-diagnostics/invariants
-      '@types/react':
-        specifier: ~18.3.1
-        version: 18.3.31
-      '@types/react-dom':
-        specifier: ~18.3.0
-        version: 18.3.7(@types/react@18.3.31)
 
   packages/client/ui-settings:
     dependencies:
@@ -2446,9 +2440,6 @@ importers:
       '@deepseek-ai/schemastery':
         specifier: link:../../../vendor/schemastery
         version: link:../../../vendor/schemastery
-      clsx:
-        specifier: ^2.0.0
-        version: 2.1.1
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -2489,6 +2480,9 @@ importers:
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
+      clsx:
+        specifier: ^2.0.0
+        version: 2.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -2581,10 +2575,6 @@ importers:
         version: 18.3.1(react@18.3.1)
 
   packages/client/ui-settings-plugins:
-    dependencies:
-      clsx:
-        specifier: ^2.0.0
-        version: 2.1.1
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -2622,15 +2612,14 @@ importers:
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
+      clsx:
+        specifier: ^2.0.0
+        version: 2.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
 
   packages/client/ui-sidebar:
-    dependencies:
-      clsx:
-        specifier: ^2.0.0
-        version: 2.1.1
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -2659,6 +2648,9 @@ importers:
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
+      clsx:
+        specifier: ^2.0.0
+        version: 2.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -2724,10 +2716,6 @@ importers:
         version: 18.3.31
 
   packages/client/ui-subagent:
-    dependencies:
-      react:
-        specifier: ^18.2.0
-        version: 18.3.1
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -2765,6 +2753,9 @@ importers:
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
+      react:
+        specifier: ^18.2.0
+        version: 18.3.1
 
   packages/client/ui-theme:
     dependencies:
@@ -2777,9 +2768,6 @@ importers:
       '@deepseek-ai/schemastery':
         specifier: link:../../../vendor/schemastery
         version: link:../../../vendor/schemastery
-      clsx:
-        specifier: ^2.0.0
-        version: 2.1.1
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -2814,15 +2802,14 @@ importers:
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
+      clsx:
+        specifier: ^2.0.0
+        version: 2.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
 
   packages/client/ui-tool:
-    dependencies:
-      clsx:
-        specifier: ^2.0.0
-        version: 2.1.1
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -2863,6 +2850,9 @@ importers:
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
+      clsx:
+        specifier: ^2.0.0
+        version: 2.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -2871,13 +2861,6 @@ importers:
         version: 18.3.1(react@18.3.1)
 
   packages/client/ui-trajectory:
-    dependencies:
-      '@tanstack/react-virtual':
-        specifier: ^3.14.9
-        version: 3.14.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-      diff:
-        specifier: ^9.0.0
-        version: 9.0.0
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -2912,12 +2895,18 @@ importers:
       '@deepseek-ai/dsh-tools':
         specifier: workspace:^
         version: link:../../core/tools
+      '@tanstack/react-virtual':
+        specifier: ^3.14.9
+        version: 3.14.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
       '@types/react-dom':
         specifier: ~18.3.0
         version: 18.3.7(@types/react@18.3.31)
+      diff:
+        specifier: ^9.0.0
+        version: 9.0.0
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -2942,12 +2931,6 @@ importers:
       '@deepseek-ai/dsh-client-ui-slots':
         specifier: workspace:^
         version: link:../ui-slots
-      clsx:
-        specifier: ^2.0.0
-        version: 2.1.1
-      react:
-        specifier: ^18.2.0
-        version: 18.3.1
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -2976,12 +2959,14 @@ importers:
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
-
-  packages/client/ui-workflow-run:
-    dependencies:
+      clsx:
+        specifier: ^2.0.0
+        version: 2.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
+
+  packages/client/ui-workflow-run:
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -3019,12 +3004,11 @@ importers:
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
+      react:
+        specifier: ^18.2.0
+        version: 18.3.1
 
   packages/client/ui-workspace:
-    dependencies:
-      clsx:
-        specifier: ^2.0.0
-        version: 2.1.1
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -3056,6 +3040,9 @@ importers:
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
+      clsx:
+        specifier: ^2.0.0
+        version: 2.1.1
       react:
         specifier: ^18.2.0
         version: 18.3.1
@@ -3083,12 +3070,6 @@ importers:
       '@deepseek-ai/dsh-client-web-react':
         specifier: workspace:^
         version: link:../web-react
-      react:
-        specifier: ^18.2.0
-        version: 18.3.1
-      react-dom:
-        specifier: ^18.2.0
-        version: 18.3.1(react@18.3.1)
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -3111,6 +3092,12 @@ importers:
       '@types/react-dom':
         specifier: ~18.3.0
         version: 18.3.7(@types/react@18.3.31)
+      react:
+        specifier: ^18.2.0
+        version: 18.3.1
+      react-dom:
+        specifier: ^18.2.0
+        version: 18.3.1(react@18.3.1)
       typescript:
         specifier: ^6.0.3
         version: 6.0.3
@@ -3120,12 +3107,6 @@ importers:
       '@deepseek-ai/dsh-client-ui-slots':
         specifier: workspace:^
         version: link:../ui-slots
-      react:
-        specifier: ^18.2.0
-        version: 18.3.1
-      use-sync-external-store:
-        specifier: 1.2.0
-        version: 1.2.0(react@18.3.1)
     devDependencies:
       '@deepseek-ai/cordis':
         specifier: workspace:^
@@ -3136,6 +3117,12 @@ importers:
       '@types/react':
         specifier: ~18.3.1
         version: 18.3.31
+      react:
+        specifier: ^18.2.0
+        version: 18.3.1
+      use-sync-external-store:
+        specifier: 1.2.0
+        version: 1.2.0(react@18.3.1)
 
   packages/code-runtime/code-runtime:
     devDependencies:

+ 230 - 0
scripts/browser-bundled-externals.ts

@@ -0,0 +1,230 @@
+/**
+ * The external packages a published browser artifact carries a copy of.
+ *
+ * Read from the real build configurations rather than declared by hand: each
+ * `lib/client.js` plugin bundle is driven through its own `tsdown.config.ts`, and
+ * the shell `dist` through `apps/web`'s Vite config. A recording plugin resolves
+ * every bare specifier as external and notes it, so the pass walks our own source
+ * and stops at the package boundary — which is both fast (about two seconds for
+ * the whole repository) and exactly the direct-dependency granularity
+ * THIRD_PARTY_NOTICES.md discloses. Erased type imports never appear, because the
+ * transform drops them before resolution.
+ *
+ * Workspace names are followed only on the Vite side, where the shell's aliases
+ * map them to source: that is how a browser-only library's own third-party
+ * imports — katex and shiki through `ui-primitives`, for one — become visible. A
+ * plugin bundle keeps them external, matching the frozen module table it is built
+ * against; the wire layers it inlines are host packages that declare their own
+ * dependencies, so nothing goes undisclosed.
+ *
+ * A specifier is recorded only once the host resolves it to a file inside a
+ * package. A bundler's own virtual module has no package behind it —
+ * `vite/modulepreload-polyfill` is generated by a Vite plugin rather than shipped
+ * as a file, so the polyfill in the published `dist` is build glue in the same
+ * category as an emitted TypeScript helper, not a redistributed copy of Vite.
+ *
+ * The pass runs on a clean tree, as a static gate must. The shell's Vite config
+ * aliases a few workspace packages to source; every other workspace name would
+ * resolve through `node_modules` to a `lib/` entry the real build has emitted but
+ * a clean checkout has not, so this module resolves those names to their own
+ * source instead. `lib/` is compiled from `src/`, so the third-party edges the
+ * pass records are the same either way.
+ *
+ * rolldown is resolved through tsdown deliberately: the dry run must use the
+ * exact bundler the real build uses, which a separate root pin could drift from.
+ */
+
+import { existsSync, globSync, readFileSync } from 'node:fs'
+import { createRequire } from 'node:module'
+import { dirname, join } from 'node:path'
+
+/** The plugin-context member the recorder needs to resolve before recording. */
+interface ResolveContext {
+  resolve: (
+    source: string,
+    importer: string,
+    options: { skipSelf: boolean },
+  ) => Promise<{ id: string } | null>
+}
+
+/** A rolldown/Vite plugin shape, narrowed to what the recorder needs. */
+interface RecorderPlugin {
+  name: string
+  enforce?: 'pre'
+  resolveId: (
+    this: ResolveContext,
+    source: string,
+    importer: string | undefined,
+  ) => Promise<{ id: string; external: true } | null>
+}
+
+/**
+ * The package a resolved module file belongs to.
+ * @param file - absolute path of a resolved module.
+ * @returns the package name, or undefined when the file is not inside a package.
+ */
+function packageOfFile(file: string): string | undefined {
+  const marker = file.lastIndexOf('node_modules/')
+  if (marker < 0) return undefined
+  const rest = file.slice(marker + 'node_modules/'.length)
+  const parts = rest.split('/')
+  return rest.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]
+}
+
+/**
+ * Source aliases for the workspace packages the shell does not already alias.
+ *
+ * A clean checkout has no `lib/`, so a workspace name would otherwise resolve
+ * through `node_modules` to an entry that does not exist yet. Aliases are the
+ * right seam rather than a plugin hook, because Vite resolves a stylesheet
+ * `@import` through them too — the theme package publishes its stylesheets from
+ * `lib/styles/`. `lib/` is compiled from `src/`, so the third-party edges the
+ * pass records are the same either way.
+ * @param root - repository root.
+ * @param existing - the shell's own alias patterns, whose entry choices win.
+ * @returns alias entries mapping each remaining workspace name to its source.
+ */
+function workspaceSourceAliases(root: string, existing: readonly string[]): { find: RegExp | string; replacement: string }[] {
+  const aliases: { find: RegExp | string; replacement: string }[] = []
+  for (const pattern of ['packages/*/*/package.json', 'vendor/*/package.json']) {
+    for (const relative of globSync(pattern, { cwd: root })) {
+      const dir = join(root, dirname(relative))
+      const manifest = JSON.parse(readFileSync(join(root, relative), 'utf8')) as Manifest & { name?: string }
+      const name = manifest.name
+      if (name === undefined || !existsSync(join(dir, 'src'))) continue
+      if (existing.some(find => find.includes(name))) continue
+      const root_ = manifest.exports?.['.']
+      const target = typeof root_ === 'string' ? root_ : root_?.default
+      const stem = (target ?? './lib/index.js')
+        .replace(/^\.\/lib\/types\//, '').replace(/^\.\/lib\//, '').replace(/\.js$/, '')
+      const entry = [`${stem}.ts`, `${stem}.tsx`, `${stem}/index.ts`, `${stem}/index.tsx`]
+        .map(candidate => join(dir, 'src', candidate))
+        .find(candidate => existsSync(candidate))
+      // The subpath prefix carries `./client`, `./types`, and `./styles/*` alike:
+      // each published subpath mirrors a path under `src/`.
+      aliases.push({ find: `${name}/`, replacement: `${join(dir, 'src')}/` })
+      if (entry !== undefined) aliases.push({ find: new RegExp(`^${name.replaceAll('/', '\\/')}$`), replacement: entry })
+    }
+  }
+  return aliases
+}
+
+/**
+ * Build the plugin that records bare specifiers and stops the walk at them.
+ * @param seen - set the recorder adds package names to.
+ * @returns the recording plugin.
+ */
+function recorder(seen: Set<string>): RecorderPlugin {
+  return {
+    name: 'dsh-record-direct-externals',
+    enforce: 'pre',
+    async resolveId(source, importer) {
+      if (importer === undefined) return null // the entry itself
+      if (source.startsWith('.') || source.startsWith('/') || source.startsWith('\0')) return null
+      if (source.startsWith('virtual:') || source.includes('?')) return null
+      // A workspace name that reaches here is one no alias mapped to source, so
+      // nothing of ours is left to walk; it is never a third-party disclosure.
+      if (source.startsWith('@deepseek-ai/')) return { id: source, external: true }
+      if (source.startsWith('node:')) return { id: source, external: true }
+      if (!source.startsWith('@deepseek-ai/')) {
+        const resolved = await this.resolve(source, importer, { skipSelf: true })
+        const name = resolved === null ? undefined : packageOfFile(resolved.id)
+        if (name !== undefined) seen.add(name)
+      }
+      return { id: source, external: true }
+    },
+  }
+}
+
+interface Manifest {
+  exports?: Record<string, { default?: string } | string | null>
+  files?: string[]
+}
+
+/** Read one workspace manifest. */
+function manifestOf(dir: string): Manifest {
+  return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as Manifest
+}
+
+/** Whether a manifest publishes a tsdown browser bundle at `lib/client.js`. */
+function publishesClientBundle(manifest: Manifest): boolean {
+  const target = manifest.exports?.['./client']
+  return typeof target === 'object' && target !== null && target.default === './lib/client.js'
+}
+
+/**
+ * Record every external package the plugin client bundles carry.
+ * @param root - repository root.
+ * @param seen - set the recorder adds package names to.
+ */
+async function collectFromClientBundles(root: string, seen: Set<string>): Promise<void> {
+  const requireFromTsdown = createRequire(createRequire(import.meta.url).resolve('tsdown'))
+  const { rolldown } = await import(requireFromTsdown.resolve('rolldown')) as {
+    rolldown: (options: Record<string, unknown>) => Promise<{
+      generate: (output: Record<string, unknown>) => Promise<unknown>
+      close: () => Promise<void>
+    }>
+  }
+
+  for (const relative of globSync('packages/*/*/tsdown.config.ts', { cwd: root }).sort()) {
+    const dir = join(root, dirname(relative))
+    if (!publishesClientBundle(manifestOf(dir))) continue
+    const loaded = await import(join(root, relative)) as { default: unknown }
+    const factory = loaded.default
+    const configs = (typeof factory === 'function'
+      ? (factory as (inline: { env: Record<string, string> }) => unknown[])({ env: {} })
+      : [factory]) as { name?: string; entry?: unknown; plugins?: unknown[] }[]
+    // The `/client` config is the browser bundle; its siblings emit the node half.
+    const client = configs.find(config => config.name?.endsWith('/client') === true)
+    if (client === undefined) continue
+    const bundle = await rolldown({
+      cwd: dir,
+      input: client.entry,
+      plugins: [recorder(seen), ...(client.plugins ?? [])],
+      platform: 'browser',
+    })
+    await bundle.generate({ format: 'cjs', minify: false, sourcemap: false })
+    await bundle.close()
+  }
+}
+
+/**
+ * Record every external package the prebuilt shell bundle carries.
+ * @param root - repository root.
+ * @param seen - set the recorder adds package names to.
+ */
+async function collectFromShellBundle(root: string, seen: Set<string>): Promise<void> {
+  for (const relative of globSync('apps/*/vite.config.ts', { cwd: root }).sort()) {
+    const dir = join(root, dirname(relative))
+    // Vite belongs to the app that builds with it, so it resolves from there.
+    const { build, resolveConfig } = await import(createRequire(join(dir, 'package.json')).resolve('vite')) as {
+      build: (options: Record<string, unknown>) => Promise<unknown>
+      resolveConfig: (options: Record<string, unknown>, command: string) => Promise<{
+        resolve: { alias: { find: string | RegExp }[] }
+      }>
+    }
+    // The shell already aliases some workspace names to source, and its entry
+    // choices win: a stylesheet `@import` resolves through aliases rather than a
+    // plugin hook, so only the names it leaves out get one from here.
+    const resolved = await resolveConfig({ root: dir, logLevel: 'error' }, 'build')
+    await build({
+      root: dir,
+      logLevel: 'error',
+      plugins: [recorder(seen)],
+      resolve: { alias: workspaceSourceAliases(root, resolved.resolve.alias.map(entry => String(entry.find))) },
+      build: { write: false, minify: false, sourcemap: false, reportCompressedSize: false },
+    })
+  }
+}
+
+/**
+ * The external packages a published browser artifact carries a copy of.
+ * @param root - repository root.
+ * @returns package names, workspace names excluded.
+ */
+export async function browserBundledExternals(root: string): Promise<Set<string>> {
+  const seen = new Set<string>()
+  await collectFromClientBundles(root, seen)
+  await collectFromShellBundle(root, seen)
+  return seen
+}

+ 21 - 3
scripts/gen-third-party-notices.spec.ts

@@ -24,11 +24,12 @@ describe('THIRD_PARTY_NOTICES.md', () => {
   // already runs in the test lane, so the check costs no extra CI process.
   // Pre-commit regenerates the file whenever a manifest is staged, so reaching
   // this assertion means the notices were committed without that hook.
-  it('matches what the generator produces from the current manifests', () => {
-    const generated = render()
+  it('matches what the generator produces from the current manifests', async () => {
+    const generated = await render()
     expect(generated).toContain('It depends on the third-party software listed below.')
     expect(readFileSync(resolve(root, 'THIRD_PARTY_NOTICES.md'), 'utf8'), 'stale notices — run `pnpm run gen-third-party-notices`').toBe(generated)
-  })
+    // Driving the two real bundlers to learn what ships costs a few seconds.
+  }, 60_000)
 })
 
 /** Build the (manifests, names) pair `tierExternalDeps` consumes. */
@@ -67,6 +68,23 @@ describe('tierExternalDeps', () => {
     ]))
   })
 
+  it('keeps a devDependency runtime when a published browser artifact carries it', () => {
+    const { manifests, names } = workspace({
+      // The client build inlines these, so a copy ships even though no manifest
+      // resolves the specifier at run time.
+      'packages/client/ui-primitives/package.json': {
+        name: '@deepseek-ai/dsh-client-ui-primitives',
+        devDependencies: { katex: '^0.16', 'test-only-helper': '^1' },
+      },
+    })
+
+    expect(tierExternalDeps(manifests, names, new Set(['katex']))).toEqual(new Map([
+      ['tsx', true],
+      ['katex', true],
+      ['test-only-helper', false],
+    ]))
+  })
+
   it('keeps a package runtime when any shipping area declares it, and excludes workspace links', () => {
     const { manifests, names } = workspace({
       'package.json': { devDependencies: { shared: '^1' } },

+ 31 - 16
scripts/gen-third-party-notices.ts

@@ -13,6 +13,7 @@ import { resolve } from 'node:path'
 import * as yaml from 'js-yaml'
 import { parse as parseToml, type TomlTableWithoutBigInt, type TomlValueWithoutBigInt } from 'smol-toml'
 import parseSpdx from 'spdx-expression-parse'
+import { browserBundledExternals } from './browser-bundled-externals.ts'
 
 const root = resolve(import.meta.dirname, '..')
 const OUT = 'THIRD_PARTY_NOTICES.md'
@@ -347,15 +348,17 @@ function normalizeRepo(raw: string | undefined): string | undefined {
 }
 
 /**
- * External npm dependencies, tiered by which workspace area declares them at
- * runtime: a package is runtime when any manifest outside `DEV_ONLY_AREAS`
- * names it in `dependencies`/`optionalDependencies`. A package declared only
- * by tooling, test infrastructure, the website, or the demo leaves — whatever
- * the declaring section is called — is development-only.
+ * External npm dependencies, tiered by what reaches a user: a package is runtime
+ * when any manifest outside `DEV_ONLY_AREAS` names it in
+ * `dependencies`/`optionalDependencies`, or when a published browser artifact
+ * carries a copy of it. A package declared only by tooling, test infrastructure,
+ * the website, or the demo leaves — whatever the declaring section is called, and
+ * with no shipped artifact carrying it — is development-only.
+ * @returns every external dependency with its tier and metadata.
  */
-function collectNpmDeps(): ExternalDep[] {
+async function collectNpmDeps(): Promise<ExternalDep[]> {
   const { manifests, names } = loadWorkspaceManifests()
-  return [...tierExternalDeps(manifests, names)]
+  return [...tierExternalDeps(manifests, names, await browserBundledExternals(root))]
     .filter(([name]) => !FIRST_PARTY.has(name))
     .sort(([a], [b]) => a.localeCompare(b))
     .map(([name, runtime]) => ({ name, ...installedMetadata(name), runtime }))
@@ -363,11 +366,23 @@ function collectNpmDeps(): ExternalDep[] {
 
 /**
  * Tier every external dependency the workspace declares.
+ *
+ * A package a published browser artifact carries is runtime whatever section
+ * declares it: the client build inlines its code, or the shell `dist` answers it
+ * from the frozen module table, so a copy is redistributed even though nothing on
+ * a user's machine resolves the specifier. Those packages are declared as
+ * `devDependencies` — `verify-client-runtime-deps` owns that rule — and tiering
+ * them by section alone would understate the notice.
  * @param manifests - workspace manifests keyed by repository-relative path.
  * @param names - every workspace package name, which never counts as external.
+ * @param bundled - external packages a published browser artifact carries.
  * @returns each external package mapped to whether it is a runtime dependency.
  */
-export function tierExternalDeps(manifests: Map<string, Manifest>, names: Set<string>): Map<string, boolean> {
+export function tierExternalDeps(
+  manifests: Map<string, Manifest>,
+  names: Set<string>,
+  bundled: ReadonlySet<string> = new Set(),
+): Map<string, boolean> {
   const tiers = new Map<string, boolean>()
   // `tsx` is runtime by fiat: the root source-run scripts execute through its ESM hook.
   tiers.set('tsx', true)
@@ -376,7 +391,7 @@ export function tierExternalDeps(manifests: Map<string, Manifest>, names: Set<st
     for (const kind of ALL_KINDS) {
       for (const [dep, range] of Object.entries(manifest[kind] ?? {})) {
         if (names.has(dep) || range.startsWith('workspace:')) continue
-        const runtime = !devOnly && (RUNTIME_KINDS as readonly string[]).includes(kind)
+        const runtime = bundled.has(dep) || (!devOnly && (RUNTIME_KINDS as readonly string[]).includes(kind))
         tiers.set(dep, (tiers.get(dep) ?? false) || runtime)
       }
     }
@@ -660,9 +675,9 @@ ${rows.join('\n')}
  * Render the complete notices document.
  * @returns the exact bytes `THIRD_PARTY_NOTICES.md` must hold.
  */
-export function render(): string {
+export async function render(): Promise<string> {
   verifyBuildTimePins()
-  const npm = collectNpmDeps()
+  const npm = await collectNpmDeps()
   const runtimeDeps = npm.filter(dep => dep.runtime)
   const devDeps = npm.filter(dep => !dep.runtime)
   const vendored = collectVendored()
@@ -707,7 +722,7 @@ ${vendored.map(row => `| \`${row.npmName}\` | \`${row.upstreamName}\` | [${row.u
 
 ## Runtime npm dependencies
 
-External packages that a workspace package resolves at runtime. The tier covers every plugin a user can mount from \`cordis.yml\` — not only what the \`dsh\` CLI, Web UI, and Python SDK runtime load by default.
+External packages that reach a user: a workspace package resolves them at runtime, or a published browser artifact carries a copy of their code. The tier covers every plugin a user can mount from \`cordis.yml\` — not only what the \`dsh\` CLI, Web UI, and Python SDK runtime load by default — and the packages the client build inlines into a plugin bundle or the shell \`dist\`, which are declared as \`devDependencies\` because nothing on a user's machine resolves their specifiers.
 
 ${renderNpmTable(runtimeDeps)}
 
@@ -718,7 +733,7 @@ ${renderClaudeDistribution(claudeDistribution)}
 
 ## Development-only npm dependencies
 
-External packages **directly declared** only by repository tooling, test infrastructure, the documentation site, the demo leaves, or the native launcher's build workspace. No shipped surface names them itself. A package here may still be pulled in transitively by a runtime dependency — \`pnpm-lock.yaml\` is the authority on the full closure — so this tier records who declares a package, not what a build ultimately bundles.
+External packages **directly declared** only by repository tooling, test infrastructure, the documentation site, the demo leaves, or the native launcher's build workspace, and carried by no published artifact. No shipped surface names them itself. A package here may still be pulled in transitively by a runtime dependency — \`pnpm-lock.yaml\` is the authority on the full closure — so this tier records who declares a package, not what a build ultimately bundles.
 
 ${renderNpmTable(devDeps)}
 ${renderNonPermissiveNote(nonPermissiveDev)}
@@ -746,8 +761,8 @@ ${BUILD_TIME_TOOLS.map(tool => `| [\`${tool.name}\`](${tool.repo}) | ${tool.lice
 /** CLI entry: default writes the notices, `--check` fails if the committed copy
  * is stale. Guarded behind an entry-point check so importing this module for
  * tests neither regenerates the committed file nor calls process.exit. */
-function main(): void {
-  const content = render()
+async function main(): Promise<void> {
+  const content = await render()
   if (process.argv.includes('--check')) {
     let committed: string | null = null
     try {
@@ -771,5 +786,5 @@ function main(): void {
 
 // Run only when invoked as a script, not when imported by a test.
 if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) {
-  main()
+  await main()
 }

+ 368 - 0
scripts/verify-client-runtime-deps.ts

@@ -0,0 +1,368 @@
+/**
+ * Keep browser-only external packages out of installed dependency sections.
+ *
+ * A browser artifact resolves nothing on the user's machine: tsdown inlines
+ * every non-platform specifier into `lib/client.js`, the shell `dist` answers
+ * `PLATFORM_MODULES` from its frozen module table, and Vite inlines the shell's
+ * own imports into that published `dist`. A specifier only browser source
+ * reaches is therefore a build-time input and belongs in `devDependencies`,
+ * because npm installs `dependencies` and non-optional `peerDependencies` for
+ * every consumer of the published package.
+ *
+ * Each face is walked from the entries the manifest publishes, not by a
+ * directory rule, so a module under `src/` that only the browser entry reaches
+ * counts as browser source:
+ *
+ *   `./client` is `lib/client.js`   host: the other export targets; browser: the bundle
+ *   `packages/client/*` with no     browser-only library: host is `src/invariant.ts`,
+ *     `./client` export               the companion the host mounts; `.` is browser code
+ *   no `.` export, ships a `dist`   prebuilt browser bundle: no host face at all
+ *
+ * Only external packages are subject: they are what an install downloads. A
+ * workspace name stays where its manifest puts it, because that declaration also
+ * states which package supplies an injected service or a mounted Remote
+ * contribution, and the app installs it either way. A reference from the host
+ * face, an erased type import included, likewise keeps a declaration in place.
+ *
+ * Run: pnpm exec tsx scripts/verify-client-runtime-deps.ts [--json]
+ */
+
+import { existsSync, globSync, readFileSync } from 'node:fs'
+import { dirname, join, resolve } from 'node:path'
+import ts from 'typescript'
+import { TypeScriptProject, type CompilerFace } from './ts-project.ts'
+
+const root = resolve(import.meta.dirname, '..')
+
+/**
+ * `@deepseek-ai/cordis` placement belongs to check-workspace-constraints, which
+ * requires it as a peerDependency plus devDependency of every harness package
+ * regardless of face.
+ */
+const PLACEMENT_OWNED_ELSEWHERE = new Set(['@deepseek-ai/cordis'])
+
+/** Dependency sections npm installs for a consumer of the published package. */
+const INSTALLED_SECTIONS = ['dependencies', 'peerDependencies'] as const
+
+type Section = (typeof INSTALLED_SECTIONS)[number]
+
+interface Manifest {
+  name?: string
+  files?: string[]
+  exports?: Record<string, unknown>
+  dependencies?: Record<string, string>
+  peerDependencies?: Record<string, string>
+  peerDependenciesMeta?: Record<string, { optional?: boolean }>
+}
+
+/** How a package reaches the browser, which fixes the entries Node can load. */
+type Kind = 'bundle-half' | 'browser-library' | 'prebuilt-dist'
+
+/** What settles an external specifier as build-time only. */
+type Reached = 'browser' | 'nothing'
+
+interface Violation {
+  readonly section: Section
+  readonly dep: string
+  readonly reached: Reached
+  /** Whether the browser face names it, which decides dev-move versus deletion. */
+  readonly browserReferenced: boolean
+}
+
+interface Offender {
+  readonly name: string
+  readonly dir: string
+  readonly kind: Kind
+  readonly violations: Violation[]
+}
+
+/** Why each class needs no install, for the failure report. */
+const REASON: Record<Reached, string> = {
+  browser: 'only a browser artifact reaches it, and that resolves nothing on the user machine',
+  nothing: 'no reference names it at all',
+}
+
+/**
+ * Classify a package by the browser artifact it produces.
+ * @param dir - repository-relative package directory.
+ * @param manifest - the package manifest.
+ * @returns the package kind, or undefined when the package has no browser face.
+ */
+function kindOf(dir: string, manifest: Manifest): Kind | undefined {
+  if (manifest.exports?.['./client'] !== undefined) return 'bundle-half'
+  if (dir.startsWith('packages/client/')) return 'browser-library'
+  const shipsDist = (manifest.files ?? []).some(entry => entry === 'dist' || entry.startsWith('dist/'))
+  if (shipsDist && manifest.exports?.['.'] === undefined) return 'prebuilt-dist'
+  return undefined
+}
+
+/** The bare package name a specifier names, keeping a leading scope. */
+function packageOf(specifier: string): string {
+  const parts = specifier.split('/')
+  return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0] ?? specifier
+}
+
+/** One compiler face's bound program plus its module resolution state. */
+interface Face {
+  readonly project: TypeScriptProject
+  readonly host: ts.CompilerHost
+  readonly cache: ts.ModuleResolutionCache
+}
+
+const faces = new Map<CompilerFace, Face>()
+for (const face of ['host', 'client'] as const) {
+  const project = new TypeScriptProject(root, face)
+  const options = project.program.getCompilerOptions()
+  faces.set(face, {
+    project,
+    host: ts.createCompilerHost(options, false),
+    cache: ts.createModuleResolutionCache(root, fileName => fileName, options),
+  })
+}
+
+/** Which face's program bound each workspace module, keyed by absolute path. */
+const boundIn = new Map<string, CompilerFace>()
+for (const [face, { project }] of faces) {
+  for (const sourceFile of project.sourceFiles()) {
+    if (sourceFile.isDeclarationFile) continue
+    if (!boundIn.has(sourceFile.fileName)) boundIn.set(sourceFile.fileName, face)
+  }
+}
+
+/**
+ * Read every module specifier one source file names.
+ *
+ * An import clause is not the only way to reach a package: `require`,
+ * `require.resolve`, and a dynamic `import()` on a literal each name one, and a
+ * type-only import still names a package the build must resolve.
+ * @param sourceFile - a bound source file.
+ * @returns every specifier, relative ones included.
+ */
+function specifiersOf(sourceFile: ts.SourceFile): string[] {
+  const specifiers: string[] = []
+  const visit = (node: ts.Node): void => {
+    if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node) || ts.isImportEqualsDeclaration(node)) {
+      const specifier = ts.isImportEqualsDeclaration(node)
+        ? (ts.isExternalModuleReference(node.moduleReference) ? node.moduleReference.expression : undefined)
+        : node.moduleSpecifier
+      if (specifier !== undefined && ts.isStringLiteralLike(specifier)) specifiers.push(specifier.text)
+    } else if (ts.isCallExpression(node)) {
+      const target = node.expression
+      const isRequire = ts.isIdentifier(target) && target.text === 'require'
+      const isRequireResolve = ts.isPropertyAccessExpression(target)
+        && ts.isIdentifier(target.expression) && target.expression.text === 'require'
+        && target.name.text === 'resolve'
+      const argument = node.arguments[0]
+      if ((isRequire || isRequireResolve || target.kind === ts.SyntaxKind.ImportKeyword)
+        && argument !== undefined && ts.isStringLiteralLike(argument)) {
+        specifiers.push(argument.text)
+      }
+    }
+    ts.forEachChild(node, visit)
+  }
+  visit(sourceFile)
+  return specifiers
+}
+
+/**
+ * Walk one face from its entries and collect the packages it names.
+ * @param entries - absolute entry module paths.
+ * @param packageDir - absolute package directory; the walk stops at its edge.
+ * @returns package names the walk reaches.
+ */
+function walk(entries: readonly string[], packageDir: string): Set<string> {
+  const found = new Set<string>()
+  const seen = new Set<string>()
+  const queue = entries.filter(entry => boundIn.has(entry))
+  while (queue.length > 0) {
+    const file = queue.pop()
+    if (file === undefined || seen.has(file)) continue
+    seen.add(file)
+    const faceName = boundIn.get(file)
+    const face = faceName === undefined ? undefined : faces.get(faceName)
+    const sourceFile = face?.project.program.getSourceFile(file)
+    if (face === undefined || sourceFile === undefined) continue
+
+    for (const specifier of specifiersOf(sourceFile)) {
+      if (!specifier.startsWith('.')) {
+        if (!specifier.startsWith('node:')) found.add(packageOf(specifier))
+        continue
+      }
+      const resolved = ts.resolveModuleName(
+        specifier, file, face.project.program.getCompilerOptions(), face.host, face.cache,
+      ).resolvedModule?.resolvedFileName
+      // A relative specifier resolving outside the package is a packaging error
+      // verify-package-paths owns; either way it is not this package's own module.
+      if (resolved !== undefined && resolved.startsWith(`${packageDir}/`)) queue.push(resolved)
+    }
+  }
+  return found
+}
+
+/**
+ * The source module behind one published JavaScript export target.
+ *
+ * `lib/` holds the tsdown bundles and `lib/types/` the tsc emit, so both
+ * prefixes lead back to one `src` module.
+ * @param dir - absolute package directory.
+ * @param emitted - the export target, as written in the manifest.
+ * @returns the absolute source path, or undefined when nothing in `src` emits it.
+ */
+function sourceBehind(dir: string, emitted: string): string | undefined {
+  const stem = emitted.replace(/^\.\/lib\/types\//, '').replace(/^\.\/lib\//, '').replace(/\.js$/, '')
+  return [`src/${stem}.ts`, `src/${stem}.tsx`, `src/${stem}/index.ts`, `src/${stem}/index.tsx`]
+    .map(candidate => join(dir, candidate))
+    .find(candidate => existsSync(candidate))
+}
+
+interface Entries {
+  readonly host: string[]
+  readonly browser: string[]
+  /**
+   * Published JavaScript entries no `src` module emits — a generated artifact
+   * such as `lib/typert.host.js`, whose own runtime imports are invisible here.
+   */
+  readonly generated: string[]
+}
+
+/**
+ * The entry modules of each face, derived from what the manifest publishes.
+ * @param dir - absolute package directory.
+ * @param manifest - the package manifest.
+ * @param kind - the package kind.
+ * @returns absolute entry module paths per face, plus unmapped published entries.
+ */
+function faceEntries(dir: string, manifest: Manifest, kind: Kind): Entries {
+  if (kind === 'prebuilt-dist') return { host: [], browser: [], generated: [] }
+  if (kind === 'browser-library') {
+    return { host: [join(dir, 'src/invariant.ts')], browser: [join(dir, 'src/index.ts')], generated: [] }
+  }
+  const host = [join(dir, 'src/index.ts'), join(dir, 'src/invariant.ts')]
+  const browser: string[] = []
+  const generated: string[] = []
+  for (const [key, target] of Object.entries(manifest.exports ?? {})) {
+    if (key === '.' || key === './package.json' || key.includes('*')) continue
+    const emitted = typeof target === 'string' ? target : (target as { default?: unknown }).default
+    if (typeof emitted !== 'string' || !emitted.endsWith('.js')) continue
+    // Keyed on the artifact path, not the subpath name: `./client` is the tsdown
+    // browser bundle only when it resolves to lib/client.js, while other packages
+    // publish a plain browser-shared module under the same subpath.
+    const source = emitted === './lib/client.js'
+      ? sourceBehind(dir, './lib/client/index.js')
+      : sourceBehind(dir, emitted)
+    if (source === undefined) generated.push(`${key} -> ${emitted}`)
+    else if (key === './client') browser.push(source)
+    else host.push(source)
+  }
+  return { host, browser, generated }
+}
+
+/** Every installed dependency of a manifest, paired with its section. */
+function installedDeps(manifest: Manifest): { section: Section; dep: string }[] {
+  const deps: { section: Section; dep: string }[] = []
+  for (const section of INSTALLED_SECTIONS) {
+    for (const dep of Object.keys(manifest[section] ?? {})) {
+      if (section === 'peerDependencies' && manifest.peerDependenciesMeta?.[dep]?.optional === true) continue
+      if (PLACEMENT_OWNED_ELSEWHERE.has(dep)) continue
+      deps.push({ section, dep })
+    }
+  }
+  return deps
+}
+
+/**
+ * Test whether a Loader config names a package as a whole word.
+ * @param text - raw config text.
+ * @param dep - package name to look for.
+ * @returns true when the name appears outside a longer specifier.
+ */
+function namesPackage(text: string, dep: string): boolean {
+  const escaped = dep.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+  return new RegExp(`(^|[^\\w@/.-])${escaped}(?![\\w.-])`).test(text)
+}
+
+interface Candidate {
+  readonly name: string
+  readonly relativeDir: string
+  readonly manifest: Manifest
+  readonly kind: Kind
+}
+
+const candidates: Candidate[] = []
+for (const path of [
+  ...globSync('packages/*/*/package.json', { cwd: root }),
+  ...globSync('apps/*/package.json', { cwd: root }),
+].sort()) {
+  const relativeDir = dirname(path)
+  const manifest = JSON.parse(readFileSync(join(root, path), 'utf8')) as Manifest
+  if (manifest.name === undefined) continue
+  const kind = kindOf(relativeDir, manifest)
+  if (kind !== undefined) candidates.push({ name: manifest.name, relativeDir, manifest, kind })
+}
+
+const offenders: Offender[] = []
+const unchecked: string[] = []
+for (const { name, relativeDir, manifest, kind } of candidates) {
+  const dir = join(root, relativeDir)
+  const entries = faceEntries(dir, manifest, kind)
+  // A generated Node entry carries runtime imports of its own that no source
+  // states, so this package's declarations cannot be judged from `src` alone.
+  if (entries.generated.length > 0) {
+    unchecked.push(`${name}: generated entry ${entries.generated.join(', ')}`)
+    continue
+  }
+  const host = walk(entries.host, dir)
+  const browser = walk(entries.browser, dir)
+  // A Loader row names its plugin package instead of importing it, so a config
+  // the package owns is part of its host face. YAML keys carry no quotes, so
+  // these are matched as whole names against the raw text.
+  const configs = globSync('cordis*.yml', { cwd: dir }).map(config => readFileSync(join(dir, config), 'utf8'))
+
+  const violations = installedDeps(manifest)
+    // A workspace name stays where the manifest puts it. Such a declaration also
+    // states which package supplies an injected service, which Remote contribution
+    // an assembly mounts, or which Loader row must resolve; the app installs the
+    // package regardless, so moving one saves no download while deleting what
+    // verify-runtime-closure and the Loader read. External packages are the
+    // download, and this gate is about the download.
+    .filter(({ dep }) => !dep.startsWith('@deepseek-ai/'))
+    .filter(({ dep }) => !host.has(dep) && !configs.some(text => namesPackage(text, dep)))
+    .map(({ section, dep }) => ({
+      section,
+      dep,
+      browserReferenced: browser.has(dep),
+      // A prebuilt bundle publishes no Node entry, so everything it declares is
+      // build-time by construction, named in its Vite graph rather than in src.
+      reached: kind === 'prebuilt-dist' || browser.has(dep) ? 'browser' as const : 'nothing' as const,
+    }))
+  if (violations.length > 0) offenders.push({ name, dir: relativeDir, kind, violations })
+}
+
+if (process.argv.includes('--json')) {
+  console.log(JSON.stringify(offenders, null, 2))
+  process.exit(0)
+}
+
+if (unchecked.length > 0) {
+  console.log(`verify-client-runtime-deps: ${String(unchecked.length)} package(s) not checked, no source states their entry's imports:`)
+  for (const entry of unchecked) console.log(`  ${entry}`)
+}
+
+if (offenders.length > 0) {
+  const all = offenders.flatMap(offender => offender.violations)
+  console.error(`verify-client-runtime-deps: ${String(all.length)} build-time specifier(s) in installed sections:`)
+  for (const { name, dir, kind, violations } of offenders) {
+    console.error(`  ${name} (${dir}, ${kind})`)
+    for (const { section, dep, reached } of violations) {
+      console.error(`      ${section}.${dep}  ->  devDependencies [${reached}]`)
+    }
+  }
+  console.error('')
+  for (const reached of ['browser', 'nothing'] as const) {
+    const count = all.filter(violation => violation.reached === reached).length
+    if (count > 0) console.error(`  ${String(count).padStart(4)} ${reached}: ${REASON[reached]}`)
+  }
+  console.error('\nDeclaration rules: packages/client/AGENTS.md.')
+  process.exit(1)
+}
+console.log(`verify-client-runtime-deps: browser-face specifiers are dev-only across ${String(candidates.length)} browser-facing packages.`)

部分文件因文件數量過多而無法顯示