dsh-context 0.5.0 → 0.6.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
@@ -72,6 +72,8 @@ pnpm test # typecheck + functional tests for both halves
72
72
 
73
73
  `build.mjs` also smoke-checks the outputs (both halves must parse; the host half must import with the `name`/`inject`/`apply` plugin shape).
74
74
 
75
+ Commits are guarded by a [husky](https://typicode.github.io/husky/) pre-commit hook that runs `pnpm run typecheck` (`tsc --noEmit`, strict).
76
+
75
77
  ## Files
76
78
 
77
79
  | File | Role |
package/lib/client.js CHANGED
@@ -18,14 +18,17 @@ const DICT_ZH = {
18
18
  "overview.title": "\u5F53\u524D\u6784\u6210",
19
19
  "overview.ofWindow": "tokens\uFF08\u7EA6 {p}%\uFF09",
20
20
  "overview.estimate": "tokens\uFF08\u4F30\u7B97\uFF09",
21
+ "overview.actual": "\xB7 \u4E0A\u6B21\u5B9E\u9645 prompt {n}",
21
22
  "tools.top": "\u5DE5\u5177\u5B9A\u4E49 Top\uFF1A",
22
23
  "tools.more": "\u7B49 {n} \u4E2A",
23
24
  "trend.title": "\u5386\u53F2\u8D8B\u52BF",
24
25
  "gran.step": "\u6B65\u9AA4",
25
26
  "gran.turn": "\u8F6E\u6B21",
26
- "trend.hint": "\u6BCF\u6B21\u6A21\u578B\u8BF7\u6C42\u4E00\u6BB5\uFF1B\u70B9\u51FB\u67F1\u5B50\u67E5\u770B\u8BE6\u60C5\uFF0C\u2702 \u8868\u793A\u538B\u7F29/\u526A\u679D",
27
+ "trend.hint": "\u6BCF\u6B21\u6A21\u578B\u8BF7\u6C42\u4E00\u6BB5\uFF1B\u70B9\u51FB\u67F1\u5B50\u67E5\u770B\u8BE6\u60C5\uFF0C\u2702 \u8868\u793A\u538B\u7F29/\u526A\u679D\uFF1B\u5DE6\u53F3\u6ED1\u52A8\u53EF\u56DE\u770B\u66F4\u65E9",
27
28
  "trend.empty": "\u53D1\u8D77\u4E00\u8F6E\u5BF9\u8BDD\u540E\uFF0C\u8FD9\u91CC\u4F1A\u5C55\u793A\u6BCF\u6B21\u6A21\u578B\u8BF7\u6C42\u7684\u4E0A\u4E0B\u6587\u6784\u6210",
28
29
  "detail.step": "T{t} \xB7 \u7B2C {s} \u6B65",
30
+ "detail.turn": "T{t} \xB7 \u5171 {n} \u6B65",
31
+ "detail.lastStep": "\u672B\u6B65",
29
32
  "detail.estTotal": "\u4F30\u7B97\u5408\u8BA1 \u2248 {n}",
30
33
  "detail.actual": "\u5B9E\u9645 prompt {n}",
31
34
  "detail.output": "\u8F93\u51FA {n}",
@@ -39,6 +42,7 @@ const DICT_ZH = {
39
42
  "error": "\u4E0A\u4E0B\u6587\u6570\u636E\u8BFB\u53D6\u5931\u8D25\uFF1A",
40
43
  "footer": "\u4F30\u7B97\u53E3\u5F84\uFF1A\u4E0E dsh \u5185\u7F6E tokenMeter \u76F8\u540C\u7684\u56FA\u5B9A\u5BC6\u5EA6\u542F\u53D1\u5F0F\uFF08\u7EA6 4 \u5B57\u7B26 \u2248 1 token\uFF09\uFF1B\u300C\u5B9E\u9645\u300D\u4E3A\u4F9B\u5E94\u5546\u4E0A\u62A5\u7528\u91CF\u3002",
41
44
  "tip.step": "T{t} \xB7 \u7B2C{s}\u6B65",
45
+ "tip.turn": "T{t} \xB7 \u5171 {n} \u6B65",
42
46
  "tip.total": "\u5408\u8BA1 \u2248 {n}",
43
47
  "tip.actual": "\uFF08\u5B9E\u9645 {n}\uFF09",
44
48
  "ev.compaction": "\u538B\u7F29\u4E0A\u4E0B\u6587\uFF08\u6458\u8981\u66FF\u6362 {n} \u6761\u6D88\u606F\uFF09",
@@ -69,14 +73,17 @@ const DICT_EN = {
69
73
  "overview.title": "Current composition",
70
74
  "overview.ofWindow": "tokens (~{p}%)",
71
75
  "overview.estimate": "tokens (estimated)",
76
+ "overview.actual": "\xB7 last actual prompt {n}",
72
77
  "tools.top": "Top tool schemas:",
73
78
  "tools.more": "of {n}",
74
79
  "trend.title": "History",
75
80
  "gran.step": "Step",
76
81
  "gran.turn": "Turn",
77
- "trend.hint": "one bar per model request; click a bar for details, \u2702 marks compaction/prune",
82
+ "trend.hint": "one bar per model request; click a bar for details, \u2702 marks compaction/prune; scroll sideways to see earlier",
78
83
  "trend.empty": "Send a message and each model request\u2019s context makeup shows up here",
79
84
  "detail.step": "T{t} \xB7 step {s}",
85
+ "detail.turn": "T{t} \xB7 {n} steps",
86
+ "detail.lastStep": "last step",
80
87
  "detail.estTotal": "estimated \u2248 {n}",
81
88
  "detail.actual": "actual prompt {n}",
82
89
  "detail.output": "output {n}",
@@ -90,6 +97,7 @@ const DICT_EN = {
90
97
  "error": "Failed to read context data: ",
91
98
  "footer": "Estimate: same fixed-density heuristic as dsh\u2019s built-in tokenMeter (~4 chars \u2248 1 token); \u201Cactual\u201D is provider-reported usage.",
92
99
  "tip.step": "T{t} \xB7 step {s}",
100
+ "tip.turn": "T{t} \xB7 {n} steps",
93
101
  "tip.total": "total \u2248 {n}",
94
102
  "tip.actual": " (actual {n})",
95
103
  "ev.compaction": "Context compacted (summary replaced {n} messages)",
@@ -124,10 +132,16 @@ function fmtTime(t) {
124
132
  }
125
133
  function aggregateByTurn(requests) {
126
134
  const out = [];
135
+ let runSteps = 0;
127
136
  for (const req of requests) {
128
137
  const last = out.length > 0 ? out[out.length - 1] : null;
129
- if (last !== null && (last.turn ?? 0) === (req.turn ?? 0)) out[out.length - 1] = req;
130
- else out.push(req);
138
+ if (last !== null && (last.turn ?? 0) === (req.turn ?? 0)) {
139
+ runSteps++;
140
+ out[out.length - 1] = { ...req, stepCount: runSteps };
141
+ } else {
142
+ runSteps = 1;
143
+ out.push({ ...req, stepCount: 1 });
144
+ }
131
145
  }
132
146
  return out;
133
147
  }
@@ -183,6 +197,11 @@ function makeView(ctx, t) {
183
197
  text: catLabel(p.key) + " " + fmt(p.value) + " (" + Math.round(p.value / total * 100) + "%)",
184
198
  left: Math.max(48, Math.min(left, width - 48))
185
199
  });
200
+ if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
201
+ };
202
+ const clearHover = () => {
203
+ setTip(null);
204
+ if (props.onHoverKey !== void 0) props.onHoverKey(null);
186
205
  };
187
206
  return h(
188
207
  "div",
@@ -193,14 +212,14 @@ function makeView(ctx, t) {
193
212
  className: "lc-stacked",
194
213
  style: { height: (props.height || 14) + "px" },
195
214
  ref: wrapRef,
196
- onMouseLeave: () => {
197
- setTip(null);
198
- }
215
+ onMouseLeave: clearHover
199
216
  },
200
217
  total <= 0 ? null : props.parts.map((p) => {
201
218
  if (!p.value) return null;
219
+ const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
202
220
  return h("div", {
203
221
  key: p.key,
222
+ className: "lc-stacked-seg" + (on ? " lc-stacked-seg-on" : ""),
204
223
  style: { width: p.value / scale * 100 + "%", background: p.color },
205
224
  onMouseEnter: (e) => {
206
225
  showTip(e, p);
@@ -221,9 +240,19 @@ function makeView(ctx, t) {
221
240
  "div",
222
241
  { className: "lc-legend" },
223
242
  props.parts.map((p) => {
243
+ const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
224
244
  return h(
225
245
  "span",
226
- { key: p.key, className: "lc-chip" },
246
+ {
247
+ key: p.key,
248
+ className: "lc-chip" + (on ? " lc-chip-on" : ""),
249
+ onMouseEnter: () => {
250
+ if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
251
+ },
252
+ onMouseLeave: () => {
253
+ if (props.onHoverKey !== void 0) props.onHoverKey(null);
254
+ }
255
+ },
227
256
  h("i", { style: { background: p.color } }),
228
257
  catLabel(p.key) + " " + fmt(p.value),
229
258
  total > 0 ? h("em", null, Math.round(p.value / total * 100) + "%") : null
@@ -257,17 +286,31 @@ function makeView(ctx, t) {
257
286
  const groups = [];
258
287
  for (const req of requests) {
259
288
  let grp = groups.length > 0 ? groups[groups.length - 1] : null;
260
- if (grp === null || grp.turn !== req.turn) {
261
- grp = { turn: req.turn ?? 0, count: 0 };
289
+ if (grp === null || grp.turn !== (req.turn ?? 0)) {
290
+ grp = { turn: req.turn ?? 0, count: 0, span: 0, agg: req.stepCount !== void 0 };
262
291
  groups.push(grp);
263
292
  }
264
293
  grp.count++;
294
+ grp.span += req.stepCount ?? 1;
265
295
  }
266
296
  const scrollRef = React.useRef(null);
267
- React.useEffect(() => {
297
+ const scrolledOnce = React.useRef(false);
298
+ const [edges, setEdges] = React.useState({ left: false, right: false });
299
+ const updateEdges = (el) => {
300
+ const left = el.scrollLeft > 4;
301
+ const right = el.scrollLeft + el.clientWidth < el.scrollWidth - 4;
302
+ setEdges((prev) => prev.left === left && prev.right === right ? prev : { left, right });
303
+ };
304
+ React.useLayoutEffect(() => {
268
305
  const el = scrollRef.current;
269
306
  if (el === null) return;
270
- if (el.scrollLeft + el.clientWidth >= el.scrollWidth - 24) el.scrollLeft = el.scrollWidth;
307
+ if (!scrolledOnce.current) {
308
+ scrolledOnce.current = true;
309
+ el.scrollLeft = el.scrollWidth;
310
+ } else if (el.scrollLeft + el.clientWidth >= el.scrollWidth - 24) {
311
+ el.scrollLeft = el.scrollWidth;
312
+ }
313
+ updateEdges(el);
271
314
  });
272
315
  return h(
273
316
  "div",
@@ -281,7 +324,16 @@ function makeView(ctx, t) {
281
324
  ),
282
325
  h(
283
326
  "div",
284
- { className: "lc-chart-scroll", ref: scrollRef },
327
+ {
328
+ className: "lc-chart-scroll",
329
+ ref: scrollRef,
330
+ onScroll: (e) => {
331
+ updateEdges(e.currentTarget);
332
+ }
333
+ },
334
+ // Edge fades: visible whenever more history sits beyond the viewport,
335
+ // so the horizontal scroll affordance is obvious.
336
+ edges.left ? h("div", { className: "lc-chart-fade lc-chart-fade-l" }) : null,
285
337
  // Hovering a bar previews it in the detail below; leaving the plot
286
338
  // clears the preview (a pinned selection, if any, takes over again).
287
339
  h(
@@ -295,12 +347,15 @@ function makeView(ctx, t) {
295
347
  const selected = props.selectedSeq === req.seq;
296
348
  const hovered = props.hoveredSeq === req.seq;
297
349
  const inTurn = props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn;
298
- const tip = tr("tip.step", { t: req.turn ?? 0, s: req.step ?? 0 }) + " \xB7 " + fmtTime(req.time) + "\n" + tr("tip.total", { n: fmt(req.total) }) + (req.prompt !== void 0 ? tr("tip.actual", { n: fmt(req.prompt) }) : "") + "\n" + CATS.map((c) => catLabel(c.key) + " " + fmt(req[c.key] || 0)).join(" / ");
350
+ const tip = (req.stepCount !== void 0 && req.stepCount > 1 ? tr("tip.turn", { t: req.turn ?? 0, n: req.stepCount }) : tr("tip.step", { t: req.turn ?? 0, s: req.step ?? 0 })) + " \xB7 " + fmtTime(req.time) + "\n" + tr("tip.total", { n: fmt(req.total) }) + (req.prompt !== void 0 ? tr("tip.actual", { n: fmt(req.prompt) }) : "") + "\n" + CATS.map((c) => catLabel(c.key) + " " + fmt(req[c.key] || 0)).join(" / ");
299
351
  return h(
300
352
  "div",
301
353
  {
302
354
  key: req.seq,
355
+ // Uniform column width in BOTH granularities: turn aggregates
356
+ // keep the same fixed width as step bars.
303
357
  className: "lc-bar" + (selected ? " lc-bar-selected" : "") + (hovered ? " lc-bar-hovered" : "") + (inTurn ? " lc-bar-in-turn" : ""),
358
+ style: { width: BAR_W + "px" },
304
359
  title: tip,
305
360
  onClick: () => {
306
361
  props.onSelect(selected ? null : req.seq);
@@ -334,11 +389,12 @@ function makeView(ctx, t) {
334
389
  } },
335
390
  groups.map((grp, gi) => {
336
391
  const on = props.activeTurn === grp.turn;
392
+ const blockW = grp.agg ? BAR_W : grp.span * (BAR_W + BAR_GAP) - BAR_GAP;
337
393
  return h("span", {
338
394
  key: "turn-" + gi,
339
395
  className: "lc-turn" + (on ? " lc-turn-on" : ""),
340
396
  style: {
341
- width: grp.count * (BAR_W + BAR_GAP) - BAR_GAP + "px",
397
+ width: blockW + "px",
342
398
  background: TURN_COLORS[gi % TURN_COLORS.length]
343
399
  },
344
400
  title: "T" + grp.turn,
@@ -347,20 +403,24 @@ function makeView(ctx, t) {
347
403
  }
348
404
  }, "T" + grp.turn);
349
405
  })
350
- )
406
+ ),
407
+ edges.right ? h("div", { className: "lc-chart-fade lc-chart-fade-r" }) : null
351
408
  )
352
409
  );
353
410
  }
354
411
  function RequestDetail(props) {
355
412
  const req = props.request;
356
413
  if (!req) return null;
414
+ const isTurn = req.stepCount !== void 0 && req.stepCount > 1;
415
+ const head = isTurn ? tr("detail.turn", { t: req.turn ?? 0, n: req.stepCount ?? 0 }) : tr("detail.step", { t: req.turn ?? 0, s: req.step ?? 0 });
357
416
  return h(
358
417
  "div",
359
418
  { className: "lc-detail" },
360
419
  h(
361
420
  "div",
362
421
  { className: "lc-detail-head" },
363
- h("b", null, tr("detail.step", { t: req.turn ?? 0, s: req.step ?? 0 })),
422
+ h("b", null, head),
423
+ isTurn ? h("span", { className: "lc-detail-tag" }, t("detail.lastStep")) : null,
364
424
  h("span", null, fmtTime(req.time)),
365
425
  h("span", null, tr("detail.estTotal", { n: fmt(req.total) })),
366
426
  req.prompt !== void 0 ? h("span", { className: "lc-actual" }, tr("detail.actual", { n: fmt(req.prompt) })) : null,
@@ -432,6 +492,8 @@ function makeView(ctx, t) {
432
492
  { key: n.seq, className: "lc-node" },
433
493
  h("i", { style: { background: catColor[n.cat] || "#999" } }),
434
494
  h("span", { className: "lc-node-preview", title: text }, text),
495
+ // Timestamp when the host event carried one.
496
+ typeof n.time === "number" ? h("span", { className: "lc-node-time" }, fmtTime(n.time)) : null,
435
497
  h("span", { className: "lc-node-tokens" }, fmt(n.tokens))
436
498
  );
437
499
  })
@@ -446,6 +508,7 @@ function makeView(ctx, t) {
446
508
  const [hoverTurn, setHoverTurn] = React.useState(null);
447
509
  const [tick, setTick] = React.useState(0);
448
510
  const [granularity, setGranularity] = React.useState("step");
511
+ const [hoverCat, setHoverCat] = React.useState(null);
449
512
  React.useEffect(() => {
450
513
  if (typeof sessionId !== "string" || sessionId === "") return void 0;
451
514
  let alive = true;
@@ -521,10 +584,19 @@ function makeView(ctx, t) {
521
584
  "div",
522
585
  { className: "lc-overview-num" },
523
586
  h("b", null, fmt(current.total)),
524
- h("span", null, data.contextWindow ? " / " + fmt(data.contextWindow) + " " + tr("overview.ofWindow", { p: windowPct ?? 0 }) : " " + t("overview.estimate"))
587
+ h("span", null, data.contextWindow ? " / " + fmt(data.contextWindow) + " " + tr("overview.ofWindow", { p: windowPct ?? 0 }) : " " + t("overview.estimate")),
588
+ // The provider-reported prompt of the last request is the best
589
+ // ground truth for what the model actually received; the fixed
590
+ // density heuristic can undercount CJK-heavy content, so show the
591
+ // real number alongside the estimate.
592
+ displayRequests.length > 0 && displayRequests[displayRequests.length - 1].prompt !== void 0 ? h(
593
+ "span",
594
+ { className: "lc-actual" },
595
+ tr("overview.actual", { n: fmt(displayRequests[displayRequests.length - 1].prompt ?? 0) })
596
+ ) : null
525
597
  ),
526
- h(StackedBar, { parts: partsOf(current), height: 16, max: data.contextWindow }),
527
- h(Legend, { parts: partsOf(current) }),
598
+ h(StackedBar, { parts: partsOf(current), height: 16, max: data.contextWindow, hoverKey: hoverCat, onHoverKey: setHoverCat }),
599
+ h(Legend, { parts: partsOf(current), hoverKey: hoverCat, onHoverKey: setHoverCat }),
528
600
  data.toolList && data.toolList.length > 0 ? h(
529
601
  "div",
530
602
  { className: "lc-tools" },
@@ -565,7 +637,12 @@ function makeView(ctx, t) {
565
637
  "div",
566
638
  null,
567
639
  h(TrendChart, {
568
- requests: displayRequests.slice(-80),
640
+ // Remount per session: switching sessions re-anchors the chart
641
+ // at the newest bars instead of inheriting stale scroll state.
642
+ key: sessionId,
643
+ // The host caps the log at 160 requests; render them ALL so
644
+ // earlier turns/steps stay reachable via horizontal scroll.
645
+ requests: displayRequests,
569
646
  events,
570
647
  selectedSeq: pinnedReq ? pinnedReq.seq : null,
571
648
  hoveredSeq,
@@ -620,10 +697,13 @@ const STYLES = [
620
697
  ".lc-stacked { display: flex; width: 100%; border-radius: 5px; overflow: hidden; background: rgba(128,128,128,0.18); }",
621
698
  ".lc-bar-tip { position: absolute; bottom: calc(100% + 6px); transform: translateX(-50%); z-index: 5; white-space: nowrap; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 3px 8px; font-size: 12px; color: var(--dsw-alias-label-primary); box-shadow: 0 2px 8px rgba(0,0,0,0.18); pointer-events: none; }",
622
699
  ".lc-stacked > div { height: 100%; }",
700
+ ".lc-stacked-seg-on { filter: brightness(1.18); }",
623
701
  ".lc-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 10px; }",
624
702
  ".lc-chip { display: inline-flex; align-items: center; gap: 5px; color: var(--dsw-alias-label-primary); }",
625
703
  ".lc-chip i, .lc-detail-row i, .lc-node i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }",
626
704
  ".lc-chip em { font-style: normal; color: var(--dsw-alias-label-secondary); }",
705
+ ".lc-chip-on { font-weight: 600; }",
706
+ ".lc-chip-on i { box-shadow: 0 0 0 1px var(--dsw-alias-brand-primary); }",
627
707
  ".lc-tools { margin-top: 10px; color: var(--dsw-alias-label-secondary); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }",
628
708
  ".lc-tool-chip { background: var(--dsw-alias-bg-layer-2); border-radius: 4px; padding: 1px 7px; font-size: 12px; color: var(--dsw-alias-label-primary); }",
629
709
  ".lc-chartrow { display: flex; gap: 6px; align-items: stretch; }",
@@ -632,10 +712,14 @@ const STYLES = [
632
712
  ".lc-axis-top { top: 13px; }",
633
713
  ".lc-axis-mid { top: 69px; }",
634
714
  ".lc-axis-bot { top: 125px; }",
635
- ".lc-chart-scroll { flex: 1; overflow-x: auto; overflow-y: hidden; min-width: 0; scrollbar-width: thin; }",
636
- ".lc-chart-scroll::-webkit-scrollbar { height: 6px; }",
637
- ".lc-chart-scroll::-webkit-scrollbar-thumb { background: var(--dsw-alias-border-l1); border-radius: 3px; }",
715
+ ".lc-chart-scroll { position: relative; flex: 1; overflow-x: auto; overflow-y: hidden; min-width: 0; scrollbar-width: thin; }",
716
+ ".lc-chart-scroll::-webkit-scrollbar { height: 8px; }",
717
+ ".lc-chart-scroll::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--dsw-alias-label-secondary) 45%, transparent); border-radius: 4px; }",
718
+ ".lc-chart-scroll::-webkit-scrollbar-thumb:hover { background: color-mix(in srgb, var(--dsw-alias-label-secondary) 70%, transparent); }",
638
719
  ".lc-chart-scroll::-webkit-scrollbar-track { background: transparent; }",
720
+ ".lc-chart-fade { position: absolute; top: 0; bottom: 0; width: 26px; pointer-events: none; z-index: 2; }",
721
+ ".lc-chart-fade-l { left: 0; background: linear-gradient(to right, var(--dsw-alias-bg-layer-1), transparent); }",
722
+ ".lc-chart-fade-r { right: 0; background: linear-gradient(to left, var(--dsw-alias-bg-layer-1), transparent); }",
639
723
  ".lc-chart { position: relative; display: flex; align-items: flex-end; gap: 2px; height: 130px; padding-top: 18px; box-sizing: border-box; width: max-content; min-width: 100%; }",
640
724
  ".lc-grid { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--dsw-alias-border-l1); pointer-events: none; }",
641
725
  ".lc-grid-top { top: 18px; }",
@@ -649,12 +733,13 @@ const STYLES = [
649
733
  ".lc-bar-stack > div { width: 100%; }",
650
734
  ".lc-bar-marker { position: absolute; top: -16px; left: 50%; transform: translateX(-50%); font-size: 11px; color: var(--dsw-alias-state-warn-primary); }",
651
735
  ".lc-turns { display: flex; gap: 2px; width: max-content; min-width: 100%; margin-top: 4px; }",
652
- ".lc-turn { flex: none; min-width: 16px; box-sizing: border-box; text-align: center; font-size: 10px; line-height: 14px; font-weight: 600; color: #fff; border-radius: 3px; height: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; transition: filter 120ms; }",
736
+ ".lc-turn { flex: none; box-sizing: border-box; text-align: center; font-size: 10px; line-height: 14px; font-weight: 600; color: #fff; border-radius: 3px; height: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; transition: filter 120ms; }",
653
737
  ".lc-turn-on { filter: brightness(1.35); box-shadow: 0 0 0 1px rgba(255,255,255,0.4); }",
654
738
  ".lc-detail { margin-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); padding-top: 12px; }",
655
739
  ".lc-detail-head { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 8px; color: var(--dsw-alias-label-secondary); }",
656
740
  ".lc-detail-head b { color: var(--dsw-alias-label-primary); }",
657
741
  ".lc-detail-head .lc-actual { color: var(--dsw-alias-state-success-primary); }",
742
+ ".lc-detail-tag { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 4px; padding: 0 6px; font-size: 11px; color: var(--dsw-alias-label-secondary); }",
658
743
  ".lc-detail-rows { margin-top: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px 24px; }",
659
744
  ".lc-detail-row { display: flex; align-items: center; gap: 8px; }",
660
745
  ".lc-detail-label { min-width: 70px; white-space: nowrap; color: var(--dsw-alias-label-secondary); }",
@@ -675,6 +760,7 @@ const STYLES = [
675
760
  ".lc-event-time { color: var(--dsw-alias-label-secondary); font-size: 12px; }",
676
761
  ".lc-node { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
677
762
  ".lc-node-preview { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); }",
763
+ ".lc-node-time { color: var(--dsw-alias-label-secondary); font-size: 12px; min-width: 54px; text-align: right; }",
678
764
  ".lc-node-tokens { color: var(--dsw-alias-label-secondary); }",
679
765
  ".lc-nodes-more { color: var(--dsw-alias-label-secondary); padding: 3px 0; }",
680
766
  ".lc-empty { color: var(--dsw-alias-label-secondary); padding: 18px 0; text-align: center; }",
package/lib/index.js CHANGED
@@ -25,7 +25,10 @@ function estimateBlocks(blocks) {
25
25
  }
26
26
  return tokens;
27
27
  }
28
- function estimateMessage(message) {
28
+ function estimateMessage(message, emptyIsZero = false) {
29
+ if (emptyIsZero && (message === null || message === void 0 || !Array.isArray(message.content) || message.content.length === 0)) {
30
+ return 0;
31
+ }
29
32
  return estimateBlocks(message?.content) + ROLE_OVERHEAD;
30
33
  }
31
34
  function estimateSystem(text) {
@@ -55,6 +58,23 @@ function toolCallNames(blocks) {
55
58
  function isInjection(source) {
56
59
  return source !== null && typeof source === "object" && (source.kind === "plugin" || source.kind === "skill-invocation" || typeof source.form === "string");
57
60
  }
61
+ const MAX_REQUEST_STEPS = 1500;
62
+ const MAX_KEPT_TURNS = 300;
63
+ function trimToLastTurns(requests, maxTurns) {
64
+ let runs = 0;
65
+ let start = requests.length;
66
+ let prevTurn;
67
+ for (let i = requests.length - 1; i >= 0; i--) {
68
+ const turn = requests[i].turn;
69
+ if (turn !== prevTurn) {
70
+ if (runs >= maxTurns) break;
71
+ runs++;
72
+ prevTurn = turn;
73
+ }
74
+ start = i;
75
+ }
76
+ return requests.slice(start);
77
+ }
58
78
  function createFold() {
59
79
  return {
60
80
  n: 0,
@@ -86,7 +106,14 @@ function categoryOf(type, message) {
86
106
  }
87
107
  function applySurface(st, ev, type, data, message) {
88
108
  const cat = categoryOf(type, message);
89
- const node = { seq: ev.seq, cat, tokens: estimateMessage(message) };
109
+ const node = {
110
+ seq: ev.seq,
111
+ time: ev.time,
112
+ cat,
113
+ // Empty assistant messages project to no model message (usage-only), so
114
+ // they price 0 — mirroring dsh's deriveEventMessage/estimate.
115
+ tokens: estimateMessage(message, type === "assistant/message")
116
+ };
90
117
  const source = message?.source;
91
118
  const form = source?.form;
92
119
  if (typeof form === "string") node.form = form;
@@ -151,8 +178,7 @@ function foldInto(st, events) {
151
178
  name: typeof t.name === "string" ? t.name : "?",
152
179
  tokens: estimateToolSchema(t)
153
180
  }));
154
- st.toolsTokens = st.toolList.reduce((a, t) => a + t.tokens, 0);
155
- if (tools.length > 0) st.toolsTokens += BLOCK_OVERHEAD;
181
+ st.toolsTokens = tools.length > 0 ? Math.ceil(JSON.stringify(tools).length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD : 0;
156
182
  st.systemTokens = estimateSystem(header.system);
157
183
  if (header.config && typeof header.config.model === "string") st.model = header.config.model;
158
184
  if (header.config && typeof header.config.provider === "string") st.provider = header.config.provider;
@@ -192,9 +218,11 @@ function foldInto(st, events) {
192
218
  }
193
219
  break;
194
220
  }
195
- case "tool/result":
196
- applySurface(st, ev, ev.type, data, data);
221
+ case "tool/result": {
222
+ const toolMsg = data?.message ?? null;
223
+ applySurface(st, ev, ev.type, data, toolMsg);
197
224
  break;
225
+ }
198
226
  case "assistant/message": {
199
227
  const usage = data?.usage;
200
228
  const total = st.systemTokens + st.toolsTokens + st.sums.user + st.sums.inject + st.sums.assistant + st.sums.tool;
@@ -216,7 +244,8 @@ function foldInto(st, events) {
216
244
  if (typeof usage.outputTokens === "number") record.output = usage.outputTokens;
217
245
  }
218
246
  st.requests.push(record);
219
- applySurface(st, ev, ev.type, data, data);
247
+ const asstMsg = data?.message ?? null;
248
+ applySurface(st, ev, ev.type, data, asstMsg);
220
249
  break;
221
250
  }
222
251
  case "compaction/summary":
@@ -241,8 +270,11 @@ function foldInto(st, events) {
241
270
  }
242
271
  }
243
272
  st.n = events.length;
244
- if (st.requests.length > 160) st.requests = st.requests.slice(-160);
245
- if (st.events.length > 150) st.events = st.events.slice(-150);
273
+ if (st.requests.length > MAX_REQUEST_STEPS) {
274
+ st.requests = trimToLastTurns(st.requests, MAX_KEPT_TURNS);
275
+ if (st.requests.length > MAX_REQUEST_STEPS) st.requests = st.requests.slice(-MAX_REQUEST_STEPS);
276
+ }
277
+ if (st.events.length > 400) st.events = st.events.slice(-400);
246
278
  }
247
279
  function buildResult(st) {
248
280
  const surfaceTotal = st.sums.user + st.sums.inject + st.sums.assistant + st.sums.tool;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Context insight panel for DeepSeek Harness: a Context tab (beside Chat/Trajectory) showing what the model's context window is made of and how it evolves — composition, per-request history, compactions, injections, and model switches.",
5
5
  "author": "bowenliang123",
6
6
  "repository": {
@@ -28,7 +28,8 @@
28
28
  "build": "node scripts/build.mjs",
29
29
  "typecheck": "tsc --noEmit",
30
30
  "test": "npm run typecheck && node scripts/test-host.mjs && node scripts/test-client.mjs",
31
- "release": "bash scripts/publish.sh"
31
+ "release": "bash scripts/publish.sh",
32
+ "prepare": "husky"
32
33
  },
33
34
  "dsh": {
34
35
  "bundle": {
@@ -58,6 +59,7 @@
58
59
  "@deepseek-ai/cordis": "^4.0.1",
59
60
  "@types/react": "^18.3.31",
60
61
  "esbuild": "^0.28.2",
62
+ "husky": "^9.1.7",
61
63
  "typescript": "^7.0.2"
62
64
  },
63
65
  "packageManager": "pnpm@11.9.0"