dsh-context 0.61.0 → 0.62.1

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/lib/client.js CHANGED
@@ -83,6 +83,7 @@ window.__ModuleLoader__.load({
83
83
  "timing.empty": "暂无耗时数据 · 对话开始后这里会显示时长分布",
84
84
  "timing.tps": "{tps} tok/s",
85
85
  "timing.tpsTip": "输出速度(TPS):累计解码输出 tokens ÷ 累计解码时长,与 DeepSeek Harness 会话统计同口径。",
86
+ "timing.strip": "耗时时间轴:各段按发生次序紧凑铺排,宽度为耗时占比,刻度为零、四分位与全程",
86
87
  "tokens.title": "Token 统计",
87
88
  "tokens.total": "总用量",
88
89
  "tokens.output": "输出",
@@ -480,6 +481,7 @@ window.__ModuleLoader__.load({
480
481
  "timing.empty": "No timing data yet — durations appear as the conversation runs",
481
482
  "timing.tps": "{tps} tok/s",
482
483
  "timing.tpsTip": "Output speed (TPS): summed decoded output tokens over summed decode time — the same pairing as the harness session stats.",
484
+ "timing.strip": "Session-time strip: slices packed in occurrence order, width as share of the active time; ticks at zero, the quartiles, and the full span",
483
485
  "tokens.title": "Token Stats",
484
486
  "tokens.total": "Total",
485
487
  "tokens.output": "Output",
@@ -1376,7 +1378,8 @@ window.__ModuleLoader__.load({
1376
1378
  ...typeof data.surfaceFloor === "number" ? { surfaceFloor: data.surfaceFloor } : {},
1377
1379
  ...typeof data.archiveFloor === "number" ? { archiveFloor: data.archiveFloor } : {},
1378
1380
  ...data.fileOps !== void 0 ? { fileOps: objectsOf(data.fileOps) } : {},
1379
- ...typeof data.fileOpsFloor === "number" ? { fileOpsFloor: data.fileOpsFloor } : {}
1381
+ ...typeof data.fileOpsFloor === "number" ? { fileOpsFloor: data.fileOpsFloor } : {},
1382
+ ...data.spans !== void 0 ? { spans: objectsOf(data.spans) } : {}
1380
1383
  };
1381
1384
  }
1382
1385
  /**
@@ -2238,7 +2241,8 @@ window.__ModuleLoader__.load({
2238
2241
  ...typeof data.surfaceFloor === "number" ? { surfaceFloor: data.surfaceFloor } : {},
2239
2242
  ...typeof data.archiveFloor === "number" ? { archiveFloor: data.archiveFloor } : {},
2240
2243
  ...data.fileOps !== void 0 ? { fileOps: objectsOf(data.fileOps) } : {},
2241
- ...typeof data.fileOpsFloor === "number" ? { fileOpsFloor: data.fileOpsFloor } : {}
2244
+ ...typeof data.fileOpsFloor === "number" ? { fileOpsFloor: data.fileOpsFloor } : {},
2245
+ ...data.spans !== void 0 ? { spans: objectsOf(data.spans) } : {}
2242
2246
  };
2243
2247
  }
2244
2248
  /**
@@ -2446,7 +2450,8 @@ window.__ModuleLoader__.load({
2446
2450
  ...detail.surfaceFloor !== void 0 ? { surfaceFloor: detail.surfaceFloor } : {},
2447
2451
  ...detail.archiveFloor !== void 0 ? { archiveFloor: detail.archiveFloor } : {},
2448
2452
  ...detail.fileOps !== void 0 ? { fileOps: detail.fileOps } : {},
2449
- ...detail.fileOpsFloor !== void 0 ? { fileOpsFloor: detail.fileOpsFloor } : {}
2453
+ ...detail.fileOpsFloor !== void 0 ? { fileOpsFloor: detail.fileOpsFloor } : {},
2454
+ ...detail.spans !== void 0 ? { spans: detail.spans } : {}
2450
2455
  },
2451
2456
  detailState: "ready",
2452
2457
  retryDetail: retry
@@ -5518,7 +5523,7 @@ window.__ModuleLoader__.load({
5518
5523
  * widening after this many slots and late bands simply join within the cap (stackedBar.css delays by
5519
5524
  * `--lc-i`); the composition bar's few category segments never reach it.
5520
5525
  */
5521
- const STAGGER_CAP = 8;
5526
+ const STAGGER_CAP$1 = 8;
5522
5527
  function makeStackedBar(kit) {
5523
5528
  const { t, fmt, catLabel } = kit;
5524
5529
  return function StackedBar(props) {
@@ -5601,7 +5606,7 @@ window.__ModuleLoader__.load({
5601
5606
  style: {
5602
5607
  width: `${widths[i]}%`,
5603
5608
  backgroundColor: p.color,
5604
- "--lc-i": Math.min(i, STAGGER_CAP)
5609
+ "--lc-i": Math.min(i, STAGGER_CAP$1)
5605
5610
  },
5606
5611
  onMouseEnter: () => {
5607
5612
  if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
@@ -7988,6 +7993,126 @@ window.__ModuleLoader__.load({
7988
7993
  };
7989
7994
  }
7990
7995
  //#endregion
7996
+ //#region src/client/components/timingStrip.tsx
7997
+ /**
7998
+ * The timing card's session-time strip: the fold's painted spans (every
7999
+ * completed step's TTFT wait, decode blocks in stream order, tool-run
8000
+ * windows, and in-step residue) packed shoulder-to-shoulder in occurrence
8001
+ * order — the strip reads as WHERE THE TIME WENT, not when: each band's
8002
+ * width is its share of the session's cumulative active time (the donut's
8003
+ * Active Time), and idle time between steps takes no track at all. The DNA
8004
+ * idiom applied to time consumed. The axis runs 0 → the total active time
8005
+ * with quartile ticks between (0, ¼, ½, ¾, full). Hover relays the span's
8006
+ * KIND through the card's shared hover key, so every span of a category
8007
+ * lights together with its donut arc and legend row (the DNA
8008
+ * lifetime-highlight idiom); the floating tip answers only the strip's OWN
8009
+ * pointer — a mirrored legend/donut hover lights the matching bands without
8010
+ * floating a second tip over a surface the pointer is not on (the stacked
8011
+ * bar's mirror rule).
8012
+ */
8013
+ /** The strip's palette: the timing card's own slice colors, keyed by span kind (statsTiming.tsx shares it). */
8014
+ const TIMING_COLOR = {
8015
+ ttft: "var(--color-blue-500)",
8016
+ reasoning: "var(--color-violet-500)",
8017
+ text: "var(--color-pink-500)",
8018
+ toolarg: "var(--color-amber-500)",
8019
+ tools: "var(--color-teal-500)",
8020
+ other: "var(--color-slate-400)"
8021
+ };
8022
+ /** The legend-label key of each span kind (the timing card's own vocabulary). */
8023
+ const KIND_LABEL = {
8024
+ ttft: "timing.ttft",
8025
+ reasoning: "timing.reasoning",
8026
+ text: "timing.text",
8027
+ toolarg: "timing.toolArgs",
8028
+ tools: "timing.tools",
8029
+ other: "timing.other"
8030
+ };
8031
+ /** Entrance stagger cap (the stacked bar's own rule): late bands join within the cap so a long span list settles fast. */
8032
+ const STAGGER_CAP = 8;
8033
+ /** The axis's fixed tick fractions: zero, the three quartiles, and the full span. */
8034
+ const TICK_FRACS = [
8035
+ 0,
8036
+ .25,
8037
+ .5,
8038
+ .75,
8039
+ 1
8040
+ ];
8041
+ function makeTimingStrip(kit) {
8042
+ const { t, fmtDuration, fmtTime } = kit;
8043
+ return function TimingStrip(props) {
8044
+ const [tipKey, setTipKey] = (0, react.useState)(null);
8045
+ const valid = props.spans.filter((s) => Object.hasOwn(TIMING_COLOR, s.kind) && Number.isFinite(s.start) && Number.isFinite(s.end) && s.end > s.start);
8046
+ if (valid.length === 0) return null;
8047
+ let total = 0;
8048
+ for (const s of valid) total += s.end - s.start;
8049
+ let cursor = 0;
8050
+ const bands = valid.map((s, i) => {
8051
+ const leftPct = cursor / total * 100;
8052
+ cursor += s.end - s.start;
8053
+ return {
8054
+ key: i,
8055
+ kind: s.kind,
8056
+ leftPct,
8057
+ widthPct: (s.end - s.start) / total * 100,
8058
+ tip: `${t(KIND_LABEL[s.kind])} ${fmtDuration(s.end - s.start)} · ${fmtTime(s.start)}`
8059
+ };
8060
+ });
8061
+ const hoverKey = props.hoverKey;
8062
+ const hovering = hoverKey !== null && hoverKey !== void 0 && bands.some((b) => b.kind === hoverKey);
8063
+ let tip = null;
8064
+ for (const b of bands) if (b.key === tipKey) {
8065
+ tip = {
8066
+ text: b.tip,
8067
+ leftPct: Math.max(10, Math.min(b.leftPct + b.widthPct / 2, 90))
8068
+ };
8069
+ break;
8070
+ }
8071
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8072
+ className: "lc-tstrip",
8073
+ role: "img",
8074
+ "aria-label": t("timing.strip"),
8075
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8076
+ className: "lc-tstrip-wrap",
8077
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8078
+ className: "lc-tstrip-bar" + (hovering ? " lc-tstrip-dim" : ""),
8079
+ onMouseLeave: () => {
8080
+ setTipKey(null);
8081
+ props.onHoverKey?.(null);
8082
+ },
8083
+ children: bands.map((b, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8084
+ className: "lc-tstrip-seg animate-lc-stacked-in motion-reduce:animate-none" + (hoverKey === b.kind ? " lc-tstrip-seg-on" : ""),
8085
+ style: {
8086
+ left: `${b.leftPct}%`,
8087
+ width: `${b.widthPct}%`,
8088
+ background: TIMING_COLOR[b.kind],
8089
+ "--lc-i": Math.min(i, STAGGER_CAP)
8090
+ },
8091
+ onMouseEnter: () => {
8092
+ setTipKey(b.key);
8093
+ props.onHoverKey?.(b.kind);
8094
+ }
8095
+ }, b.key))
8096
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8097
+ className: "lc-tip lc-bar-tip" + (tip !== null ? " lc-bar-tip-on" : ""),
8098
+ style: { left: `${tip !== null ? tip.leftPct : 50}%` },
8099
+ children: tip !== null ? tip.text : ""
8100
+ })]
8101
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8102
+ className: "lc-tstrip-axis",
8103
+ children: TICK_FRACS.map((frac, i) => {
8104
+ const ms = total * frac;
8105
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8106
+ className: "lc-tstrip-tick" + (i === 0 ? " lc-tstrip-tick-first" : i === TICK_FRACS.length - 1 ? " lc-tstrip-tick-last" : ""),
8107
+ style: { left: `${frac * 100}%` },
8108
+ children: ms === 0 ? "0" : fmtDuration(ms)
8109
+ }, frac);
8110
+ })
8111
+ })]
8112
+ });
8113
+ };
8114
+ }
8115
+ //#endregion
7991
8116
  //#region src/client/components/statsTiming.tsx
