@gamegeek-saikel/dsh-cost-meter 0.3.0-alpha.2 → 0.3.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/README.md CHANGED
@@ -47,7 +47,8 @@ Chat costs in DeepSeek pricing change over time (list prices, USD→CNY exchange
47
47
  | Peak pricing | 2026-08-17 00:00 Beijing rollout; peak windows apply Monday–Friday only (the zh and en pages may state different timezones; fallback 09:00–12:00 / 14:00–18:00 Beijing), so Saturdays, Sundays, and all other hours are off-peak at half price |
48
48
  | Cost formula | Uncached input + cache reads (hit rate) + cache writes (billed at uncached input rate) + output, per 1M tokens, CNY |
49
49
  | Account balance | Official `GET /user/balance`, cached 60 s, single in-flight request, trust-fenced route |
50
- | Subagent support | BFS over the live agent tree; conversation totals = main session + descendants |
50
+ | Subagent support | Enumerates the conversation's durable subagent tree through `subagents.listDescendants` (nested delegations, settled children, and cold subagent sessions included, with no depth cap); falls back to a BFS over the live agent tree when that service is not mounted |
51
+ | Conversation total | Main session + every descendant subagent (any depth); subagent totals still render while the main session's own ledger is not materialized |
51
52
  | UI surfaces | Composer dock · Cost tab · per-reply chip · header pill (live estimate) · settings card |
52
53
  | Locale | Simplified Chinese (source) + English |
53
54
  | Complexity | Fully synchronous fold; O(1) price lookups via in-memory mirror |
@@ -59,7 +60,7 @@ Once installed, the plugin contributes five browser surfaces (all text shown in
59
60
  | Surface | Slot | Description |
60
61
  |---|---|---|
61
62
  | Composer dock readout | `conversation.composer.dock` | Anchored session spend + account balance, refreshed every minute; hover for the category breakdown and snapshot info |
62
- | Cost view tab | `conversation.view` | Whole-conversation totals (main + subagents), category totals, per-subagent list, and the per-reply anchored ledger |
63
+ | Cost view tab | `conversation.view` | Whole-conversation totals (main + every subagent nesting level), category totals, the per-subagent list with its depth, and the per-reply anchored ledger |
63
64
  | Per-reply cost chip | `conversation.chat.assistant-actions` | The anchored cost of one finalized reply as the row's own stat capsule — the same trigger + anchored dialog the shipped 用量 and 用时 capsules use (28px pill, hover fill, `aria-expanded` dialog above the trigger). It closes the row's stat run: after 用量 and 用时, before the timestamp, spaced by the row's own gap, and coloured by the band that priced it (green off-peak, red peak). The dialog lists the three billed categories, the band with its multiplier, the model, and the snapshot version; the slot hands over the message id, which the `sessionCostIndex` projection resolves to ledger coordinates (dash `—` when unpriced) |
64
65
  | Header pill | `conversation.session.header.utilities` | Anchored total, or a live `≈ ¥x.xx (estimate)` while streaming; click for the detail panel |
65
66
  | Plugin card | `settings.plugin.item` | Per-model overrides, OpenRouter aliases, cache-read discount, FX mode, toggles, and manual refresh |
@@ -85,7 +86,7 @@ src/
85
86
  pricebook.ts # Append-only snapshots, priority chain, storage domain
86
87
  session-cost-projection.ts # sessionCost projection (immutable per-step ledger)
87
88
  session-cost-index.ts # sessionCostIndex: message id → ledger coordinates
88
- subagent-cost.ts # BFS subagent cost aggregation
89
+ subagent-cost.ts # Subagent cost aggregation (durable tree + live-registry fallback)
89
90
  invariant.ts # Route-disposer symmetry invariant companion
90
91
  client/ # Browser half: 5 slot components + math/format/locales
91
92
  shared/
@@ -104,7 +105,7 @@ pnpm test # vitest run (hermetic, network stubbed)
104
105
  pnpm build # tsc -b && tsdown (lib/ + lib/client.js)
105
106
  ```
106
107
 
107
- The test suites are fully offline: pricing-page HTML, OpenRouter models, and the FX endpoint are all stubbed. Tests cover the trust fence, balance parsing, the pricebook priority chain and snapshot selection, the immutable ledger fold (including same-step replacement and peak/off-peak band selection at the *event* time), subagent BFS aggregation, and the client surfaces (jsdom).
108
+ The test suites are fully offline: pricing-page HTML, OpenRouter models, and the FX endpoint are all stubbed. Tests cover the trust fence, balance parsing, the pricebook priority chain and snapshot selection, the immutable ledger fold (including same-step replacement and peak/off-peak band selection at the *event* time), the subagent aggregation (nested and cold children, listing-failure fallback, and a multi-level chain over a real `SessionStore` plus projection registry), and the client surfaces (jsdom).
108
109
 
109
110
  ## Documentation
110
111
 
package/README.zh-CN.md CHANGED
@@ -47,7 +47,8 @@ DeepSeek 的价格随时间变化(官方价目表、USD→CNY 汇率、2026-08
47
47
  | 峰值定价 | 2026-08-17 00:00 北京生效;高峰窗口仅周一至周五(中英文页面时区可能不同,缺省 09:00–12:00 / 14:00–18:00 北京),因此周六、周日全天及其余时段均为半价闲时 |
48
48
  | 成本公式 | 未命中输入 + 缓存命中(命中价)+ 缓存写入(按未命中输入价计)+ 输出,每百万 tokens,CNY |
49
49
  | 账户余额 | 官方 `GET /user/balance`,缓存 60 秒,单飞请求,路由带信任围栏 |
50
- | 子代理支持 | 沿活跃代理树 BFS;对话总花费 = 主会话 + 全部后代 |
50
+ | 子代理支持 | 沿 `subagents.listDescendants` 枚举本会话持久子代理树(子代理再派生的孙代理、已结束或已冷启动的子会话都计入,无层数上限);未挂载该服务时回退到活跃代理树 BFS |
51
+ | 对话总花费 | 主会话 + 全部后代子代理(任意层数);主会话自身账本尚未就绪时仍展示子代理合计 |
51
52
  | UI 表面 | 输入框读数 · 花费标签页 · 回复小标签 · 头部胶囊(实时估算)· 设置卡 |
52
53
  | 本地化 | 简体中文(键源)+ 英文 |
53
54
  | 复杂度 | 全同步折叠;经内存镜像 O(1) 查价 |
@@ -59,7 +60,7 @@ DeepSeek 的价格随时间变化(官方价目表、USD→CNY 汇率、2026-08
59
60
  | 表面 | 插槽 | 说明 |
60
61
  |---|---|---|
61
62
  | 输入框下方读数 | `conversation.composer.dock` | 锚定的本会话花费 + 账户余额,每分钟刷新;悬停查看分类明细与快照信息 |
62
- | 花费标签页 | `conversation.view` | 全对话总花费(主会话 + 子代理)、分类小计、子代理列表与逐回复锚定账本 |
63
+ | 花费标签页 | `conversation.view` | 全对话总花费(主会话 + 任意层级的子代理)、分类小计、带层级的子代理列表与逐回复锚定账本 |
63
64
  | 每条回复成本小标签 | `conversation.chat.assistant-actions` | 单条已定稿回复的锚定成本,做成与官方「用量」「用时」完全同形的统计胶囊(28px 药丸、悬停底色、`aria-expanded` 展开锚定在触发件上方的对话框);位置收在统计串末:在「用量」「用时」之后、末尾时刻文本之前,间距沿用行自身的 gap;按计价档位着色(闲时绿、高峰红);对话框列出三类计费、档位与倍率、模型与快照版本;插槽只给出消息 id,由 `sessionCostIndex` 投影解析为账本坐标(无价格时显示 `—`) |
64
65
  | 头部胶囊 | `conversation.session.header.utilities` | 锚定总花费;流式中显示 `预计 ¥x.xx(估算)`;点击展开详情面板 |
65
66
  | 插件配置卡 | `settings.plugin.item` | 按模型覆盖价、OpenRouter 别名、缓存折扣、汇率模式、开关与立即刷新 |
@@ -85,7 +86,7 @@ src/
85
86
  pricebook.ts # 只追加快照、优先级链、存储域
86
87
  session-cost-projection.ts # sessionCost 投影(不可变逐步账本)
87
88
  session-cost-index.ts # sessionCostIndex:消息 id → 账本坐标
88
- subagent-cost.ts # BFS 子代理成本聚合
89
+ subagent-cost.ts # 子代理成本聚合(持久子代理树 + 活跃代理树回退)
89
90
  invariant.ts # 路由释放对称性 invariant 伴生插件
90
91
  client/ # 浏览器半区:5 个插槽组件 + 数学/格式化/本地化
91
92
  shared/
@@ -104,7 +105,7 @@ pnpm test # vitest run(封闭式,网络 stub)
104
105
  pnpm build # tsc -b && tsdown(lib/ + lib/client.js)
105
106
  ```
106
107
 
107
- 测试套件完全离线:价格页 HTML、OpenRouter 模型目录与汇率接口全部 stub。覆盖:信任围栏、余额解析、价格簿优先级链与快照选取、不可变账本折叠(含同一步替换与按*事件时间*选峰值/闲时档)、子代理 BFS 聚合,以及客户端表面(jsdom)。
108
+ 测试套件完全离线:价格页 HTML、OpenRouter 模型目录与汇率接口全部 stub。覆盖:信任围栏、余额解析、价格簿优先级链与快照选取、不可变账本折叠(含同一步替换与按*事件时间*选峰值/闲时档)、子代理聚合(持久子代理树的嵌套/冷会话/失败回退,以及真实 `SessionStore` + 投影注册表上的多层链路),以及客户端表面(jsdom)。
108
109
 
109
110
  ## 文档
110
111
 
package/lib/client.js CHANGED
@@ -37,6 +37,7 @@ window.__ModuleLoader__.load({
37
37
  "line.cacheRead": "缓存输入 {amount}",
38
38
  "line.output": "输出 {amount}",
39
39
  "line.subagent": "子代理 {amount}",
40
+ "subagent.depth": "第 {depth} 层",
40
41
  "pill.label": "花费 {amount}",
41
42
  "pill.estimate": "预计 {amount}",
42
43
  "pill.estimateNote": "估算",
@@ -111,6 +112,7 @@ window.__ModuleLoader__.load({
111
112
  "line.cacheRead": "Cached input {amount}",
112
113
  "line.output": "Output {amount}",
113
114
  "line.subagent": "Subagents {amount}",
115
+ "subagent.depth": "level {depth}",
114
116
  "pill.label": "Cost {amount}",
115
117
  "pill.estimate": "≈ {amount}",
116
118
  "pill.estimateNote": "estimate",
@@ -246,23 +248,46 @@ window.__ModuleLoader__.load({
246
248
  }
247
249
  return sum;
248
250
  }
249
- /** Total spend of every subagent (CNY); 0 without subagents. */
250
- function subagentSpend(subagents) {
251
- if (subagents === void 0 || subagents.length === 0) return 0;
252
- return sumTotals(subagents.map((entry) => entry.totals)).cost;
251
+ /**
252
+ * Select the conversation's main ledger, the host-aggregated subagent rows,
253
+ * and their combined totals together, so no surface has to remember which
254
+ * pointer to compare against which total.
255
+ *
256
+ * All three are derived consistently: `combined` sums the main ledger with
257
+ * EVERY subagent row the host served — including nested ones — and is defined
258
+ * whenever either side has data. A conversation whose own ledger is not
259
+ * materialized yet (a session switch mid-flight, or a host that serves no main
260
+ * ledger) therefore still totals its subagents instead of rendering nothing.
261
+ *
262
+ * `main` is `null` when the main ledger is absent, in which case `mainSpend`
263
+ * is unknown (the dock shows 0 for a session with no spend of its own).
264
+ * @param main - the main session's projection totals, or undefined.
265
+ * @param subagents - the host-aggregated subagent rows.
266
+ * @returns the main readout, the subagent rows, combined totals, and both spend figures.
267
+ */
268
+ function costLookupFor(main, subagents) {
269
+ const rows = subagents ?? [];
270
+ return {
271
+ main,
272
+ subagents: rows,
273
+ combined: combineTotals(main, rows),
274
+ mainSpend: main?.cost ?? 0,
275
+ subagentTotal: sumTotals(rows.map((entry) => entry.totals)).cost
276
+ };
253
277
  }
254
278
  /**
255
279
  * The conversation-wide totals: the main session's ledger plus every
256
- * subagent's ledger (host-aggregated). Undefined while the main ledger is
257
- * absent; identical to the main totals without subagents.
280
+ * subagent's ledger (host-aggregated, all nesting levels). Undefined only when
281
+ * neither side carries a ledger.
258
282
  * @param main - the main session's projection totals.
259
283
  * @param subagents - the host-aggregated subagent totals.
260
- * @returns the combined totals, or undefined.
284
+ * @returns the combined totals, or undefined without any ledger.
261
285
  */
262
286
  function combineTotals(main, subagents) {
263
- if (main === void 0) return void 0;
264
- if (subagents === void 0 || subagents.length === 0) return main;
265
- return sumTotals([main, ...subagents.map((entry) => entry.totals)]);
287
+ const rows = subagents ?? [];
288
+ if (main === void 0) return rows.length === 0 ? void 0 : sumTotals(rows.map((entry) => entry.totals));
289
+ if (rows.length === 0) return main;
290
+ return sumTotals([main, ...rows.map((entry) => entry.totals)]);
266
291
  }
267
292
  /**
268
293
  * The price band in effect at one instant (mirror of the host helper). The
@@ -507,10 +532,10 @@ window.__ModuleLoader__.load({
507
532
  }, [sessionId, currency]);
508
533
  const balance = response?.balance;
509
534
  const balanceEnabled = response?.pricebook?.balanceEnabled !== false;
510
- const subagents = response?.subagents;
511
- const combined = combineTotals(cost?.totals, subagents);
512
- const subSpend = subagentSpend(subagents);
513
- const mainSpend = cost?.totals?.cost ?? 0;
535
+ const lookup = costLookupFor(cost?.totals, response?.subagents);
536
+ const combined = lookup.combined;
537
+ const subSpend = lookup.subagentTotal;
538
+ const mainSpend = lookup.mainSpend;
514
539
  const parts = [];
515
540
  try {
516
541
  const symbol = currencySymbol(currency);
@@ -526,10 +551,15 @@ window.__ModuleLoader__.load({
526
551
  } catch {}
527
552
  if (parts.length === 0) return null;
528
553
  const model = cost?.model?.model;
554
+ const subagents = lookup.subagents;
529
555
  const tooltipParts = (0, react.useMemo)(() => {
530
556
  const rows = [];
531
557
  try {
532
558
  rows.push(...costDetail(cost, model, t, currency));
559
+ for (const subagent of subagents) {
560
+ const label = subagent.depth > 1 ? `${subagent.sessionId.slice(0, 8)} (${t("subagent.depth", { depth: String(subagent.depth) })})` : subagent.sessionId.slice(0, 8);
561
+ rows.push(`${label} ${currencySymbol(currency)}${formatMoney(subagent.totals?.cost ?? 0)}`);
562
+ }
533
563
  if (subSpend > 0) rows.push(t("line.subagent", { amount: `${currencySymbol(currency)}${formatMoney(subSpend)}` }));
534
564
  const pricebook = response?.pricebook;
535
565
  if (pricebook !== void 0 && pricebook.current !== null) {
@@ -548,6 +578,7 @@ window.__ModuleLoader__.load({
548
578
  balanceEnabled,
549
579
  response,
550
580
  subSpend,
581
+ subagents,
551
582
  currency
552
583
  ]);
553
584
  const line = parts.join(" · ");
@@ -570,7 +601,7 @@ window.__ModuleLoader__.load({
570
601
  });
571
602
  //#endregion
572
603
  //#region \0dsh-css:/home/runner/work/dsh-cost-meter/dsh-cost-meter/src/client/CostView.module.css.mjs
573
- const css$3 = ".fZF-xq_root{max-width:var(--dsh-chat-content-width);box-sizing:border-box;width:100%;color:var(--dsw-alias-label-primary);flex-direction:column;gap:16px;margin:0 auto;padding:16px 24px;font-size:13px;line-height:20px;display:flex}.fZF-xq_summary{flex-direction:column;gap:8px;display:flex}.fZF-xq_totalRow{justify-content:space-between;align-items:baseline;display:flex}.fZF-xq_totalLabel{font-size:15px;font-weight:600}.fZF-xq_totalValue{font-variant-numeric:tabular-nums;font-size:18px;font-weight:700}.fZF-xq_categories{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex-wrap:wrap;gap:4px 16px;font-size:12px;display:flex}.fZF-xq_unpricedNote{color:var(--dsw-alias-label-secondary);opacity:.75;font-size:12px}.fZF-xq_steps{flex-direction:column;gap:8px;display:flex}.fZF-xq_stepsTitle{margin:0;font-size:13px;font-weight:600}.fZF-xq_stepList{flex-direction:column;gap:6px;margin:0;padding:0;list-style:none;display:flex}.fZF-xq_step{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:8px;flex-direction:column;gap:2px;padding:8px 10px;display:flex}.fZF-xq_step[data-band=peak]{border-color:var(--dsw-alias-state-error-secondary)}.fZF-xq_step[data-band=offPeak]{border-color:var(--dsw-alias-state-success-secondary)}.fZF-xq_step[data-band=peak] .fZF-xq_stepCost{color:var(--dsw-alias-state-error-primary)}.fZF-xq_step[data-band=offPeak] .fZF-xq_stepCost{color:var(--dsw-alias-state-success-primary)}.fZF-xq_stepHead{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}.fZF-xq_stepTurn{font-weight:600}.fZF-xq_stepModel{color:var(--dsw-alias-label-secondary);font-size:12px}.fZF-xq_stepTime{color:var(--dsw-alias-label-secondary);opacity:.75;font-size:12px}.fZF-xq_stepCost{font-variant-numeric:tabular-nums;align-items:baseline;gap:6px;margin-left:auto;font-weight:600;display:inline-flex}.fZF-xq_stepBody{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex-wrap:wrap;gap:12px;font-size:12px;display:flex}.fZF-xq_stepMeta{color:var(--dsw-alias-label-secondary);opacity:.75}";
604
+ const css$3 = ".fZF-xq_root{max-width:var(--dsh-chat-content-width);box-sizing:border-box;width:100%;color:var(--dsw-alias-label-primary);flex-direction:column;gap:16px;margin:0 auto;padding:16px 24px;font-size:13px;line-height:20px;display:flex}.fZF-xq_summary{flex-direction:column;gap:8px;display:flex}.fZF-xq_totalRow{justify-content:space-between;align-items:baseline;display:flex}.fZF-xq_totalLabel{font-size:15px;font-weight:600}.fZF-xq_totalValue{font-variant-numeric:tabular-nums;font-size:18px;font-weight:700}.fZF-xq_categories{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex-wrap:wrap;gap:4px 16px;font-size:12px;display:flex}.fZF-xq_unpricedNote{color:var(--dsw-alias-label-secondary);opacity:.75;font-size:12px}.fZF-xq_steps{flex-direction:column;gap:8px;display:flex}.fZF-xq_subagents{flex-direction:column;gap:8px;margin-top:4px;display:flex}.fZF-xq_stepsTitle{margin:0;font-size:13px;font-weight:600}.fZF-xq_stepList{flex-direction:column;gap:6px;margin:0;padding:0;list-style:none;display:flex}.fZF-xq_step{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);border-radius:8px;flex-direction:column;gap:2px;padding:8px 10px;display:flex}.fZF-xq_step[data-band=peak]{border-color:var(--dsw-alias-state-error-secondary)}.fZF-xq_step[data-band=offPeak]{border-color:var(--dsw-alias-state-success-secondary)}.fZF-xq_step[data-band=peak] .fZF-xq_stepCost{color:var(--dsw-alias-state-error-primary)}.fZF-xq_step[data-band=offPeak] .fZF-xq_stepCost{color:var(--dsw-alias-state-success-primary)}.fZF-xq_stepHead{flex-wrap:wrap;align-items:baseline;gap:8px;display:flex}.fZF-xq_stepTurn{font-weight:600}.fZF-xq_stepModel{color:var(--dsw-alias-label-secondary);font-size:12px}.fZF-xq_stepTime{color:var(--dsw-alias-label-secondary);opacity:.75;font-size:12px}.fZF-xq_stepCost{font-variant-numeric:tabular-nums;align-items:baseline;gap:6px;margin-left:auto;font-weight:600;display:inline-flex}.fZF-xq_stepBody{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex-wrap:wrap;gap:12px;font-size:12px;display:flex}.fZF-xq_stepMeta{color:var(--dsw-alias-label-secondary);opacity:.75}";
574
605
  const tagId$3 = "@gamegeek-saikel/dsh-cost-meter/CostView.module.css";
575
606
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
576
607
  const tag = document.createElement("style");
@@ -593,6 +624,7 @@ window.__ModuleLoader__.load({
593
624
  "stepTurn": "fZF-xq_stepTurn",
594
625
  "steps": "fZF-xq_steps",
595
626
  "stepsTitle": "fZF-xq_stepsTitle",
627
+ "subagents": "fZF-xq_subagents",
596
628
  "summary": "fZF-xq_summary",
597
629
  "totalLabel": "fZF-xq_totalLabel",
598
630
  "totalRow": "fZF-xq_totalRow",
@@ -616,6 +648,14 @@ window.__ModuleLoader__.load({
616
648
  function reasonText(step, t) {
617
649
  return t(`reason.${step.unpricedReason ?? "NO_PRICE"}`);
618
650
  }
651
+ /**
652
+ * Indent a subagent row by its root-relative depth, so the nested delegation
653
+ * chain reads as a chain. Depth `-1` (`0`) marks a row the runtime fallback
654
+ * discovered, which states no distance: it keeps the top-level inset.
655
+ */
656
+ function subagentIndent(depth) {
657
+ return { paddingLeft: `${Math.max(0, depth - 1) * 14}px` };
658
+ }
619
659
  const CostView = (0, react.memo)(function CostView({ useProjection, sessionId, t }) {
620
660
  const currency = displayCurrency(t);
621
661
  const costCny = useProjection("sessionCost");
@@ -633,15 +673,18 @@ window.__ModuleLoader__.load({
633
673
  alive = false;
634
674
  };
635
675
  }, [sessionId, currency]);
636
- if (cost === void 0 || cost.steps.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
676
+ const rows = cost?.steps ?? [];
677
+ const lookup = costLookupFor(cost?.totals, response?.subagents);
678
+ const subagents = lookup.subagents;
679
+ const combined = lookup.combined;
680
+ const subSpend = lookup.subagentTotal;
681
+ const money = (value) => value === null ? "—" : `${symbol}${formatMoney(value)}`;
682
+ if (combined === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
637
683
  className: CostView_module_css_default.root,
638
684
  "data-testid": "cost-view-empty",
639
685
  children: t("view.empty")
640
686
  });
641
- const rows = cost.steps;
642
- const totals = combineTotals(cost.totals, response?.subagents) ?? cost.totals;
643
- const subSpend = subagentSpend(response?.subagents);
644
- const money = (value) => value === null ? "—" : `${symbol}${formatMoney(value)}`;
687
+ const totals = combined;
645
688
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
646
689
  className: CostView_module_css_default.root,
647
690
  "data-testid": "cost-view",
@@ -682,7 +725,7 @@ window.__ModuleLoader__.load({
682
725
  className: CostView_module_css_default.unpricedNote,
683
726
  children: t("view.unpriced", { count: String(totals.unpricedSteps) })
684
727
  }),
685
- response !== null && response.subagents.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
728
+ subagents.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
686
729
  className: CostView_module_css_default.subagents,
687
730
  "data-testid": "cost-view-subagents",
688
731
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -690,34 +733,48 @@ window.__ModuleLoader__.load({
690
733
  children: t("line.subagent", { amount: `${symbol}${formatMoney(subSpend)}` })
691
734
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
692
735
  className: CostView_module_css_default.stepList,
693
- children: response.subagents.map((sub) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
736
+ children: subagents.map((sub) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
694
737
  className: CostView_module_css_default.step,
738
+ "data-testid": "cost-subagent",
739
+ "data-depth": String(sub.depth),
695
740
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
696
741
  className: CostView_module_css_default.stepHead,
697
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
698
- className: CostView_module_css_default.stepTurn,
699
- children: sub.sessionId.slice(0, 8)
700
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
701
- className: CostView_module_css_default.stepCost,
702
- children: money(sub.totals.cost)
703
- })]
742
+ children: [
743
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
744
+ className: CostView_module_css_default.stepTurn,
745
+ style: subagentIndent(sub.depth),
746
+ children: sub.sessionId.slice(0, 8)
747
+ }),
748
+ sub.depth > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
749
+ className: CostView_module_css_default.stepModel,
750
+ children: t("subagent.depth", { depth: String(sub.depth) })
751
+ }),
752
+ sub.label !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
753
+ className: CostView_module_css_default.stepModel,
754
+ children: sub.label
755
+ }),
756
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
757
+ className: CostView_module_css_default.stepCost,
758
+ children: money(sub.totals?.cost ?? null)
759
+ })
760
+ ]
704
761
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
705
762
  className: CostView_module_css_default.stepBody,
706
763
  children: [
707
764
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
708
765
  t("view.uncached"),
709
766
  ": ",
710
- money(sub.totals.uncachedCost)
767
+ money(sub.totals?.uncachedCost ?? null)
711
768
  ] }),
