dsh-context 0.62.2 → 0.63.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
@@ -174,7 +174,7 @@ A centered dialog opens with the **Current Composition** card and the **Context
174
174
 
175
175
  ## ⚙️ Settings
176
176
 
177
- The **Context** preferences card holds this plugin's per-user settings — default placement, trend granularity (Step/Turn), trend mode (Total/Delta), tool and File Activity sort, and the sidebar insights entry. Where it lives depends on your dsh release:
177
+ The **Context** preferences card holds this plugin's per-user settings — default placement, trend granularity (Step/Turn), trend mode (Total/Delta), the trend duration curve (shown by default), tool and File Activity sort, and the sidebar insights entry. Where it lives depends on your dsh release:
178
178
 
179
179
  - **dsh 0.1.7+** — the sidebar **Plugins** entry → the **dsh-context** bundle's page → its **Configuration** section (the card is served by the entry's live Config form).
180
180
  - **older releases** — **Settings → Plugins → Plugin configuration** → the **Context** card.
package/lib/client.js CHANGED
@@ -108,6 +108,7 @@ window.__ModuleLoader__.load({
108
108
  "insightsEntry.hide": "隐藏",
109
109
  "settings.gran": "趋势图 · 默认粒度",
110
110
  "settings.mode": "趋势图 · 默认展示方式",
111
+ "settings.durationCurve": "趋势图 · 耗时曲线",
111
112
  "settings.deltaBase": "上下文浏览器 · 对比基线",
112
113
  "settings.toolSort": "工具定义 · 默认排序",
113
114
  "settings.fileSort": "文件活动 · 默认排序",
@@ -120,6 +121,8 @@ window.__ModuleLoader__.load({
120
121
  "trend.focus": "仅展示 {cat}",
121
122
  "trend.adaptive": "自适应",
122
123
  "trend.adaptiveHint": "按当前可视范围内的柱子重新缩放高度,滚动时随之变化",
124
+ "trend.duration": "耗时",
125
+ "trend.durationTip": "在柱子上叠加每步的活跃耗时曲线(步骤开始至结束,含模型与工具调用,不含等待你决策或回答的时间),按图中右侧的四分位坐标轴读数;轮次粒度下为一轮各步耗时之和",
123
126
  "trend.dna": "DNA 模式",
124
127
  "trend.dnaTip": "把每根柱子画成该次请求的上下文指纹:按模型读取顺序逐条铺色,配合增量模式逐条展示每次请求的增减;悬停查看条目,点击让上下文浏览器定位到该条目,同一条目在所有柱子中亮出其存续变化",
125
128
  "trend.dnaItem": "{label}:{n}",
@@ -255,6 +258,7 @@ window.__ModuleLoader__.load({
255
258
  "tip.total": "合计 ≈ {n}",
256
259
  "tip.cat": "{cat} ≈ {n}",
257
260
  "tip.delta": "Δ {n}",
261
+ "tip.duration": "耗时 {n}",
258
262
  "ev.compaction": "压缩上下文(摘要替换 {n} 条消息)",
259
263
  "ev.prune": "剪枝工具输出",
260
264
  "ev.skill": "Skill 注入({name})",
@@ -506,6 +510,7 @@ window.__ModuleLoader__.load({
506
510
  "insightsEntry.hide": "Hide",
507
511
  "settings.gran": "Trend Chart · Default granularity",
508
512
  "settings.mode": "Trend Chart · Default display",
513
+ "settings.durationCurve": "Trend Chart · Duration curve",
509
514
  "settings.deltaBase": "Context Browser · Diff baseline",
510
515
  "settings.toolSort": "Tool Schemas · Default sort",
511
516
  "settings.fileSort": "File Activity · Default sort",
@@ -518,6 +523,8 @@ window.__ModuleLoader__.load({
518
523
  "trend.focus": "Showing {cat} only",
519
524
  "trend.adaptive": "Adaptive",
520
525
  "trend.adaptiveHint": "Rescale bar heights to the visible range, following the scroll",
526
+ "trend.duration": "Duration",
527
+ "trend.durationTip": "Overlay each step’s active time (step start to end — model and tool calls, excluding waits for your decisions or answers) as a curve over the bars, read off the quartile axis on the chart’s right; turn granularity sums a turn’s steps",
521
528
  "trend.dna": "DNA Mode",
522
529
  "trend.dnaTip": "Draw each bar as that request’s context fingerprint — one band per item in the order the model reads it; with Delta, each band shows that item’s change against the previous bar. Hover for the item, click to reveal it in the Context browser, and every bar lights the same item’s lifetime",
523
530
  "trend.dnaItem": "{label}: {n}",
@@ -653,6 +660,7 @@ window.__ModuleLoader__.load({
653
660
  "tip.total": "Total ≈ {n}",
654
661
  "tip.cat": "{cat} ≈ {n}",
655
662
  "tip.delta": "Δ {n}",
663
+ "tip.duration": "Duration {n}",
656
664
  "ev.compaction": "Context compacted (summary replaced {n} messages)",
657
665
  "ev.prune": "Tool output pruned",
658
666
  "ev.skill": "Skill injected ({name})",
@@ -2201,9 +2209,9 @@ window.__ModuleLoader__.load({
2201
2209
  * The no-stale-content guarantees: the store is per session and shared by
2202
2210
  * the tab and the modal; a refetch is single-flight with a trailing edge
2203
2211
  * (a rev bumped mid-flight re-reads after settle); responses race-safe by
2204
- * revision (latest wins, with the refold exception — a host that refolded
2205
- * restarts the revision, and a response matching the requested rev is
2206
- * accepted regardless of order); a transport failure keeps the last good
2212
+ * generation and revision (a host refold invalidates reads started before
2213
+ * its revisions restarted, and latest wins within a generation); a
2214
+ * transport failure keeps the last good
2207
2215
  * detail and backs off; an absent answer (the session left the live set)
2208
2216
  * stops the trailing until the head moves again. With no detail at all,
2209
2217
  * failure surfaces as a retryable note on the detail cards instead of an
@@ -2292,6 +2300,7 @@ window.__ModuleLoader__.load({
2292
2300
  targetRev = -1;
2293
2301
  /** The head rev seen last — a DECREASE means the host refolded (revisions restart). */
2294
2302
  lastHeadRev = -1;
2303
+ generation = 0;
2295
2304
  failed = false;
2296
2305
  inFlight = false;
2297
2306
  timer = null;
@@ -2315,6 +2324,7 @@ window.__ModuleLoader__.load({
2315
2324
  */
2316
2325
  request(rev) {
2317
2326
  if (rev < this.lastHeadRev) {
2327
+ this.generation++;
2318
2328
  this.acceptedRev = -1;
2319
2329
  this.wantedRev = -1;
2320
2330
  this.detail = null;
@@ -2352,9 +2362,11 @@ window.__ModuleLoader__.load({
2352
2362
  }
2353
2363
  this.inFlight = true;
2354
2364
  this.targetRev = this.wantedRev;
2365
+ const generation = this.generation;
2355
2366
  this.emit();
2356
2367
  try {
2357
2368
  const d = await this.fetcher();
2369
+ if (generation !== this.generation) return;
2358
2370
  if (d !== null) {
2359
2371
  if (d.rev >= this.acceptedRev || d.rev === this.targetRev) {
2360
2372
  this.detail = d;
@@ -2368,12 +2380,15 @@ window.__ModuleLoader__.load({
2368
2380
  this.failures++;
2369
2381
  }
2370
2382
  } catch {
2371
- this.failed = this.detail === null;
2372
- this.failures++;
2383
+ if (generation === this.generation) {
2384
+ this.failed = this.detail === null;
2385
+ this.failures++;
2386
+ }
2387
+ } finally {
2388
+ this.inFlight = false;
2389
+ if (this.wantedRev > this.acceptedRev) this.schedule();
2390
+ this.emit();
2373
2391
  }
2374
- this.inFlight = false;
2375
- if (this.wantedRev > this.acceptedRev) this.schedule();
2376
- this.emit();
2377
2392
  }
2378
2393
  emit() {
2379
2394
  const pending = this.timer !== null || this.inFlight;
@@ -3003,21 +3018,31 @@ window.__ModuleLoader__.load({
3003
3018
  */
3004
3019
  /**
3005
3020
  * Collapse per-step requests into one bar per turn — each turn is represented by its LAST step's record, tagged `stepCount` for the bar's
3006
- * column width; the log keeps one turn's requests consecutive, so a run of equal turns collapses to its final record.
3021
+ * column width; the log keeps one turn's requests consecutive, so a run of equal turns collapses to its final record. The duration
3022
+ * overlay's `activeMs` is the run's steps SUMMED (the turn's total step active time), absent only when no step of the run carried one.
3007
3023
  */
3008
3024
  function aggregateByTurn(requests) {
3009
3025
  const out = [];
3010
3026
  let runSteps = 0;
3027
+ let runActiveMs = 0;
3028
+ let runHasActiveMs = false;
3011
3029
  for (const req of requests) {
3012
3030
  const last = out.length > 0 ? out[out.length - 1] : null;
3013
3031
  if (last !== null && (last.turn ?? 0) === (req.turn ?? 0)) {
3014
3032
  runSteps++;
3033
+ if (req.activeMs !== void 0) {
3034
+ runActiveMs += req.activeMs;
3035
+ runHasActiveMs = true;
3036
+ }
3015
3037
  out[out.length - 1] = {
3016
3038
  ...req,
3017
- stepCount: runSteps
3039
+ stepCount: runSteps,
3040
+ ...runHasActiveMs ? { activeMs: runActiveMs } : {}
3018
3041
  };
3019
3042
  } else {
3020
3043
  runSteps = 1;
3044
+ runActiveMs = req.activeMs ?? 0;
3045
+ runHasActiveMs = req.activeMs !== void 0;
3021
3046
  out.push({
3022
3047
  ...req,
3023
3048
  stepCount: 1
@@ -3064,7 +3089,7 @@ window.__ModuleLoader__.load({
3064
3089
  return requests.length > 0 ? requests[0] : null;
3065
3090
  }
3066
3091
  function makeTrendChart(kit) {
3067
- const { t, fmt, eventLabel, eventAt, catLabel } = kit;
3092
+ const { t, fmt, fmtDuration, eventLabel, eventAt, catLabel } = kit;
3068
3093
  const CHART_H = 112;
3069
3094
  const fmtSigned = (v) => (v > 0 ? "+" : "") + fmt(v);
3070
3095
  const BAR_W = 14;
@@ -3335,6 +3360,7 @@ window.__ModuleLoader__.load({
3335
3360
  const dnaOn = dnaBands !== null;
3336
3361
  const delta = props.mode === "delta";
3337
3362
  const dnaDeltaOn = dnaOn && delta;
3363
+ const durationOn = props.durationCurve === true;
3338
3364
  const focus = !dnaOn && props.focusCat !== null && props.focusCat !== void 0 && CATS.some((c) => c.key === props.focusCat) ? props.focusCat : null;
3339
3365
  const [dnaHit, setDnaHit] = (0, react.useState)(null);
3340
3366
  const dnaDeltas = (0, react.useMemo)(() => dnaDeltaOn ? dnaBands.map((bands, i) => deltaBandsOf(bands, i > 0 ? dnaBands[i - 1] : null)) : null, [dnaDeltaOn, dnaBands]);
@@ -3359,12 +3385,15 @@ window.__ModuleLoader__.load({
3359
3385
  let total = 0;
3360
3386
  let up = 0;
3361
3387
  let down = 0;
3388
+ let activeMs = 0;
3362
3389
  const from = Math.max(0, Math.floor(sl / pitch));
3363
3390
  const to = Math.min(n - 1, Math.max(from, Math.floor((vr - 1) / pitch)));
3364
3391
  for (let i = from; i <= to; i++) {
3365
3392
  const col = i * pitch;
3366
3393
  if (col >= vr || col + BAR_W <= sl) continue;
3367
3394
  const req = requests[i];
3395
+ const m = req.activeMs ?? 0;
3396
+ if (m > activeMs) activeMs = m;
3368
3397
  if (dnaDeltaOn && dnaDeltas !== null) {
3369
3398
  let bu = 0;
3370
3399
  let bd = 0;
@@ -3384,10 +3413,11 @@ window.__ModuleLoader__.load({
3384
3413
  if (bd > down) down = bd;
3385
3414
  } else if (req.total > total) total = req.total;
3386
3415
  }
3387
- setVisMax((prev) => prev !== null && prev.total === total && prev.up === up && prev.down === down ? prev : {
3416
+ setVisMax((prev) => prev !== null && prev.total === total && prev.up === up && prev.down === down && prev.activeMs === activeMs ? prev : {
3388
3417
  total,
3389
3418
  up,
3390
- down
3419
+ down,
3420
+ activeMs
3391
3421
  });
3392
3422
  };
3393
3423
  let maxTotal = 1;
@@ -3421,6 +3451,34 @@ window.__ModuleLoader__.load({
3421
3451
  }
3422
3452
  } else maxTotal = Math.max(1, visMax.total);
3423
3453
  }
3454
+ let maxActiveMs = 0;
3455
+ if (durationOn) {
3456
+ for (const req of requests) {
3457
+ const m = req.activeMs ?? 0;
3458
+ if (m > maxActiveMs) maxActiveMs = m;
3459
+ }
3460
+ if (adaptive && visMax !== null) maxActiveMs = visMax.activeMs;
3461
+ }
3462
+ const durationRuns = [];
3463
+ const durationDots = [];
3464
+ if (durationOn) {
3465
+ const scale = CHART_H / Math.max(1, maxActiveMs);
3466
+ let run = [];
3467
+ const flushRun = () => {
3468
+ if (run.length >= 2) durationRuns.push(run.map((p) => `${p[0]},${p[1]}`).join(" "));
3469
+ else if (run.length === 1) durationDots.push(run[0]);
3470
+ run = [];
3471
+ };
3472
+ for (let i = 0; i < requests.length; i++) {
3473
+ const m = requests[i].activeMs;
3474
+ if (m === void 0) {
3475
+ flushRun();
3476
+ continue;
3477
+ }
3478
+ run.push([i * 16 + BAR_W / 2, Math.round((CHART_H - m * scale) * 100) / 100]);
3479
+ }
3480
+ flushRun();
3481
+ }
3424
3482
  const span = Math.max(1, maxUp + maxDown);
3425
3483
  const deltaScale = CHART_H / span;
3426
3484
  const upPx = Math.round(maxUp * deltaScale);
@@ -3624,18 +3682,22 @@ window.__ModuleLoader__.load({
3624
3682
  n: stepsOf(req.turn)
3625
3683
  });
3626
3684
  const band = hitBand ?? hoveredDelta;
3627
- if (band !== null) return [head, t("trend.dnaItem", {
3685
+ let metric;
3686
+ if (band !== null) metric = t("trend.dnaItem", {
3628
3687
  label: dnaBaseLabel(band, t, catLabel),
3629
3688
  n: dnaDeltaOn ? fmtSigned(band.tokens) : fmt(band.tokens)
3630
- })];
3631
- if (delta)
3689
+ });
3690
+ else if (delta)
3632
3691
  /* v8 ignore next 1 -- delta mode only receives records from
3633
3692
  deltaOf, which always assigns net; the fallback is defensive. */
3634
- return [head, t("tip.delta", { n: fmtSigned(req.net ?? 0) })];
3635
- return [head, focus !== null ? t("tip.cat", {
3693
+ metric = t("tip.delta", { n: fmtSigned(req.net ?? 0) });
3694
+ else metric = focus !== null ? t("tip.cat", {
3636
3695
  cat: catLabel(focus),
3637
3696
  n: fmt(req.total)
3638
- }) : t("tip.total", { n: fmt(req.total) })];
3697
+ }) : t("tip.total", { n: fmt(req.total) });
3698
+ const rows = [head, metric];
3699
+ if (durationOn && req.activeMs !== void 0) rows.push(t("tip.duration", { n: fmtDuration(req.activeMs) }));
3700
+ return rows;
3639
3701
  };
3640
3702
  const tipColRef = (0, react.useRef)(0);
3641
3703
  /**
@@ -3665,134 +3727,173 @@ window.__ModuleLoader__.load({
3665
3727
  });
3666
3728
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3667
3729
  className: "lc-chartrow",
3668
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3669
- className: "lc-axis",
3670
- children: delta ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3671
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3672
- className: "lc-axis-top",
3673
- children: fmtSigned(maxUp)
3674
- }),
3675
- q3Clear ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3676
- className: "lc-axis-q3",
3677
- children: fmtSigned(Math.round(maxUp - span / 4))
3678
- }) : null,
3679
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3680
- className: "lc-axis-mid",
3681
- style: { top: `${13 + upPx}px` },
3682
- children: "0"
3683
- }),
3684
- q1Clear ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3685
- className: "lc-axis-q1",
3686
- children: fmtSigned(Math.round(maxUp - 3 * span / 4))
3687
- }) : null,
3688
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3689
- className: "lc-axis-bot",
3690
- children: fmtSigned(-maxDown)
3691
- })
3692
- ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3693
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3694
- className: "lc-axis-top",
3695
- children: fmt(maxTotal)
3696
- }),
3697
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3698
- className: "lc-axis-q3",
3699
- children: fmt(Math.round(maxTotal * 3 / 4))
3700
- }),
3701
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3702
- className: "lc-axis-mid",
3703
- children: fmt(Math.round(maxTotal / 2))
3704
- }),
3705
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3706
- className: "lc-axis-q1",
3707
- children: fmt(Math.round(maxTotal / 4))
3708
- }),
3709
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3710
- className: "lc-axis-bot",
3711
- children: "0"
3712
- })
3713
- ] })
3714
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3715
- className: "lc-chart-wrap",
3716
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3717
- className: "lc-chart-scroll" + (props.activeTurn !== null ? " lc-chart-dim" : ""),
3718
- ref: scrollRef,
3719
- onScroll: (e) => {
3720
- updateTurnLabels(e.currentTarget);
3721
- syncTip(e.currentTarget);
3722
- measureVisible(e.currentTarget);
3723
- },
3730
+ children: [
3731
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3732
+ className: "lc-axis",
3733
+ children: delta ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3734
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3735
+ className: "lc-axis-top",
3736
+ children: fmtSigned(maxUp)
3737
+ }),
3738
+ q3Clear ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3739
+ className: "lc-axis-q3",
3740
+ children: fmtSigned(Math.round(maxUp - span / 4))
3741
+ }) : null,
3742
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3743
+ className: "lc-axis-mid",
3744
+ style: { top: `${13 + upPx}px` },
3745
+ children: "0"
3746
+ }),
3747
+ q1Clear ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3748
+ className: "lc-axis-q1",
3749
+ children: fmtSigned(Math.round(maxUp - 3 * span / 4))
3750
+ }) : null,
3751
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3752
+ className: "lc-axis-bot",
3753
+ children: fmtSigned(-maxDown)
3754
+ })
3755
+ ] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
3756
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3757
+ className: "lc-axis-top",
3758
+ children: fmt(maxTotal)
3759
+ }),
3760
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3761
+ className: "lc-axis-q3",
3762
+ children: fmt(Math.round(maxTotal * 3 / 4))
3763
+ }),
3764
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3765
+ className: "lc-axis-mid",
3766
+ children: fmt(Math.round(maxTotal / 2))
3767
+ }),
3768
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3769
+ className: "lc-axis-q1",
3770
+ children: fmt(Math.round(maxTotal / 4))
3771
+ }),
3772
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3773
+ className: "lc-axis-bot",
3774
+ children: "0"
3775
+ })
3776
+ ] })
3777
+ }),
3778
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3779
+ className: "lc-chart-wrap",
3724
3780
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3725
- className: "lc-chart",
3726
- "data-catdim": props.hoverCat ?? void 0,
3727
- onMouseLeave: () => {
3728
- props.onHover(null);
3729
- setDnaHit(null);
3730
- },
3731
- children: [
3732
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "lc-grid lc-grid-top" }),
3733
- !delta || q3Clear ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "lc-grid lc-grid-q3" }) : null,
3734
- !delta || q1Clear ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "lc-grid lc-grid-q1" }) : null,
3735
- !delta ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "lc-grid lc-grid-mid" }) : null,
3736
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3737
- className: "lc-grid lc-grid-zero",
3738
- style: delta ? { top: `${18 + upPx}px` } : void 0
3739
- }),
3740
- requests.map((req, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartBar, {
3741
- req,
3742
- marker: markers[i],
3743
- selected: props.selectedSeq === req.seq,
3744
- hovered: props.hoveredSeq === req.seq,
3745
- inTurn: props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn,
3746
- flag: props.granularity === "step" && (i + 1) % STEP_FLAG_EVERY === 0 ? i + 1 : null,
3747
- maxTotal,
3748
- upPx: delta ? upPx : void 0,
3749
- downPx: delta ? downPx : void 0,
3750
- deltaScale: delta ? deltaScale : void 0,
3751
- enterIndex: i,
3752
- dna: dnaOn ? dnaBands[i] : null,
3753
- dnaDelta: dnaDeltaOn && dnaDeltas !== null ? dnaDeltas[i] : null,
3754
- dnaScale: dnaDeltaOn ? deltaScale : void 0,
3755
- zeroBottom: dnaDeltaOn ? downPx : void 0,
3756
- onDnaHit: setDnaHit,
3757
- onPickBand: props.onPickBand,
3758
- onSelect: props.onSelect,
3759
- onHover: props.onHover
3760
- }, `${req.seq}:${props.granularity}`)),
3761
- dnaOn ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DnaHighlights, { slices: dnaSlices }) : null
3762
- ]
3763
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3764
- className: "lc-turns",
3765
- style: labelFont !== "" ? { fontSize: labelFont } : void 0,
3766
- onMouseLeave: () => {
3767
- props.onHoverTurn(null);
3781
+ className: "lc-chart-scroll" + (props.activeTurn !== null ? " lc-chart-dim" : ""),
3782
+ ref: scrollRef,
3783
+ onScroll: (e) => {
3784
+ updateTurnLabels(e.currentTarget);
3785
+ syncTip(e.currentTarget);
3786
+ measureVisible(e.currentTarget);
3768
3787
  },
3769
- children: groups.map((grp, gi) => {
3770
- const on = props.activeTurn === grp.turn;
3771
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3772
- className: "lc-turn" + (on ? " lc-turn-on" : ""),
3773
- style: {
3774
- width: `${turnWidths[gi]}px`,
3775
- background: TURN_FILLS[gi % TURN_FILLS.length]
3776
- },
3777
- title: `T${grp.turn}`,
3778
- onMouseEnter: () => {
3779
- props.onHoverTurn(grp.turn);
3780
- },
3781
- onClick: () => {
3782
- props.onPickTurn(grp.turn);
3783
- },
3784
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3785
- className: "lc-turn-label",
3786
- children: `${grp.turn}`
3787
- })
3788
- }, `turn-${gi}`);
3788
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3789
+ className: "lc-chart",
3790
+ "data-catdim": props.hoverCat ?? void 0,
3791
+ onMouseLeave: () => {
3792
+ props.onHover(null);
3793
+ setDnaHit(null);
3794
+ },
3795
+ children: [
3796
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "lc-grid lc-grid-top" }),
3797
+ !delta || q3Clear ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "lc-grid lc-grid-q3" }) : null,
3798
+ !delta || q1Clear ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "lc-grid lc-grid-q1" }) : null,
3799
+ !delta ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "lc-grid lc-grid-mid" }) : null,
3800
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3801
+ className: "lc-grid lc-grid-zero",
3802
+ style: delta ? { top: `${18 + upPx}px` } : void 0
3803
+ }),
3804
+ requests.map((req, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartBar, {
3805
+ req,
3806
+ marker: markers[i],
3807
+ selected: props.selectedSeq === req.seq,
3808
+ hovered: props.hoveredSeq === req.seq,
3809
+ inTurn: props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn,
3810
+ flag: props.granularity === "step" && (i + 1) % STEP_FLAG_EVERY === 0 ? i + 1 : null,
3811
+ maxTotal,
3812
+ upPx: delta ? upPx : void 0,
3813
+ downPx: delta ? downPx : void 0,
3814
+ deltaScale: delta ? deltaScale : void 0,
3815
+ enterIndex: i,
3816
+ dna: dnaOn ? dnaBands[i] : null,
3817
+ dnaDelta: dnaDeltaOn && dnaDeltas !== null ? dnaDeltas[i] : null,
3818
+ dnaScale: dnaDeltaOn ? deltaScale : void 0,
3819
+ zeroBottom: dnaDeltaOn ? downPx : void 0,
3820
+ onDnaHit: setDnaHit,
3821
+ onPickBand: props.onPickBand,
3822
+ onSelect: props.onSelect,
3823
+ onHover: props.onHover
3824
+ }, `${req.seq}:${props.granularity}`)),
3825
+ dnaOn ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DnaHighlights, { slices: dnaSlices }) : null,
3826
+ durationOn && (durationRuns.length > 0 || durationDots.length > 0) ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
3827
+ className: "lc-duration",
3828
+ width: requests.length * 16 - BAR_GAP,
3829
+ height: CHART_H,
3830
+ "aria-hidden": "true",
3831
+ children: [durationRuns.map((pts, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: pts }, i)), durationDots.map((p, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
3832
+ cx: p[0],
3833
+ cy: p[1],
3834
+ r: 1.5
3835
+ }, i))]
3836
+ }) : null
3837
+ ]
3838
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3839
+ className: "lc-turns",
3840
+ style: labelFont !== "" ? { fontSize: labelFont } : void 0,
3841
+ onMouseLeave: () => {
3842
+ props.onHoverTurn(null);
3843
+ },
3844
+ children: groups.map((grp, gi) => {
3845
+ const on = props.activeTurn === grp.turn;
3846
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3847
+ className: "lc-turn" + (on ? " lc-turn-on" : ""),
3848
+ style: {
3849
+ width: `${turnWidths[gi]}px`,
3850
+ background: TURN_FILLS[gi % TURN_FILLS.length]
3851
+ },
3852
+ title: `T${grp.turn}`,
3853
+ onMouseEnter: () => {
3854
+ props.onHoverTurn(grp.turn);
3855
+ },
3856
+ onClick: () => {
3857
+ props.onPickTurn(grp.turn);
3858
+ },
3859
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3860
+ className: "lc-turn-label",
3861
+ children: `${grp.turn}`
3862
+ })
3863
+ }, `turn-${gi}`);
3864
+ })
3865
+ })]
3866
+ }), hoveredReq !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3867
+ className: "lc-chart-tip",
3868
+ children: tipRowsOf(hoveredReq).map((row, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: row }, i))
3869
+ }) : null]
3870
+ }),
3871
+ durationOn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3872
+ className: "lc-axis lc-axis-r",
3873
+ children: [
3874
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3875
+ className: "lc-axis-top",
3876
+ children: fmtDuration(maxActiveMs)
3877
+ }),
3878
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3879
+ className: "lc-axis-q3",
3880
+ children: fmtDuration(Math.round(maxActiveMs * 3 / 4))
3881
+ }),
3882
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3883
+ className: "lc-axis-mid",
3884
+ children: fmtDuration(Math.round(maxActiveMs / 2))
3885
+ }),
3886
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3887
+ className: "lc-axis-q1",
3888
+ children: fmtDuration(Math.round(maxActiveMs / 4))
3889
+ }),
3890
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3891
+ className: "lc-axis-bot",
3892
+ children: "0"
3789
3893
  })
3790
- })]
3791
- }), hoveredReq !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3792
- className: "lc-chart-tip",
3793
- children: tipRowsOf(hoveredReq).map((row, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: row }, i))
3794
- }) : null]
3795
- })]
3894
+ ]
3895
+ }) : null
3896
+ ]
3796
3897
  });
3797
3898
  };
3798
3899
  }
@@ -8915,7 +9016,7 @@ window.__ModuleLoader__.load({
8915
9016
  //#endregion
8916
9017
  //#region src/client/components/settingsCard.tsx
8917
9018
  /**
8918
- * The dsh-context preference cards — two seats over the same seven rows. The
9019
+ * The dsh-context preference cards — two seats over the same eight rows. The
8919
9020
  * settings-section card (`settings.plugin.item`, the older harness lines)
8920
9021
  * renders a collapsible list item in Settings → Plugins → Plugin
8921
9022
  * configuration; the Plugins-page card (`plugins.bundle.config`, the
@@ -9034,6 +9135,21 @@ window.__ModuleLoader__.load({
9034
9135
  set?.("defaultTrendMode", id);
9035
9136
  }
9036
9137
  }),
9138
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PrefRow, {
9139
+ label: t("settings.durationCurve"),
9140
+ value: state.durationCurve,
9141
+ disabled,
9142
+ options: [{
9143
+ id: "show",
9144
+ label: t("insightsEntry.show")
9145
+ }, {
9146
+ id: "hide",
9147
+ label: t("insightsEntry.hide")
9148
+ }],
9149
+ onPick: (id) => {
9150
+ set?.("defaultDurationCurve", id);
9151
+ }
9152
+ }),
9037
9153
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PrefRow, {
9038
9154
  label: t("settings.deltaBase"),
9039
9155
  value: state.deltaBase,
@@ -9182,7 +9298,8 @@ window.__ModuleLoader__.load({
9182
9298
  ...v.defaultDeltaBase === "step" || v.defaultDeltaBase === "turn" ? { deltaBase: v.defaultDeltaBase } : {},
9183
9299
  ...v.defaultToolSort === "size" || v.defaultToolSort === "count" || v.defaultToolSort === "name" ? { toolSort: v.defaultToolSort } : {},
9184
9300
  ...v.defaultFileSort === "count" || v.defaultFileSort === "latest" || v.defaultFileSort === "path" ? { fileSort: v.defaultFileSort } : {},
9185
- ...v.insightsEntry === "show" || v.insightsEntry === "hide" ? { insightsEntry: v.insightsEntry } : {}
9301
+ ...v.insightsEntry === "show" || v.insightsEntry === "hide" ? { insightsEntry: v.insightsEntry } : {},
9302
+ ...v.defaultDurationCurve === "show" || v.defaultDurationCurve === "hide" ? { durationCurve: v.defaultDurationCurve } : {}
9186
9303
  };
9187
9304
  }
9188
9305
  function createContextSettings() {
@@ -9195,12 +9312,13 @@ window.__ModuleLoader__.load({
9195
9312
  toolSort: "count",
9196
9313
  fileSort: "count",
9197
9314
  insightsEntry: "show",
9315
+ durationCurve: "show",
9198
9316
  writable: false
9199
9317
  };
9200
9318
  let scope;
9201
9319
  const listeners = /* @__PURE__ */ new Set();
9202
9320
  const publish = (next) => {
9203
- if (next.status === state.status && next.placement === state.placement && next.granularity === state.granularity && next.mode === state.mode && next.deltaBase === state.deltaBase && next.toolSort === state.toolSort && next.fileSort === state.fileSort && next.insightsEntry === state.insightsEntry && next.writable === state.writable) return;
9321
+ if (next.status === state.status && next.placement === state.placement && next.granularity === state.granularity && next.mode === state.mode && next.deltaBase === state.deltaBase && next.toolSort === state.toolSort && next.fileSort === state.fileSort && next.insightsEntry === state.insightsEntry && next.durationCurve === state.durationCurve && next.writable === state.writable) return;
9204
9322
  state = next;
9205
9323
  for (const listener of listeners) listener();
9206
9324
  };
@@ -9217,6 +9335,7 @@ window.__ModuleLoader__.load({
9217
9335
  toolSort: prefs.toolSort ?? state.toolSort,
9218
9336
  fileSort: prefs.fileSort ?? state.fileSort,
9219
9337
  insightsEntry: prefs.insightsEntry ?? (raw?.insightsEntry === void 0 ? state.insightsEntry : "show"),
9338
+ durationCurve: prefs.durationCurve ?? (raw?.defaultDurationCurve === void 0 ? state.durationCurve : "show"),
9220
9339
  writable: snap.writable
9221
9340
  });
9222
9341
  return {
@@ -9241,6 +9360,7 @@ window.__ModuleLoader__.load({
9241
9360
  defaultToolSort: () => state.toolSort,
9242
9361
  defaultFileSort: () => state.fileSort,
9243
9362
  insightsEntry: () => state.insightsEntry,
9363
+ defaultDurationCurve: () => state.durationCurve,
9244
9364
  attach(bound) {
9245
9365
  scope = bound;
9246
9366
  sync(bound);
@@ -11404,7 +11524,7 @@ window.__ModuleLoader__.load({
11404
11524
  return function PluginInfo() {
11405
11525
  const [latest, setLatest] = (0, react.useState)(null);
11406
11526
  (0, react.useEffect)(() => {
11407
- if ("0.62.2".includes("-dev")) return;
11527
+ if ("0.63.0".includes("-dev")) return;
11408
11528
  let on = true;
11409
11529
  fetchLatestVersion().then((v) => {
11410
11530
  if (on && v) setLatest(v);
@@ -11413,8 +11533,8 @@ window.__ModuleLoader__.load({
11413
11533
  on = false;
11414
11534
  };
11415
11535
  }, []);
11416
- const update = latest !== null && isNewerVersion(latest, "0.62.2") ? latest : null;
11417
- const nameText = "dsh-context (v0.62.2)";
11536
+ const update = latest !== null && isNewerVersion(latest, "0.63.0") ? latest : null;
11537
+ const nameText = "dsh-context (v0.63.0)";
11418
11538
  const nameValue = [nameText];
11419
11539
  if (update) nameValue.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
11420
11540
  className: "lc-pi-update",
@@ -12379,6 +12499,7 @@ window.__ModuleLoader__.load({
12379
12499
  const [trendMode, setTrendMode] = (0, react.useState)(() => settings.defaultTrendMode());
12380
12500
  const [dna, setDna] = (0, react.useState)(false);
12381
12501
  const [adaptive, setAdaptive] = (0, react.useState)(false);
12502
+ const [durationCurve, setDurationCurve] = (0, react.useState)(() => settings.defaultDurationCurve() === "show");
12382
12503
  const [focusTurn, setFocusTurn] = (0, react.useState)(null);
12383
12504
  const [jumpSeq, setJumpSeq] = (0, react.useState)(null);
12384
12505
  const [hoverCat, setHoverCat] = (0, react.useState)(null);
@@ -12645,6 +12766,19 @@ window.__ModuleLoader__.load({
12645
12766
  children: t("trend.adaptive")
12646
12767
  })
12647
12768
  }),
12769
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
12770
+ className: "lc-gran lc-trend-duration",
12771
+ role: "group",
12772
+ title: t("trend.durationTip"),
12773
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
12774
+ type: "button",
12775
+ className: "lc-gran-btn" + (durationCurve ? " lc-gran-on" : ""),
12776
+ onClick: () => {
12777
+ setDurationCurve((on) => !on);
12778
+ },
12779
+ children: t("trend.duration")
12780
+ })
12781
+ }),
12648
12782
  focusCat !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
12649
12783
  className: "lc-card-sub",
12650
12784
  children: t("trend.focus", { cat: kit.catLabel(focusCat) })
@@ -12703,6 +12837,7 @@ window.__ModuleLoader__.load({
12703
12837
  hoverCat: trendHoverCat,
12704
12838
  focusCat,
12705
12839
  adaptive,
12840
+ durationCurve,
12706
12841
  dna: dnaBands,
12707
12842
  onPickBand: revealBand,
12708
12843
  onSelect: setSelectedSeq,
@@ -13060,7 +13195,7 @@ window.__ModuleLoader__.load({
13060
13195
  }
13061
13196
  //#endregion
13062
13197
  //#region \0dsh-global-css:/home/runner/work/dsh-context/dsh-context/src/client/styles/trendChart.css.mjs
13063
- const css$9 = ".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:18px 2px 0;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:color-mix(in srgb, var(--color-neutral-500) 50%, transparent);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-chart-tip span+span{color:var(--dsw-alias-label-secondary)}.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:color-mix(in srgb, var(--color-neutral-500) 14%, transparent)}.lc-bar-stack{flex-direction:column-reverse;width:100%;display:flex}.lc-bar-stack>div{width:100%}.lc-bar-dna{border-radius:2px;width:100%}.lc-bar-stack,.lc-bar-up,.lc-bar-down,.lc-bar-dna{transform-origin:50% 100%}.lc-bar-down{transform-origin:50% 0}.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=skill] .lc-cat-seg[data-cat=skill],.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-dna-hl{z-index:1;pointer-events:none;align-items:stretch;gap:2px;display:flex;position:absolute;inset:18px 2px 0}.lc-dna-cell{flex:none;width:14px;position:relative}.lc-dna-slice{box-sizing:border-box;border:1px solid var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary) 26%, transparent);border-radius:1px;width:100%;position:absolute;left:0}.lc-bar-marker{z-index:2;color:var(--dsw-alias-state-warn-primary);font-size:11px;position:absolute;top:-16px;left:50%;transform:translate(-50%)}.lc-step-flag{z-index:2;background:color-mix(in srgb, var(--color-neutral-500) 45%, transparent);pointer-events:none;width:1px;position:absolute;top:-17px;bottom:0;right:-1px}.lc-step-flag-label{box-sizing:border-box;border:1px solid color-mix(in srgb, var(--color-neutral-500) 45%, transparent);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:3px;padding:1px 3px;font-size:9px;font-weight:600;line-height:1;position:absolute;top:0;right:0}.lc-turns{box-sizing:border-box;gap:2px;width:max-content;min-width:100%;margin-top:4px;padding:0 2px;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}";
13198
+ const css$9 = ".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-r{flex:none;width:34px}.lc-axis-r span{left:0;right:auto}.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:18px 2px 0;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:color-mix(in srgb, var(--color-neutral-500) 50%, transparent);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-chart-tip span+span{color:var(--dsw-alias-label-secondary)}.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:color-mix(in srgb, var(--color-neutral-500) 14%, transparent)}.lc-bar-stack{flex-direction:column-reverse;width:100%;display:flex}.lc-bar-stack>div{width:100%}.lc-bar-dna{border-radius:2px;width:100%}.lc-bar-stack,.lc-bar-up,.lc-bar-down,.lc-bar-dna{transform-origin:50% 100%}.lc-bar-down{transform-origin:50% 0}.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=skill] .lc-cat-seg[data-cat=skill],.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-dna-hl{z-index:1;pointer-events:none;align-items:stretch;gap:2px;display:flex;position:absolute;inset:18px 2px 0}.lc-dna-cell{flex:none;width:14px;position:relative}.lc-dna-slice{box-sizing:border-box;border:1px solid var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary) 26%, transparent);border-radius:1px;width:100%;position:absolute;left:0}.lc-duration{z-index:1;pointer-events:none;position:absolute;top:18px;left:2px;overflow:visible}.lc-duration polyline,.lc-duration circle{fill:none;stroke:var(--dsw-alias-brand-primary);stroke-width:1.5px;stroke-linejoin:round;stroke-linecap:round}.lc-duration circle{fill:var(--dsw-alias-brand-primary)}.lc-chart-dim .lc-duration{opacity:.35}.lc-bar-marker{z-index:2;color:var(--dsw-alias-state-warn-primary);font-size:11px;position:absolute;top:-16px;left:50%;transform:translate(-50%)}.lc-step-flag{z-index:2;background:color-mix(in srgb, var(--color-neutral-500) 45%, transparent);pointer-events:none;width:1px;position:absolute;top:-17px;bottom:0;right:-1px}.lc-step-flag-label{box-sizing:border-box;border:1px solid color-mix(in srgb, var(--color-neutral-500) 45%, transparent);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);white-space:nowrap;border-radius:3px;padding:1px 3px;font-size:9px;font-weight:600;line-height:1;position:absolute;top:0;right:0}.lc-turns{box-sizing:border-box;gap:2px;width:max-content;min-width:100%;margin-top:4px;padding:0 2px;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}";
13064
13199
  const tagId$9 = "dsh-context/trendChart.css";
13065
13200
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$9) + "]") === null) {
13066
13201
  const tag = document.createElement("style");
package/lib/index.d.ts CHANGED
@@ -157,6 +157,39 @@ interface TimelineState {
157
157
  time: number;
158
158
  firstToken?: number;
159
159
  };
160
+ /**
161
+ * The open step's CLOSED user-wait intervals (real log instants), pushed
162
+ * replace-style as each wait settles: an approval decision (its asked →
163
+ * decided pair closes via `stepApprovals`) and an `ask_user_question`
164
+ * call's whole tool window (its body is the answer wait itself, in both
165
+ * the blocking and the timed schema's foreground phase). `step/end`
166
+ * subtracts their clamped union from the step window to price the
167
+ * request's `activeMs`, then DELETES the slot. Same arm/remove lifecycle
168
+ * as `stepStart` — never written in place, so no CloneKey covers it.
169
+ */
170
+ stepWaits?: {
171
+ start: number;
172
+ end: number;
173
+ }[];
174
+ /**
175
+ * The open step's PENDING approval waits (request id → its `approval/asked`
176
+ * instant), armed only while a step is open and disarmed by the matching
177
+ * `approval/decided`. A crash mid-decision leaves the pair dangling —
178
+ * resume repair appends no synthetic decision — so `step/end` closes the
179
+ * survivors at its own instant. One record, not a map: pending approvals
180
+ * are a handful at most, and the rebuild-on-change copy stays trivial.
181
+ * Deleted at `step/start` (a superseded step's leftovers) and `step/end`.
182
+ */
183
+ stepApprovals?: Record<string, number>;
184
+ /**
185
+ * The seq of the request record committed by the open step's
186
+ * `assistant/message` — the back-pointer `step/end` stamps `activeMs`
187
+ * onto. It stays the requests TAIL until then (nothing pushes between a
188
+ * step's message and its end, and the turn trim only drops the oldest),
189
+ * so the stamp is an O(1) tail write guarded by the seq. Same arm/remove
190
+ * lifecycle as `stepStart`.
191
+ */
192
+ stepRequestSeq?: number;
160
193
  /**
161
194
  * The timing strip's painted spans (shared/types.ts TimingSpan): every
162
195
  * completed step's time slices in log order, stamped with their REAL
@@ -364,6 +397,8 @@ type DefaultToolSort = 'size' | 'count' | 'name';
364
397
  type DefaultPlacement = 'all' | 'tab' | 'sidebar';
365
398
  /** Whether the Context Insights panel's sidebar entry is offered at all. */
366
399
  type InsightsEntry = 'show' | 'hide';
400
+ /** Whether the trend chart mounts with the per-request duration curve overlaid. */
401
+ type DefaultDurationCurve = 'show' | 'hide';
367
402
  interface Snapshot {
368
403
  ok: boolean;
369
404
  /**
@@ -790,6 +825,15 @@ interface RequestRecord {
790
825
  */
791
826
  cacheRead?: number;
792
827
  output?: number;
828
+ /**
829
+ * The request's step ACTIVE milliseconds (`step/start` → `step/end`: the
830
+ * model call plus its tool runs) minus the in-step user waits — approval
831
+ * decisions and `ask_user_question` answer windows — the trend chart's
832
+ * duration overlay. Stamped at `step/end` onto the step's committed record;
833
+ * absent while that step is still open (the live tail) or when the step
834
+ * closed without one (an unpaired/hostile log).
835
+ */
836
+ activeMs?: number;
793
837
  /**
794
838
  * Turn-mode aggregate marker, set by the Client's aggregateByTurn (one bar
795
839
  * per turn shows its LAST step's record). The Host never sets it.
@@ -913,6 +957,8 @@ export interface Config {
913
957
  defaultFileSort?: DefaultFileSort;
914
958
  /** Whether the Context Insights panel's sidebar entry is offered. */
915
959
  insightsEntry?: InsightsEntry;
960
+ /** Whether the trend chart mounts with the duration curve overlaid. */
961
+ defaultDurationCurve?: DefaultDurationCurve;
916
962
  }
917
963
  /** The cordis `Config` validator and the Config-form generation's served schema. */
918
964
  export declare const Config: z$1<Schemastery.ObjectS<NoInfer<{
@@ -929,6 +975,7 @@ export declare const Config: z$1<Schemastery.ObjectS<NoInfer<{
929
975
  defaultToolSort: z$1<"count" | "name" | "size", "count" | "name" | "size", Mode>;
930
976
  defaultFileSort: z$1<"count" | "latest" | "path", "count" | "latest" | "path", Mode>;
931
977
  insightsEntry: z$1<"hide" | "show", "hide" | "show", Mode>;
978
+ defaultDurationCurve: z$1<"hide" | "show", "hide" | "show", Mode>;
932
979
  }>>, Schemastery.ObjectT<NoInfer<{
933
980
  maxRequestSteps: z$1<number, number, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
934
981
  maxKeptTurns: z$1<number, number, Mode extends "volatile" | "volatile-defined" ? "volatile-defined" : "defined">;
@@ -943,6 +990,7 @@ export declare const Config: z$1<Schemastery.ObjectS<NoInfer<{
943
990
  defaultToolSort: z$1<"count" | "name" | "size", "count" | "name" | "size", Mode>;
944
991
  defaultFileSort: z$1<"count" | "latest" | "path", "count" | "latest" | "path", Mode>;
945
992
  insightsEntry: z$1<"hide" | "show", "hide" | "show", Mode>;
993
+ defaultDurationCurve: z$1<"hide" | "show", "hide" | "show", Mode>;
946
994
  }>>, "plain">;
947
995
  //#endregion
948
996
  //#region src/host/index.d.ts
package/lib/index.js CHANGED
@@ -259,8 +259,8 @@ function estimateBlocks(blocks) {
259
259
  }
260
260
  /**
261
261
  * Price one surface message exactly like dsh's token-meter estimate:
262
- * an empty-content assistant/message projects to NO message (it only hosts
263
- * usage), so it prices 0; every other message pays content + role framing.
262
+ * an empty-content assistant/developer event projects to NO message, so it
263
+ * prices 0; every other message pays content + role framing.
264
264
  */
265
265
  function estimateMessage(message, emptyIsZero = false) {
266
266
  if (emptyIsZero && (message === null || message === void 0 || !Array.isArray(message.content) || message.content.length === 0)) return 0;
@@ -844,6 +844,7 @@ function createTimelineState() {
844
844
  function categoryOf(type, message) {
845
845
  if (type === "assistant/message") return "assistant";
846
846
  if (type === "tool/result") return "tool";
847
+ if (type === "developer/message") return "inject";
847
848
  const kind = message?.source?.kind;
848
849
  if (kind === "skill-invocation" || kind === "skill-catalog") return "skill";
849
850
  if (isInjection(message?.source)) return "inject";
@@ -943,12 +944,9 @@ function removeSurfaceSeqs(st, claimed, goneSeq) {
943
944
  return removed;
944
945
  }
945
946
  /**
946
- * The message nested under an event payload's `message` field
947
- * (`system/message`, `assistant/message`, `tool/result`) — read structurally
948
- * because the fold needs a TOTAL read over untrusted payloads: a malformed
949
- * message reads null here, never throws or guesses a shape. Purely a type
950
- * cast: `deriveEventMessage` returns `event.data` for a user/message (no
951
- * `data.message` indirection), so its result feeds every other case.
947
+ * A message nested under data.message. A structural read keeps malformed
948
+ * payloads total and handles developer messages absent from the V3
949
+ * dependency's deriveEventMessage. User messages instead live in data itself.
952
950
  */
953
951
  function messageOf$1(data) {
954
952
  const message = data?.message;
@@ -988,7 +986,7 @@ function applySurface(st, ev, type, data, message) {
988
986
  seq: ev.seq,
989
987
  time: ev.time,
990
988
  cat,
991
- tokens: estimateMessage(message, type === "assistant/message")
989
+ tokens: estimateMessage(message, type === "assistant/message" || type === "developer/message")
992
990
  };
993
991
  const imgs = imageCountOf(message?.content);
994
992
  if (imgs > 0) node.imgs = imgs;
@@ -1397,13 +1395,38 @@ function applyTimeline(state, event, bounds) {
1397
1395
  const s = ensure([]);
1398
1396
  s.stepStart = { time: event.time };
1399
1397
  delete s.stepSpans;
1398
+ delete s.stepWaits;
1399
+ delete s.stepApprovals;
1400
+ delete s.stepRequestSeq;
1400
1401
  break;
1401
1402
  }
1402
1403
  case "step/end": {
1403
1404
  const start = state.stepStart;
1404
1405
  if (start === void 0) return state;
1405
- const s = ensure(["timing"]);
1406
+ const s = ensure(state.stepRequestSeq !== void 0 ? ["timing", "requests"] : ["timing"]);
1406
1407
  ensureTiming(s).wallMs += durOf(start.time, event.time);
1408
+ const waits = [...state.stepWaits ?? []];
1409
+ for (const id in state.stepApprovals) waits.push({
1410
+ start: state.stepApprovals[id],
1411
+ end: event.time
1412
+ });
1413
+ waits.sort((a, b) => a.start - b.start || a.end - b.end);
1414
+ let waitMs = 0;
1415
+ let waitCursor = start.time;
1416
+ for (const wait of waits) {
1417
+ const from = Math.max(waitCursor, wait.start);
1418
+ const to = Math.min(wait.end, event.time);
1419
+ if (to > from) {
1420
+ waitMs += to - from;
1421
+ waitCursor = to;
1422
+ }
1423
+ }
1424
+ const activeMs = durOf(start.time, event.time) - waitMs;
1425
+ const last = state.requests.at(-1);
1426
+ if (last !== void 0 && last.seq === state.stepRequestSeq) s.requests[s.requests.length - 1] = {
1427
+ ...last,
1428
+ activeMs
1429
+ };
1407
1430
  const painted = [];
1408
1431
  for (const span of state.stepSpans ?? []) {
1409
1432
  const from = Math.max(start.time, span.start);
@@ -1440,11 +1463,43 @@ function applyTimeline(state, event, bounds) {
1440
1463
  s.spans = [...state.spans, ...flushed];
1441
1464
  delete s.stepStart;
1442
1465
  delete s.stepSpans;
1466
+ delete s.stepWaits;
1467
+ delete s.stepApprovals;
1468
+ delete s.stepRequestSeq;
1443
1469
  bumpDetailRev(s);
1444
1470
  break;
1445
1471
  }
1446
- case "user/message": {
1447
- const msg = deriveEventMessage(event);
1472
+ case "approval/asked": {
1473
+ const id = data?.id;
1474
+ if (state.stepStart === void 0 || typeof id !== "string") return state;
1475
+ const s = ensure([]);
1476
+ s.stepApprovals = {
1477
+ ...state.stepApprovals,
1478
+ [id]: event.time
1479
+ };
1480
+ break;
1481
+ }
1482
+ case "approval/decided": {
1483
+ const id = data?.id;
1484
+ const open = state.stepApprovals;
1485
+ const askedAt = typeof id === "string" ? open?.[id] : void 0;
1486
+ if (open === void 0 || askedAt === void 0) return state;
1487
+ const s = ensure([]);
1488
+ s.stepWaits = [...state.stepWaits ?? [], {
1489
+ start: askedAt,
1490
+ end: event.time
1491
+ }];
1492
+ const kept = {};
1493
+ for (const k in open) if (k !== id) kept[k] = open[k];
1494
+ if (Object.keys(kept).length > 0) s.stepApprovals = kept;
1495
+ else delete s.stepApprovals;
1496
+ break;
1497
+ }
1498
+ case "user/message":
1499
+ case "developer/message": {
1500
+ const developer = event.type === "developer/message";
1501
+ const msg = developer ? messageOf$1(data) : deriveEventMessage(event);
1502
+ if (developer && (msg === null || !Array.isArray(msg.content))) return state;
1448
1503
  const s = ensure([
1449
1504
  "surface",
1450
1505
  "sums",
@@ -1454,15 +1509,15 @@ function applyTimeline(state, event, bounds) {
1454
1509
  bumpDetailRev(s);
1455
1510
  const node = applySurface(s, event, event.type, data, msg);
1456
1511
  const source = msg?.source;
1457
- if (isInjection(source)) {
1512
+ if (developer || isInjection(source)) {
1458
1513
  const rec = {
1459
1514
  seq: event.seq,
1460
1515
  time: event.time,
1461
1516
  kind: "inject",
1462
- form: typeof source.form === "string" && source.form !== "" ? source.form : "context",
1517
+ form: typeof source?.form === "string" && source.form !== "" ? source.form : "context",
1463
1518
  tokens: node.tokens
1464
1519
  };
1465
- if (source.kind === "skill-invocation") {
1520
+ if (source?.kind === "skill-invocation") {
1466
1521
  rec.sub = "skill";
1467
1522
  rec.name = typeof source.name === "string" ? source.name : "?";
1468
1523
  } else {
@@ -1471,7 +1526,7 @@ function applyTimeline(state, event, bounds) {
1471
1526
  rec.name = label;
1472
1527
  node.name = label;
1473
1528
  }
1474
- if (source.form === "notice" && typeof source.summary === "string" && source.summary !== "") rec.detail = source.summary;
1529
+ if (source?.form === "notice" && typeof source.summary === "string" && source.summary !== "") rec.detail = source.summary;
1475
1530
  }
1476
1531
  s.events.push(rec);
1477
1532
  } else {
@@ -1498,6 +1553,10 @@ function applyTimeline(state, event, bounds) {
1498
1553
  bumpDetailRev(s);
1499
1554
  const node = applySurface(s, event, event.type, data, toolMsg);
1500
1555
  if (node.tool === ASK_USER_TOOL) s.humanInputs = (s.humanInputs ?? 0) + 1;
1556
+ if (pendingEntry !== void 0 && pendingEntry.name === ASK_USER_TOOL && state.stepStart !== void 0) s.stepWaits = [...state.stepWaits ?? [], {
1557
+ start: pendingEntry.start,
1558
+ end: event.time
1559
+ }];
1501
1560
  if (pendingEntry !== void 0) pushFileOps(s, opsOfCall({
1502
1561
  seq: event.seq,
1503
1562
  time: event.time,
@@ -1565,6 +1624,7 @@ function applyTimeline(state, event, bounds) {
1565
1624
  };
1566
1625
  if (data && typeof data.turn === "number") record.turn = data.turn;
1567
1626
  if (data && typeof data.step === "number") record.step = data.step;
1627
+ if (state.stepStart !== void 0) s.stepRequestSeq = record.seq;
1568
1628
  let output = null;
1569
1629
  if (usage !== null && typeof usage === "object") {
1570
1630
  const input = tokenCountOf(usage.inputTokens);
@@ -2155,12 +2215,15 @@ const FIRST_PARTY_SOURCES = Object.freeze({
2155
2215
  * first argument, so `reader.fiber.name` identifies the plugin that is about
2156
2216
  * to call `register()`.
2157
2217
  *
2158
- * - The `internal/get` handler records who last read the `tools` service and
2159
- * wraps that instance's `register` (once — an earlier wrapper of a previous
2160
- * hook incarnation is peeled back to the original, so a plugin reload
2161
- * re-wraps without stacking) to capture the reader at registration time
2162
- * into a live map. Every wrapper is undone when the plugin unloads: the
2163
- * original `register` goes back on the instance, unless a newer hook
2218
+ * - The `internal/get` handler records the reading fiber's NAME — a scalar,
2219
+ * never the reader context, which would pin the caller's whole agent — and
2220
+ * wraps the tools service's `register` (once per underlying instance —
2221
+ * cordis hands out a fresh traced proxy per read of a tracked service, so
2222
+ * the proxy is peeled to the stable instance first; an earlier wrapper of a
2223
+ * previous hook incarnation is peeled back to the original, so a plugin
2224
+ * reload re-wraps without stacking) to capture the reader at registration
2225
+ * time into a live map. Every wrapper is undone when the plugin unloads:
2226
+ * the original `register` goes back on the instance, unless a newer hook
2164
2227
  * incarnation re-wrapped it first (that incarnation's own cleanup then
2165
2228
  * owns the restore).
2166
2229
  * - When the reader slot is missing, root-named, or this plugin's own (e.g.
@@ -2221,6 +2284,20 @@ function packageNameFrom(file) {
2221
2284
  }
2222
2285
  }
2223
2286
  const FRAME_POSITION = /:\d+:\d+$/;
2287
+ /** Symbol cordis registers (`Symbol.for`) on its traced proxies: reading it
2288
+ * yields the wrapped target — the stable underlying service instance. */
2289
+ const CORDIS_ORIGINAL = Symbol.for("cordis.original");
2290
+ /**
2291
+ * Peel cordis's per-read traced proxy of a tracked service (dsh's ToolRuntime
2292
+ * is one — every `ctx.tools` read returns a fresh proxy closing over the
2293
+ * reader's context) down to the instance it wraps, so identity-keyed
2294
+ * bookkeeping sees one stable object. Plain service objects pass through
2295
+ * unchanged.
2296
+ * @param tools - the value as handed out by a context read.
2297
+ */
2298
+ function rawInstanceOf(tools) {
2299
+ return tools?.[CORDIS_ORIGINAL] ?? tools;
2300
+ }
2224
2301
  /** Package name of this module's own package (self-fallbacks are filtered). */
2225
2302
  const selfPackage = packageNameFrom(selfUrl);
2226
2303
  /**
@@ -2279,7 +2356,7 @@ function createToolAttribution(ctx) {
2279
2356
  const instance = tools;
2280
2357
  const wrappedRegister = function(definition) {
2281
2358
  const toolName = definition?.name;
2282
- let owner = lastReader?.fiber.name;
2359
+ let owner = lastReader;
2283
2360
  if (!owner || owner === "root" || owner === self) owner = callerPackageFrom((/* @__PURE__ */ new Error()).stack);
2284
2361
  const dispose = original.call(this, definition);
2285
2362
  if (typeof toolName === "string" && owner && owner !== "root" && owner !== self && owner !== selfPackage) {
@@ -2303,14 +2380,14 @@ function createToolAttribution(ctx) {
2303
2380
  ctx.on("internal/get", (reader, name, _error, next) => {
2304
2381
  if (name !== "tools") return next();
2305
2382
  const tools = next();
2306
- lastReader = reader;
2307
- wrapInstance(tools);
2383
+ lastReader = reader.fiber.name;
2384
+ wrapInstance(rawInstanceOf(tools));
2308
2385
  return tools;
2309
2386
  });
2310
2387
  ctx.effect(() => () => {
2311
2388
  for (const restore of patched.splice(0)) restore();
2312
2389
  }, "tools.register attribution");
2313
- const toolsService = ctx.get("tools", false);
2390
+ const toolsService = rawInstanceOf(ctx.get("tools", false));
2314
2391
  wrapInstance(toolsService);
2315
2392
  const boot = /* @__PURE__ */ new Set();
2316
2393
  try {
@@ -2777,7 +2854,8 @@ const Config = z$1.object({
2777
2854
  "latest",
2778
2855
  "path"
2779
2856
  ]).default("count").loose()),
2780
- insightsEntry: volatileField(z$1.union(["show", "hide"]).default("show").loose())
2857
+ insightsEntry: volatileField(z$1.union(["show", "hide"]).default("show").loose()),
2858
+ defaultDurationCurve: volatileField(z$1.union(["show", "hide"]).default("show").loose())
2781
2859
  });
2782
2860
  /** Resolve the fold's retention bounds (the schema fills every default). */
2783
2861
  function resolveBounds(config) {
@@ -3157,6 +3235,11 @@ const requestRecordSchema = z.object({
3157
3235
  skill: z.number().int().nonnegative().optional(),
3158
3236
  cacheRead: z.number().int().nonnegative().optional(),
3159
3237
  output: z.number().int().nonnegative().optional(),
3238
+ /**
3239
+ * The request's step active milliseconds (see RequestRecord.activeMs).
3240
+ * Optional so a record whose step never closed (the live tail) still parses.
3241
+ */
3242
+ activeMs: z.number().nonnegative().optional(),
3160
3243
  stepCount: z.number().int().positive().optional()
3161
3244
  }).strict();
3162
3245
  const contextEventSchema = z.object({
@@ -3356,6 +3439,12 @@ const timelineStateSchema = z.object({
3356
3439
  time: z.number(),
3357
3440
  firstToken: z.number().optional()
3358
3441
  }).strict().optional(),
3442
+ stepWaits: z.array(z.object({
3443
+ start: z.number(),
3444
+ end: z.number()
3445
+ }).strict()).optional(),
3446
+ stepApprovals: z.record(z.string(), z.number()).optional(),
3447
+ stepRequestSeq: z.number().optional(),
3359
3448
  callNames: z.record(z.string(), z.object({
3360
3449
  name: z.string(),
3361
3450
  start: z.number(),
@@ -3415,7 +3504,7 @@ function createContextTimelineDefinition(config, slim) {
3415
3504
  },
3416
3505
  init: () => createTimelineState(),
3417
3506
  apply: (state, event) => applyTimeline(state, event, bounds),
3418
- stateVersion: 24
3507
+ stateVersion: 26
3419
3508
  };
3420
3509
  }
3421
3510
  //#endregion
@@ -3538,7 +3627,8 @@ const SettingsSchema = z$1.object({
3538
3627
  "latest",
3539
3628
  "path"
3540
3629
  ]).default("count").loose(),
3541
- insightsEntry: z$1.union(["show", "hide"]).default("show").loose()
3630
+ insightsEntry: z$1.union(["show", "hide"]).default("show").loose(),
3631
+ defaultDurationCurve: z$1.union(["show", "hide"]).default("show").loose()
3542
3632
  });
3543
3633
  /** Serve the namespace while a settings provider with the register face is composed; inert otherwise. */
3544
3634
  function installSettings(ctx) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.62.2",
3
+ "version": "0.63.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
  "icon": "icon.svg",
6
6
  "author": "bowenliang123",