7992
8117
  /**
7993
8118
  * The Timing card: where the session's ACTIVE time went. The donut splits
@@ -8006,16 +8131,13 @@ window.__ModuleLoader__.load({
8006
8131
  * read as "no time, many calls". The donut and the rows sit side by side so
8007
8132
  * the head row stays half-height. Parallel tool calls each count, so the tools
8008
8133
  * figure can overlap — the ring clamps it into the post-model window while the
8009
- * row numbers stay true.
8134
+ * row numbers stay true. Below them the session-time strip packs every
8135
+ * completed step's slices gapless in occurrence order (the DNA idiom applied
8136
+ * to time consumed — each band's width is its share of the cumulative active
8137
+ * time, idle time takes no track), joining the same hover link by KIND.
8010
8138
  */
8011
- const COLOR = {
8012
- ttft: "var(--color-blue-500)",
8013
- reasoning: "var(--color-violet-500)",
8014
- text: "var(--color-pink-500)",
8015
- toolarg: "var(--color-amber-500)",
8016
- tools: "var(--color-teal-500)",
8017
- other: "var(--color-slate-400)"
8018
- };
8139
+ /** The card's slice palette (timingStrip.tsx owns it — the strip paints the same kinds). */
8140
+ const COLOR = TIMING_COLOR;
8019
8141
  /**
8020
8142
  * The decode-throughput figure, formatted exactly as the harness's own
8021
8143
  * session-stats seat formats it: whole tokens from ten up, one decimal below.
@@ -8027,6 +8149,7 @@ window.__ModuleLoader__.load({
8027
8149
  function makeStatsTiming(kit, Donut) {
8028
8150
  const { t, fmt, fmtDuration, fmtShare } = kit;
8029
8151
  const SliceList = makeSliceList(kit);
8152
+ const TimingStrip = makeTimingStrip(kit);
8030
8153
  return function StatsTiming(props) {
8031
8154
  const [hoverKey, setHoverKey] = (0, react.useState)(null);
8032
8155
  const timing = props.timing;
@@ -8158,7 +8281,7 @@ window.__ModuleLoader__.load({
8158
8281
  }), rows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8159
8282
  className: "lc-empty",
8160
8283
  children: t("timing.empty")
8161
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8284
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8162
8285
  className: "lc-donut-row flex items-center justify-start gap-3 min-w-0 @max-[320px]/lc-card:gap-2 @max-[240px]/lc-card:flex-wrap",
8163
8286
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Donut, {
8164
8287
  segments,
@@ -8172,7 +8295,11 @@ window.__ModuleLoader__.load({
8172
8295
  hoverKey,
8173
8296
  onHoverKey: setHoverKey
8174
8297
  })]
8175
- })]
8298
+ }), props.spans !== void 0 && props.spans.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimingStrip, {
8299
+ spans: props.spans,
8300
+ hoverKey,
8301
+ onHoverKey: setHoverKey
8302
+ }) : null] })]
8176
8303
  });
8177
8304
  };
8178
8305
  }
@@ -11277,7 +11404,7 @@ window.__ModuleLoader__.load({
11277
11404
  return function PluginInfo() {
11278
11405
  const [latest, setLatest] = (0, react.useState)(null);
11279
11406
  (0, react.useEffect)(() => {
11280
- if ("0.61.0".includes("-dev")) return;
11407
+ if ("0.62.1".includes("-dev")) return;
11281
11408
  let on = true;
11282
11409
  fetchLatestVersion().then((v) => {
11283
11410
  if (on && v) setLatest(v);
@@ -11286,8 +11413,8 @@ window.__ModuleLoader__.load({
11286
11413
  on = false;
11287
11414
  };
11288
11415
  }, []);
11289
- const update = latest !== null && isNewerVersion(latest, "0.61.0") ? latest : null;
11290
- const nameText = "dsh-context (v0.61.0)";
11416
+ const update = latest !== null && isNewerVersion(latest, "0.62.1") ? latest : null;
11417
+ const nameText = "dsh-context (v0.62.1)";
11291
11418
  const nameValue = [nameText];
11292
11419
  if (update) nameValue.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
11293
11420
  className: "lc-pi-update",
@@ -12643,7 +12770,10 @@ window.__ModuleLoader__.load({
12643
12770
  usage,
12644
12771
  current: data.current,
12645
12772
  breakdown
12646
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatsTiming, { timing: data.timing ?? null })]
12773
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatsTiming, {
12774
+ timing: data.timing ?? null,
12775
+ spans: data.spans
12776
+ })]
12647
12777
  }),
12648
12778
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
12649
12779
  className: "lc-cols lc-cols-main",
@@ -12886,7 +13016,7 @@ window.__ModuleLoader__.load({
12886
13016
  }
12887
13017
  //#endregion
12888
13018
  //#region \0dsh-global-css:/home/runner/work/dsh-context/dsh-context/src/client/styles/stats.css.mjs
12889
- const css$13 = ".lc-stat{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;flex-direction:column;gap:2px;min-width:0;padding:6px;display:flex}a.lc-stat{color:inherit;cursor:pointer;text-decoration:none}a.lc-stat:hover{border-color:var(--dsw-alias-label-secondary)}.lc-stats .lc-stat{padding:6px 3px}.lc-stats .lc-stat-q{margin:0;position:absolute;top:3px;right:4px}.lc-stat-label{color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;font-size:11px;font-weight:600;overflow:hidden}.lc-stat-value{color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;text-align:center;font-size:14px;font-weight:600;overflow:hidden}.lc-stat-sub{color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;font-size:11px;overflow:hidden}.lc-stat-tipped{position:relative}.lc-stat-q{text-align:center;width:11px;height:11px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);cursor:help;transition:color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), border-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:50%;margin-left:4px;font-size:9px;font-style:normal;font-weight:700;line-height:11px;display:inline-block}.lc-stat-tip{max-width:280px;font-weight:400;line-height:1.5;top:calc(100% + 6px);left:0}.lc-stat-tip-prices{border-top:1px dashed var(--dsw-alias-border-l1);margin-top:5px;padding-top:5px;display:block}.lc-stat-tip-head{font-weight:600;display:block}.lc-timing-tps{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);font-variant-numeric:tabular-nums;cursor:default;border-radius:999px;flex:none;margin-left:auto;padding:1px 8px;font-size:12px;line-height:18px}.lc-stat-tip-row{margin-top:2px;display:block}.lc-stat-tip-band{justify-content:space-between;gap:2rem;display:flex}.lc-stat-tip-band i{font-style:normal}.lc-stat-tip-by{color:var(--dsw-alias-label-secondary);margin-top:3px;display:block}.lc-card-sub{color:var(--dsw-alias-label-secondary);margin-left:auto;font-size:12px;font-weight:400}.lc-overview-num{align-items:baseline;gap:6px;margin-bottom:8px;display:flex}.lc-overview-num>b{font-size:20px}.lc-overview-num span{color:var(--dsw-alias-label-secondary)}.lc-overview-pct{margin-left:auto;font-size:11px}.lc-overview-pct b{color:var(--dsw-alias-label-primary);margin-right:4px;font-size:20px}.lc-stat-head{color:var(--dsw-alias-label-secondary);margin:12px 0 8px;font-size:11px;font-weight:600}.lc-col-donut{flex-direction:column;display:flex}.lc-col-donut .lc-donut-row{flex:1}.lc-donut{flex:none;position:relative}.lc-donut svg{display:block}.lc-donut-track{stroke:color-mix(in srgb, var(--color-neutral-500) 18%, transparent)}.lc-donut-seg{transition:opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out)}.lc-donut-dim .lc-donut-seg{opacity:.35}.lc-donut-dim .lc-donut-seg-on{opacity:1}.lc-donut-center{pointer-events:none;text-align:center;box-sizing:border-box;flex-direction:column;justify-content:center;align-items:center;gap:1px;padding:0 7px;display:flex;position:absolute;inset:0}.lc-donut-center b{white-space:nowrap;text-overflow:ellipsis;max-width:100%;line-height:1.15;overflow:hidden}.lc-donut-center span{color:var(--dsw-alias-label-secondary);white-space:nowrap}.lc-sl{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:4px 20px;min-width:0;display:grid;overflow:hidden}.lc-sl-row{min-width:0;transition:background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;flex-direction:column;gap:1px;margin:0 -6px;padding:1px 6px;font-size:12px;display:flex}.lc-sl-row-on{background:var(--dsw-alias-interactive-bg-hover)}.lc-sl-row-dim{opacity:.55}.lc-sl-main{align-items:center;gap:6px;min-width:0;display:flex}.lc-sl-dot{border-radius:2px;flex:none;width:8px;height:8px}.lc-sl-label{white-space:nowrap;text-overflow:ellipsis;flex:auto;min-width:0;font-weight:600;line-height:1.25;overflow:hidden}.lc-sl-pct{text-align:right;font-variant-numeric:tabular-nums;flex:none;min-width:34px;font-weight:700}.lc-sl-sub{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;white-space:nowrap;text-overflow:ellipsis;margin-left:14px;font-size:11px;line-height:1.3;overflow:hidden}.lc-pi-grid{grid-template-columns:1fr;gap:8px;display:grid}.lc-pi-row-btn{appearance:none;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;width:100%;padding:0}.lc-pi-row{min-width:0;color:inherit;flex-direction:row;justify-content:space-between;align-items:baseline;gap:12px;text-decoration:none;display:flex}.lc-pi-hint{text-decoration:none}.lc-pi-update{color:var(--dsw-alias-state-warn-primary);white-space:nowrap;margin-left:6px;font-size:11px}.lc-pi-label{color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;flex:none;font-size:11px;font-weight:600;overflow:hidden}.lc-pi-labelicon{vertical-align:-2px;margin-right:4px;display:inline-block}.lc-pi-value{min-width:0;color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;text-align:right;font-size:13px;font-weight:600;overflow:hidden}";
13019
+ const css$13 = ".lc-stat{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;flex-direction:column;gap:2px;min-width:0;padding:6px;display:flex}a.lc-stat{color:inherit;cursor:pointer;text-decoration:none}a.lc-stat:hover{border-color:var(--dsw-alias-label-secondary)}.lc-stats .lc-stat{padding:6px 3px}.lc-stats .lc-stat-q{margin:0;position:absolute;top:3px;right:4px}.lc-stat-label{color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;font-size:11px;font-weight:600;overflow:hidden}.lc-stat-value{color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;text-align:center;font-size:14px;font-weight:600;overflow:hidden}.lc-stat-sub{color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;font-size:11px;overflow:hidden}.lc-stat-tipped{position:relative}.lc-stat-q{text-align:center;width:11px;height:11px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);cursor:help;transition:color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), border-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:50%;margin-left:4px;font-size:9px;font-style:normal;font-weight:700;line-height:11px;display:inline-block}.lc-stat-tip{max-width:280px;font-weight:400;line-height:1.5;top:calc(100% + 6px);left:0}.lc-stat-tip-prices{border-top:1px dashed var(--dsw-alias-border-l1);margin-top:5px;padding-top:5px;display:block}.lc-stat-tip-head{font-weight:600;display:block}.lc-timing-tps{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);font-variant-numeric:tabular-nums;cursor:default;border-radius:999px;flex:none;margin-left:auto;padding:1px 8px;font-size:12px;line-height:18px}.lc-stat-tip-row{margin-top:2px;display:block}.lc-stat-tip-band{justify-content:space-between;gap:2rem;display:flex}.lc-stat-tip-band i{font-style:normal}.lc-stat-tip-by{color:var(--dsw-alias-label-secondary);margin-top:3px;display:block}.lc-card-sub{color:var(--dsw-alias-label-secondary);margin-left:auto;font-size:12px;font-weight:400}.lc-overview-num{align-items:baseline;gap:6px;margin-bottom:8px;display:flex}.lc-overview-num>b{font-size:20px}.lc-overview-num span{color:var(--dsw-alias-label-secondary)}.lc-overview-pct{margin-left:auto;font-size:11px}.lc-overview-pct b{color:var(--dsw-alias-label-primary);margin-right:4px;font-size:20px}.lc-stat-head{color:var(--dsw-alias-label-secondary);margin:12px 0 8px;font-size:11px;font-weight:600}.lc-col-donut{flex-direction:column;display:flex}.lc-col-donut .lc-donut-row{flex:1}.lc-donut{flex:none;position:relative}.lc-donut svg{display:block}.lc-donut-track{stroke:color-mix(in srgb, var(--color-neutral-500) 18%, transparent)}.lc-donut-seg{transition:opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out)}.lc-donut-dim .lc-donut-seg{opacity:.35}.lc-donut-dim .lc-donut-seg-on{opacity:1}.lc-donut-center{pointer-events:none;text-align:center;box-sizing:border-box;flex-direction:column;justify-content:center;align-items:center;gap:1px;padding:0 7px;display:flex;position:absolute;inset:0}.lc-donut-center b{white-space:nowrap;text-overflow:ellipsis;max-width:100%;line-height:1.15;overflow:hidden}.lc-donut-center span{color:var(--dsw-alias-label-secondary);white-space:nowrap}.lc-sl{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:4px 20px;min-width:0;display:grid;overflow:hidden}.lc-sl-row{min-width:0;transition:background-color var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);border-radius:6px;flex-direction:column;gap:1px;margin:0 -6px;padding:1px 6px;font-size:12px;display:flex}.lc-sl-row-on{background:var(--dsw-alias-interactive-bg-hover)}.lc-sl-row-dim{opacity:.55}.lc-sl-main{align-items:center;gap:6px;min-width:0;display:flex}.lc-sl-dot{border-radius:2px;flex:none;width:8px;height:8px}.lc-sl-label{white-space:nowrap;text-overflow:ellipsis;flex:auto;min-width:0;font-weight:600;line-height:1.25;overflow:hidden}.lc-sl-pct{text-align:right;font-variant-numeric:tabular-nums;flex:none;min-width:34px;font-weight:700}.lc-sl-sub{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;white-space:nowrap;text-overflow:ellipsis;margin-left:14px;font-size:11px;line-height:1.3;overflow:hidden}.lc-pi-grid{grid-template-columns:1fr;gap:8px;display:grid}.lc-pi-row-btn{appearance:none;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;width:100%;padding:0}.lc-pi-row{min-width:0;color:inherit;flex-direction:row;justify-content:space-between;align-items:baseline;gap:12px;text-decoration:none;display:flex}.lc-pi-hint{text-decoration:none}.lc-pi-update{color:var(--dsw-alias-state-warn-primary);white-space:nowrap;margin-left:6px;font-size:11px}.lc-pi-label{color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;flex:none;font-size:11px;font-weight:600;overflow:hidden}.lc-pi-labelicon{vertical-align:-2px;margin-right:4px;display:inline-block}.lc-pi-value{min-width:0;color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;text-align:right;font-size:13px;font-weight:600;overflow:hidden}.lc-tstrip{margin-top:10px}.lc-tstrip-wrap{position:relative}.lc-tstrip-bar{background:color-mix(in srgb, var(--color-neutral-500) 18%, transparent);border-radius:4px;width:100%;height:12px;position:relative;overflow:hidden}.lc-tstrip-seg{transform-origin:0;height:100%;transition:opacity var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out), filter var(--ds-transition-duration,.2s) var(--ds-ease-in-out,ease-in-out);position:absolute;top:0}.lc-tstrip-seg-on{filter:brightness(1.18)}.lc-tstrip-dim .lc-tstrip-seg{opacity:.35}.lc-tstrip-dim .lc-tstrip-seg-on{opacity:1}.lc-tstrip-axis{height:13px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;margin-top:5px;font-size:10px;line-height:13px;position:relative}.lc-tstrip-tick{white-space:nowrap;position:absolute;transform:translate(-50%)}.lc-tstrip-tick:before{content:\"\";background:var(--dsw-alias-border-l1);width:1px;height:3px;position:absolute;top:-4px;left:50%}.lc-tstrip-tick-first{transform:none}.lc-tstrip-tick-first:before{left:0}.lc-tstrip-tick-last{transform:translate(-100%)}.lc-tstrip-tick-last:before{left:auto;right:0}";
12890
13020
  const tagId$13 = "dsh-context/stats.css";
12891
13021
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$13) + "]") === null) {
12892
13022
  const tag = document.createElement("style");
package/lib/index.d.ts CHANGED
@@ -157,6 +157,28 @@ interface TimelineState {
157
157
  time: number;
158
158
  firstToken?: number;
159
159
  };
160
+ /**
161
+ * The timing strip's painted spans (shared/types.ts TimingSpan): every
162
+ * completed step's time slices in log order, stamped with their REAL
163
+ * instants — the TTFT wait, the decode blocks in stream order, the tool-run
164
+ * windows, and the in-step residue. The client packs them gapless, each
165
+ * band's width its share of the cumulative active time (idle time BETWEEN
166
+ * steps carries no span and takes no track). Bounded by TIMING_SPANS_MAX
167
+ * (newest tail kept). Flushed per step at `step/end` from the `stepSpans`
168
+ * accumulator.
169
+ */
170
+ spans: TimingSpan[];
171
+ /**
172
+ * The open step's accumulated painted spans: the assistant message pushes
173
+ * the model slices (TTFT + the decode blocks), each paired tool result its
174
+ * run window; `step/end` clamps them into the step window, de-overlaps
175
+ * first-wins (parallel runs paint their union), fills the holes with the
176
+ * residue kind, and flushes the tiling into `spans`. Absent until the first
177
+ * in-step span; DELETED at the flush (the plain-JSON precondition). Same
178
+ * arm/remove lifecycle as `stepStart` — never written in place, always
179
+ * replaced, so no CloneKey covers it.
180
+ */
181
+ stepSpans?: TimingSpan[];
160
182
  /**
161
183
  * Tool callId → the call's name, start instant, and raw arguments, armed by
162
184
  * `tool/call` and DELETED when its `tool/result` folds in (one result per
@@ -469,6 +491,13 @@ interface Snapshot {
469
491
  */
