dsh-context 0.7.3 → 0.8.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
@@ -39,6 +39,10 @@ One stacked bar per model request, finer than per-message. Toggle between **Turn
39
39
 
40
40
  Above: a real session that grew to ~563k tokens across 48 turns, then compaction (✂) recycled −535.5k in one step, and the conversation continued from a fresh, small window.
41
41
 
42
+ In **Step** granularity, hovering any bar shows that single step's context info instantly — its turn/step, timestamp, and estimated vs. provider-reported token counts:
43
+
44
+ ![History chart with a step hover tooltip](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/history-step-hover.png)
45
+
42
46
  ### ⚡ Context events — when and why the window changed
43
47
 
44
48
  Every compaction, tool-output prune, skill or plugin context injection, and model switch — each with its token delta, turn/step attribution, and timestamp:
package/lib/client.js CHANGED
@@ -19,6 +19,7 @@ var DICT_ZH = {
19
19
  "overview.ofWindow": "tokens\uFF08\u7EA6 {p}%\uFF09",
20
20
  "overview.estimate": "tokens\uFF08\u4F30\u7B97\uFF09",
21
21
  "overview.free": "\u5269\u4F59\u7A97\u53E3",
22
+ "overview.splitEst": "\xB7 \u5206\u7C7B\u6309\u4F30\u7B97\u6BD4\u4F8B\u5206\u914D",
22
23
  "stats.title": "\u4E0A\u4E0B\u6587\u7EDF\u8BA1",
23
24
  "stats.hint": "\u7EDF\u8BA1\u5F53\u524D\u4FDD\u7559\u7684\u5386\u53F2\u7A97\u53E3\uFF08\u4E0E\u8D8B\u52BF\u56FE\u4E00\u81F4\uFF09",
24
25
  "stats.turns": "\u8F6E\u6B21",
@@ -88,6 +89,7 @@ var DICT_EN = {
88
89
  "overview.ofWindow": "tokens (~{p}%)",
89
90
  "overview.estimate": "tokens (estimated)",
90
91
  "overview.free": "Free window",
92
+ "overview.splitEst": "\xB7 category split is estimated",
91
93
  "stats.title": "Context stats",
92
94
  "stats.hint": "Over the retained history window (same as the History chart)",
93
95
  "stats.turns": "Turns",
@@ -196,7 +198,14 @@ var STYLES = [
196
198
  ".lc-grid { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--dsw-alias-border-l1); pointer-events: none; }",
197
199
  ".lc-grid-top { top: 18px; }",
198
200
  ".lc-grid-mid { top: 74px; }",
199
- ".lc-bar { position: relative; width: 14px; flex: none; height: 100%; display: flex; align-items: flex-end; cursor: pointer; border-radius: 2px; }",
201
+ ".lc-bar { position: relative; width: 14px; flex: none; height: 100%; display: flex; align-items: flex-end; cursor: pointer; border-radius: 2px; transition: opacity 120ms ease; }",
202
+ // Turn-aware dimming: while a turn is focused, bars OUTSIDE the active
203
+ // turn fade to 35% and the whole current turn stays fully opaque.
204
+ ".lc-chart-dim .lc-bar { opacity: 0.35; }",
205
+ ".lc-chart-dim .lc-bar-in-turn { opacity: 1; }",
206
+ ".lc-chart-dim .lc-turn { opacity: 0.35; }",
207
+ ".lc-chart-dim .lc-turn-on { opacity: 1; }",
208
+ ".lc-chart-tip { position: absolute; top: 0; 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; }",
200
209
  ".lc-bar:hover { background: var(--dsw-alias-bg-layer-2); }",
201
210
  ".lc-bar-selected { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }",
202
211
  ".lc-bar-hovered { outline: 1px dashed var(--dsw-alias-brand-primary); outline-offset: 1px; }",
@@ -205,7 +214,7 @@ var STYLES = [
205
214
  ".lc-bar-stack > div { width: 100%; }",
206
215
  ".lc-bar-marker { position: absolute; top: -16px; left: 50%; transform: translateX(-50%); font-size: 11px; color: var(--dsw-alias-state-warn-primary); }",
207
216
  ".lc-turns { display: flex; gap: 2px; width: max-content; min-width: 100%; margin-top: 4px; }",
208
- ".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; }",
217
+ ".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, opacity 120ms; }",
209
218
  ".lc-turn-on { filter: brightness(1.35); box-shadow: 0 0 0 1px rgba(255,255,255,0.4); }",
210
219
  ".lc-detail { margin-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); padding-top: 12px; }",
211
220
  ".lc-detail-head { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 8px; color: var(--dsw-alias-label-secondary); }",
@@ -269,6 +278,14 @@ function partsOf(breakdown) {
269
278
  return { key: c.key, color: c.color, value: breakdown[c.key] || 0 };
270
279
  });
271
280
  }
281
+ function anchoredParts(parts, target) {
282
+ if (target === null || target <= 0) return parts;
283
+ let total = 0;
284
+ for (const p of parts) total += p.value;
285
+ if (total <= 0 || total === target) return parts;
286
+ const scale = target / total;
287
+ return parts.map((p) => ({ ...p, value: Math.round(p.value * scale) }));
288
+ }
272
289
 
273
290
  // src/client/format.ts
274
291
  function fmt(n) {
@@ -433,7 +450,7 @@ function makeRequestDetail(kit, StackedBar) {
433
450
  { className: "lc-bar-track" },
434
451
  h("span", { className: "lc-bar-fill", style: { width: (req.total > 0 ? v / req.total * 100 : 0) + "%", background: c.color } })
435
452
  ),
436
- h("span", { className: "lc-detail-num" }, fmt3(v)),
453
+ h("span", { className: "lc-detail-num" }, "\u2248" + fmt3(v)),
437
454
  h("span", { className: "lc-detail-pct" }, req.total > 0 ? Math.round(v / req.total * 100) + "%" : "")
438
455
  );
439
456
  })
