dsh-composer-ux 0.23.4 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,101 @@
2
2
 
3
3
  本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [0.25.0] — 2026-10-18(「OpenCode 请求头」降级为旧宿主兼容层:宿主已原生支持就不注入)
6
+
7
+ 用户 2026-10-18 在设置页看到这一栏,问:**「是否还需要这个功能」**。查完上下游后答案是
8
+ **不需要了,而且开着比不开更差**,用户拍板:关掉 + 改成"启动时探测宿主,有原生支持就不注入"。
9
+
10
+ **三条证据(都在本机核过)**
11
+
12
+ 1. OpenCode Go 官方文档「可以在哪里使用?」第 3 条仍然要求客户端发
13
+ `x-opencode-session`(用于优化路由与 prompt cache),所以这个头本身还是要的;
14
+ 2. 但**上游已经自己发了**:`@earendil-works/pi-ai` 从 **1.0.2** 起自带
15
+ `dist/providers/opencode-headers.js`,`opencode` 与 `opencode-go` 两个 provider 都被
16
+ `withOpenCodeSessionHeader()` 包过;它用的是 `options.sessionId`(**每段对话的真实会话 id**),
17
+ 而且 **`hasHeader()` 命中就不覆盖**;
18
+ 3. DSH 的 `llm-pi-ai` 调用时是 `headers: requestHeaders(profile.headers)` 与
19
+ `sessionId` **一起**交给 pi-ai 的 ⇒ 本插件注入的那个**固定 UUID**(设置页原话"所有对话共用
20
+ 这一个值")会**顶掉**真实 session id。也就是说:开着它,所有对话在 OpenCode 那边都是同一个
21
+ 会话 —— 正是官方要用来做路由与缓存优化的那一维被抹平了。
22
+
23
+ **改动**
24
+
25
+ - 新增 `src/opencode-header.ts`:启动时探测宿主能力。判据是**宿主 pi-ai 安装里有没有
26
+ `dist/providers/opencode-headers.js`**(能力就是那个模块,版本号只用来写证据),探测面覆盖
27
+ 桌面端(`resources/app.asar` 里的运行时)与网页版(从入口往上找 `node_modules` / `.pnpm`);
28
+ 探不到时**按旧宿主处理**(继续注入,失败方向是多做一次旧行为),并可用环境变量
29
+ `DSH_COMPOSER_UX_HEADER_NATIVE=1|0` 强制。
30
+ - 宿主半(`src/host.ts`):探测到原生支持时 —— 不注入、不生成 UUID、不记"已注入"的账;
31
+ 状态行写「宿主已原生支持(pi-ai 1.1.0 自带 opencode-headers.js):按每段对话的真实
32
+ session id 自动发送,本栏不再注入」。
33
+ - 设置页(`src/client/SettingsSection.tsx`):卡片顶部多一条说明("本宿主已原生支持…这一栏
34
+ 什么都不做",与自检卡、状态行说同一件事);概览写「宿主已原生支持(不需要)」而不是像故障的
35
+ "未启用";「值」那一行的说明改成"旧宿主专用"。
36
+ - 自检卡(`src/doctor.ts`):`header` 那一行在原生支持时如实说明"这一栏不需要、也不会注入"。
37
+ - 桌面 / 网页两个 profile 的 `headerEnabled` 均置 `false`(用户要求"关掉";不过即便打开,
38
+ 探测到原生支持也不会注入)。
39
+
40
+ **验证**:33 套件 **2800 条** 0 失败;变异 **220 条全部咬住**(新增 `SG`/`SH`/`SI`)。`test/host-header-mirror.mjs` 新增第 19 / 20 节(原生支持不注入 + 不生成 UUID +
41
+ 状态行;探测判据用假目录逐条钉:模块在⇒native、模块缺⇒legacy、目录缺⇒legacy、环境变量强制、
42
+ 版本比较按数字不按字符串);变异新增 `SG`/`SH`/`SI` 三条。
43
+
44
+ **未验证(如实说明)**:探测只在本机两种布局(Electron asar / pnpm 检出)上验过;发行渠道若把
45
+ pi-ai 装到别处,会落到"按旧宿主处理",此时用环境变量强制即可。桌面端要重启后才加载这一版。
46
+
47
+
48
+ ## [0.24.0] — 2026-10-17(金额胶囊把**子代理**的费用算进去)
49
+
50
+ 用户 2026-10-17:「**我发现金额胶囊不计算子代理的费用**」。
51
+
52
+ **根因不在格式化,在数据源头**:子代理(`subagent` / `subagent_fork` / `subagent_control` 那些)
53
+ 跑在**自己的会话**里,它的 usage 事件落在**自己的**会话日志里 —— 父会话的折叠器看不到,
54
+ 官方 `tokenUsage` 投影同样只有本会话。于是 0.23.x 的胶囊只报父会话自己那一份,跑一堆子代理
55
+ 之后数字几乎不动,而屏幕上只是一个**偏小的数字**,不报错、不提示。
56
+
57
+ **宿主半(`src/host.ts` / `src/usage-fold.ts`)**
58
+
59
+ - **子会话从哪来**:两条来源取并集。
60
+ 1. 父会话自己的 `subagent/catalog` 事件(官方 `manager.ts` 在建立子会话时追加,事件里带
61
+ `childId` / `mode`)—— 折叠器顺手读出来,不额外读日志;
62
+ 2. **子会话 header 反查**(`parentSession` + `origin: 'subagent'`,走 `sessionQuery.listSessions()`,
63
+ 30 秒 TTL 缓存)。为什么非得有第二条:2026-10-17 在本机 `~/.dsh/sessions` 上核过 —— 211 份
64
+ 会话日志里 **35 份**是子代理子会话,而**没有任何一份**父会话日志里有 `subagent/catalog`
65
+ (那个事件是较新版本 DSH 才写的)。只认 catalog 的话,用户去看老会话,钱还是少的。
66
+ 两条来源都**只认子代理关系**:用户自己"从某会话 fork 出来的会话"在 header 里同样有
67
+ `parentSession`、但没有 `origin`,见谁都认会把用户自己开的会话算进别人的账上。
68
+ - **递归**:子代理还能派孙代理,所以按 catalog/header 一路折下去(深度 ≤ 4、会话数 ≤ 32,
69
+ 超出记 `skipped`)。活着的子会话靠 `sessions.list()` 的 `seq` 判落后,事件订阅还会继续增量喂。
70
+ - **fork 型子代理的继承前缀要剔掉**(`readSession()` 的 `inheritedEventCount`):`subagent_fork`
71
+ 的子会话日志**开头就是父会话那段历史**,整段照折等于把父会话的账再收一遍,而且金额随上下文
72
+ 大小成倍放大。判据与官方投影同一条(`event.seq < inheritedEventCount` 即继承来的)。
73
+ - **顶层字段一个都没动**:`total` / `cost` / `routes` / `tiers` 仍然是"本会话自己"。客户端拿它们
74
+ 跟官方 `tokenUsage` 投影对账(`agreesWithProjection`),一合并就永远对不上、浮层退回估算口径
75
+ (那是 0.9.1 之前的老毛病)。子代理的合计**单列**在响应新增的 `subagents` 里:
76
+ `sessions` / `live` / `missing` / `external` / `skipped` / `total` / `tiers` / `cost` / `unpriced`。
77
+ 后面四个计数是"没算进来"的四种原因(读不到日志 / 外部执行如 claude-code 本机没有会话 /
78
+ 超上限 / 单价认不出),界面必须把它们说出来 —— 否则用户只会觉得"钱怎么比预期少"。
79
+
80
+ **客户端半(`src/client/CostChipEntry.tsx` / `src/client/session-cost.ts`)**
81
+
82
+ - 胶囊那个数字与浮层「合计」= **本会话 + 子代理**(只在"本会话分列与投影对得上"时并;
83
+ 否则保持本地估算口径,不拿估出来的本会话去加算准的子代理)。
84
+ - 浮层多一行「子代理(N 个会话)」;三个分项(未缓存输入 / 缓存命中 / 输出)也并进去,
85
+ 所以"分项之和 = 合计"仍然精确成立;命中率同源。
86
+ - **峰谷两档只并 token、不并金额**:宿主没把子代理的钱按峰谷拆开,拿"token × 某档单价"凑一个
87
+ 按档金额就是编数字。浮层里写明"两档的 token 已含子代理,金额只含本会话"。
88
+ - 读不全的部分写成一句人话("另有 N 个子代理的费用没算进来:…"),子代理全未定价时那一行写
89
+ 「未定价」而不是 ¥0.00(与 route 行同口径)。没有子代理时浮层、胶囊、文案逐字节与 0.23.x 相同。
90
+
91
+ **验证**:`npm test` **33 套件 2780 条 0 失败**(新增 `test/subagent-cost.mjs` 38 条:纯逻辑 +
92
+ 真宿主半,含 fork 继承前缀、递归与上限、外部执行/读不到、老会话 header 反查、TTL 缓存);
93
+ 变异 **217 条全部咬住**(新增 `SA`–`SF` 六条);`tsc` 0 错误;`cost-panel-render` 新增第 1.2 节
94
+ 17 条渲染断言(旧夹具与旧断言一条没动)。
95
+
96
+ **未验证(如实说明)**:没有真机截图;桌面端要等用户重启后才会加载这一版(那个进程是
97
+ 2026-10-17 23:30 起的 **0.2.1-rc.2**,装的还是 0.23.4);子代理的钱只有总量、没有按峰谷拆开的金额。
98
+
99
+
5
100
  ## [0.23.4] — 2026-10-16(适配 dsh 0.2.1-rc.1:无功能变化,只做验证与版本对齐)
6
101
 
7
102
  用户 2026-10-16:「给网页版 dsh 更新最新版,并适配插件」。上游最新 tag 是 **`dsh-v0.2.1-rc.1`**
package/README.en.md CHANGED
@@ -188,7 +188,7 @@ edited in settings; the JSON output contract is always appended and cannot be ov
188
188
  | Read-only tool fence (including symlinks) / every cap / out-of-bounds rejection | `node test/optimize-tools.mjs` (66 checks against a **real temporary directory**) |
189
189
  | Tool-loop message shape / round cap / exceptions must degrade | same file, section 6 (a fake `llm.stream` drives the whole loop; `role:'tool'` and `toolCallId` are asserted) |
190
190
  | Tool path: off by default, fires only when enabled, **prose after lookups falls back**, no cwd ⇒ no tool at all | `test/quick-commands.mjs` 5g (13 checks) |
191
- | Mutation guards really bite, and **the *named* test must be the one going red** | `npm run test:mutations`: **137/137 bite** |
191
+ | Mutation guards really bite, and **the *named* test must be the one going red** | `npm run test:mutations`: **217/217 bite** |
192
192
  | 0.13.2: `✕` collapses (result kept) + a two-click 丢弃 | `node test/panel-split-render.mjs` §6, mutations EE–EI |
193
193
  | Panel split + pinned result-box buttons (0.13.1) | `node test/panel-split-render.mjs` (20 checks, real `react-dom/server` render) |
194
194
  | Release gates | `npm run gates` (tag target / packed file list / docs drift) |
package/README.md CHANGED
@@ -45,7 +45,7 @@ DeepSeek Harness Web 输入体验增强插件:
45
45
  - **OpenCode 请求头**:给 OpenCode 的模型请求自动附加 `x-opencode-session`(详见下节)。
46
46
  - **默认终端**:Windows 上把模型用的终端工具从 PowerShell 换成 Git Bash(三档在标题行;详见下节)。
47
47
  - **统计行**:输入框下方那行「缓存命中 xx%」按三位小数显示(0.7.0 新增,**唯一默认开的栏**,只有一个开关;详见下节)。
48
- - **金额**:输入框下方一颗极简金额胶囊(本会话费用估算,按 DeepSeek 官方刊例价;点开看三分项明细)。**这一栏没有卡级开关**(胶囊常驻显示),卡内是**按模型改高峰/空闲两档单价**的编辑器(0.9.1 新增;留空=沿用官方价,可自加任意模型名;详见下节)。
48
+ - **金额**:输入框下方一颗极简金额胶囊(本会话费用估算,按 DeepSeek 官方刊例价;点开看三分项明细)。**0.24.0 起把子代理(含孙代理)花的钱也并进来** —— 那个数字是「本会话 + 子代理」的合计。**这一栏没有卡级开关**(胶囊常驻显示),卡内是**按模型改高峰/空闲两档单价**的编辑器(0.9.1 新增;留空=沿用官方价,可自加任意模型名;详见下节)。
49
49
  - **重启 DSH**:卡片抬头右端(GitHub 链接左边)那枚按钮,两步确认后原地重启(详见「维护:重启 DSH」)。
50
50
  2. **快捷指令按钮**:输入框工具行里、「展开」按钮左侧的胶囊按钮,点开是常备提示词清单 + 「优化提示词」。
51
51
  3. **键位生效**(仅主聊天输入框):默认值 = 现状(Enter 发送、Shift+Enter 换行、Ctrl+Enter 加速提交),改动即时生效并持久保存。
@@ -493,9 +493,11 @@ Windows 上 DSH 给模型的终端工具是 **PowerShell**(工具名 `pwsh`)
493
493
  - **写完价目要立刻生效,靠的是"写的人通知",不是设置变更事件**:0.1.7 的 `settings/document-updated` 只在 `describe()` 里比对 raw 变化后发出,而我们自己 `mutate` 写设置时并不调 `describe()` —— 设置页开着时会被界面刷新顺带触发,**设置页没开着**(例如后台的自动同步跑完)就不会发;而折叠缓存按 `seq` 去重、改规则不会重折旧事件,金额会**静静地停在旧价格档**上、界面上看不出。所以宿主半加了模块级 `moneyInvalidators` 注册表:用量路由登记"重读规则 + 作废价目缓存 + 作废折叠缓存",两条同步路径写完设置直接调 `invalidateMoney()`,事件只作补充。
494
494
  - **内置第三方价目快照(0.10.0)**:`src/provider-prices.ts`(**生成**的,别手改:`node scripts/gen-provider-prices.mjs <models.dev-api.json>`)收了 **11 个 provider / 346 个模型**,快照日期写在文件头。**没点过同步时**就是非 DeepSeek 模型的兜底价;点过一次「同步第三方价目」后最新数据会盖住同名条目,界面分别标明"内置快照价"还是已同步。查价优先级:**用户覆盖价 > 已同步价目 > 内置快照 > 未定价**。
495
495
  - **非 DeepSeek 模型定价(0.10.0)**:按 `(provider, model)` 查价 —— 精确 provider → `PROVIDER_ALIASES` 别名(`deepseek-official`→`deepseek`、`kimi-coding`→`moonshotai`…)→ **按模型 id 全局唯一匹配**(`glm-5` 在两家都有的价时**拒绝猜**)。**认不出价就说"未定价"**(单价全 0,明细页写明"去同步第三方价目或给这行填个价"),**不再把 DeepSeek 的 flash 价静默套到第三方模型头上** —— 编一个看着合理的假数字,比承认不知道更糟。
496
+ - **「OpenCode 请求头」这一栏(0.25.0 起只给旧宿主)**:OpenCode Go 要求客户端按**每段对话**发 `x-opencode-session`。上游 `@earendil-works/pi-ai` 从 **1.0.2** 起自带 `dist/providers/opencode-headers.js`(`opencode` / `opencode-go` 两个 provider 都被包过),用`options.sessionId` 发**真实会话 id**,且「已有同名头就不覆盖」。本插件注入的是**固定 UUID**,开着反而会顶掉真实 id ⇒ 0.25.0 起宿主半**先探测**(判据是宿主 pi-ai 里有没有那个模块;桌面 asar与 pnpm 检出两种布局都覆盖),探到原生支持就**不注入、不生成 UUID、不记账**,状态行写「宿主已原生支持…本栏不再注入」;探不到按旧宿主处理(继续注入),也可用`DSH_COMPOSER_UX_HEADER_NATIVE=1|0` 强制。`test/host-header-mirror.mjs` 第 19 / 20 节把两条分支与探测判据逐条钉住。
496
497
  - **为什么金额必须自己算**:DSH 送到浏览器的 `tokenUsage` 投影**只有 token 桶**,全库没有一处把"钱"送到客户端;`llm-pi-ai` 里那个 `cost` 只活在 provider 内部,而且用户自定义的路由(profile 里手写的 provider)拿到的是 `NO_COST`(全 0)。所以费用只能由本插件按刊例价算。
497
498
  - **数据全部来自官方、不新增采集**:token 桶读 `tokenUsage` 投影(与官方统计行**同一份**),模型读 `modelSelection` 投影(来自 `request/header` 事件)。会话中途换过模型时按**最后一次请求**的模型计价(近似,浮层里写明)。
498
499
  - **按 route 分列与"逐笔准时"的取数**(点开才去问宿主半;折叠态那颗胶囊也走同一条路由):面板里列出每条 route(provider + model + 高峰/空闲)的 token 与花费,**金额由宿主半按档算好**(用户覆盖价一并读自设置),所以"各行加起来等于总额""三项相加等于合计"都精确成立。逐请求归因只能宿主半做,走只读路由 `GET /composer-ux/usage?sessionId=…`(`src/host.ts` 注册):首次(或发现 `seq` 落后)用官方 `sessionQuery.readSession(sessionId)` 完整读一次,之后靠 `session/event` 订阅**增量**喂折叠缓存(`createUsageCache`,纯逻辑在 [src/usage-fold.ts](src/usage-fold.ts))—— 所以胶囊取价是 O(1),不必反复重读整份日志。**播种窗口**(读日志期间追加的事件)会先缓冲、读完按 `seq` 补上:不这么做的话水位会跳过快照里那些中间事件,金额**永久少算**(`feed()` 的 seq 幂等,补也补不进去)。**事件形状按本版官方 `SessionEventMap` 的真实声明**(`assistant/attempt` 的 `stream` 里取最后一条 usage 块,或 `assistant/message` 的 `data.usage`;**本版没有 `assistant/chunk` 事件**),口径对齐官方 `token-meter` 的 `usage-projection`(同一步替换、`llm/retry-started` 重开替换槽)。`test/usage-fold.mjs`(89 项)逐条钉住差分、归属、重试、脏数据、逐笔分档与增量缓存;分列出不来时面板会显示"读了 N 条事件、M 条 usage、来源哪条路",一眼就能定位。
500
+ - **子代理的钱(0.24.0)**:子代理跑在**自己的**会话里,usage 在自己的日志里 —— 父会话的折叠器与官方 `tokenUsage` 投影都看不到它(用户 2026-10-17 报的「金额胶囊不计算子代理的费用」就是这个)。现在宿主半会把**整棵子代理树**折进来:子会话 id 有两条来源(父会话日志里的 `subagent/catalog` 事件 + 子会话 header 里 `origin: subagent` 的反查,后者覆盖那些父会话日志里没有 catalog 的老会话),递归到深度 4 / 32 个会话;**fork 型子代理的继承前缀要剔掉**(`readSession()` 的 `inheritedEventCount`,否则等于把父会话的账再收一遍);读不到 / 外部执行(claude-code 那类,本机没有会话)/ 超上限 / 单价认不出 四种「没算进来」的原因分开计数,浮层用人话说出来。顶层 `total`/`cost`/`routes` 仍是**本会话自己**(客户端拿它与官方投影对账,一合并就永远对不上),子代理的合计单列在响应的 `subagents` 里、由客户端相加;峰谷两档**只并 token 不并金额**(宿主没按档拆子代理的钱,不编数字)。`test/subagent-cost.mjs`(38 项)把 fork 前缀、递归上限、外部执行、老会话反查、TTL 缓存逐条钉住。
499
501
  - **折叠态那个数字怎么来的**:客户端投影只有累计 token 桶、**没有任何时间信息**,所以"逐笔准时"的数字只能从宿主半拿(`src/client/session-cost.ts`):最短 600ms 间隔、期间有变化排一次**尾随**请求(保证流结束后的数字是准的)、同一时刻只有一个请求在飞、旧响应按序号丢弃。宿主半那份还没到手、或会话日志与投影对不上时,退回本地的"按当前档位估算"并在面板里说明是哪种口径 —— 胶囊从不空着。
500
502
  - **命中率与旁边那行必然是同一个数**:面板只在"分列与投影**四个桶全等**"(`agreesWithProjection` 是精确比较)时才把分列当权威,此时分列算出的命中率与官方胶囊逐位相同;否则退回投影口径。**两个坑**:客户端投影的未缓存输入叫 `uncachedInputTokens`、日志里叫 `inputTokens` —— 第一版在 `billedInputTokens()` 里认对了名字,却在**调用处**传了 `inputTokens`,于是分母丢掉整块未缓存输入、命中率恒 100%(旁边官方胶囊 98.206%)。现在调用处也走官方键名,`test/client-registration.mjs` 同时钉住"调用处键名"与"与输入框下面那一行同一套函数"。
501
503
  - **计价按 `(provider, model)`**(0.10.0 起):DeepSeek 路由(provider 名含 `deepseek` 或模型名以 `deepseek` 开头)走官方价目表与历史档;其它 provider 走同步来的第三方价目,认不出就写"未定价"。所以用 OpenCode(Zen)之类的接口跑 DeepSeek 模型,用量仍按 **DeepSeek 官方价**进这个金额;跑非 DeepSeek 模型则按 models.dev 的价估算。代价如浮层所写:Zen 可能有自己的加价/订阅,这个数字不等于你付给 Zen 的钱。
@@ -720,9 +722,9 @@ node build.mjs # 产出 lib/index.js + lib/client.js
720
722
  # ⚠️ 宿主半会**内联** schemastery / cosmokit:
721
723
  # 优先用 DSH 检出里的 vendor 副本,检出不在时退到 node_modules
722
724
  # 里同版本的 npm 包(两者逐字节相同)——CI 上走的就是退路
723
- npm test # 32 个套件;当前 2726 passed, 0 failed(2026-10-09 实测;CI 三平台同样全绿)
725
+ npm test # 33 个套件;当前 2800 passed, 0 failed(2026-10-18 实测;CI 三平台同样全绿)
724
726
  # 走 scripts/run-tests.mjs:顺带把"多少套件/多少条"记进 test/.last-run.json
725
- npm run test:mutations # 手动跑:变异测试,证明那套护栏真的在咬人(211 条,须单独跑)
727
+ npm run test:mutations # 手动跑:变异测试,证明那套护栏真的在咬人(220 条,须单独跑)
726
728
  # 同样记录结果,供下面的文档门禁核对
727
729
  npm run gates # 发版门禁三条一起跑:tag 指向 / 包内容 / 文档数字
728
730
  npm run check:tag # ① tag 名里的版本 == 该 tag 所指提交里的 package.json 版本
package/dsh-plugin.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifestVersion": "0.15",
4
4
  "id": "dsh-composer-ux.input-experience",
5
5
  "name": "dsh-composer-ux",
6
- "version": "0.23.4",
6
+ "version": "0.25.0",
7
7
  "license": "MIT",
8
8
  "facets": {
9
9
  "host": {
package/lib/client.js CHANGED
@@ -1281,6 +1281,7 @@ var HEADER_ROUTES_FIELD = "headerRoutes";
1281
1281
  var HEADER_APPLIED_NAME_FIELD = "headerAppliedName";
1282
1282
  var HEADER_APPLIED_VALUE_FIELD = "headerAppliedValue";
1283
1283
  var HEADER_STATUS_FIELD = "headerStatus";
1284
+ var NATIVE_STATUS_PREFIX = "\u5BBF\u4E3B\u5DF2\u539F\u751F\u652F\u6301";
1284
1285
  var DEFAULT_HEADER_NAME = "x-opencode-session";
1285
1286
  var HEADER_VALUE_MAX = 200;
1286
1287
  var HEADER_NAME_MAX = 64;
@@ -6350,10 +6351,11 @@ function SettingsSection({ useLive, useBook, useBookStatus, useWriteNotice, acti
6350
6351
  const menuSummary = settings.menuMode === "official" ? "\u5F53\u524D\uFF1A\u5B98\u65B9\u4E0D\u4ECB\u5165" : settings.menuMode === "browser" ? "\u5F53\u524D\uFF1A\u6D4F\u89C8\u5668\u83DC\u5355\uFF08\u7C98\u8D34\u514D\u6388\u6743\uFF09" : `\u5F53\u524D\uFF1A\u81EA\u5B9A\u4E49\u83DC\u5355 \xB7 ${menuEnabled} / ${MENU_ITEMS.length} \u9879\u5F00\u542F`;
6351
6352
  const panelSize = typeof settings.panelWidth === "number" && typeof settings.panelHeight === "number" ? `${settings.panelWidth}\xD7${settings.panelHeight}` : "\u5C3A\u5BF8\u7528\u5B98\u65B9\u9ED8\u8BA4";
6352
6353
  const panelSummary = `\u8FB9\u7F18\u7F29\u653E ${settings.panelResize ? "\u5F00" : "\u5173"} \xB7 ${panelSize}`;
6353
- const headerSummary = settings.headerEnabled ? settings.headerStatus === "" ? "\u5DF2\u542F\u7528" : settings.headerStatus : "\u672A\u542F\u7528";
6354
+ const headerNative = settings.headerStatus.startsWith(NATIVE_STATUS_PREFIX);
6355
+ const headerSummary = headerNative ? "\u5BBF\u4E3B\u5DF2\u539F\u751F\u652F\u6301\uFF08\u4E0D\u9700\u8981\uFF09" : settings.headerEnabled ? settings.headerStatus === "" ? "\u5DF2\u542F\u7528" : settings.headerStatus : "\u672A\u542F\u7528";
6354
6356
  const quickCounts = bookCounts(book);
6355
6357
  const quickSummary = `${quickCounts.categories} \u4E2A\u5206\u7C7B \xB7 ${quickCounts.prompts} \u6761 \xB7 \u6BCF\u6B21 ${quickCounts.always} \xB7 \u4EC5\u9996\u6B21 ${quickCounts.first} \xB7 \u4F18\u5316\u6863\u4F4D ${OPTIMIZER_TIERS.find((item) => item.id === settings.optimizerTier)?.label ?? "\u9AD8\u7EA7"}`;
6356
- const headerStatusText = settings.headerEnabled ? settings.headerStatus === "" ? "\u7B49\u5F85\u9996\u6B21\u5199\u5165\u2026" : settings.headerStatus : "\u672A\u542F\u7528\uFF08\u6253\u5F00\u4E0A\u65B9\u5F00\u5173\u5373\u5199\u5165\uFF09";
6358
+ const headerStatusText = headerNative ? settings.headerStatus : settings.headerEnabled ? settings.headerStatus === "" ? "\u7B49\u5F85\u9996\u6B21\u5199\u5165\u2026" : settings.headerStatus : "\u672A\u542F\u7528\uFF08\u6253\u5F00\u4E0A\u65B9\u5F00\u5173\u5373\u5199\u5165\uFF09";
6357
6359
  const terminalSummary = sections.terminal === false ? settings.terminalMode === "pwsh" ? "\u4FDD\u6301 PowerShell" : "\u6253\u5F00\u540E\u63A5\u7BA1\u7EC8\u7AEF" : settings.terminalEffective === "unsupported" ? "\u975E Windows\uFF1A\u4E0D\u9700\u8981" : settings.terminalMode === "pwsh" ? "\u5F53\u524D\uFF1A\u4FDD\u6301 PowerShell" : settings.terminalEffective === "bash" ? "\u5F53\u524D\uFF1AGit Bash" : "\u5F53\u524D\uFF1APowerShell\uFF08\u672A\u63A5\u7BA1\uFF09";
6358
6360
  const statsSummary = `${HIT_DIGITS} \u4F4D\u5C0F\u6570`;
6359
6361
  const costSummary = (() => {
@@ -6781,8 +6783,13 @@ function SettingsSection({ useLive, useBook, useBookStatus, useWriteNotice, acti
6781
6783
  }
6782
6784
  },
6783
6785
  children: [
6786
+ headerNative ? /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("p", { style: hintInfo, children: [
6787
+ "\u672C\u5BBF\u4E3B\u5DF2\u539F\u751F\u652F\u6301\uFF1Api-ai \u4F1A\u6309\u6BCF\u6BB5\u5BF9\u8BDD\u7684\u771F\u5B9E session id \u53D1\u9001 ",
6788
+ DEFAULT_HEADER_NAME,
6789
+ "\uFF08\u5B83\u540C\u65F6\u4E5F\u662F OpenCode \u7528\u6765\u4F18\u5316\u8DEF\u7531\u4E0E\u63D0\u793A\u8BCD\u7F13\u5B58\u7684\u90A3\u4E00\u7EF4\uFF09\u3002\u6240\u4EE5\u8FD9\u4E00\u680F\u73B0\u5728\u4EC0\u4E48\u90FD\u4E0D\u505A\u2014\u2014 \u6253\u5F00\u5F00\u5173\u4E5F\u4E0D\u4F1A\u6CE8\u5165\uFF08\u6CE8\u5165\u7684\u662F\u56FA\u5B9A\u503C\uFF0C\u53CD\u800C\u4F1A\u9876\u6389\u771F\u5B9E id\uFF09\u3002\u53EA\u6709\u65E7\u5BBF\u4E3B\uFF08pi-ai 1.0.2 \u4E4B\u524D\u3001 \u6CA1\u6709 opencode-headers \u6A21\u5757\uFF09\u624D\u9700\u8981\u6253\u5F00\u5B83\u3002"
6790
+ ] }) : null,
6784
6791
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: hintError, children: "\u26A0\uFE0F \u8BF7\u4F7F\u7528\u81EA\u5B9A\u4E49\u7684 OpenCode API \u5730\u5740\uFF0C\u4E0D\u8981\u7528 DSH \u81EA\u5E26\u7684 API \u5730\u5740\u914D\u7F6E\uFF1A \u7528\u81EA\u5E26\u7684\u914D\u7F6E\u65E0\u6CD5\u4E0A\u4F20\u56FE\u7247\uFF0C\u4E00\u65E6\u53D1\u9001\u56FE\u7247\u5C31\u4F1A\u62A5\u9519\u3002" }),
6785
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: bodyLead, children: "OpenCode \u7684\u63A5\u53E3\u8981\u6C42\u5BA2\u6237\u7AEF\u6BCF\u6B21\u8BF7\u6C42\u90FD\u5E26\u4E0A\u4E00\u4E2A\u7A33\u5B9A\u7684\u4F1A\u8BDD ID \u8BF7\u6C42\u5934\uFF08\u5B98\u65B9 Go \u6587\u6863 \u300C\u53EF\u4EE5\u5728\u54EA\u91CC\u4F7F\u7528\uFF1F\u300D\u7B2C 3 \u6761\uFF1A\u4E3A\u6BCF\u6BB5\u5BF9\u8BDD\u5728 x-opencode-session \u4E2D\u53D1\u9001\u4F1A\u8BDD ID\uFF0C \u4EE5\u4FBF\u5176\u4F18\u5316\u8DEF\u7531\u4E0E\u63D0\u793A\u8BCD\u7F13\u5B58\uFF09\u3002DSH \u7684\u8BBE\u7F6E\u9875\u4E0D\u63D0\u4F9B\u8BF7\u6C42\u5934\u7F16\u8F91\u5668\uFF0C\u6240\u4EE5\u8FD9\u91CC\u76F4\u63A5\u628A\u5B83\u5199\u8FDB llm-pi-ai \u7684 provider \u914D\u7F6E\u2014\u2014\u4E0B\u4E00\u6B21\u6A21\u578B\u8BF7\u6C42\u5C31\u751F\u6548\uFF0C\u4E0D\u7528\u91CD\u542F\uFF0C\u4E5F\u4E0D\u7528\u624B\u5DE5\u6539 settings.yaml\u3002 \u53EA\u5BF9 llm-pi-ai \u91CC\u300C\u5DF2\u5B58\u5728\u300D\u7684 opencode \u7CFB\u8DEF\u7531\u751F\u6548\uFF0C\u4E0D\u4F1A\u51ED\u7A7A\u65B0\u5EFA provider\uFF1B \u603B\u5F00\u5173\u5173\u95ED\u6216\u672C\u680F\u76EE\u505C\u7528\u65F6\uFF0C\u5199\u5165\u7684\u5934\u4F1A\u81EA\u52A8\u64A4\u9500\u3002" }),
6792
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("p", { style: bodyLead, children: "OpenCode \u7684\u63A5\u53E3\u8981\u6C42\u5BA2\u6237\u7AEF\u6BCF\u6B21\u8BF7\u6C42\u90FD\u5E26\u4E0A\u4E00\u4E2A\u7A33\u5B9A\u7684\u4F1A\u8BDD ID \u8BF7\u6C42\u5934\uFF08\u5B98\u65B9 Go \u6587\u6863 \u300C\u53EF\u4EE5\u5728\u54EA\u91CC\u4F7F\u7528\uFF1F\u300D\u7B2C 3 \u6761\uFF1A\u4E3A\u6BCF\u6BB5\u5BF9\u8BDD\u5728 x-opencode-session \u4E2D\u53D1\u9001\u4F1A\u8BDD ID\uFF0C \u4EE5\u4FBF\u5176\u4F18\u5316\u8DEF\u7531\u4E0E\u63D0\u793A\u8BCD\u7F13\u5B58\uFF09\u3002\u65E7\u7248 DSH \u4E0D\u53D1\u8FD9\u4E2A\u5934\uFF0C\u6240\u4EE5\u8FD9\u4E00\u680F\u628A\u5B83\u8865\u8FDB llm-pi-ai \u7684 provider \u914D\u7F6E\u2014\u2014\u4E0B\u4E00\u6B21\u6A21\u578B\u8BF7\u6C42\u5C31\u751F\u6548\uFF0C\u4E0D\u7528\u91CD\u542F\uFF0C\u4E5F\u4E0D\u7528\u624B\u5DE5\u6539 settings.yaml\u3002 \u53EA\u5BF9 llm-pi-ai \u91CC\u300C\u5DF2\u5B58\u5728\u300D\u7684 opencode \u7CFB\u8DEF\u7531\u751F\u6548\uFF0C\u4E0D\u4F1A\u51ED\u7A7A\u65B0\u5EFA provider\uFF1B \u603B\u5F00\u5173\u5173\u95ED\u6216\u672C\u680F\u76EE\u505C\u7528\u65F6\uFF0C\u8865\u7684\u5934\u4F1A\u81EA\u52A8\u64A4\u9500\u3002" }),
6786
6793
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
6787
6794
  TextFieldRow,
6788
6795
  {
@@ -6800,7 +6807,7 @@ function SettingsSection({ useLive, useBook, useBookStatus, useWriteNotice, acti
6800
6807
  TextFieldRow,
6801
6808
  {
6802
6809
  title: "\u503C",
6803
- desc: "\u6240\u6709\u5BF9\u8BDD\u5171\u7528\u8FD9\u4E00\u4E2A\u503C\uFF1B\u7559\u7A7A\u65F6\u9996\u6B21\u542F\u7528\u4F1A\u81EA\u52A8\u751F\u6210\u4E00\u4E2A UUID \u5E76\u4FDD\u5B58\u6CBF\u7528",
6810
+ desc: "\u65E7\u5BBF\u4E3B\u4E13\u7528\uFF1A\u6240\u6709\u5BF9\u8BDD\u5171\u7528\u8FD9\u4E00\u4E2A\u503C\uFF1B\u7559\u7A7A\u65F6\u9996\u6B21\u542F\u7528\u4F1A\u81EA\u52A8\u751F\u6210\u4E00\u4E2A UUID\uFF08\u5BBF\u4E3B\u5DF2\u539F\u751F\u652F\u6301\u65F6\u4E0D\u4F1A\u7528\u5230\u5B83\uFF09",
6804
6811
  value: settings.headerValue,
6805
6812
  placeholder: "\u7559\u7A7A = \u81EA\u52A8\u751F\u6210",
6806
6813
  maxLength: HEADER_VALUE_MAX,
@@ -7275,13 +7282,45 @@ function CostChipEntry({ useLive, sessionId, useProjection }) {
7275
7282
  { miss: 0, hit: 0, out: 0 }
7276
7283
  );
7277
7284
  const useBreakdown = routeRows.length > 0 && foldAgrees && foldTotal !== null;
7285
+ const subRaw = useBreakdown ? data?.subagents : void 0;
7286
+ const subSessions = num(subRaw?.sessions);
7287
+ const sub = subSessions > 0 ? subRaw : void 0;
7288
+ const subMissing = num(sub?.missing);
7289
+ const subExternal = num(sub?.external);
7290
+ const subSkipped = num(sub?.skipped);
7291
+ const subUnpriced = num(sub?.unpriced);
7292
+ const subCost = num(sub?.cost?.total);
7293
+ const subTokens = tokenTotal(sub?.total);
7294
+ const subParts = {
7295
+ miss: num(sub?.total?.inputTokens),
7296
+ hit: num(sub?.total?.cacheReadTokens),
7297
+ write: num(sub?.total?.cacheWriteTokens),
7298
+ out: num(sub?.total?.outputTokens)
7299
+ };
7300
+ const peakTierTokens = peakTier.tokens + (peakTier.tokens > 0 ? tokenTotal(sub?.tiers?.peak) : 0);
7301
+ const offTierTokens = offTier.tokens + (offTier.tokens > 0 ? tokenTotal(sub?.tiers?.offPeak) : 0);
7302
+ const subTierMerged = peakTierTokens > peakTier.tokens || offTierTokens > offTier.tokens;
7303
+ const subGapParts = [];
7304
+ if (subMissing > 0) subGapParts.push(`${subMissing} \u4E2A\u8BFB\u4E0D\u5230\u4F1A\u8BDD\u65E5\u5FD7\uFF08\u5DF2\u5F52\u6863\uFF0C\u6216\u672C\u673A\u6CA1\u6709\u5B83\u7684\u65E5\u5FD7\uFF09`);
7305
+ if (subExternal > 0) subGapParts.push(`${subExternal} \u4E2A\u662F\u5916\u90E8\u6267\u884C\uFF08\u672C\u673A\u6CA1\u6709\u8FD9\u4E2A\u4F1A\u8BDD\uFF0C\u7528\u91CF\u62FF\u4E0D\u5230\uFF09`);
7306
+ if (subSkipped > 0) subGapParts.push(`${subSkipped} \u4E2A\u8D85\u51FA\u5355\u6B21\u8BFB\u53D6\u4E0A\u9650`);
7307
+ if (subUnpriced > 0) subGapParts.push(`${subUnpriced} \u884C\u6CA1\u8BA4\u51FA\u4EF7\uFF0C\u6309 0 \u8BA1`);
7308
+ const subUnaccounted = subMissing + subExternal + subSkipped;
7309
+ const subGapText = subGapParts.length === 0 ? null : subUnaccounted > 0 ? `\u53E6\u6709 ${subUnaccounted} \u4E2A\u5B50\u4EE3\u7406\u7684\u8D39\u7528\u6CA1\u7B97\u8FDB\u6765\uFF1A${subGapParts.join("\u3001")}` : `\u5B50\u4EE3\u7406\u91CC\u6709\u8D39\u7528\u6CA1\u7B97\u8FDB\u6765\uFF1A${subGapParts.join("\u3001")}`;
7278
7310
  const view = useBreakdown && foldTotal !== null ? {
7279
- miss: foldTotal.inputTokens,
7280
- hit: foldTotal.cacheReadTokens,
7281
- write: foldTotal.cacheWriteTokens,
7282
- out: foldTotal.outputTokens,
7283
- parts: { miss: num(data?.cost?.miss), hit: num(data?.cost?.hit), out: num(data?.cost?.out) },
7284
- total: num(data?.cost?.total)
7311
+ // 子代理的四桶并进展示桶(`sub` 为 undefined 时每一项都是 0,即原样):
7312
+ // 这样"分项之和 = 合计 = 全额"与命中率都是同一份量,见文件头 0.24.0 第三条。
7313
+ miss: foldTotal.inputTokens + subParts.miss,
7314
+ hit: foldTotal.cacheReadTokens + subParts.hit,
7315
+ write: foldTotal.cacheWriteTokens + subParts.write,
7316
+ out: foldTotal.outputTokens + subParts.out,
7317
+ // ⚠️ 三个分项**也要**并钱:只并桶不并钱,"未缓存输入 + 缓存命中 + 输出 = 合计"当场破。
7318
+ parts: {
7319
+ miss: num(data?.cost?.miss) + num(sub?.cost?.miss),
7320
+ hit: num(data?.cost?.hit) + num(sub?.cost?.hit),
7321
+ out: num(data?.cost?.out) + num(sub?.cost?.out)
7322
+ },
7323
+ total: num(data?.cost?.total) + subCost
7285
7324
  } : {
7286
7325
  miss: buckets.miss,
7287
7326
  hit: buckets.hit,
@@ -7310,7 +7349,7 @@ function CostChipEntry({ useLive, sessionId, useProjection }) {
7310
7349
  type: "button",
7311
7350
  "data-composer-ux-cost": "",
7312
7351
  "aria-expanded": open,
7313
- title: (useBreakdown ? "\u672C\u4F1A\u8BDD\u8D39\u7528\uFF08\u6309\u6BCF\u7B14\u7528\u91CF\u53D1\u751F\u7684\u65F6\u95F4\u8BA1\u4EF7\uFF09" : "\u672C\u4F1A\u8BDD\u8D39\u7528\uFF08\u4F30\u7B97\uFF09") + ` \xB7 ${peakText}`,
7352
+ title: (useBreakdown ? sub !== void 0 ? "\u672C\u4F1A\u8BDD\u8D39\u7528\uFF08\u542B\u5B50\u4EE3\u7406\uFF0C\u6309\u6BCF\u7B14\u7528\u91CF\u53D1\u751F\u7684\u65F6\u95F4\u8BA1\u4EF7\uFF09" : "\u672C\u4F1A\u8BDD\u8D39\u7528\uFF08\u6309\u6BCF\u7B14\u7528\u91CF\u53D1\u751F\u7684\u65F6\u95F4\u8BA1\u4EF7\uFF09" : "\u672C\u4F1A\u8BDD\u8D39\u7528\uFF08\u4F30\u7B97\uFF09") + ` \xB7 ${peakText}`,
7314
7353
  onClick: toggle,
7315
7354
  onMouseEnter: () => setHover(true),
7316
7355
  onMouseLeave: () => setHover(false),
@@ -7396,15 +7435,22 @@ function CostChipEntry({ useLive, sessionId, useProjection }) {
7396
7435
  `${formatTokens(route.tokens)} \xB7 ${route.unpriced ? "\u672A\u5B9A\u4EF7" : formatMoney(route.cost)}`,
7397
7436
  `${route.provider}|${route.model}|${String(route.peak)}|${route.era}`
7398
7437
  )),
7438
+ sub !== void 0 ? detail(
7439
+ `\u5B50\u4EE3\u7406\uFF08${subSessions} \u4E2A\u4F1A\u8BDD\uFF09`,
7440
+ `${formatTokens(subTokens)} \xB7 ${subUnpriced > 0 ? "\u672A\u5B9A\u4EF7" : formatMoney(subCost)}`,
7441
+ "__subagents__"
7442
+ ) : null,
7399
7443
  detail("\u5408\u8BA1", `${formatTokens(view.miss + view.hit + view.write + view.out)} \xB7 ${formatMoney(view.total)}`, "__total__"),
7400
- peakTier.tokens > 0 ? detail("\u9AD8\u5CF0\u6863", `${formatTokens(peakTier.tokens)} \xB7 ${formatMoney(peakTier.cost)}`, "__peak__") : null,
7401
- offTier.tokens > 0 ? detail("\u7A7A\u95F2\u6863", `${formatTokens(offTier.tokens)} \xB7 ${formatMoney(offTier.cost)}`, "__off__") : null,
7444
+ peakTier.tokens > 0 ? detail("\u9AD8\u5CF0\u6863", `${formatTokens(peakTierTokens)} \xB7 ${formatMoney(peakTier.cost)}`, "__peak__") : null,
7445
+ offTier.tokens > 0 ? detail("\u7A7A\u95F2\u6863", `${formatTokens(offTierTokens)} \xB7 ${formatMoney(offTier.cost)}`, "__off__") : null,
7402
7446
  otherTier.tokens > 0 ? detail("\u5176\u4ED6\u8DEF\u7531\uFF08\u65E0\u5CF0\u8C37\uFF09", `${formatTokens(otherTier.tokens)} \xB7 ${formatMoney(otherTier.cost)}`, "__other__") : null,
7447
+ subTierMerged ? note("\u9AD8\u5CF0 / \u7A7A\u95F2\u4E24\u6863\u7684 token \u5DF2\u542B\u5B50\u4EE3\u7406\uFF0C\u91D1\u989D\u53EA\u542B\u672C\u4F1A\u8BDD\uFF08\u5BBF\u4E3B\u534A\u6CA1\u628A\u5B50\u4EE3\u7406\u7684\u94B1\u6309\u5CF0\u8C37\u62C6\u5F00\uFF0C\u6240\u4EE5\u4E0D\u62FF token \u4E58\u5355\u4EF7\u7F16\u4E00\u4E2A\u6309\u6863\u91D1\u989D\u51FA\u6765\uFF09\u3002", "__sub_tier__") : null,
7403
7448
  detail("\u672A\u7F13\u5B58\u8F93\u5165", `${formatTokens(view.miss)} \xB7 ${formatMoney(view.parts.miss)}`, "__miss__"),
7404
7449
  detail("\u7F13\u5B58\u547D\u4E2D", `${formatTokens(view.hit)} \xB7 ${formatMoney(view.parts.hit)}`, "__hit__"),
7405
7450
  detail("\u8F93\u51FA", `${formatTokens(view.out)} \xB7 ${formatMoney(view.parts.out)}`, "__out__"),
7406
7451
  builtinCount > 0 ? note(`\u6709 ${builtinCount} \u884C\u7528\u7684\u662F\u5185\u7F6E\u5FEB\u7167\u4EF7\uFF08models.dev \u5FEB\u7167 ${PROVIDER_PRICES_SNAPSHOT_AT}\uFF09\u2014\u2014\u60F3\u6362\u6210\u6700\u65B0\u4EF7\uFF0C\u53BB\u8BBE\u7F6E\u9875\u300C\u91D1\u989D\u300D\u70B9\u4E00\u6B21\u300C\u540C\u6B65\u7B2C\u4E09\u65B9\u4EF7\u76EE\u300D\u3002`, "__builtin__") : null,
7407
- unpricedCount > 0 ? note(`\u6709 ${unpricedCount} \u884C\u300C\u672A\u5B9A\u4EF7\u300D\uFF1A\u90A3\u662F\u975E DeepSeek \u6A21\u578B\uFF0C\u540C\u6B65\u4EF7\u76EE\u4E0E\u5185\u7F6E\u5FEB\u7167\u91CC\u90FD\u6CA1\u6709\u5B83\uFF0C\u6240\u4EE5\u90A3\u90E8\u5206\u6309 0 \u8BA1\u3002\u53BB\u8BBE\u7F6E\u9875\u300C\u91D1\u989D\u300D\u5237\u65B0\u4E00\u6B21\u300C\u540C\u6B65\u7B2C\u4E09\u65B9\u4EF7\u76EE\u300D\uFF0C\u6216\u7ED9\u90A3\u884C\u76F4\u63A5\u586B\u4E2A\u4EF7\u3002`, "__unpriced__") : null
7452
+ unpricedCount > 0 ? note(`\u6709 ${unpricedCount} \u884C\u300C\u672A\u5B9A\u4EF7\u300D\uFF1A\u90A3\u662F\u975E DeepSeek \u6A21\u578B\uFF0C\u540C\u6B65\u4EF7\u76EE\u4E0E\u5185\u7F6E\u5FEB\u7167\u91CC\u90FD\u6CA1\u6709\u5B83\uFF0C\u6240\u4EE5\u90A3\u90E8\u5206\u6309 0 \u8BA1\u3002\u53BB\u8BBE\u7F6E\u9875\u300C\u91D1\u989D\u300D\u5237\u65B0\u4E00\u6B21\u300C\u540C\u6B65\u7B2C\u4E09\u65B9\u4EF7\u76EE\u300D\uFF0C\u6216\u7ED9\u90A3\u884C\u76F4\u63A5\u586B\u4E2A\u4EF7\u3002`, "__unpriced__") : null,
7453
+ subGapText !== null ? note(subGapText, "__sub_gap__") : null
7408
7454
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
7409
7455
  detail("\u672A\u7F13\u5B58\u8F93\u5165", `${formatTokens(buckets.miss)} \xB7 ${formatMoney(estimateParts.miss)}`),
7410
7456
  detail("\u7F13\u5B58\u547D\u4E2D", `${formatTokens(buckets.hit)} \xB7 ${formatMoney(estimateParts.hit)}`),