470
492
  fileOps?: FileOpRecord[];
471
493
  fileOpsFloor?: number;
494
+ /**
495
+ * The timing strip's painted spans (see TimingSpan) — present on the INLINE
496
+ * wire value and on the detail payload, absent from the slim head (they ride
497
+ * the detail channel there) and from rows folded before the collection
498
+ * existed (older hosts; the client then shows no strip).
499
+ */
500
+ spans?: TimingSpan[];
472
501
  }
473
502
  /**
474
503
  * The on-demand DETAIL payload of the split `contextTimeline` generation —
@@ -509,6 +538,13 @@ interface ContextTimelineDetail {
509
538
  fileOps?: FileOpRecord[];
510
539
  /** The newest dropped op's seq when the op log trimmed (coverage honesty, same family as archiveFloor). */
511
540
  fileOpsFloor?: number;
541
+ /**
542
+ * The timing strip's painted spans (see TimingSpan): the completed steps'
543
+ * time slices in log order, positioned by their real instants. Additive-
544
+ * optional: a payload folded before the collection existed (older hosts)
545
+ * serves without it, and the timing card shows no strip then.
546
+ */
547
+ spans?: TimingSpan[];
512
548
  }
513
549
  /**
514
550
  * One executed file operation (a settled file-tool call with a resolved
@@ -559,6 +595,22 @@ interface FileOpRecord {
559
595
  * delivered projection is by definition available); kept for wire compatibility with the snapshot shape.
560
596
  */