@@ -524,7 +541,7 @@ function makeStackedBar(kit) {
524
541
  const pct = scale > 0 ? p.value / scale * 100 : 0;
525
542
  if (p.key === props.hoverKey && p.value > 0) {
526
543
  tip = {
527
- text: catLabel(p.key) + " " + fmt3(p.value) + " (" + Math.round(p.value / total * 100) + "%)",
544
+ text: catLabel(p.key) + " \u2248" + fmt3(p.value) + " (" + Math.round(p.value / total * 100) + "%)",
528
545
  leftPct: Math.max(12, Math.min(acc + pct / 2, 88))
529
546
  };
530
547
  break;
@@ -593,7 +610,7 @@ function makeLegend(kit) {
593
610
  }
594
611
  },
595
612
  h("i", { style: { background: p.color } }),
596
- catLabel(p.key) + " " + fmt3(p.value),
613
+ catLabel(p.key) + " \u2248" + fmt3(p.value),
597
614
  total > 0 ? h("em", null, Math.round(p.value / total * 100) + "%") : null
598
615
  );
599
616
  })
@@ -639,8 +656,13 @@ function makeTrendChart(kit) {
639
656
  return function TrendChart(props) {
640
657
  const requests = props.requests;
641
658
  const markers = props.markers;
659
+ const anchorOf = (req) => typeof req.prompt === "number" && req.prompt > 0 && req.total > 0 ? req.prompt / req.total : 1;
660
+ const barTotalOf = (req) => typeof req.prompt === "number" && req.prompt > 0 ? req.prompt : req.total;
642
661
  let maxTotal = 1;
643
- for (const req of requests) if (req.total > maxTotal) maxTotal = req.total;
662
+ for (const req of requests) {
663
+ const bt = barTotalOf(req);
664
+ if (bt > maxTotal) maxTotal = bt;
665
+ }
644
666
  const groups = [];
645
667
  for (const req of requests) {
646
668
  let grp = groups.length > 0 ? groups[groups.length - 1] : null;
@@ -679,6 +701,12 @@ function makeTrendChart(kit) {
679
701
  }
680
702
  updateEdges(el);
681
703
  });
704
+ const tipOf = (req) => {
705
+ const head = 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 });
706
+ return head + " \xB7 " + fmtTime2(req.time) + " \xB7 " + tr("tip.total", { n: fmt3(req.total) }) + (req.prompt !== void 0 ? " \xB7 " + tr("tip.actual", { n: fmt3(req.prompt) }) : "");
707
+ };
708
+ const hoveredIdx = props.hoveredSeq !== null ? requests.findIndex((r) => r.seq === props.hoveredSeq) : -1;
709
+ const hoveredReq = hoveredIdx >= 0 ? requests[hoveredIdx] : null;
682
710
  return h(
683
711
  "div",
684
712
  { className: "lc-chartrow" },
@@ -692,7 +720,9 @@ function makeTrendChart(kit) {
692
720
  h(
693
721
  "div",
694
722
  {
695
- className: "lc-chart-scroll",
723
+ // Turn-aware dim scope: while a turn is focused (bar or strip hover),
724
+ // bars and strip blocks OUTSIDE the active turn fade to 35%.
725
+ className: "lc-chart-scroll" + (props.activeTurn !== null ? " lc-chart-dim" : ""),
696
726
  ref: scrollRef,
697
727
  onScroll: (e) => {
698
728
  updateEdges(e.currentTarget);
@@ -705,9 +735,12 @@ function makeTrendChart(kit) {
705
735
  // clears the preview (a pinned selection, if any, takes over again).
706
736
  h(
707
737
  "div",
708
- { className: "lc-chart", onMouseLeave: () => {
709
- props.onHover(null);
710
- } },
738
+ {
739
+ className: "lc-chart",
740
+ onMouseLeave: () => {
741
+ props.onHover(null);
742
+ }
743
+ },
711
744
  h("div", { className: "lc-grid lc-grid-top" }),
712
745
  h("div", { className: "lc-grid lc-grid-mid" }),
713
746
  requests.map((req, i) => {
@@ -716,7 +749,6 @@ function makeTrendChart(kit) {
716
749
  const selected = props.selectedSeq === req.seq;
717
750
  const hovered = props.hoveredSeq === req.seq;
718
751
  const inTurn = props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn;
719
- 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 " + fmtTime2(req.time) + "\n" + tr("tip.total", { n: fmt3(req.total) }) + (req.prompt !== void 0 ? tr("tip.actual", { n: fmt3(req.prompt) }) : "") + "\n" + CATS.map((c) => catLabel(c.key) + " " + fmt3(req[c.key] || 0)).join(" / ");
720
752
  return h(
721
753
  "div",
722
754
  {
@@ -725,7 +757,6 @@ function makeTrendChart(kit) {
725
757
  // keep the same fixed width as step bars.
726
758
  className: "lc-bar" + (selected ? " lc-bar-selected" : "") + (hovered ? " lc-bar-hovered" : "") + (inTurn ? " lc-bar-in-turn" : ""),
727
759
  style: { width: BAR_W + "px" },
728
- title: tip,
729
760
  onClick: () => {
730
761
  props.onSelect(selected ? null : req.seq);
731
762
  },
@@ -743,7 +774,7 @@ function makeTrendChart(kit) {
743
774
  "div",
744
775
  { className: "lc-bar-stack" },
745
776
  CATS.map((c) => {
746
- const v = req[c.key] || 0;
777
+ const v = (req[c.key] || 0) * anchorOf(req);
747
778
  if (!v) return null;
748
779
  return h("div", { key: c.key, style: { height: Math.max(1, Math.round(v / maxTotal * CHART_H)) + "px", background: c.color } });
749
780
  })
@@ -751,6 +782,12 @@ function makeTrendChart(kit) {
751
782
  );
752
783
  })
753
784
  ),
785
+ // Instant hover tooltip, glued to its bar's column (it lives in the
786
+ // scrolling content, so it follows the bar while the chart scrolls).
787
+ hoveredReq !== null ? h("div", {
788
+ className: "lc-chart-tip",
789
+ style: { left: hoveredIdx * (BAR_W + BAR_GAP) + BAR_W / 2 + "px" }
790
+ }, tipOf(hoveredReq)) : null,
754
791
  // Turn strip: one COLOR BLOCK per turn, spanning exactly its bars'
755
792
  // columns, so the partition reads at a glance and lines up with the
756
793
  // steps above. Hovering a block highlights that turn's bars in the
@@ -785,6 +822,7 @@ function makeTrendChart(kit) {
785
822
  }
786
823
 
787
824
  // src/client/components/contextView.ts
825
+ var viewScroll = /* @__PURE__ */ new Map();
788
826
  function makeContextView(ctx, kit) {
789
827
  const { t, tr, fmt: fmt3, fmtTime: fmtTime2, catLabel } = kit;
790
828
  const StackedBar = makeStackedBar(kit);
@@ -809,6 +847,26 @@ function makeContextView(ctx, kit) {
809
847
  React.useEffect(() => {
810
848
  dataRef.current = data;
811
849
  }, [data]);
850
+ const rootRef = React.useRef(null);
851
+ const scrollerRef = React.useRef(null);
852
+ const restoredRef = React.useRef(null);
853
+ React.useLayoutEffect(() => {
854
+ if (typeof sessionId !== "string" || sessionId === "" || data === null) return;
855
+ if (restoredRef.current === sessionId) return;
856
+ restoredRef.current = sessionId;
857
+ const scroller = rootRef.current !== null ? rootRef.current.closest("[data-conversation-scroll]") : null;
858
+ if (scroller === null) return;
859
+ scrollerRef.current = scroller;
860
+ scroller.scrollTop = viewScroll.get(sessionId) ?? 0;
861
+ }, [sessionId, data]);
862
+ React.useLayoutEffect(() => {
863
+ return () => {
864
+ if (typeof sessionId !== "string" || sessionId === "") return;
865
+ const scroller = scrollerRef.current;
866
+ if (scroller === null) return;
867
+ viewScroll.set(sessionId, scroller.scrollTop);
868
+ };
869
+ }, [sessionId]);
812
870
  React.useEffect(() => {
813
871
  if (typeof sessionId !== "string" || sessionId === "") return void 0;
814
872
  let alive = true;
@@ -850,10 +908,10 @@ function makeContextView(ctx, kit) {
850
908
  }, []);
851
909
  void tick;
852
910
  if (error) {
853
- return h("div", { className: "lc-root" }, h("div", { className: "lc-empty" }, t("error") + error));
911
+ return h("div", { className: "lc-root", ref: rootRef }, h("div", { className: "lc-empty" }, t("error") + error));
854
912
  }
855
913
  if (!data) {
856
- return h("div", { className: "lc-root" }, h("div", { className: "lc-empty" }, t("loading")));
914
+ return h("div", { className: "lc-root", ref: rootRef }, h("div", { className: "lc-empty" }, t("loading")));
857
915
  }
858
916
  const current = data.current;
859
917
  const requests = data.requests || [];
@@ -880,10 +938,18 @@ function makeContextView(ctx, kit) {
880
938
  break;
881
939
  }
882
940
  }
883
- const windowPct = data.contextWindow ? Math.min(100, Math.round(current.total / data.contextWindow * 100)) : null;
941
+ const occ = data.occupancy;
942
+ const projected = occ !== void 0 && typeof occ.projectedTokens === "number" ? occ.projectedTokens : void 0;
943
+ const lastReq = displayRequests.length > 0 ? displayRequests[displayRequests.length - 1] : null;
944
+ const derived = lastReq !== null && typeof lastReq.prompt === "number" ? lastReq.prompt + (current.total - lastReq.total) : void 0;
945
+ const occupancyTokens = projected ?? derived ?? null;
946
+ const occupancyWindow = occ !== void 0 && typeof occ.contextWindow === "number" ? occ.contextWindow : data.contextWindow;
947
+ const headline = occupancyTokens ?? current.total;
948
+ const headlinePct = occupancyWindow ? Math.min(100, Math.round(headline / occupancyWindow * 100)) : null;
949
+ const parts = anchoredParts(partsOf(current), occupancyTokens !== null && headline > 0 ? headline : null);
884
950
  return h(
885
951
  "div",
886
- { className: "lc-root" },
952
+ { className: "lc-root", ref: rootRef },
887
953
  // ---- session context stats (over the retained window) ----
888
954
  h(StatsBoard, { requests, events }),
889
955
  // ---- overview ----
@@ -903,20 +969,12 @@ function makeContextView(ctx, kit) {
903
969
  h(
904
970
  "div",
905
971
  { className: "lc-overview-num" },
906
- h("b", null, fmt3(current.total)),
907
- h("span", null, data.contextWindow ? " / " + fmt3(data.contextWindow) + " " + tr("overview.ofWindow", { p: windowPct ?? 0 }) : " " + t("overview.estimate")),
908
- // The provider-reported prompt of the last request is the best
909
- // ground truth for what the model actually received; the fixed
910
- // density heuristic can undercount CJK-heavy content, so show the
911
- // real number alongside the estimate.
912
- displayRequests.length > 0 && displayRequests[displayRequests.length - 1].prompt !== void 0 ? h(
913
- "span",
914
- { className: "lc-actual" },
915
- tr("overview.actual", { n: fmt3(displayRequests[displayRequests.length - 1].prompt ?? 0) })
916
- ) : null
972
+ h("b", null, fmt3(headline)),
973
+ h("span", null, occupancyWindow ? " / " + fmt3(occupancyWindow) + " " + tr("overview.ofWindow", { p: headlinePct ?? 0 }) : " " + t("overview.estimate")),
974
+ occupancyTokens !== null ? h("span", { className: "lc-card-sub" }, t("overview.splitEst")) : null
917
975
  ),
918
- h(StackedBar, { parts: partsOf(current), height: 16, max: data.contextWindow, hoverKey: hoverCat, onHoverKey: setHoverCat }),
919
- h(Legend, { parts: partsOf(current), hoverKey: hoverCat, onHoverKey: setHoverCat }),
976
+ h(StackedBar, { parts, height: 16, max: occupancyWindow, hoverKey: hoverCat, onHoverKey: setHoverCat }),
977
+ h(Legend, { parts, hoverKey: hoverCat, onHoverKey: setHoverCat }),
920
978
  data.toolList && data.toolList.length > 0 ? h(
921
979
  "div",
922
980
  { className: "lc-tools" },
package/lib/index.js CHANGED
@@ -94,6 +94,9 @@ function createFold() {
94
94
  provider: void 0,
95
95
  lastModel: void 0,
96
96
  contextWindow: void 0,
97
+ pressureTokens: void 0,
98
+ sampledSurfaceTokens: void 0,
99
+ occupancyWindow: void 0,
97
100
  requests: [],
98
101
  // one entry per answered model call
99
102
  events: [],
@@ -147,8 +150,22 @@ function applySurface(st, ev, type, data, message) {
147
150
  const utext = firstText(message?.content);
148
151
  if (utext !== "") node.text = utext;
149
152
  }
153
+ const shadowedSeqs = st.pendingShadowedSeqs;
154
+ st.pendingShadowedSeqs = void 0;
150
155
  const op = ev.surfaceOp;
151
156
  if (op !== null && typeof op === "object" && op.op === "replace") {
157
+ if (Array.isArray(shadowedSeqs) && shadowedSeqs.length > 0) {
158
+ const shadowed = new Set(shadowedSeqs);
159
+ const kept = [];
160
+ for (const n of st.surface) {
161
+ if (shadowed.has(n.seq)) st.sums[n.cat] -= n.tokens;
162
+ else kept.push(n);
163
+ }
164
+ st.surface = kept;
165
+ st.sums[cat] += node.tokens;
166
+ st.surface.push(node);
167
+ return node;
168
+ }
152
169
  let si = -1;
153
170
  let ei = -1;
154
171
  for (let i = 0; i < st.surface.length; i++) {
@@ -169,6 +186,16 @@ function applySurface(st, ev, type, data, message) {
169
186
  st.sums[cat] += node.tokens;
170
187
  return node;
171
188
  }
189
+ function pressureOf(usage) {
190
+ if (usage === void 0 || typeof usage.inputTokens !== "number") return void 0;
191
+ return usage.inputTokens + (usage.cacheReadTokens || 0) + (usage.cacheWriteTokens || 0);
192
+ }
193
+ function sampleUsage(st, usage) {
194
+ const pressureTokens = pressureOf(usage);
195
+ if (pressureTokens === void 0) return;
196
+ st.pressureTokens = pressureTokens;
197
+ st.sampledSurfaceTokens = st.sums.user + st.sums.inject + st.sums.assistant + st.sums.tool;
198
+ }
172
199
  function foldInto(st, events) {
173
200
  for (let e = st.n; e < events.length; e++) {
174
201
  const ev = events[e];
@@ -196,6 +223,7 @@ function foldInto(st, events) {
196
223
  if (data && typeof data.contextWindow === "number") st.contextWindow = data.contextWindow;
197
224
  if (data && typeof data.model === "string") st.model = data.model;
198
225
  if (data && typeof data.provider === "string") st.provider = data.provider;
226
+ st.occupancyWindow = data && typeof data.contextWindow === "number" ? data.contextWindow : void 0;
199
227
  break;
200
228
  case "tool/call":
201
229
  if (data && data.callId !== void 0 && typeof data.name === "string") st.callNames[String(data.callId)] = data.name;
@@ -227,8 +255,14 @@ function foldInto(st, events) {
227
255
  applySurface(st, ev, ev.type, data, toolMsg);
228
256
  break;
229
257
  }
258
+ case "assistant/chunk": {
259
+ const chunk = data?.chunk;
260
+ if (chunk !== void 0 && chunk.type === "usage") sampleUsage(st, chunk.usage);
261
+ break;
262
+ }
230
263
  case "assistant/message": {
231
264
  const usage = data?.usage;
265
+ sampleUsage(st, usage);
232
266
  const total = st.systemTokens + st.toolsTokens + st.sums.user + st.sums.inject + st.sums.assistant + st.sums.tool;
233
267
  const record = {
234
268
  turn: data && typeof data.turn === "number" ? data.turn : void 0,
@@ -253,6 +287,9 @@ function foldInto(st, events) {
253
287
  break;
254
288
  }
255
289
  case "compaction/summary":
290
+ if (data && Array.isArray(data.shadowedSeqs)) {
291
+ st.pendingShadowedSeqs = data.shadowedSeqs.filter((s) => typeof s === "number");
292
+ }
256
293
  st.events.push({
257
294
  seq: ev.seq,
258
295
  time: ev.time,
@@ -262,6 +299,9 @@ function foldInto(st, events) {
262
299
  });
263
300
  break;
264
301
  case "compaction/prune":
302
+ if (data && Array.isArray(data.shadowedSeqs)) {
303
+ st.pendingShadowedSeqs = data.shadowedSeqs.filter((s) => typeof s === "number");
304
+ }
265
305
  st.events.push({
266
306
  seq: ev.seq,
267
307
  time: ev.time,
@@ -284,6 +324,7 @@ function foldInto(st, events) {
284
324
  // src/host/snapshot.ts
285
325
  function buildResult(st) {
286
326
  const surfaceTotal = st.sums.user + st.sums.inject + st.sums.assistant + st.sums.tool;
327
+ const projectedTokens = st.pressureTokens !== void 0 && st.sampledSurfaceTokens !== void 0 ? Math.max(0, st.pressureTokens + surfaceTotal - st.sampledSurfaceTokens) : void 0;
287
328
  const result = {
288
329
  ok: true,
289
330
  model: st.model,
@@ -298,6 +339,13 @@ function buildResult(st) {
298
339
  tool: st.sums.tool,
299
340
  total: surfaceTotal + st.systemTokens + st.toolsTokens
300
341
  },
342
+ occupancy: {
343
+ ...st.pressureTokens === void 0 ? {} : { pressureTokens: st.pressureTokens },
344
+ surfaceTokens: surfaceTotal,
345
+ ...st.sampledSurfaceTokens === void 0 ? {} : { sampledSurfaceTokens: st.sampledSurfaceTokens },
346
+ ...projectedTokens === void 0 ? {} : { projectedTokens },
347
+ ...st.occupancyWindow === void 0 ? {} : { contextWindow: st.occupancyWindow }
348
+ },
301
349
  toolList: st.toolList,
302
350
  requests: st.requests,
303
351
  events: st.events,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "description": "Context insight panel for DeepSeek Harness: see 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": {