dsh-context 0.47.0 → 0.48.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
@@ -10,7 +10,7 @@
10
10
 
11
11
  [`dsh-context`](https://www.npmjs.com/package/dsh-context) provides full context lifecycle management features.
12
12
  - **Context tab** — an UI context dashboard for DeepSeek Harness's context stats, composition, trend, events, and messages.
13
- - **Context panel** — the same dashboard as a right-sidebar tab (dsh 0.1.5+): pick **Context** on the sidebar's guide page and the panel opens beside the chat.
13
+ - **Context panel** — the same dashboard as a right-sidebar tab (dsh 0.1.5-alpha.2+): pick **Context** on the sidebar's guide page and the panel opens beside the chat.
14
14
  - **`/context` command** — the slash command shows the context model for current context composition and recent context evolution.
15
15
 
16
16
  ## Install / Update
package/lib/client.js CHANGED
@@ -10,7 +10,6 @@ window.__ModuleLoader__.load({
10
10
  //#region src/client/i18n.ts
11
11
  const DICT_ZH = {
12
12
  "tab": "上下文",
13
- "sidebar.guideDescription": "查看这个会话的上下文构成、每步变化与消息列表。",
14
13
  "cat.system": "系统提示词",
15
14
  "cat.tools": "工具定义",
16
15
  "cat.user": "用户消息",
@@ -32,15 +31,18 @@ window.__ModuleLoader__.load({
32
31
  "stats.toolCalls": "工具调用",
33
32
  "stats.images": "图片",
34
33
  "stats.cost": "预估费用",
35
- "stats.costTip": "按 DeepSeek 官方刊例价估算整个会话的累计费用:输入区分缓存命中/未命中,输出含思考;按请求时间区分高峰(北京时间工作日 9:00–12:00、14:00–18:00)与空闲时段(半价,周末全天均为空闲时段)。适用于 deepseek-v4-flash / deepseek-v4-pro(不限 provider),价格写死在代码中,仅供参考。",
34
+ "stats.costTip": "按 DeepSeek 官方刊例价估算整个会话的累计费用:输入区分缓存命中/未命中,输出含思考;按请求时间区分高峰(北京时间工作日 9:00–12:00、14:00–18:00)与空闲时段(半价,周末全天均为空闲时段)。适用于 deepseek-v4.1-flash / deepseek-v4-pro(不限 provider;V4.1 Pro 上线前,V4 Pro 请求将被路由并按 V4.1 Flash 单价计费),价格写死在代码中,仅供参考。",
36
35
  "stats.costPriceHead": "每百万 tokens 价格(高峰 | 空闲半价):",
37
36
  "stats.costHit": "命中",
38
37
  "stats.costMiss": "未命中",
39
38
  "stats.costOut": "输出",
40
39
  "timing.title": "耗时统计",
41
- "timing.total": "总活跃时长",
40
+ "timing.total": "活跃时长",
42
41
  "timing.ttft": "模型等待",
43
42
  "timing.gen": "模型生成",
43
+ "timing.reasoning": "模型思考",
44
+ "timing.text": "正文输出",
45
+ "timing.toolArgs": "工具参数",
44
46
  "timing.tools": "工具执行",
45
47
  "timing.other": "其他开销",
46
48
  "timing.callTimes": "{n}次",
@@ -297,7 +299,6 @@ window.__ModuleLoader__.load({
297
299
  };
298
300
  const DICT_EN = {
299
301
  "tab": "Context",
300
- "sidebar.guideDescription": "Inspect this session's context composition, per-step history, and messages.",
301
302
  "cat.system": "System Prompt",
302
303
  "cat.tools": "Tool Schemas",
303
304
  "cat.user": "User Messages",
@@ -319,7 +320,7 @@ window.__ModuleLoader__.load({
319
320
  "stats.toolCalls": "Tool Calls",
320
321
  "stats.images": "Images",
321
322
  "stats.cost": "Cost",
322
- "stats.costTip": "Rough cumulative cost of the whole session at DeepSeek’s list prices: input split by cache hit/miss, output includes reasoning; each request is priced at the peak (Beijing Time weekdays 09:00–12:00, 14:00–18:00) or half-price off-peak rate by its time — weekends bill at off-peak all day. Applies to deepseek-v4-flash / deepseek-v4-pro on any provider; prices are hardcoded, for reference only.",
323
+ "stats.costTip": "Rough cumulative cost of the whole session at DeepSeek’s list prices: input split by cache hit/miss, output includes reasoning; each request is priced at the peak (Beijing Time weekdays 09:00–12:00, 14:00–18:00) or half-price off-peak rate by its time — weekends bill at off-peak all day. Applies to deepseek-v4.1-flash / deepseek-v4-pro on any provider (until V4.1 Pro ships, Pro requests are routed to and billed at V4.1 Flash rates); prices are hardcoded, for reference only.",
323
324
  "stats.costPriceHead": "Per-1M-token rates (peak | off-peak at half price):",
324
325
  "stats.costHit": "hit",
325
326
  "stats.costMiss": "miss",
@@ -328,6 +329,9 @@ window.__ModuleLoader__.load({
328
329
  "timing.total": "Active Time",
329
330
  "timing.ttft": "TTFT",
330
331
  "timing.gen": "LLM Gen",
332
+ "timing.reasoning": "Thinking",
333
+ "timing.text": "Answer",
334
+ "timing.toolArgs": "Tool args",
331
335
  "timing.tools": "Tool runs",
332
336
  "timing.other": "Overhead",
333
337
  "timing.callTimes": "{n} calls",
@@ -1136,6 +1140,14 @@ window.__ModuleLoader__.load({
1136
1140
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
1137
1141
  }
1138
1142
  /**
1143
+ * The OPTIONAL timing scalars (the generation split): a real non-negative
1144
+ * number passes, anything else — including absence — reads as undefined so the
1145
+ * field stays absent on the narrowed value (see `timingOf`).
1146
+ */
1147
+ function optMsNumOf(value) {
1148
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
1149
+ }
1150
+ /**
1139
1151
  * Cheap whole-value check for the pass-through path of `timelineOf`: absent
1140
1152
  * timing passes; present timing must already be well-formed (every scalar
1141
1153
  * numeric, every per-name row shaped) — anything else sends the payload down
@@ -1153,6 +1165,14 @@ window.__ModuleLoader__.load({
1153
1165
  "toolsMs",
1154
1166
  "toolCalls"
1155
1167
  ]) if (typeof t[k] !== "number") return false;
1168
+ for (const k of [
1169
+ "reasoningMs",
1170
+ "textMs",
1171
+ "toolArgMs"
1172
+ ]) {
1173
+ const v = t[k];
1174
+ if (v !== void 0 && (typeof v !== "number" || !Number.isFinite(v) || v < 0)) return false;
1175
+ }
1156
1176
  const tools = t.tools;
1157
1177
  if (tools === null || typeof tools !== "object" || Array.isArray(tools)) return false;
1158
1178
  for (const k in tools) {
@@ -1187,7 +1207,7 @@ window.__ModuleLoader__.load({
1187
1207
  ms
1188
1208
  };
1189
1209
  }
1190
- return {
1210
+ const totals = {
1191
1211
  wallMs: msNumOf(data.wallMs),
1192
1212
  ttftMs: msNumOf(data.ttftMs),
1193
1213
  genMs: msNumOf(data.genMs),
@@ -1196,6 +1216,13 @@ window.__ModuleLoader__.load({
1196
1216
  toolCalls: msNumOf(data.toolCalls),
1197
1217
  tools
1198
1218
  };
1219
+ const reasoning = optMsNumOf(data.reasoningMs);
1220
+ if (reasoning !== void 0) totals.reasoningMs = reasoning;
1221
+ const textMs = optMsNumOf(data.textMs);
1222
+ if (textMs !== void 0) totals.textMs = textMs;
1223
+ const toolArgMs = optMsNumOf(data.toolArgMs);
1224
+ if (toolArgMs !== void 0) totals.toolArgMs = toolArgMs;
1225
+ return totals;
1199
1226
  }
1200
1227
  /**
1201
1228
  * Narrow a delivered projection value to the plugin's `contextHeaders`
@@ -2453,22 +2480,20 @@ window.__ModuleLoader__.load({
2453
2480
  return `${pct.toFixed(1)}%`;
2454
2481
  }
2455
2482
  /**
2456
- * Whole-session durations for the timing card, in the locale's units: raw ms
2457
- * under a second, one-decimal seconds under a minute, then m/s and h/m.
2483
+ * Whole-session durations for the timing card, locale-free compact units: raw
2484
+ * ms under a second, one-decimal seconds under a minute, then m/s and h/m.
2458
2485
  * Non-finite or non-positive input shows the dash (callers render their empty
2459
2486
  * state anyway).
2460
2487
  */
2461
- function fmtDuration(ms, lang) {
2488
+ function fmtDuration(ms) {
2462
2489
  if (!Number.isFinite(ms) || ms <= 0) return "—";
2463
2490
  if (ms < 1e3) return `${Math.round(ms)}ms`;
2464
- const zh = lang === "zh";
2465
- if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}${zh ? "秒" : "s"}`;
2491
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
2466
2492
  const totalSec = Math.floor(ms / 1e3);
2467
2493
  const m = Math.floor(totalSec / 60);
2468
2494
  const s = totalSec % 60;
2469
- if (ms < 36e5) return zh ? `${m}分${s}秒` : `${m}m ${s}s`;
2470
- const h = Math.floor(m / 60);
2471
- return zh ? `${h}时${m % 60}分` : `${h}h ${m % 60}m`;
2495
+ if (ms < 36e5) return `${m}m${s}s`;
2496
+ return `${Math.floor(m / 60)}h${m % 60}m`;
2472
2497
  }
2473
2498
  //#endregion
2474
2499
  //#region src/shared/imageTokens.ts
@@ -3924,7 +3949,7 @@ window.__ModuleLoader__.load({
3924
3949
  className: "lc-stacked-seg" + (on ? " lc-stacked-seg-on" : "") + (pickKey !== void 0 ? " lc-stacked-seg-pick" : ""),
3925
3950
  style: {
3926
3951
  width: `${widths[i]}%`,
3927
- background: p.color
3952
+ backgroundColor: p.color
3928
3953
  },
3929
3954
  onMouseEnter: () => {
3930
3955
  if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
@@ -5662,9 +5687,9 @@ window.__ModuleLoader__.load({
5662
5687
  }
5663
5688
  /**
5664
5689
  * Compact duration: `42s`, `3m05s`, `1h07m` (shared by both locales).
5665
- * Deliberately distinct from format.ts's locale-aware `fmtDuration` (the
5666
- * timing card's `12.3s` / `4分0秒`): the inspector's caption column needs
5667
- * whole-second, fixed-width, locale-free text.
5690
+ * Deliberately distinct from format.ts's `fmtDuration` (the timing card's
5691
+ * `12.3s` / `3m25s`): the inspector's caption column needs whole-second,
5692
+ * fixed-width text.
5668
5693
  */
5669
5694
  function fmtDurationCompact(ms) {
5670
5695
  if (!Number.isFinite(ms) || ms < 0) return "—";
@@ -6579,7 +6604,7 @@ window.__ModuleLoader__.load({
6579
6604
  return function PluginInfo() {
6580
6605
  const [latest, setLatest] = (0, react.useState)(null);
6581
6606
  (0, react.useEffect)(() => {
6582
- if ("0.47.0".includes("-dev")) return;
6607
+ if ("0.48.0".includes("-dev")) return;
6583
6608
  let on = true;
6584
6609
  fetchLatestVersion().then((v) => {
6585
6610
  if (on && v) setLatest(v);
@@ -6588,8 +6613,8 @@ window.__ModuleLoader__.load({
6588
6613
  on = false;
6589
6614
  };
6590
6615
  }, []);
6591
- const update = latest !== null && isNewerVersion(latest, "0.47.0") ? latest : null;
6592
- const nameText = "dsh-context (v0.47.0)";
6616
+ const update = latest !== null && isNewerVersion(latest, "0.48.0") ? latest : null;
6617
+ const nameText = "dsh-context (v0.48.0)";
6593
6618
  const nameValue = [nameText];
6594
6619
  if (update) nameValue.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6595
6620
  className: "lc-pi-update",
@@ -7046,61 +7071,49 @@ window.__ModuleLoader__.load({
7046
7071
  }
7047
7072
  //#endregion
7048
7073
  //#region src/client/cost.ts
7049
- const PRICES = {
7074
+ /** The Flash-series rates (peak = 2× off-peak), per currency. */
7075
+ const FLASH_RATES = {
7050
7076
  usd: {
7051
- flash: {
7052
- peak: {
7053
- hit: .014,
7054
- miss: .44,
7055
- out: 1.32
7056
- },
7057
- off: {
7058
- hit: .007,
7059
- miss: .22,
7060
- out: .66
7061
- }
7077
+ peak: {
7078
+ hit: .006,
7079
+ miss: .3,
7080
+ out: 1.2
7062
7081
  },
7063
- pro: {
7064
- peak: {
7065
- hit: .044,
7066
- miss: 1.32,
7067
- out: 3.96
7068
- },
7069
- off: {
7070
- hit: .022,
7071
- miss: .66,
7072
- out: 1.98
7073
- }
7082
+ off: {
7083
+ hit: .003,
7084
+ miss: .15,
7085
+ out: .6
7074
7086
  }
7075
7087
  },
7076
7088
  cny: {
7077
- flash: {
7078
- peak: {
7079
- hit: .1,
7080
- miss: 3,
7081
- out: 9
7082
- },
7083
- off: {
7084
- hit: .05,
7085
- miss: 1.5,
7086
- out: 4.5
7087
- }
7089
+ peak: {
7090
+ hit: .04,
7091
+ miss: 2,
7092
+ out: 8
7088
7093
  },
7089
- pro: {
7090
- peak: {
7091
- hit: .3,
7092
- miss: 9,
7093
- out: 27
7094
- },
7095
- off: {
7096
- hit: .15,
7097
- miss: 4.5,
7098
- out: 13.5
7099
- }
7094
+ off: {
7095
+ hit: .02,
7096
+ miss: 1,
7097
+ out: 4
7100
7098
  }
7101
7099
  }
7102
7100
  };
7103
7101
  /**
7102
+ * Pro rides the Flash triple: between the V4.1 Flash launch and the V4.1 Pro
7103
+ * release, DeepSeek routes every Pro request to V4.1 Flash and bills it at
7104
+ * the Flash rates — point `pro` at the new table once V4.1 Pro is priced.
7105
+ */
7106
+ const PRICES = {
7107
+ usd: {
7108
+ flash: FLASH_RATES.usd,
7109
+ pro: FLASH_RATES.usd
7110
+ },
7111
+ cny: {
7112
+ flash: FLASH_RATES.cny,
7113
+ pro: FLASH_RATES.cny
7114
+ }
7115
+ };
7116
+ /**
7104
7117
  * Price the session's cumulative billed-token totals. Cache reads bill at
7105
7118
  * the hit rate; uncached input AND cache writes bill at the miss rate;
7106
7119
  * output (reasoning included) bills at the out rate. Null when nothing was
@@ -7135,7 +7148,7 @@ window.__ModuleLoader__.load({
7135
7148
  */
7136
7149
  function sessionPrices(currency) {
7137
7150
  return ["flash", "pro"].map((id) => ({
7138
- family: id === "flash" ? "deepseek-v4-flash" : "deepseek-v4-pro",
7151
+ family: id === "flash" ? "deepseek-v4.1-flash" : "deepseek-v4-pro",
7139
7152
  peak: PRICES[currency][id].peak,
7140
7153
  off: PRICES[currency][id].off
7141
7154
  }));
@@ -7330,90 +7343,139 @@ window.__ModuleLoader__.load({
7330
7343
  * token, the wait) and LLM generation (first token → assistant message) —
7331
7344
  * and the tool-execution slice, with the residue as overhead; a call whose
7332
7345
  * stream recorded no token delta (legacy or aborted) stays unattributed and
7333
- * lands in the residue. The slice rows lead with the true duration and
7334
- * qualify it with the call count on the secondary line. The donut and the
7335
- * rows sit side by side so the head row stays half-height. Parallel tool
7336
- * calls each count, so the tools figure can overlap the ring clamps it
7337
- * into the post-model window while the row numbers stay true.
7338
- */
7346
+ * lands in the residue. When the host folded the generation split (see
7347
+ * TimingTotals), the generation slice expands into what was being decoded:
7348
+ * thinking, answer text, and tool-call arguments. The slice rows lead with
7349
+ * the true duration and qualify it with the call count on the secondary line
7350
+ * except the decode slices, which count BLOCKS (zero to many per call, so a
7351
+ * call count would be a false tally) and therefore carry no qualifier. A slice
7352
+ * that never happened (zero time) is omitted entirely — the ring already skips
7353
+ * its arc, and a zero row would otherwise borrow the session's call count and
7354
+ * read as "no time, many calls". The donut and the rows sit side by side so
7355
+ * the head row stays half-height. Parallel tool calls each count, so the tools
7356
+ * figure can overlap — the ring clamps it into the post-model window while the
7357
+ * row numbers stay true.
7358
+ */
7359
+ const COLOR = {
7360
+ ttft: "#3b82f6",
7361
+ reasoning: "#8b5cf6",
7362
+ text: "#ec4899",
7363
+ toolarg: "#f59e0b",
7364
+ tools: "#14b8a6",
7365
+ other: "#94a3b8"
7366
+ };
7339
7367
  function makeStatsTiming(kit, Donut) {
7340
7368
  const { t, fmt, fmtDuration, fmtShare } = kit;
7341
7369
  const SliceList = makeSliceList(kit);
7342
7370
  return function StatsTiming(props) {
7343
- const lang = props.locale === "zh" ? "zh" : "en";
7344
7371
  const [hoverKey, setHoverKey] = (0, react.useState)(null);
7345
7372
  const timing = props.timing;
7346
7373
  const wall = timing !== null && Number.isFinite(timing.wallMs) && timing.wallMs > 0 ? timing.wallMs : 0;
7347
7374
  let segments = [];
7348
7375
  let rows = [];
7349
7376
  if (timing !== null && (wall > 0 || timing.calls > 0 || timing.toolCalls > 0)) {
7350
- const ttft = Math.min(timing.ttftMs, wall);
7351
- const gen = Math.max(0, Math.min(timing.genMs, wall - ttft));
7352
- const toolRing = Math.max(0, Math.min(timing.toolsMs, wall - ttft - gen));
7353
- const other = Math.max(0, wall - ttft - gen - toolRing);
7377
+ const callTimes = t("timing.callTimes", { n: fmt(timing.calls) });
7378
+ const modelSlices = [{
7379
+ key: "ttft",
7380
+ color: COLOR.ttft,
7381
+ label: t("timing.ttft"),
7382
+ ms: timing.ttftMs,
7383
+ times: callTimes
7384
+ }];
7385
+ const buckets = [
7386
+ [
7387
+ "reasoning",
7388
+ COLOR.reasoning,
7389
+ t("timing.reasoning"),
7390
+ timing.reasoningMs ?? 0
7391
+ ],
7392
+ [
7393
+ "text",
7394
+ COLOR.text,
7395
+ t("timing.text"),
7396
+ timing.textMs ?? 0
7397
+ ],
7398
+ [
7399
+ "toolarg",
7400
+ COLOR.toolarg,
7401
+ t("timing.toolArgs"),
7402
+ timing.toolArgMs ?? 0
7403
+ ]
7404
+ ];
7405
+ if (buckets.some(([, , , ms]) => ms > 0)) for (const [key, color, label, ms] of buckets) modelSlices.push({
7406
+ key,
7407
+ color,
7408
+ label,
7409
+ ms
7410
+ });
7411
+ else modelSlices.push({
7412
+ key: "gen",
7413
+ color: COLOR.reasoning,
7414
+ label: t("timing.gen"),
7415
+ ms: timing.genMs
7416
+ });
7417
+ const ttftRing = Math.min(timing.ttftMs, wall);
7418
+ const genRing = Math.max(0, Math.min(timing.genMs, wall - ttftRing));
7419
+ const modelRings = [ttftRing];
7420
+ let used = ttftRing;
7421
+ const genEnd = ttftRing + genRing;
7422
+ for (const slice of modelSlices.slice(1)) {
7423
+ const take = Math.max(0, Math.min(slice.ms, genEnd - used));
7424
+ modelRings.push(take);
7425
+ used += take;
7426
+ }
7427
+ const toolRing = Math.max(0, Math.min(timing.toolsMs, wall - ttftRing - genRing));
7428
+ const other = Math.max(0, wall - ttftRing - genRing - toolRing);
7354
7429
  const share = (ms) => wall > 0 ? ms / wall : 0;
7355
7430
  const countOf = (ms, times) => {
7356
- const dur = fmtDuration(ms, lang);
7431
+ const dur = fmtDuration(ms);
7357
7432
  if (times === void 0) return dur;
7358
7433
  return ms > 0 ? `${dur} · ${times}` : times;
7359
7434
  };
7360
- const callTimes = t("timing.callTimes", { n: fmt(timing.calls) });
7435
+ const modelSegments = modelSlices.map((slice, index) => ({
7436
+ key: slice.key,
7437
+ color: slice.color,
7438
+ value: share(modelRings[index])
7439
+ }));
7440
+ const toolSlice = {
7441
+ key: "tools",
7442
+ color: COLOR.tools,
7443
+ label: t("timing.tools"),
7444
+ ms: timing.toolsMs,
7445
+ times: t("timing.toolTimes", { n: fmt(timing.toolCalls) })
7446
+ };
7447
+ const otherSlice = {
7448
+ key: "other",
7449
+ color: COLOR.other,
7450
+ label: t("timing.other"),
7451
+ ms: other
7452
+ };
7361
7453
  segments = [
7362
- {
7363
- key: "ttft",
7364
- color: "#3b82f6",
7365
- value: share(ttft)
7366
- },
7367
- {
7368
- key: "gen",
7369
- color: "#8b5cf6",
7370
- value: share(gen)
7371
- },
7454
+ ...modelSegments,
7372
7455
  {
7373
7456
  key: "tools",
7374
- color: "#14b8a6",
7457
+ color: COLOR.tools,
7375
7458
  value: share(toolRing)
7376
7459
  },
7377
7460
  {
7378
7461
  key: "other",
7379
- color: "#94a3b8",
7462
+ color: COLOR.other,
7380
7463
  value: share(other)
7381
7464
  }
7382
7465
  ];
7466
+ const toRow = (slice) => ({
7467
+ key: slice.key,
7468
+ color: slice.color,
7469
+ label: slice.label,
7470
+ dim: slice.ms === 0,
7471
+ pct: fmtShare(slice.ms, wall),
7472
+ count: countOf(slice.ms, slice.times)
7473
+ });
7383
7474
  rows = [
7384
- {
7385
- key: "ttft",
7386
- color: "#3b82f6",
7387
- label: t("timing.ttft"),
7388
- dim: timing.ttftMs === 0,
7389
- pct: fmtShare(timing.ttftMs, wall),
7390
- count: countOf(timing.ttftMs, callTimes)
7391
- },
7392
- {
7393
- key: "gen",
7394
- color: "#8b5cf6",
7395
- label: t("timing.gen"),
7396
- dim: timing.genMs === 0,
7397
- pct: fmtShare(timing.genMs, wall),
7398
- count: countOf(timing.genMs, callTimes)
7399
- },
7400
- {
7401
- key: "tools",
7402
- color: "#14b8a6",
7403
- label: t("timing.tools"),
7404
- dim: timing.toolsMs === 0,
7405
- pct: fmtShare(timing.toolsMs, wall),
7406
- count: countOf(timing.toolsMs, t("timing.toolTimes", { n: fmt(timing.toolCalls) }))
7407
- },
7408
- {
7409
- key: "other",
7410
- color: "#94a3b8",
7411
- label: t("timing.other"),
7412
- dim: other === 0,
7413
- pct: fmtShare(other, wall),
7414
- count: countOf(other)
7415
- }
7416
- ];
7475
+ ...modelSlices.map(toRow),
7476
+ toRow(toolSlice),
7477
+ toRow(otherSlice)
7478
+ ].filter((row) => !row.dim);
7417
7479
  }
7418
7480
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7419
7481
  className: "lc-card lc-col-stats lc-col-donut",
@@ -7431,7 +7493,7 @@ window.__ModuleLoader__.load({
7431
7493
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Donut, {
7432
7494
  segments,
7433
7495
  size: 96,
7434
- centerTop: wall > 0 ? fmtDuration(wall, lang) : "—",
7496
+ centerTop: wall > 0 ? fmtDuration(wall) : "—",
7435
7497
  centerSub: t("timing.total"),
7436
7498
  hoverKey,
7437
7499
  onHoverKey: setHoverKey
@@ -8500,35 +8562,19 @@ window.__ModuleLoader__.load({
8500
8562
  locale: activeLocale
8501
8563
  }),
8502
8564
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatsTokens, { usage }),
8503
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatsTiming, {
8504
- timing: data.timing ?? null,
8505
- locale: activeLocale
8506
- }),
8565
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatsTiming, { timing: data.timing ?? null }),
8507
8566
  inSidebar ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PluginInfo, {})
8508
8567
  ]
8509
8568
  }),
8510
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8569
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8511
8570
  className: "lc-cols lc-cols-main",
8512
- children: inSidebar ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
8513
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8514
- className: "lc-col",
8515
- children: compositionCard
8516
- }),
8517
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8518
- className: "lc-col lc-col-browser",
8519
- children: browserCard
8520
- }),
8521
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8522
- className: "lc-col",
8523
- children: trendCard
8524
- })
8525
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8571
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8526
8572
  className: "lc-col",
8527
8573
  children: [compositionCard, trendCard]
8528
8574
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8529
8575
  className: "lc-col lc-col-browser",
8530
8576
  children: browserCard
8531
- })] })
8577
+ })]
8532
8578
  }),
8533
8579
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8534
8580
  className: "lc-cols",
@@ -8676,7 +8722,7 @@ window.__ModuleLoader__.load({
8676
8722
  //#endregion
8677
8723
  //#region src/client/sidebar.ts
8678
8724
  /**
8679
- * The right Sidebar's Context tab (dsh 0.1.5-alpha.1+).
8725
+ * The right Sidebar's Context tab (dsh 0.1.5-alpha.2+).
8680
8726
  *
8681
8727
  * The tab reuses the Context conversation-view component VERBATIM: the
8682
8728
  * `sidebar.right.pane.tab` seat is session-scoped and delivers the same
@@ -8684,14 +8730,15 @@ window.__ModuleLoader__.load({
8684
8730
  * `t` seat) the `conversation.view` seat does, so the panel and the tab are
8685
8731
  * one component with one data path. The tab type contributes a guide entry, so
8686
8732
  * the sidebar's guide page offers "Context" and picking it opens the panel —
8687
- * the product's own path, exactly as the shipped Files type does.
8733
+ * the product's own path, exactly as the shipped Files type does: a capsule of
8734
+ * glyph + title.
8688
8735
  *
8689
- * OPTIONAL BY CONTRACT. `ctx.sidebarRightTabs` and the seat exist only from
8690
- * dsh 0.1.5-alpha.1; the registration therefore rides a DEFERRED inject (the
8691
- * plugin's hard injects stay `slots` + `locale`), so on every older supported
8692
- * line the callback never fires, the plugin fiber never pends, and nothing is
8693
- * registered. The registry is re-proved structurally and the whole
8694
- * registration is guarded: a foreign or hostile registry (a throwing
8736
+ * OPTIONAL BY CONTRACT. `ctx.sidebarRightTabs` and the seat ship only on the
8737
+ * 0.1.5 line (0.1.5-alpha.2+ supported); the registration therefore rides a
8738
+ * DEFERRED inject (the plugin's hard injects stay `slots` + `locale`), so on
8739
+ * every older supported line the callback never fires, the plugin fiber never
8740
+ * pends, and nothing is registered. The registry is re-proved structurally and
8741
+ * the whole registration is guarded: a foreign or hostile registry (a throwing
8695
8742
  * `register`, a taken id/kind) leaves the sidebar without the tab instead of
8696
8743
  * taking the browser down.
8697
8744
  *
@@ -8705,13 +8752,13 @@ window.__ModuleLoader__.load({
8705
8752
  * non-coexisting band, and a foreign plugin may well own `context`.
8706
8753
  */
8707
8754
  const SIDEBAR_CONTEXT_KIND = "dsh-context";
8708
- /** The guide box's position: after the shipped Files entry (order 10). */
8755
+ /** The guide capsule's position: after the shipped Files entry (order 10). */
8709
8756
  const GUIDE_ORDER = 20;
8710
8757
  /**
8711
- * The guide box's glyph, read defensively: the icon is OPTIONAL in the entry,
8712
- * and a primitives module that does not serve it — or whose namespace THROWS
8713
- * on the read (an interop/mock shape does exactly that) — must cost the glyph,
8714
- * never the whole tab.
8758
+ * The guide capsule's glyph, read defensively: the icon is OPTIONAL in the
8759
+ * entry, and a primitives module that does not serve it — or whose namespace
8760
+ * THROWS on the read (an interop/mock shape does exactly that) — must cost the
8761
+ * glyph, never the whole tab.
8715
8762
  * @returns the icon component, or undefined to register the tab without one.
8716
8763
  */
8717
8764
  function guideIcon() {
@@ -8745,7 +8792,6 @@ window.__ModuleLoader__.load({
8745
8792
  guide: [{
8746
8793
  order: GUIDE_ORDER,
8747
8794
  title: () => t("tab"),
8748
- description: () => t("sidebar.guideDescription"),
8749
8795
  icon: guideIcon()
8750
8796
  }]
8751
8797
  });
@@ -8783,7 +8829,7 @@ window.__ModuleLoader__.load({
8783
8829
  }
8784
8830
  //#endregion
8785
8831
  //#region \0dsh-global-css:/home/runner/work/dsh-context/dsh-context/src/client/styles/base.css.mjs
8786
- const css$13 = ".lc-root{box-sizing:border-box;height:100%;color:var(--dsw-alias-label-primary);padding:16px 20px 32px;font-size:13px;overflow-y:auto}[data-sidebar-right-open] .lc-root,[data-sidebar-right-float-host] .lc-root{height:auto;padding:0;overflow:visible}.lc-card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin-bottom:14px;padding:14px 16px}.lc-root .lc-card{margin-bottom:7px;padding:7px 8px;container:lc-card/inline-size}.lc-root .lc-cols{gap:7px;margin-bottom:7px}.lc-card-title{flex-wrap:wrap;align-items:baseline;gap:8px;margin-bottom:10px;font-weight:600;display:flex}.lc-card-title-text{white-space:nowrap;flex:none}.lc-gran,.lc-kinds{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;gap:2px;margin-left:auto;padding:1px;display:flex}.lc-trend-ctl{align-items:center;gap:6px;margin-left:auto;display:flex}.lc-trend-ctl .lc-gran{margin-left:0}.lc-trend-adaptive{flex:none;margin-left:0}.lc-gran-btn{color:var(--dsw-alias-label-secondary);cursor:pointer;transition:color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;border-radius:5px;padding:3px 8px;font-family:inherit;font-size:11px;line-height:1}.lc-gran-btn:hover{color:var(--dsw-alias-label-primary)}.lc-gran-on,.lc-gran-on:hover{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.lc-tip{z-index:6;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);width:max-content;color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-shadow-lv3,0 2px 8px #0000002e);pointer-events:none;opacity:0;transition:opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;padding:6px 10px;font-size:12px;position:absolute}.lc-events,.lc-fa-list{flex-direction:column;gap:2px;height:320px;display:flex;overflow-y:auto}.lc-cols{flex-wrap:wrap;gap:14px;margin-bottom:14px;display:flex}.lc-col{flex:1;min-width:min(360px,100%)}.lc-cols>.lc-card,.lc-col>.lc-card:last-child{margin-bottom:0}.lc-col-browser{flex-direction:column;display:flex}.lc-col-browser>.lc-card{flex:1}.lc-head{container:lc-head-row/inline-size}.lc-head>.lc-card{flex:1 1 0;min-width:0}@container lc-head-row (width<=1045px){.lc-head>.lc-card{flex-basis:40%}}@container lc-head-row (width<=480px){.lc-head>.lc-card{flex-basis:100%}}.lc-head>.lc-card:first-child{flex-direction:column;display:flex}.lc-head>.lc-card:first-child .lc-stats{flex:1;align-content:stretch}.lc-head>.lc-card:first-child .lc-stat{justify-content:center}.lc-empty{color:var(--dsw-alias-label-secondary);text-align:center;padding:18px 0}.lc-error{flex-direction:column;align-items:center;gap:8px;padding:40px 16px;display:flex}.lc-error-msg{font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, monospace);color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);overflow-wrap:anywhere;border-radius:6px;max-width:100%;padding:4px 8px;font-size:12px}.lc-error-retry{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;transition:border-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;padding:4px 14px;font-size:12px}.lc-error-retry:hover{border-color:var(--dsw-alias-label-primary)}.lc-foot{color:var(--dsw-alias-label-secondary);margin-top:4px;font-size:12px}[data-conversation-scroll]:has(.lc-root)>[data-composer-seat]:not(:has([data-approval-key],[data-question-key],[data-plan-review-key])),[data-conversation-scroll]:has(.lc-root,.lc-modal-backdrop)~[data-width-handle]{display:none}";
8832
+ const css$13 = ".lc-root{box-sizing:border-box;height:100%;color:var(--dsw-alias-label-primary);padding:16px 20px 32px;font-size:13px;overflow-y:auto}[data-sidebar-right-open] .lc-root,[data-sidebar-right-float-host] .lc-root{height:auto;padding:12px;overflow:visible}.lc-card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin-bottom:14px;padding:14px 16px}.lc-root .lc-card{margin-bottom:7px;padding:7px 8px;container:lc-card/inline-size}.lc-root .lc-cols{gap:7px;margin-bottom:7px}.lc-card-title{flex-wrap:wrap;align-items:baseline;gap:8px;margin-bottom:10px;font-weight:600;display:flex}.lc-card-title-text{white-space:nowrap;flex:none}.lc-gran,.lc-kinds{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;gap:2px;margin-left:auto;padding:1px;display:flex}.lc-trend-ctl{align-items:center;gap:6px;margin-left:auto;display:flex}.lc-trend-ctl .lc-gran{margin-left:0}.lc-trend-adaptive{flex:none;margin-left:0}.lc-gran-btn{color:var(--dsw-alias-label-secondary);cursor:pointer;transition:color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;border-radius:5px;padding:3px 8px;font-family:inherit;font-size:11px;line-height:1}.lc-gran-btn:hover{color:var(--dsw-alias-label-primary)}.lc-gran-on,.lc-gran-on:hover{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.lc-tip{z-index:6;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);width:max-content;color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-shadow-lv3,0 2px 8px #0000002e);pointer-events:none;opacity:0;transition:opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;padding:6px 10px;font-size:12px;position:absolute}.lc-events,.lc-fa-list{flex-direction:column;gap:2px;height:320px;display:flex;overflow-y:auto}.lc-cols{flex-wrap:wrap;gap:14px;margin-bottom:14px;display:flex}.lc-col{flex:1;min-width:min(360px,100%)}.lc-cols>.lc-card,.lc-col>.lc-card:last-child{margin-bottom:0}.lc-col-browser{flex-direction:column;display:flex}.lc-col-browser>.lc-card{flex:1}.lc-head{container:lc-head-row/inline-size}.lc-head>.lc-card{flex:1 1 0;min-width:0}@container lc-head-row (width<=1045px){.lc-head>.lc-card{flex-basis:40%}}@container lc-head-row (width<=480px){.lc-head>.lc-card{flex-basis:100%}}.lc-head>.lc-card:first-child{flex-direction:column;display:flex}.lc-head>.lc-card:first-child .lc-stats{flex:1;align-content:stretch}.lc-head>.lc-card:first-child .lc-stat{justify-content:center}.lc-empty{color:var(--dsw-alias-label-secondary);text-align:center;padding:18px 0}.lc-error{flex-direction:column;align-items:center;gap:8px;padding:40px 16px;display:flex}.lc-error-msg{font-family:var(--ds-font-family-code,ui-monospace, SFMono-Regular, Menlo, monospace);color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);overflow-wrap:anywhere;border-radius:6px;max-width:100%;padding:4px 8px;font-size:12px}.lc-error-retry{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;transition:border-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;padding:4px 14px;font-size:12px}.lc-error-retry:hover{border-color:var(--dsw-alias-label-primary)}.lc-foot{color:var(--dsw-alias-label-secondary);margin-top:4px;font-size:12px}[data-conversation-scroll]:has(.lc-root)>[data-composer-seat]:not(:has([data-approval-key],[data-question-key],[data-plan-review-key])),[data-conversation-scroll]:has(.lc-root,.lc-modal-backdrop)~[data-width-handle]{display:none}";
8787
8833
  const tagId$13 = "dsh-context/base.css";
8788
8834
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$13) + "]") === null) {
8789
8835
  const tag = document.createElement("style");
@@ -8838,7 +8884,7 @@ window.__ModuleLoader__.load({
8838
8884
  }
8839
8885
  //#endregion
8840
8886
  //#region \0dsh-global-css:/home/runner/work/dsh-context/dsh-context/src/client/styles/trendChart.css.mjs
8841
- const css$8 = ".lc-chartrow{align-items:stretch;gap:6px;display:flex}.lc-axis{box-sizing:border-box;width:40px;height:150px;color:var(--dsw-alias-label-secondary);padding-top:18px;font-size:11px;position:relative}.lc-axis span{line-height:1;position:absolute;right:0}.lc-axis-top{top:13px}.lc-axis-q3{top:41px}.lc-axis-mid{top:69px}.lc-axis-q1{top:97px}.lc-axis-bot{top:125px}.lc-chart-wrap{flex:1;min-width:0;position:relative}.lc-chart-scroll{overscroll-behavior-x:contain;scrollbar-gutter:stable;padding-bottom:var(--dsh-scrollbar-width,8px);overflow:auto hidden}.lc-chart{box-sizing:border-box;align-items:flex-end;gap:2px;width:max-content;min-width:100%;height:130px;padding-top:18px;display:flex;position:relative}.lc-grid{border-top:1px dashed var(--dsw-alias-border-l2);pointer-events:none;position:absolute;left:0;right:0}.lc-grid-top{top:18px}.lc-grid-q3{top:46px}.lc-grid-mid{top:74px}.lc-grid-q1{top:102px}.lc-grid-zero{border-top-style:solid;border-top-color:#80808080;top:130px}.lc-bar{cursor:pointer;outline-offset:1px;width:14px;height:100%;transition:opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), outline-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:2px;outline:1px dashed #0000;flex:none;align-items:flex-end;display:flex;position:relative}.lc-chart-dim .lc-bar{opacity:.35}.lc-chart-dim .lc-bar-in-turn{opacity:1}.lc-chart-dim .lc-turn{opacity:.35}.lc-chart-dim .lc-turn-on{opacity:1}.lc-chart-tip{z-index:5;box-sizing:border-box;overflow-wrap:break-word;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);max-width:100%;color:var(--dsw-alias-label-primary);pointer-events:none;border-radius:6px;flex-direction:column;gap:2px;padding:3px 8px;font-size:12px;display:flex;position:absolute;bottom:calc(100% + 4px);left:0;box-shadow:0 2px 8px #0000002e}.lc-bar:hover{background:var(--dsw-alias-bg-layer-2)}.lc-bar-hovered{outline-color:var(--dsw-alias-brand-primary)}.lc-bar-selected{outline:2px solid var(--dsw-alias-label-primary);outline-offset:1px}.lc-bar-in-turn{background:#80808024}.lc-bar-stack{flex-direction:column-reverse;width:100%;display:flex}.lc-bar-stack>div{width:100%}.lc-bar-up,.lc-bar-down{width:100%;display:flex;position:absolute;left:0}.lc-bar-up{flex-direction:column-reverse}.lc-bar-down{flex-direction:column}.lc-bar-up>div,.lc-bar-down>div{width:100%}.lc-cat-seg{transition:filter var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out)}.lc-chart[data-catdim] .lc-cat-seg{opacity:.35}.lc-chart[data-catdim=system] .lc-cat-seg[data-cat=system],.lc-chart[data-catdim=tools] .lc-cat-seg[data-cat=tools],.lc-chart[data-catdim=user] .lc-cat-seg[data-cat=user],.lc-chart[data-catdim=inject] .lc-cat-seg[data-cat=inject],.lc-chart[data-catdim=assistant] .lc-cat-seg[data-cat=assistant],.lc-chart[data-catdim=tool] .lc-cat-seg[data-cat=tool]{opacity:1;filter:brightness(1.18)}.lc-bar-marker{color:var(--dsw-alias-state-warn-primary);font-size:11px;position:absolute;top:-16px;left:50%;transform:translate(-50%)}.lc-turns{gap:2px;width:max-content;min-width:100%;margin-top:4px;display:flex;overflow:hidden}.lc-turn{box-sizing:border-box;text-align:center;color:var(--dsw-alias-label-secondary);white-space:nowrap;cursor:pointer;height:14px;transition:filter var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:3px;flex:none;font-size:10px;font-weight:600;line-height:14px}.lc-turn-on{filter:brightness(1.35);color:var(--dsw-alias-label-primary)}.lc-turn-label{display:inline-block}";
8887
+ const css$8 = ".lc-chartrow{align-items:stretch;gap:6px;display:flex}.lc-axis{box-sizing:border-box;width:40px;height:150px;color:var(--dsw-alias-label-secondary);padding-top:18px;font-size:11px;position:relative}.lc-axis span{line-height:1;position:absolute;right:0}.lc-axis-top{top:13px}.lc-axis-q3{top:41px}.lc-axis-mid{top:69px}.lc-axis-q1{top:97px}.lc-axis-bot{top:125px}.lc-chart-wrap{flex:1;min-width:0;position:relative}.lc-chart-scroll{overscroll-behavior-x:contain;scrollbar-gutter:stable;padding-bottom:var(--dsh-scrollbar-width,8px);overflow:auto hidden}.lc-chart{box-sizing:border-box;align-items:flex-end;gap:2px;width:max-content;min-width:100%;height:130px;padding-top:18px;display:flex;position:relative}.lc-grid{border-top:1px dashed var(--dsw-alias-border-l2);pointer-events:none;position:absolute;left:0;right:0}.lc-grid-top{top:18px}.lc-grid-q3{top:46px}.lc-grid-mid{top:74px}.lc-grid-q1{top:102px}.lc-grid-zero{border-top-style:solid;border-top-color:#80808080;top:130px}.lc-bar{cursor:pointer;outline-offset:1px;width:14px;height:100%;transition:opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), outline-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:2px;outline:1px dashed #0000;flex:none;align-items:flex-end;display:flex;position:relative}.lc-chart-dim .lc-bar{opacity:.35}.lc-chart-dim .lc-bar-in-turn{opacity:1}.lc-chart-dim .lc-turn{opacity:.35}.lc-chart-dim .lc-turn-on{opacity:1}.lc-chart-tip{z-index:5;box-sizing:border-box;overflow-wrap:break-word;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);max-width:100%;color:var(--dsw-alias-label-primary);pointer-events:none;border-radius:6px;flex-direction:column;gap:2px;padding:3px 8px;font-size:12px;display:flex;position:absolute;bottom:calc(100% + 4px);left:0;box-shadow:0 2px 8px #0000002e}.lc-bar:hover{background:var(--dsw-alias-bg-layer-2)}.lc-bar-hovered{outline-color:var(--dsw-alias-brand-primary)}.lc-bar-selected{outline:2px solid var(--dsw-alias-label-primary);outline-offset:1px}.lc-bar-in-turn{background:#80808024}.lc-bar-stack{flex-direction:column-reverse;width:100%;display:flex}.lc-bar-stack>div{width:100%}.lc-bar-stack>div:first-child{border-radius:0 0 2px 2px}.lc-bar-stack>div:last-child{border-radius:2px 2px 0 0}.lc-bar-stack>div:only-child{border-radius:2px}.lc-bar-up,.lc-bar-down{width:100%;display:flex;position:absolute;left:0}.lc-bar-up{flex-direction:column-reverse}.lc-bar-down{flex-direction:column}.lc-bar-up>div,.lc-bar-down>div{width:100%}.lc-bar-up>div:last-child{border-radius:2px 2px 0 0}.lc-bar-down>div:last-child{border-radius:0 0 2px 2px}.lc-cat-seg{transition:filter var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out)}.lc-chart[data-catdim] .lc-cat-seg{opacity:.35}.lc-chart[data-catdim=system] .lc-cat-seg[data-cat=system],.lc-chart[data-catdim=tools] .lc-cat-seg[data-cat=tools],.lc-chart[data-catdim=user] .lc-cat-seg[data-cat=user],.lc-chart[data-catdim=inject] .lc-cat-seg[data-cat=inject],.lc-chart[data-catdim=assistant] .lc-cat-seg[data-cat=assistant],.lc-chart[data-catdim=tool] .lc-cat-seg[data-cat=tool]{opacity:1;filter:brightness(1.18)}.lc-bar-marker{color:var(--dsw-alias-state-warn-primary);font-size:11px;position:absolute;top:-16px;left:50%;transform:translate(-50%)}.lc-turns{gap:2px;width:max-content;min-width:100%;margin-top:4px;display:flex;overflow:hidden}.lc-turn{box-sizing:border-box;text-align:center;color:var(--dsw-alias-label-secondary);white-space:nowrap;cursor:pointer;height:14px;transition:filter var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:3px;flex:none;font-size:10px;font-weight:600;line-height:14px}.lc-turn-on{filter:brightness(1.35);color:var(--dsw-alias-label-primary)}.lc-turn-label{display:inline-block}";
8842
8888
  const tagId$8 = "dsh-context/trendChart.css";
8843
8889
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$8) + "]") === null) {
8844
8890
  const tag = document.createElement("style");
@@ -8893,7 +8939,7 @@ window.__ModuleLoader__.load({
8893
8939
  }
8894
8940
  //#endregion
8895
8941
  //#region \0dsh-global-css:/home/runner/work/dsh-context/dsh-context/src/client/styles/browser.css.mjs
8896
- const css$3 = ".lc-br-dna-ctl{flex:none;margin-left:0}.lc-br-bar-dna .lc-stacked-seg{box-shadow:inset -1px 0 0 var(--dsw-alias-bg-layer-1)}.lc-br-hint{color:var(--dsw-alias-label-secondary);white-space:nowrap;margin-left:auto;font-size:11px;font-weight:400}.lc-br-pick{font:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;max-width:240px;padding:3px 6px;font-size:12px}.lc-br-meta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:6px 16px;margin-bottom:8px;display:flex}.lc-br-meta b{color:var(--dsw-alias-label-primary)}.lc-br-meta .lc-actual{color:var(--dsw-alias-state-success-primary)}.lc-br-note{color:var(--dsw-alias-label-secondary);margin-top:8px;font-size:12px}.lc-br-retry{color:var(--dsw-alias-brand-primary);font:inherit;cursor:pointer;text-underline-offset:2px;transition:filter var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;padding:0;font-size:12px;text-decoration:underline}.lc-br-retry:hover{filter:brightness(1.15)}.lc-br-cats{flex-direction:column;gap:4px;margin-top:10px;display:flex}.lc-br-cat{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:8px;overflow:hidden}.lc-br-cat-empty{opacity:.55}.lc-br-cat-row{width:100%;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;text-align:left;transition:background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;align-items:center;gap:8px;padding:7px 10px;display:flex}.lc-br-cat-row:hover,.lc-br-cat-on{background:var(--dsw-alias-interactive-bg-hover)}.lc-br-cat-on i{box-shadow:0 0 0 1px var(--dsw-alias-brand-primary)}.lc-br-cat-row i{width:8px;height:8px;transition:box-shadow var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:2px;flex:none;display:inline-block}.lc-br-cat-label{font-weight:600}.lc-br-cat-count{color:var(--dsw-alias-label-secondary);white-space:nowrap;font-size:12px}.lc-br-count-grp{flex:1;align-items:center;gap:4px;min-width:0;display:inline-flex}.lc-br-chev{width:12px;height:14px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:flex}.lc-br-chev:before{content:\"\";width:5px;height:5px;transition:transform var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-bottom:1px solid;border-right:1px solid;transform:rotate(-45deg)}.lc-br-chev-on:before{transform:rotate(45deg)}.lc-br-tokens{color:var(--dsw-alias-label-secondary);flex:none;font-size:12px}.lc-br-delta,.lc-br-tdelta{white-space:nowrap;border-radius:4px;flex:none;padding:0 5px;font-size:11px;font-weight:600}.lc-br-delta-up,.lc-br-tdelta-up{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent)}.lc-br-delta-down,.lc-br-tdelta-down{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 15%, transparent)}.lc-br-tokens-grp{flex:none;align-items:center;gap:4px;min-width:0;display:inline-flex}.lc-br-pct{text-align:right;width:36px;color:var(--dsw-alias-label-secondary);flex:none;font-size:12px}.lc-br-body{border-top:1px solid var(--dsw-alias-border-l1);flex-direction:column;gap:2px;padding:4px 6px;display:flex}.lc-br-elem{border-radius:6px}.lc-br-elem-on{background:var(--dsw-alias-interactive-bg-active)}.lc-br-elem-row{width:100%;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;text-align:left;transition:background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;border-radius:6px;align-items:center;gap:8px;padding:5px 6px;font-size:12px;display:flex}.lc-br-elem-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.lc-br-err-dot{background:var(--dsw-alias-state-error-primary);width:6px;height:6px;box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);border-radius:50%;flex:none}.lc-br-tag{min-width:0;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);text-overflow:ellipsis;white-space:nowrap;border-radius:4px;max-width:220px;padding:0 6px;font-size:11px;overflow:hidden}.lc-br-tool-plugin{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent);border-color:#0000;border-radius:6px;font-size:12px}.lc-br-preview{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.lc-br-time{color:var(--dsw-alias-label-secondary);flex:none;font-size:11px}.lc-br-content{flex-direction:column;gap:6px;padding:2px 6px 8px 26px;display:flex}.lc-br-dim{color:var(--dsw-alias-label-secondary)}";
8942
+ const css$3 = ".lc-br-dna-ctl{flex:none;margin-left:0}.lc-br-bar-dna .lc-stacked-seg{background-image:linear-gradient(to right, transparent calc(100% - min(1px, 25%)), var(--dsw-alias-bg-layer-1) 0)}.lc-br-hint{color:var(--dsw-alias-label-secondary);white-space:nowrap;margin-left:auto;font-size:11px;font-weight:400}.lc-br-pick{font:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;max-width:240px;padding:3px 6px;font-size:12px}.lc-br-meta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:6px 16px;margin-bottom:8px;display:flex}.lc-br-meta b{color:var(--dsw-alias-label-primary)}.lc-br-meta .lc-actual{color:var(--dsw-alias-state-success-primary)}.lc-br-note{color:var(--dsw-alias-label-secondary);margin-top:8px;font-size:12px}.lc-br-retry{color:var(--dsw-alias-brand-primary);font:inherit;cursor:pointer;text-underline-offset:2px;transition:filter var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;padding:0;font-size:12px;text-decoration:underline}.lc-br-retry:hover{filter:brightness(1.15)}.lc-br-cats{flex-direction:column;gap:4px;margin-top:10px;display:flex}.lc-br-cat{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);border-radius:8px;overflow:hidden}.lc-br-cat-empty{opacity:.55}.lc-br-cat-row{width:100%;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;text-align:left;transition:background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;align-items:center;gap:8px;padding:7px 10px;display:flex}.lc-br-cat-row:hover,.lc-br-cat-on{background:var(--dsw-alias-interactive-bg-hover)}.lc-br-cat-on i{box-shadow:0 0 0 1px var(--dsw-alias-brand-primary)}.lc-br-cat-row i{width:8px;height:8px;transition:box-shadow var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:2px;flex:none;display:inline-block}.lc-br-cat-label{font-weight:600}.lc-br-cat-count{color:var(--dsw-alias-label-secondary);white-space:nowrap;font-size:12px}.lc-br-count-grp{flex:1;align-items:center;gap:4px;min-width:0;display:inline-flex}.lc-br-chev{width:12px;height:14px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:flex}.lc-br-chev:before{content:\"\";width:5px;height:5px;transition:transform var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-bottom:1px solid;border-right:1px solid;transform:rotate(-45deg)}.lc-br-chev-on:before{transform:rotate(45deg)}.lc-br-tokens{color:var(--dsw-alias-label-secondary);flex:none;font-size:12px}.lc-br-delta,.lc-br-tdelta{white-space:nowrap;border-radius:4px;flex:none;padding:0 5px;font-size:11px;font-weight:600}.lc-br-delta-up,.lc-br-tdelta-up{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent)}.lc-br-delta-down,.lc-br-tdelta-down{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 15%, transparent)}.lc-br-tokens-grp{flex:none;align-items:center;gap:4px;min-width:0;display:inline-flex}.lc-br-pct{text-align:right;width:36px;color:var(--dsw-alias-label-secondary);flex:none;font-size:12px}.lc-br-body{border-top:1px solid var(--dsw-alias-border-l1);flex-direction:column;gap:2px;padding:4px 6px;display:flex}.lc-br-elem{border-radius:6px}.lc-br-elem-on{background:var(--dsw-alias-interactive-bg-active)}.lc-br-elem-row{width:100%;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;text-align:left;transition:background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);background:0 0;border:0;border-radius:6px;align-items:center;gap:8px;padding:5px 6px;font-size:12px;display:flex}.lc-br-elem-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.lc-br-err-dot{background:var(--dsw-alias-state-error-primary);width:6px;height:6px;box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);border-radius:50%;flex:none}.lc-br-tag{min-width:0;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);text-overflow:ellipsis;white-space:nowrap;border-radius:4px;max-width:220px;padding:0 6px;font-size:11px;overflow:hidden}.lc-br-tool-plugin{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent);border-color:#0000;border-radius:6px;font-size:12px}.lc-br-preview{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.lc-br-time{color:var(--dsw-alias-label-secondary);flex:none;font-size:11px}.lc-br-content{flex-direction:column;gap:6px;padding:2px 6px 8px 26px;display:flex}.lc-br-dim{color:var(--dsw-alias-label-secondary)}";
8897
8943
  const tagId$3 = "dsh-context/browser.css";
8898
8944
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
8899
8945
  const tag = document.createElement("style");
package/lib/index.d.ts CHANGED
@@ -56,6 +56,15 @@ interface HeadersState {
56
56
  headers: StoredHeaderRecord[];
57
57
  }
58
58
  //#endregion
59
+ //#region src/host/logShapes.d.ts
60
+ /**
61
+ * The decode bucket a stream's `block-start.blockType` names: the model's
62
+ * thinking, the answer text, or the tool-call arguments. Undefined for an
63
+ * unknown/hostile marker, whose interval then stays unattributed rather than
64
+ * poisoning a bucket.
65
+ */
66
+ type DecodeKind = 'reasoning' | 'text' | 'toolarg';
67
+ //#endregion
59
68
  //#region src/host/fold.d.ts
60
69
  /**
61
70
  * History retention bounds (configurable since 0.11 — see config.ts; these
@@ -153,10 +162,21 @@ interface TimelineState {
153
162
  * one those events close — a hostile interleaved log degrades to skipped
154
163
  * durations, never to unbounded state. Same arm/remove lifecycle as
155
164
  * `pendingShadowedSeqs`.
165
+ *
166
+ * `decode` and `block` carry the generation split (reasoning / answer text /
167
+ * tool arguments — see TimingTotals): a V0 log's `assistant/chunk`
168
+ * `block-start` markers open `block` and close the previous one into
169
+ * `decode`; a V2+ log carries no such events, so `decode` stays absent and
170
+ * `assistant/message` reads the spans off its embedded stream instead.
156
171
  */
157
172
  stepStart?: {
158
173
  time: number;
159
174
  firstToken?: number;
175
+ decode?: Record<DecodeKind, number>;
176
+ block?: {
177
+ kind: DecodeKind;
178
+ since: number;
179
+ };
160
180
  };
161
181
  /**
162
182
  * Tool callId → the call's name, start instant, and raw arguments, armed by
@@ -503,6 +523,18 @@ interface ToolTimingTotals {
503
523
  * both only over calls whose stream carried a token delta, `toolsMs` the sum
504
524
  * of per-call tool durations (parallel calls each count, so it can overlap).
505
525
  * Absent until the first step lifecycle completes in the log.
526
+ *
527
+ * The generation window itself splits by WHAT was being decoded, off the
528
+ * stream's `block-start` framing (`blockType`): `reasoningMs` (the model's
529
+ * thinking), `textMs` (the answer text), and `toolArgMs` (the tool-call
530
+ * arguments). Each marker owns the interval up to the next one (the last one
531
+ * up to the assistant message), so the three tile the marker span and together
532
+ * account for essentially all of `genMs` — the span opens at the first marker,
533
+ * which can sit marginally before the first token, so it is not an exact
534
+ * partition. They are ADDITIVE-OPTIONAL: cached projection rows written before
535
+ * the split carry `genMs` without them, so the card falls back to the
536
+ * un-split shape instead of the cache row being discarded (the
537
+ * stateVersion-15 rationale in host/timeline.ts).
506
538
  */
507
539
  interface TimingTotals {
508
540
  /** Summed wall time of completed steps (the session's active time). */
@@ -511,6 +543,12 @@ interface TimingTotals {
511
543
  ttftMs: number;
512
544
  /** Summed first-token → assistant-message time (the generation). */
513
545
  genMs: number;
546
+ /** Reasoning-decode slice of `genMs` (the model's thinking). */
547
+ reasoningMs?: number;
548
+ /** Answer-text decode slice of `genMs`. */
549
+ textMs?: number;
550
+ /** Tool-call-argument decode slice of `genMs`. */
551
+ toolArgMs?: number;
514
552
  /** Completed model calls (assistant messages folded). */
515
553
  calls: number;
516
554
  /** Summed per-call durations of completed tool calls. */
@@ -530,7 +568,7 @@ interface CostFamilyUsage {
530
568
  * token totals per DeepSeek V4 model family (matched on the model NAME,
531
569
  * provider-agnostic) and pricing period. The Client prices these with its
532
570
  * hardcoded list-price table in the locale's currency. Absent until a
533
- * deepseek-v4-flash / deepseek-v4-pro request reports usage.
571
+ * deepseek-v4.1-flash / deepseek-v4-pro request reports usage.
534
572
  */
535
573
  interface SessionCostUsage {
536
574
  flash?: CostFamilyUsage;
package/lib/index.js CHANGED
@@ -596,7 +596,7 @@ function isInjection(source) {
596
596
  *
597
597
  * - V0 (dsh 0.1.2-rc.1): `request/header.header.system`, `assistant/chunk`
598
598
  * stream events, `SurfaceOp { start, end }`, `tool/code-dispatch`.
599
- * - V3 (dsh 0.1.5-alpha.1+): `system/message` surface nodes,
599
+ * - V3 (dsh 0.1.5-alpha.x+): `system/message` surface nodes,
600
600
  * `assistant/message.data.stream` / `assistant/attempt.data.stream`,
601
601
  * `SurfaceOp { startSeq, endSeq }`, `tool/ptc-dispatch`.
602
602
  *
@@ -627,6 +627,45 @@ function isTokenChunk(chunk) {
627
627
  default: return false;
628
628
  }
629
629
  }
630
+ /** Map one `blockType` to its timing bucket (see {@link DecodeKind}). */
631
+ function decodeKindOfBlock(blockType) {
632
+ if (blockType === "reasoning") return "reasoning";
633
+ if (blockType === "text") return "text";
634
+ if (blockType === "tool-call") return "toolarg";
635
+ }
636
+ /**
637
+ * Per-kind decode spans inside one embedded assistant stream, tiling
638
+ * [first block-start, endTime]: each `block-start` record owns the interval up
639
+ * to the next one, the last one up to `endTime`. This is the V2+ shape, whose
640
+ * timed stream rides the settlement (`assistant/message.data.stream`) instead
641
+ * of separate `assistant/chunk` events. Total over untrusted input — a
642
+ * malformed record is skipped, a non-finite boundary yields zero, and a stream
643
+ * with no marker (or not an array) yields all zeros.
644
+ */
645
+ function decodeSpansOfStream(stream, endTime) {
646
+ const spans = {
647
+ reasoning: 0,
648
+ text: 0,
649
+ toolarg: 0
650
+ };
651
+ if (!Array.isArray(stream) || !Number.isFinite(endTime)) return spans;
652
+ let kind;
653
+ let since = 0;
654
+ for (const record of stream) {
655
+ if (record === null || typeof record !== "object") continue;
656
+ const r = record;
657
+ if (r.type !== "chunk" || r.chunk === null || typeof r.chunk !== "object") continue;
658
+ const chunk = r.chunk;
659
+ if (chunk.type !== "block-start") continue;
660
+ const time = r.time;
661
+ if (typeof time !== "number" || !Number.isFinite(time)) continue;
662
+ if (kind !== void 0) spans[kind] += Math.max(0, time - since);
663
+ kind = decodeKindOfBlock(chunk.blockType);
664
+ since = time;
665
+ }
666
+ if (kind !== void 0) spans[kind] += Math.max(0, endTime - since);
667
+ return spans;
668
+ }
630
669
  /**
631
670
  * The first token's instant inside one PACKED delta run (`text-chunks` /
632
671
  * `reasoning-chunks` / `tool-call-chunks`): the run's base time plus the
@@ -1237,7 +1276,7 @@ function tokenCountOf(value) {
1237
1276
  /**
1238
1277
  * The DeepSeek V4 model family a model name prices as — matched on the NAME
1239
1278
  * alone (provider-agnostic: official API, proxies, OpenRouter spellings like
1240
- * `deepseek/deepseek-v4-flash` all land here). Null for any other model:
1279
+ * `deepseek/deepseek-v4.1-flash` all land here). Null for any other model:
1241
1280
  * non-V4 usage is simply not priced.
1242
1281
  */
1243
1282
  function costFamilyOf(model) {
@@ -1300,6 +1339,12 @@ function accumulateCost(st, time, usage) {
1300
1339
  */
1301
1340
  /** The timing card's per-tool ranking cap: the busiest 16 names are kept. */
1302
1341
  const TOOL_TIMING_CAP = 16;
1342
+ /** The decode buckets of the generation split, in card order (see TimingTotals). */
1343
+ const DECODE_KINDS = [
1344
+ "reasoning",
1345
+ "text",
1346
+ "toolarg"
1347
+ ];
1303
1348
  /** Non-negative, NaN-proof duration between two instants (hostile times degrade to 0). */
1304
1349
  function durOf(from, to) {
1305
1350
  if (!Number.isFinite(from) || !Number.isFinite(to)) return 0;
@@ -1323,6 +1368,18 @@ function ensureTiming(st) {
1323
1368
  return st.timing;
1324
1369
  }
1325
1370
  /**
1371
+ * Fold one block's decode span into the totals' generation split (see
1372
+ * TimingTotals). A zero span stays ABSENT — the field then carries the
1373
+ * "no time was decoded in this bucket" fact without adding dead properties to
1374
+ * every pre-split-shaped state, and the card reads absence as 0.
1375
+ */
1376
+ function addDecode(timing, kind, ms) {
1377
+ if (!(ms > 0)) return;
1378
+ if (kind === "reasoning") timing.reasoningMs = (timing.reasoningMs ?? 0) + ms;
1379
+ else if (kind === "text") timing.textMs = (timing.textMs ?? 0) + ms;
1380
+ else timing.toolArgMs = (timing.toolArgMs ?? 0) + ms;
1381
+ }
1382
+ /**
1326
1383
  * Tally one completed tool call into the per-name ranking, bounded to
1327
1384
  * TOOL_TIMING_CAP names: repeated names update in place, a new name beyond
1328
1385
  * the cap evicts the smallest tally first (the ranking's tail), so state
@@ -1462,11 +1519,34 @@ function applyTimeline(state, event, bounds) {
1462
1519
  }
1463
1520
  case "assistant/chunk": {
1464
1521
  const start = state.stepStart;
1465
- if (start === void 0 || start.firstToken !== void 0) return state;
1522
+ if (start === void 0) return state;
1523
+ const chunk = data?.chunk;
1524
+ if (chunk !== null && typeof chunk === "object" && chunk.type === "block-start") {
1525
+ const kind = decodeKindOfBlock(chunk.blockType);
1526
+ if (start.block === void 0 && kind === void 0) return state;
1527
+ const s = ensure();
1528
+ const decode = { ...start.decode ?? {
1529
+ reasoning: 0,
1530
+ text: 0,
1531
+ toolarg: 0
1532
+ } };
1533
+ if (start.block !== void 0) decode[start.block.kind] += durOf(start.block.since, event.time);
1534
+ s.stepStart = {
1535
+ time: start.time,
1536
+ ...start.firstToken !== void 0 ? { firstToken: start.firstToken } : {},
1537
+ decode,
1538
+ ...kind !== void 0 ? { block: {
1539
+ kind,
1540
+ since: event.time
1541
+ } } : {}
1542
+ };
1543
+ break;
1544
+ }
1545
+ if (start.firstToken !== void 0) return state;
1466
1546
  if (!isTokenChunk(data?.chunk)) return state;
1467
1547
  const s = ensure();
1468
1548
  s.stepStart = {
1469
- time: start.time,
1549
+ ...start,
1470
1550
  firstToken: event.time
1471
1551
  };
1472
1552
  break;
@@ -1613,6 +1693,14 @@ function applyTimeline(state, event, bounds) {
1613
1693
  if (firstToken !== void 0) {
1614
1694
  timing.ttftMs += durOf(stepStart.time, firstToken);
1615
1695
  timing.genMs += durOf(firstToken, event.time);
1696
+ if (stepStart.decode !== void 0) {
1697
+ const decode = { ...stepStart.decode };
1698
+ if (stepStart.block !== void 0) decode[stepStart.block.kind] += durOf(stepStart.block.since, event.time);
1699
+ for (const kind of DECODE_KINDS) addDecode(timing, kind, decode[kind]);
1700
+ } else {
1701
+ const spans = decodeSpansOfStream(data?.stream, event.time);
1702
+ for (const kind of DECODE_KINDS) addDecode(timing, kind, spans[kind]);
1703
+ }
1616
1704
  }
1617
1705
  }
1618
1706
  const asstMsg = deriveEventMessage(event);
@@ -2254,6 +2342,9 @@ const timingTotalsSchema = z.object({
2254
2342
  wallMs: z.number().nonnegative(),
2255
2343
  ttftMs: z.number().nonnegative(),
2256
2344
  genMs: z.number().nonnegative(),
2345
+ reasoningMs: z.number().nonnegative().optional(),
2346
+ textMs: z.number().nonnegative().optional(),
2347
+ toolArgMs: z.number().nonnegative().optional(),
2257
2348
  calls: z.number().int().nonnegative(),
2258
2349
  toolsMs: z.number().nonnegative(),
2259
2350
  toolCalls: z.number().int().nonnegative(),
@@ -2347,7 +2438,20 @@ const timelineStateSchema = z.object({
2347
2438
  timing: timingTotalsSchema.optional(),
2348
2439
  stepStart: z.object({
2349
2440
  time: z.number(),
2350
- firstToken: z.number().optional()
2441
+ firstToken: z.number().optional(),
2442
+ decode: z.object({
2443
+ reasoning: z.number(),
2444
+ text: z.number(),
2445
+ toolarg: z.number()
2446
+ }).strict().optional(),
2447
+ block: z.object({
2448
+ kind: z.enum([
2449
+ "reasoning",
2450
+ "text",
2451
+ "toolarg"
2452
+ ]),
2453
+ since: z.number()
2454
+ }).strict().optional()
2351
2455
  }).strict().optional(),
2352
2456
  callNames: z.record(z.string(), z.object({
2353
2457
  name: z.string(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.47.0",
3
+ "version": "0.48.0",
4
4
  "description": "A DeepSeek Harness plugin for context insight and management, with context dashboard and context command, for understanding how the context is made of, and how it evolves.",
5
5
  "author": "bowenliang123",
6
6
  "repository": {
@@ -66,7 +66,7 @@
66
66
  "dshReleases": {
67
67
  "0.1.2-rc.1": "compatible",
68
68
  "0.1.3-alpha.2": "compatible",
69
- "0.1.5-alpha.1": "compatible"
69
+ "0.1.5-alpha.2": "compatible"
70
70
  }
71
71
  }
72
72
  },