561
597
  type ContextTimeline = Snapshot;
598
+ /**
599
+ * One painted span of the timing card's session-time strip (the card's bottom
600
+ * band): a completed step's time slice — the TTFT wait, one decode block (in
601
+ * stream order), one tool-run window, or an in-step residue gap — folded
602
+ * host-side at `step/end`. The instants are the slice's REAL occurrence times
603
+ * (the hover tip reads them); the strip packs the spans gapless in log order,
604
+ * each band's width its share of the session's cumulative active time (idle
605
+ * time BETWEEN steps carries no span and takes no track). `end` > `start`
606
+ * always (the fold drops zero/negative slices).
607
+ */
608
+ interface TimingSpan {
609
+ /** The slice kind — the timing card's own slice vocabulary (same keys, same colors). */
610
+ kind: 'ttft' | 'reasoning' | 'text' | 'toolarg' | 'tools' | 'other';
611
+ start: number;
612
+ end: number;
613
+ }
562
614
  /**
563
615
  * Cumulative billed-token totals for one pricing bucket of the session-cost
564
616
  * estimate (host-folded, never trimmed — running totals over the COMPLETE
package/lib/index.js CHANGED
@@ -414,6 +414,43 @@ function decodeTallyOfStream(stream, endTime) {
414
414
  return tally;
415
415
  }
416
416
  /**
417
+ * The ORDERED decode spans of one embedded assistant stream — the same
418
+ * ownership rule as {@link decodeTallyOfStream} (each `block-start` marker
419
+ * owns the interval up to the next one, the last one up to `endTime`), but
420
+ * keeping the sequence: the timing strip paints the decode window in stream
421
+ * order. Total over untrusted input, mirroring the tally: a malformed record
422
+ * is skipped whole, an unknown `blockType` leaves its interval unspanned
423
+ * (the caller's residue fill owns it), a zero/negative-length span drops
424
+ * out, and a stream with no marker (or not an array) yields none.
425
+ */
426
+ function decodeSpansOfStream(stream, endTime) {
427
+ const spans = [];
428
+ if (!Array.isArray(stream) || !Number.isFinite(endTime)) return spans;
429
+ let kind;
430
+ let since = 0;
431
+ const close = (to) => {
432
+ if (kind !== void 0 && to > since) spans.push({
433
+ kind,
434
+ start: since,
435
+ end: to
436
+ });
437
+ };
438
+ for (const record of stream) {
439
+ if (record === null || typeof record !== "object") continue;
440
+ const r = record;
441
+ if (r.type !== "chunk" || r.chunk === null || typeof r.chunk !== "object") continue;
442
+ const chunk = r.chunk;
443
+ if (chunk.type !== "block-start") continue;
444
+ const time = r.time;
445
+ if (typeof time !== "number" || !Number.isFinite(time)) continue;
446
+ close(time);
447
+ kind = decodeKindOfBlock(chunk.blockType);
448
+ since = time;
449
+ }
450
+ close(endTime);
451
+ return spans;
452
+ }
453
+ /**
417
454
  * The first token's instant inside one PACKED delta run (`text-chunks` /
418
455
  * `reasoning-chunks` / `tool-call-chunks`): the run's base time plus the
419
456
  * accumulated inter-member deltas, taken at the first qualifying member —
@@ -758,6 +795,7 @@ function trimState(st, bounds) {
758
795
  st.turnRuns = countTurnRuns(st.requests);
759
796
  }
760
797
  if (st.events.length > bounds.maxEvents) st.events = st.events.slice(-bounds.maxEvents);
798
+ if (st.spans.length > TIMING_SPANS_MAX) st.spans = st.spans.slice(-TIMING_SPANS_MAX);
761
799
  if (st.fileOps.length > bounds.maxFileOps) {
762
800
  const drop = st.fileOps.length - bounds.maxFileOps;
763
801
  st.fileOpsFloor = Math.max(st.fileOpsFloor ?? 0, st.fileOps[drop - 1].seq);
@@ -775,6 +813,13 @@ function trimState(st, bounds) {
775
813
  }
776
814
  }
777
815
  }
816
+ /**
817
+ * Bound on the timing strip's painted spans (TimelineState.spans): a count
818
+ * cap, newest tail kept — the same hardcoded-cap family as SYSTEM_NODES_MAX.
819
+ * At ~6–8 spans per step the cap covers a few hundred recent steps, far past
820
+ * the strip's pixel resolution on any real pane.
821
+ */
822
+ const TIMING_SPANS_MAX = 2e3;
778
823
  function createTimelineState() {
779
824
  return {
780
825
  surface: [],
@@ -792,7 +837,8 @@ function createTimelineState() {
792
837
  events: [],
793
838
  archived: [],
794
839
  callNames: {},
795
- fileOps: []
840
+ fileOps: [],
841
+ spans: []
796
842
  };
797
843
  }
798
844
  function categoryOf(type, message) {
@@ -969,6 +1015,11 @@ function applySurface(st, ev, type, data, message) {
969
1015
  timing.toolsMs += dur;
970
1016
  timing.toolCalls += 1;
971
1017
  bumpToolTotals(timing, toolEntry.name, dur);
1018
+ if (st.stepStart !== void 0) st.stepSpans = [...st.stepSpans ?? [], {
1019
+ kind: "tools",
1020
+ start: toolEntry.start,
1021
+ end: ev.time
1022
+ }];
972
1023
  }
973
1024
  if (typeof srcId === "string" || typeof blockId === "string") {
974
1025
  const kept = {};
@@ -1345,6 +1396,7 @@ function applyTimeline(state, event, bounds) {
1345
1396
  case "step/start": {
1346
1397
  const s = ensure([]);
1347
1398
  s.stepStart = { time: event.time };
1399
+ delete s.stepSpans;
1348
1400
  break;
1349
1401
  }
1350
1402
  case "step/end": {
@@ -1352,7 +1404,43 @@ function applyTimeline(state, event, bounds) {
1352
1404
  if (start === void 0) return state;
1353
1405
  const s = ensure(["timing"]);
1354
1406
  ensureTiming(s).wallMs += durOf(start.time, event.time);
1407
+ const painted = [];
1408
+ for (const span of state.stepSpans ?? []) {
1409
+ const from = Math.max(start.time, span.start);
1410
+ const to = Math.min(event.time, span.end);
1411
+ if (to > from) painted.push({
1412
+ kind: span.kind,
1413
+ start: from,
1414
+ end: to
1415
+ });
1416
+ }
1417
+ painted.sort((a, b) => a.start - b.start || a.end - b.end);
1418
+ const flushed = [];
1419
+ let cursor = start.time;
1420
+ for (const span of painted) {
1421
+ const from = Math.max(cursor, span.start);
1422
+ if (span.end <= from) continue;
1423
+ if (from > cursor) flushed.push({
1424
+ kind: "other",
1425
+ start: cursor,
1426
+ end: from
1427
+ });
1428
+ flushed.push({
1429
+ kind: span.kind,
1430
+ start: from,
1431
+ end: span.end
1432
+ });
1433
+ cursor = span.end;
1434
+ }
1435
+ if (event.time > cursor) flushed.push({
1436
+ kind: "other",
1437
+ start: cursor,
1438
+ end: event.time
1439
+ });
1440
+ s.spans = [...state.spans, ...flushed];
1355
1441
  delete s.stepStart;
1442
+ delete s.stepSpans;
1443
+ bumpDetailRev(s);
1356
1444
  break;
1357
1445
  }
1358
1446
  case "user/message": {
@@ -1506,6 +1594,24 @@ function applyTimeline(state, event, bounds) {
1506
1594
  if (firstToken !== void 0) {
1507
1595
  timing.ttftMs += durOf(stepStart.time, firstToken);
1508
1596
  timing.genMs += durOf(firstToken, event.time);
1597
+ const decodeBlocks = decodeSpansOfStream(data?.stream, event.time);
1598
+ let decodeStart = firstToken;
1599
+ for (const block of decodeBlocks) decodeStart = Math.min(decodeStart, block.start);
1600
+ const modelSpans = [{
1601
+ kind: "ttft",
1602
+ start: stepStart.time,
1603
+ end: decodeStart
1604
+ }];
1605
+ for (const block of decodeBlocks) {
1606
+ const to = Math.min(event.time, block.end);
1607
+ if (to <= block.start) continue;
1608
+ modelSpans.push({
1609
+ kind: block.kind,
1610
+ start: modelSpans.length === 1 ? decodeStart : block.start,
1611
+ end: to
1612
+ });
1613
+ }
1614
+ s.stepSpans = [...s.stepSpans ?? [], ...modelSpans];
1509
1615
  if (output !== null) {
1510
1616
  timing.speedMs = (timing.speedMs ?? 0) + durOf(firstToken, event.time);
1511
1617
  timing.speedTokens = (timing.speedTokens ?? 0) + output;
@@ -1641,7 +1747,8 @@ function detailCollectionsOf(state, bounds) {
1641
1747
  droppedNodes: 0,
1642
1748
  archive: state.archived.map((n) => ({ ...n })),
1643
1749
  fileOps: state.fileOps.map((o) => ({ ...o })),
1644
- ...state.fileOpsFloor !== void 0 ? { fileOpsFloor: state.fileOpsFloor } : {}
1750
+ ...state.fileOpsFloor !== void 0 ? { fileOpsFloor: state.fileOpsFloor } : {},
1751
+ spans: state.spans.map((s) => ({ ...s }))
1645
1752
  };
1646
1753
  const overflowCount = Math.max(0, state.surface.length - bounds.maxNodes);
1647
1754
  const overflow = state.surface.slice(0, overflowCount);
@@ -3103,6 +3210,19 @@ const fileOpSchema = z.object({
3103
3210
  pattern: z.literal(true).optional(),
3104
3211
  gone: z.number().int().nonnegative().optional()
3105
3212
  }).strict();
3213
+ /** One painted span of the timing strip (shared/types.ts TimingSpan). */
3214
+ const timingSpanSchema = z.object({
3215
+ kind: z.enum([
3216
+ "ttft",
3217
+ "reasoning",
3218
+ "text",
3219
+ "toolarg",
3220
+ "tools",
3221
+ "other"
3222
+ ]),
3223
+ start: z.number(),
3224
+ end: z.number()
3225
+ }).strict();
3106
3226
  const currentSchema = z.object({
3107
3227
  system: z.number().int().nonnegative(),
3108
3228
  tools: z.number().int().nonnegative(),
@@ -3198,7 +3318,8 @@ const contextTimelineSchema = z.object({
3198
3318
  surfaceFloor: z.number().int().nonnegative().optional(),
3199
3319
  archiveFloor: z.number().int().nonnegative().optional(),
3200
3320
  fileOps: z.array(fileOpSchema).optional(),
3201
- fileOpsFloor: z.number().int().nonnegative().optional()
3321
+ fileOpsFloor: z.number().int().nonnegative().optional(),
3322
+ spans: z.array(timingSpanSchema).optional()
3202
3323
  }).strict();
3203
3324
  /**
3204
3325
  * The persisted fold-state schema (the registry's `stateSchema`
@@ -3245,6 +3366,8 @@ const timelineStateSchema = z.object({
3245
3366
  detailRev: z.number().int().nonnegative().optional(),
3246
3367
  fileOps: z.array(fileOpSchema),
3247
3368
  fileOpsFloor: z.number().int().nonnegative().optional(),
3369
+ spans: z.array(timingSpanSchema),
3370
+ stepSpans: z.array(timingSpanSchema).optional(),
3248
3371
  pendingCodeOps: z.record(z.string(), z.array(fileOpSchema)).optional()
3249
3372
  });
3250
3373
  /**
@@ -3292,7 +3415,7 @@ function createContextTimelineDefinition(config, slim) {
3292
3415
  },
3293
3416
  init: () => createTimelineState(),
3294
3417
  apply: (state, event) => applyTimeline(state, event, bounds),
3295
- stateVersion: 22
3418
+ stateVersion: 23
3296
3419
  };
3297
3420
  }
3298
3421
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.61.0",
3
+ "version": "0.62.1",
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
  "icon": "icon.svg",
6
6
  "author": "bowenliang123",