712
769
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
713
770
  t("view.cacheRead"),
714
771
  ": ",
715
- money(sub.totals.cacheReadCost)
772
+ money(sub.totals?.cacheReadCost ?? null)
716
773
  ] }),
717
774
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
718
775
  t("view.output"),
719
776
  ": ",
720
- money(sub.totals.outputCost)
777
+ money(sub.totals?.outputCost ?? null)
721
778
  ] })
722
779
  ]
723
780
  })]
@@ -1069,9 +1126,9 @@ window.__ModuleLoader__.load({
1069
1126
  const model = projection?.model ?? null;
1070
1127
  const snapshot = response?.pricebook.current ?? null;
1071
1128
  const schedule = response?.pricebook.schedule;
1072
- const subagents = response?.subagents;
1073
- const combined = combineTotals(projection?.totals, subagents);
1074
- const subSpend = subagentSpend(subagents);
1129
+ const lookup = costLookupFor(projection?.totals, response?.subagents);
1130
+ const combined = lookup.combined;
1131
+ const subSpend = lookup.subagentTotal;
1075
1132
  const symbol = currencySymbol(currency);
1076
1133
  const estimate = (0, react.useMemo)(() => {
1077
1134
  if (!running || projection === void 0) return null;
@@ -1090,7 +1147,7 @@ window.__ModuleLoader__.load({
1090
1147
  partial,
1091
1148
  currency
1092
1149
  ]);
1093
- if (projection === void 0 && estimate === null) return null;
1150
+ if (combined === void 0 && estimate === null) return null;
1094
1151
  const total = combined?.cost ?? 0;
1095
1152
  const label = estimate !== null ? t("pill.estimate", { amount: `${symbol}${formatMoney(estimate)}` }) : t("pill.label", { amount: `${symbol}${formatMoney(total)}` });
1096
1153
  const last = projection?.steps.length !== void 0 && projection.steps.length > 0 ? projection.steps[projection.steps.length - 1] : void 0;