dsh-context 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +20 -9
  2. package/lib/client.js +606 -428
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,15 +1,16 @@
1
- ![Social preview](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/social-preview.png)
1
+ <p align="center"><img src="https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/social-preview.png" width="840" alt="Social preview"></p>
2
2
 
3
3
  # dsh-context
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/dsh-context)](https://www.npmjs.com/package/dsh-context)
6
6
  [![GitHub stars](https://img.shields.io/github/stars/bowenliang123/dsh-context?style=social)](https://github.com/bowenliang123/dsh-context)
7
7
 
8
- **See what your DeepSeek Harness agent's context window is actually made of and how it evolves.**
8
+ **A DeepSeek Harness plugin for context dashboard and context command, for understanding how the context is made of, and how it evolves.**
9
9
 
10
- `dsh-context` is a [DeepSeek Harness](https://github.com/deepseek-ai/DeepSeek-Harness) plugin that adds a **Context Insight** panel to the web UI: a live, visual answer to *"what is the model carrying right now, and how did it get there?"* — context composition, per-request history, compactions, and injections, all in one place.
10
+ `dsh-context` is a [DeepSeek Harness](https://github.com/deepseek-ai/DeepSeek-Harness) plugin for context insight.
11
11
 
12
- ![Context panel overview](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/context-overview.png)
12
+ - **The Context tab** — a full insight panel for context composition, per-turn context history, context compactions, and the message surface, etc.
13
+ - **The `/context` command** — the same headline and recent trend as a centered dialog straight from the composer (the screenshot above).
13
14
 
14
15
  ## Install
15
16
 
@@ -19,7 +20,21 @@ One command, from any DeepSeek Harness installation:
19
20
  dsh plugin --profile web add dsh-context
20
21
  ```
21
22
 
22
- Then start the web UI with `dsh web`, open any session, and click the **上下文 / Context** tab. No build step, no restart.
23
+ Then start the web UI with `dsh web`. No build step, no restart.
24
+
25
+ ## Use it
26
+
27
+ ### Context tab
28
+
29
+ Open any session and click the **Context / 上下文** tab:
30
+
31
+ ![Context panel overview](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/context-overview.png)
32
+
33
+ ### ⌨️ `/context` command — In-session Context Insight modal
34
+
35
+ Type `/context` (or pick it from the `/` menu) and press Enter: a centered dialog shows the provider-anchored occupancy headline, the six-category composition bar, and the last-10-turn trend chart — hover or click a bar for its full breakdown, exactly like the tab.
36
+
37
+ ![Context command](https://raw.githubusercontent.com/bowenliang123/dsh-context/main/docs/context-command.png)
23
38
 
24
39
  ## What you'll see
25
40
 
@@ -53,10 +68,6 @@ Every compaction, tool-output prune, skill or plugin context injection, and mode
53
68
 
54
69
  The exact message list the model sees right now, newest first, with a per-message token cost.
55
70
 
56
- ## Releasing
57
-
58
- Releases are cut by tagging: `git tag vX.Y.Z && gh release create vX.Y.Z`. A [GitHub Actions workflow](.github/workflows/release.yml) then builds, tests, and publishes the package to npm automatically via [npm Trusted Publishing (OIDC)](https://docs.npmjs.com/trusted-publishers) — no long-lived token needed, provenance included.
59
-
60
71
  ## Like it?
61
72
 
62
73
  If `dsh-context` helped you understand what your agent is carrying around, a ⭐ on [GitHub](https://github.com/bowenliang123/dsh-context) is much appreciated — and issues/PRs are welcome!
package/lib/client.js CHANGED
@@ -76,7 +76,9 @@ var DICT_ZH = {
76
76
  "node.calls": "\u8C03\u7528 ",
77
77
  "node.empty": "(\u7A7A\u56DE\u590D)",
78
78
  "node.nonText": "(\u975E\u6587\u672C\u6D88\u606F)",
79
- "node.snapshot": "\u5FEB\u7167: "
79
+ "node.snapshot": "\u5FEB\u7167: ",
80
+ "cmd.desc": "\u67E5\u770B\u4E0A\u4E0B\u6587\u7684\u6784\u6210\u548C\u53D8\u5316",
81
+ "cmd.close": "\u5173\u95ED"
80
82
  };
81
83
  var DICT_EN = {
82
84
  "tab": "Context",
@@ -147,9 +149,502 @@ var DICT_EN = {
147
149
  "node.calls": "calls ",
148
150
  "node.empty": "(empty reply)",
149
151
  "node.nonText": "(non-text message)",
150
- "node.snapshot": "snapshot: "
152
+ "node.snapshot": "snapshot: ",
153
+ "cmd.desc": "View context makeup and how it evolves",
154
+ "cmd.close": "Close"
151
155
  };
152
156
 
157
+ // src/client/modalStore.ts
158
+ var stores = /* @__PURE__ */ new Map();
159
+ function modalStoreOf(sessionId) {
160
+ const existing = stores.get(sessionId);
161
+ if (existing !== void 0) return existing;
162
+ let open = false;
163
+ const listeners = /* @__PURE__ */ new Set();
164
+ const store = {
165
+ subscribe(listener) {
166
+ listeners.add(listener);
167
+ return () => {
168
+ listeners.delete(listener);
169
+ };
170
+ },
171
+ getSnapshot: () => open,
172
+ set(next) {
173
+ if (next === open) return;
174
+ open = next;
175
+ for (const listener of listeners) listener();
176
+ }
177
+ };
178
+ stores.set(sessionId, store);
179
+ return store;
180
+ }
181
+ var pendingConsume = /* @__PURE__ */ new Map();
182
+ function setPendingConsume(sessionId, guard) {
183
+ pendingConsume.set(sessionId, guard);
184
+ }
185
+ function takePendingConsume(sessionId) {
186
+ const guard = pendingConsume.get(sessionId);
187
+ if (guard !== void 0) pendingConsume.delete(sessionId);
188
+ return guard;
189
+ }
190
+
191
+ // src/client/command.ts
192
+ var COMMAND = "context";
193
+ var LINE = "/" + COMMAND;
194
+ function registerContextCommand(ctx, kit) {
195
+ ctx.effect(() => {
196
+ const inputTriggers = ctx.get("inputTriggers");
197
+ if (inputTriggers === void 0) return () => {
198
+ };
199
+ return inputTriggers.registerSource({
200
+ trigger: "/",
201
+ name: COMMAND,
202
+ order: 1,
203
+ candidates: (_session, req) => {
204
+ if (req.position !== "leading") return Promise.resolve([]);
205
+ const query = req.query.trim().toLowerCase();
206
+ if (query !== "" && !COMMAND.startsWith(query)) return Promise.resolve([]);
207
+ return Promise.resolve([{ name: COMMAND, description: kit.t("cmd.desc") }]);
208
+ },
209
+ onPick: (pick) => {
210
+ setPendingConsume(pick.session.sessionId, { kind: "span", span: pick.span });
211
+ modalStoreOf(pick.session.sessionId).set(true);
212
+ return "handled";
213
+ },
214
+ matchEnter: (session, line) => {
215
+ if (line !== LINE) return Promise.resolve(void 0);
216
+ setPendingConsume(session.sessionId, { kind: "bare-token", token: LINE });
217
+ modalStoreOf(session.sessionId).set(true);
218
+ return Promise.resolve("handled");
219
+ }
220
+ });
221
+ }, "dsh-context: /context command");
222
+ }
223
+
224
+ // src/client/categories.ts
225
+ var CATS = [
226
+ { key: "system", color: "#6366f1" },
227
+ { key: "tools", color: "#f59e0b" },
228
+ { key: "user", color: "#22c55e" },
229
+ { key: "inject", color: "#a855f7" },
230
+ { key: "assistant", color: "#3b82f6" },
231
+ { key: "tool", color: "#14b8a6" }
232
+ ];
233
+ function partsOf(breakdown) {
234
+ return CATS.map((c) => {
235
+ return { key: c.key, color: c.color, value: breakdown[c.key] || 0 };
236
+ });
237
+ }
238
+ function anchoredParts(parts, target) {
239
+ if (target === null || target <= 0) return parts;
240
+ let total = 0;
241
+ for (const p of parts) total += p.value;
242
+ if (total <= 0 || total === target) return parts;
243
+ const scale = target / total;
244
+ return parts.map((p) => ({ ...p, value: Math.round(p.value * scale) }));
245
+ }
246
+
247
+ // src/client/headline.ts
248
+ function headlineOf(data) {
249
+ const current = data.current;
250
+ const occ = data.occupancy;
251
+ const projected = occ !== void 0 && typeof occ.projectedTokens === "number" ? occ.projectedTokens : void 0;
252
+ const requests = data.requests || [];
253
+ const lastReq = requests.length > 0 ? requests[requests.length - 1] : null;
254
+ const derived = lastReq !== null && typeof lastReq.prompt === "number" ? lastReq.prompt + (current.total - lastReq.total) : void 0;
255
+ const occupancyTokens = projected ?? derived ?? null;
256
+ const window2 = occ !== void 0 && typeof occ.contextWindow === "number" ? occ.contextWindow : data.contextWindow;
257
+ const tokens = occupancyTokens ?? current.total;
258
+ const pct = window2 !== void 0 && window2 > 0 ? Math.min(100, Math.round(tokens / window2 * 100)) : null;
259
+ const parts = anchoredParts(partsOf(current), occupancyTokens !== null && tokens > 0 ? tokens : null);
260
+ return { tokens, window: window2, pct, parts, estimated: occupancyTokens === null };
261
+ }
262
+
263
+ // src/client/services.ts
264
+ function timelineOf(value) {
265
+ if (value === null || value === void 0 || typeof value !== "object") return null;
266
+ return value;
267
+ }
268
+
269
+ // src/client/react.ts
270
+ var React = require("react");
271
+ var h = React.createElement;
272
+
273
+ // src/client/components/requestDetail.tsx
274
+ function makeRequestDetail(kit, StackedBar) {
275
+ const { t, tr, fmt: fmt3, fmtTime: fmtTime2, catLabel, eventLabel, eventAt } = kit;
276
+ return function RequestDetail(props) {
277
+ const req = props.request;
278
+ if (!req) return null;
279
+ const isTurn = req.stepCount !== void 0 && req.stepCount > 1;
280
+ 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 });
281
+ const marker = props.marker ?? null;
282
+ const markerAt = marker !== null ? eventAt(marker) : null;
283
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-detail" }, /* @__PURE__ */ React.createElement("div", { className: "lc-detail-head" }, /* @__PURE__ */ React.createElement("b", null, head), marker !== null && markerAt !== null ? /* @__PURE__ */ React.createElement("span", { className: "lc-detail-marker", title: eventLabel(marker) }, "\u2702 " + markerAt) : null, isTurn ? /* @__PURE__ */ React.createElement("span", { className: "lc-detail-tag" }, t("detail.lastStep")) : null, /* @__PURE__ */ React.createElement("span", null, fmtTime2(req.time)), /* @__PURE__ */ React.createElement("span", null, tr("detail.estTotal", { n: fmt3(req.total) })), req.prompt !== void 0 ? /* @__PURE__ */ React.createElement("span", { className: "lc-actual" }, tr("detail.actual", { n: fmt3(req.prompt) })) : null, req.output !== void 0 ? /* @__PURE__ */ React.createElement("span", null, tr("detail.output", { n: fmt3(req.output) })) : null), /* @__PURE__ */ React.createElement(StackedBar, { parts: partsOf(req), height: 10 }), /* @__PURE__ */ React.createElement("div", { className: "lc-detail-rows" }, CATS.map((c) => {
284
+ const v = req[c.key] || 0;
285
+ return /* @__PURE__ */ React.createElement("div", { key: c.key, className: "lc-detail-row" }, /* @__PURE__ */ React.createElement("i", { style: { background: c.color } }), /* @__PURE__ */ React.createElement("span", { className: "lc-detail-label" }, catLabel(c.key)), /* @__PURE__ */ React.createElement("span", { className: "lc-bar-track" }, /* @__PURE__ */ React.createElement("span", { className: "lc-bar-fill", style: { width: (req.total > 0 ? v / req.total * 100 : 0) + "%", background: c.color } })), /* @__PURE__ */ React.createElement("span", { className: "lc-detail-num" }, "\u2248" + fmt3(v)), /* @__PURE__ */ React.createElement("span", { className: "lc-detail-pct" }, req.total > 0 ? Math.round(v / req.total * 100) + "%" : ""));
286
+ })));
287
+ };
288
+ }
289
+
290
+ // src/client/components/stackedBar.tsx
291
+ function makeStackedBar(kit) {
292
+ const { t, tr, fmt: fmt3, catLabel } = kit;
293
+ return function StackedBar(props) {
294
+ let total = 0;
295
+ for (const p of props.parts) total += p.value;
296
+ const scale = props.max !== void 0 && props.max > total ? props.max : total;
297
+ const free = props.max !== void 0 && props.max > total ? props.max - total : 0;
298
+ const usedPct = scale > 0 ? total / scale * 100 : 0;
299
+ const hovering = props.hoverKey !== null && props.hoverKey !== void 0;
300
+ const showBox = free > 0 && hovering;
301
+ let tip = null;
302
+ if (props.hoverKey !== null && props.hoverKey !== void 0) {
303
+ if (props.hoverKey === "free" && free > 0) {
304
+ const pct = scale > 0 ? free / scale * 100 : 0;
305
+ tip = {
306
+ text: t("overview.free") + " " + fmt3(free) + " (" + Math.round(pct) + "%)",
307
+ leftPct: Math.max(12, Math.min(total / scale * 100 + pct / 2, 88))
308
+ };
309
+ } else {
310
+ let acc = 0;
311
+ for (const p of props.parts) {
312
+ const pct = scale > 0 ? p.value / scale * 100 : 0;
313
+ if (p.key === props.hoverKey && p.value > 0) {
314
+ tip = {
315
+ // "(pct%)" is a share of the OCCUPIED total — the dashed box
316
+ // that appears on hover frames exactly this reference region.
317
+ text: catLabel(p.key) + " \u2248" + fmt3(p.value) + " (" + Math.round(p.value / total * 100) + "%) " + tr("overview.ofUsed"),
318
+ leftPct: Math.max(12, Math.min(acc + pct / 2, 88))
319
+ };
320
+ break;
321
+ }
322
+ acc += pct;
323
+ }
324
+ }
325
+ }
326
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-stacked-wrap" }, /* @__PURE__ */ React.createElement(
327
+ "div",
328
+ {
329
+ className: "lc-stacked" + (hovering ? " lc-stacked-dim" : ""),
330
+ style: { height: (props.height || 14) + "px" },
331
+ onMouseLeave: () => {
332
+ if (props.onHoverKey !== void 0) props.onHoverKey(null);
333
+ }
334
+ },
335
+ total > 0 ? props.parts.map((p) => {
336
+ if (!p.value) return null;
337
+ const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
338
+ return /* @__PURE__ */ React.createElement(
339
+ "div",
340
+ {
341
+ key: p.key,
342
+ className: "lc-stacked-seg" + (on ? " lc-stacked-seg-on" : ""),
343
+ style: { width: p.value / scale * 100 + "%", background: p.color },
344
+ onMouseEnter: () => {
345
+ if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
346
+ }
347
+ }
348
+ );
349
+ }) : null,
350
+ free > 0 ? /* @__PURE__ */ React.createElement(
351
+ "div",
352
+ {
353
+ key: "free",
354
+ className: "lc-stacked-free" + (props.hoverKey === "free" ? " lc-stacked-free-on" : ""),
355
+ style: { width: free / scale * 100 + "%" },
356
+ onMouseEnter: () => {
357
+ if (props.onHoverKey !== void 0) props.onHoverKey("free");
358
+ }
359
+ }
360
+ ) : null,
361
+ showBox ? /* @__PURE__ */ React.createElement("div", { className: "lc-occupied-box", style: { width: usedPct + "%" } }) : null
362
+ ), tip ? /* @__PURE__ */ React.createElement("div", { className: "lc-bar-tip", style: { left: tip.leftPct + "%" } }, tip.text) : null);
363
+ };
364
+ }
365
+ function makeLegend(kit) {
366
+ const { tr, fmt: fmt3, catLabel } = kit;
367
+ return function Legend(props) {
368
+ let total = 0;
369
+ for (const p of props.parts) total += p.value;
370
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-legend" }, props.parts.map((p) => {
371
+ const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
372
+ return /* @__PURE__ */ React.createElement(
373
+ "span",
374
+ {
375
+ key: p.key,
376
+ className: "lc-chip" + (on ? " lc-chip-on" : ""),
377
+ title: tr("overview.ofUsed"),
378
+ onMouseEnter: () => {
379
+ if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
380
+ },
381
+ onMouseLeave: () => {
382
+ if (props.onHoverKey !== void 0) props.onHoverKey(null);
383
+ }
384
+ },
385
+ /* @__PURE__ */ React.createElement("i", { style: { background: p.color } }),
386
+ catLabel(p.key) + " \u2248" + fmt3(p.value),
387
+ total > 0 ? /* @__PURE__ */ React.createElement("em", null, Math.round(p.value / total * 100) + "%") : null
388
+ );
389
+ }));
390
+ };
391
+ }
392
+
393
+ // src/client/components/trendChart.tsx
394
+ function aggregateByTurn(requests) {
395
+ const out = [];
396
+ let runSteps = 0;
397
+ for (const req of requests) {
398
+ const last = out.length > 0 ? out[out.length - 1] : null;
399
+ if (last !== null && (last.turn ?? 0) === (req.turn ?? 0)) {
400
+ runSteps++;
401
+ out[out.length - 1] = { ...req, stepCount: runSteps };
402
+ } else {
403
+ runSteps = 1;
404
+ out.push({ ...req, stepCount: 1 });
405
+ }
406
+ }
407
+ return out;
408
+ }
409
+ function attachMarkers(requests, events) {
410
+ const markers = new Array(requests.length);
411
+ for (const ev of events) {
412
+ if (ev.kind !== "compaction" && ev.kind !== "prune") continue;
413
+ for (let r = 0; r < requests.length; r++) {
414
+ if (requests[r].seq >= ev.seq) {
415
+ if (markers[r] === void 0) markers[r] = ev;
416
+ break;
417
+ }
418
+ }
419
+ }
420
+ return markers;
421
+ }
422
+ function makeTrendChart(kit) {
423
+ const { t, tr, fmt: fmt3, fmtTime: fmtTime2, catLabel, eventLabel, eventAt } = kit;
424
+ const CHART_H = 112;
425
+ const BAR_W = 14;
426
+ const BAR_GAP = 2;
427
+ const TURN_COLORS = ["#6366f1", "#f59e0b", "#22c55e", "#a855f7", "#3b82f6", "#14b8a6", "#ef4444", "#ec4899"];
428
+ return function TrendChart(props) {
429
+ const requests = props.requests;
430
+ const markers = props.markers;
431
+ const anchorOf = (req) => typeof req.prompt === "number" && req.prompt > 0 && req.total > 0 ? req.prompt / req.total : 1;
432
+ const barTotalOf = (req) => typeof req.prompt === "number" && req.prompt > 0 ? req.prompt : req.total;
433
+ let maxTotal = 1;
434
+ for (const req of requests) {
435
+ const bt = barTotalOf(req);
436
+ if (bt > maxTotal) maxTotal = bt;
437
+ }
438
+ const groups = [];
439
+ for (const req of requests) {
440
+ let grp = groups.length > 0 ? groups[groups.length - 1] : null;
441
+ if (grp === null || grp.turn !== (req.turn ?? 0)) {
442
+ grp = { turn: req.turn ?? 0, count: 0, span: 0, agg: req.stepCount !== void 0 };
443
+ groups.push(grp);
444
+ }
445
+ grp.count++;
446
+ grp.span += req.stepCount ?? 1;
447
+ }
448
+ const scrollRef = React.useRef(null);
449
+ const scrolledOnce = React.useRef(false);
450
+ const lastGranRef = React.useRef(props.granularity);
451
+ const [edges, setEdges] = React.useState({ left: false, right: false });
452
+ const edgesRef = React.useRef(edges);
453
+ const updateEdges = (el) => {
454
+ const left = el.scrollLeft > 4;
455
+ const right = el.scrollLeft + el.clientWidth < el.scrollWidth - 4;
456
+ const prev = edgesRef.current;
457
+ if (prev.left === left && prev.right === right) return;
458
+ edgesRef.current = { left, right };
459
+ setEdges({ left, right });
460
+ };
461
+ React.useLayoutEffect(() => {
462
+ const el = scrollRef.current;
463
+ if (el === null) return;
464
+ if (props.granularity !== lastGranRef.current) {
465
+ lastGranRef.current = props.granularity;
466
+ scrolledOnce.current = false;
467
+ }
468
+ if (!scrolledOnce.current) {
469
+ scrolledOnce.current = true;
470
+ el.scrollLeft = el.scrollWidth;
471
+ } else if (el.scrollLeft + el.clientWidth >= el.scrollWidth - 24) {
472
+ el.scrollLeft = el.scrollWidth;
473
+ }
474
+ updateEdges(el);
475
+ });
476
+ const tipOf = (req) => {
477
+ 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 });
478
+ 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) }) : "");
479
+ };
480
+ const hoveredIdx = props.hoveredSeq !== null ? requests.findIndex((r) => r.seq === props.hoveredSeq) : -1;
481
+ const hoveredReq = hoveredIdx >= 0 ? requests[hoveredIdx] : null;
482
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-chartrow" }, /* @__PURE__ */ React.createElement("div", { className: "lc-axis" }, /* @__PURE__ */ React.createElement("span", { className: "lc-axis-top" }, fmt3(maxTotal)), /* @__PURE__ */ React.createElement("span", { className: "lc-axis-mid" }, fmt3(Math.round(maxTotal / 2))), /* @__PURE__ */ React.createElement("span", { className: "lc-axis-bot" }, "0")), /* @__PURE__ */ React.createElement(
483
+ "div",
484
+ {
485
+ className: "lc-chart-scroll" + (props.activeTurn !== null ? " lc-chart-dim" : ""),
486
+ ref: scrollRef,
487
+ onScroll: (e) => {
488
+ updateEdges(e.currentTarget);
489
+ }
490
+ },
491
+ edges.left ? /* @__PURE__ */ React.createElement("div", { className: "lc-chart-fade lc-chart-fade-l" }) : null,
492
+ /* @__PURE__ */ React.createElement(
493
+ "div",
494
+ {
495
+ className: "lc-chart",
496
+ onMouseLeave: () => {
497
+ props.onHover(null);
498
+ }
499
+ },
500
+ /* @__PURE__ */ React.createElement("div", { className: "lc-grid lc-grid-top" }),
501
+ /* @__PURE__ */ React.createElement("div", { className: "lc-grid lc-grid-mid" }),
502
+ requests.map((req, i) => {
503
+ const marker = markers[i];
504
+ const markerAt = marker !== void 0 ? eventAt(marker) : null;
505
+ const selected = props.selectedSeq === req.seq;
506
+ const hovered = props.hoveredSeq === req.seq;
507
+ const inTurn = props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn;
508
+ return /* @__PURE__ */ React.createElement(
509
+ "div",
510
+ {
511
+ key: req.seq,
512
+ className: "lc-bar" + (selected ? " lc-bar-selected" : "") + (hovered ? " lc-bar-hovered" : "") + (inTurn ? " lc-bar-in-turn" : ""),
513
+ style: { width: BAR_W + "px" },
514
+ onClick: () => {
515
+ props.onSelect(selected ? null : req.seq);
516
+ },
517
+ onMouseEnter: () => {
518
+ props.onHover(req.seq);
519
+ }
520
+ },
521
+ marker !== void 0 ? /* @__PURE__ */ React.createElement(
522
+ "span",
523
+ {
524
+ className: "lc-bar-marker",
525
+ title: "\u2702 " + (markerAt !== null ? markerAt + " \u2014 " : "") + eventLabel(marker)
526
+ },
527
+ "\u2702"
528
+ ) : null,
529
+ /* @__PURE__ */ React.createElement("div", { className: "lc-bar-stack" }, CATS.map((c) => {
530
+ const v = (req[c.key] || 0) * anchorOf(req);
531
+ if (!v) return null;
532
+ return /* @__PURE__ */ React.createElement("div", { key: c.key, style: { height: Math.max(1, Math.round(v / maxTotal * CHART_H)) + "px", background: c.color } });
533
+ }))
534
+ );
535
+ })
536
+ ),
537
+ hoveredReq !== null ? /* @__PURE__ */ React.createElement(
538
+ "div",
539
+ {
540
+ className: "lc-chart-tip",
541
+ style: { left: hoveredIdx * (BAR_W + BAR_GAP) + BAR_W / 2 + "px" }
542
+ },
543
+ tipOf(hoveredReq)
544
+ ) : null,
545
+ /* @__PURE__ */ React.createElement("div", { className: "lc-turns", onMouseLeave: () => {
546
+ props.onHoverTurn(null);
547
+ } }, groups.map((grp, gi) => {
548
+ const on = props.activeTurn === grp.turn;
549
+ const blockW = grp.agg ? BAR_W : grp.span * (BAR_W + BAR_GAP) - BAR_GAP;
550
+ return /* @__PURE__ */ React.createElement(
551
+ "span",
552
+ {
553
+ key: "turn-" + gi,
554
+ className: "lc-turn" + (on ? " lc-turn-on" : ""),
555
+ style: {
556
+ width: blockW + "px",
557
+ background: TURN_COLORS[gi % TURN_COLORS.length]
558
+ },
559
+ title: "T" + grp.turn,
560
+ onMouseEnter: () => {
561
+ props.onHoverTurn(grp.turn);
562
+ }
563
+ },
564
+ "T" + grp.turn
565
+ );
566
+ }))
567
+ ), edges.right ? /* @__PURE__ */ React.createElement("div", { className: "lc-chart-fade lc-chart-fade-r" }) : null);
568
+ };
569
+ }
570
+
571
+ // src/client/components/contextModal.tsx
572
+ var TREND_TURNS = 10;
573
+ function makeContextModal(ctx, kit) {
574
+ const { t, tr, fmt: fmt3 } = kit;
575
+ const sessions = ctx.get("sessions");
576
+ const StackedBar = makeStackedBar(kit);
577
+ const Legend = makeLegend(kit);
578
+ const TrendChart = makeTrendChart(kit);
579
+ const RequestDetail = makeRequestDetail(kit, StackedBar);
580
+ return function ContextModal(props) {
581
+ const sessionId = typeof props.sessionId === "string" ? props.sessionId : "";
582
+ const open = typeof props.useContextModal === "function" ? props.useContextModal((s) => s) : false;
583
+ const data = typeof props.useProjection === "function" ? timelineOf(props.useProjection("contextTimeline")) : null;
584
+ const [selectedSeq, setSelectedSeq] = React.useState(null);
585
+ const [hoveredSeq, setHoveredSeq] = React.useState(null);
586
+ const [hoverCat, setHoverCat] = React.useState(null);
587
+ const close = React.useCallback(() => {
588
+ if (sessionId === "") return;
589
+ modalStoreOf(sessionId).set(false);
590
+ const guard = takePendingConsume(sessionId);
591
+ if (guard === void 0 || sessions === void 0) return;
592
+ const scope = sessions.scope(sessionId);
593
+ if (scope !== void 0) scope.bail(scope, "slash/input-consume-token", { guard });
594
+ }, [sessionId]);
595
+ React.useEffect(() => {
596
+ if (!open) return void 0;
597
+ const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
598
+ const onKey = (ev) => {
599
+ if (ev.key !== "Escape") return;
600
+ ev.preventDefault();
601
+ ev.stopPropagation();
602
+ close();
603
+ };
604
+ window.addEventListener("keydown", onKey, true);
605
+ return () => {
606
+ window.removeEventListener("keydown", onKey, true);
607
+ if (previous !== null && document.contains(previous)) previous.focus();
608
+ };
609
+ }, [open, close]);
610
+ if (!open) return null;
611
+ const requests = data !== null ? data.requests || [] : [];
612
+ const events = data !== null ? data.events || [] : [];
613
+ const turns = aggregateByTurn(requests).slice(-TREND_TURNS);
614
+ const markers = attachMarkers(turns, events);
615
+ let pinnedReq = null;
616
+ for (const req of turns) if (req.seq === selectedSeq) pinnedReq = req;
617
+ let activeReq = null;
618
+ if (hoveredSeq !== null) {
619
+ for (const req of turns) if (req.seq === hoveredSeq) activeReq = req;
620
+ }
621
+ if (activeReq === null) activeReq = pinnedReq;
622
+ if (activeReq === null && turns.length > 0) activeReq = turns[turns.length - 1];
623
+ const markerOf = (req) => {
624
+ const i = turns.indexOf(req);
625
+ return i >= 0 ? markers[i] : void 0;
626
+ };
627
+ const head = data !== null ? headlineOf(data) : null;
628
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-modal-backdrop", onClick: close }, /* @__PURE__ */ React.createElement("div", { className: "lc-modal-card", onClick: (ev) => {
629
+ ev.stopPropagation();
630
+ } }, /* @__PURE__ */ React.createElement("div", { className: "lc-modal-head" }, /* @__PURE__ */ React.createElement("span", { className: "lc-modal-title" }, t("tab")), data !== null && (data.model || data.provider) ? /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, (data.model ? data.model : "") + (data.provider ? " \xB7 " + data.provider : "")) : null, /* @__PURE__ */ React.createElement("button", { className: "lc-modal-close", "aria-label": t("cmd.close"), onClick: close }, "\xD7")), data === null || head === null ? /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("loading")) : /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("div", { className: "lc-overview-num" }, /* @__PURE__ */ React.createElement("b", null, fmt3(head.tokens)), /* @__PURE__ */ React.createElement("span", null, head.window ? " / " + fmt3(head.window) + " " + tr("overview.ofWindow", { p: head.pct ?? 0 }) : " " + t("overview.estimate")), !head.estimated ? /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("overview.splitEst")) : null), /* @__PURE__ */ React.createElement(StackedBar, { parts: head.parts, height: 16, max: head.window, hoverKey: hoverCat, onHoverKey: setHoverCat }), /* @__PURE__ */ React.createElement(Legend, { parts: head.parts, hoverKey: hoverCat, onHoverKey: setHoverCat }), /* @__PURE__ */ React.createElement("div", { className: "lc-card-title lc-modal-trend" }, t("trend.title")), turns.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("trend.empty")) : /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(
631
+ TrendChart,
632
+ {
633
+ requests: turns,
634
+ markers,
635
+ selectedSeq: pinnedReq !== null ? pinnedReq.seq : null,
636
+ hoveredSeq,
637
+ activeTurn: null,
638
+ granularity: "turn",
639
+ onSelect: setSelectedSeq,
640
+ onHover: setHoveredSeq,
641
+ onHoverTurn: () => {
642
+ }
643
+ }
644
+ ), /* @__PURE__ */ React.createElement(RequestDetail, { request: activeReq, marker: activeReq !== null ? markerOf(activeReq) : void 0 })))));
645
+ };
646
+ }
647
+
153
648
  // src/client/styles.ts
154
649
  var STYLES = [
155
650
  ".lc-root { padding: 16px 20px 32px; overflow-y: auto; height: 100%; box-sizing: border-box; color: var(--dsw-alias-label-primary); font-size: 13px; }",
@@ -263,38 +758,17 @@ var STYLES = [
263
758
  ".lc-node-tokens { color: var(--dsw-alias-label-secondary); }",
264
759
  ".lc-nodes-more { color: var(--dsw-alias-label-secondary); padding: 3px 0; }",
265
760
  ".lc-empty { color: var(--dsw-alias-label-secondary); padding: 18px 0; text-align: center; }",
266
- ".lc-foot { color: var(--dsw-alias-label-secondary); font-size: 12px; margin-top: 4px; }"
761
+ ".lc-foot { color: var(--dsw-alias-label-secondary); font-size: 12px; margin-top: 4px; }",
762
+ // ---- /context modal (centered dialog; escapes the composer anchor via fixed positioning) ----
763
+ ".lc-modal-backdrop { position: fixed; inset: 0; z-index: 200; background: rgba(0, 0, 0, 0.45); display: flex; align-items: center; justify-content: center; }",
764
+ ".lc-modal-card { width: min(720px, calc(100vw - 48px)); max-height: min(82vh, 760px); overflow-y: auto; box-sizing: border-box; background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; box-shadow: var(--dsw-shadow-lv3, 0 12px 32px rgba(0, 0, 0, 0.4)); padding: 16px 18px 18px; color: var(--dsw-alias-label-primary); font-size: 13px; }",
765
+ ".lc-modal-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 12px; }",
766
+ ".lc-modal-title { font-weight: 600; font-size: 14px; }",
767
+ ".lc-modal-close { margin-left: auto; border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 18px; line-height: 1; padding: 2px 6px; border-radius: 6px; cursor: pointer; font-family: inherit; }",
768
+ ".lc-modal-close:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); }",
769
+ ".lc-modal-trend { margin-top: 14px; }"
267
770
  ].join("\n");
268
771
 
269
- // src/client/categories.ts
270
- var CATS = [
271
- { key: "system", color: "#6366f1" },
272
- { key: "tools", color: "#f59e0b" },
273
- { key: "user", color: "#22c55e" },
274
- { key: "inject", color: "#a855f7" },
275
- { key: "assistant", color: "#3b82f6" },
276
- { key: "tool", color: "#14b8a6" }
277
- ];
278
- function partsOf(breakdown) {
279
- return CATS.map((c) => {
280
- return { key: c.key, color: c.color, value: breakdown[c.key] || 0 };
281
- });
282
- }
283
- function anchoredParts(parts, target) {
284
- if (target === null || target <= 0) return parts;
285
- let total = 0;
286
- for (const p of parts) total += p.value;
287
- if (total <= 0 || total === target) return parts;
288
- const scale = target / total;
289
- return parts.map((p) => ({ ...p, value: Math.round(p.value * scale) }));
290
- }
291
-
292
- // src/client/services.ts
293
- function timelineOf(value) {
294
- if (value === null || value === void 0 || typeof value !== "object") return null;
295
- return value;
296
- }
297
-
298
772
  // src/client/components/events.tsx
299
773
  var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
300
774
 
@@ -313,10 +787,6 @@ function fmtTime(t) {
313
787
  return p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds());
314
788
  }
315
789
 
316
- // src/client/react.ts
317
- var React = require("react");
318
- var h = React.createElement;
319
-
320
790
  // src/client/components/events.tsx
321
791
  var EVENT_ICONS = { compaction: "\u2702", prune: "\u2702", inject: "\uFF0B", model: "\u21C4" };
322
792
  function makeEventText(t, tr) {
@@ -331,396 +801,98 @@ function makeEventText(t, tr) {
331
801
  }
332
802
  return ev.kind;
333
803
  }
334
- function eventAt(ev) {
335
- if (ev.kind === "compaction" || ev.kind === "prune") {
336
- if (typeof ev.turn === "number" && typeof ev.step === "number") {
337
- if (typeof ev.fromTurn === "number" && typeof ev.fromStep === "number") {
338
- if (ev.fromTurn === ev.turn) return t("events.range", { t: ev.turn, a: ev.fromStep, b: ev.step });
339
- return t("events.rangeTo", { a: ev.fromTurn, as: ev.fromStep, b: ev.turn, bs: ev.step });
340
- }
341
- return t("events.at", { t: ev.turn, s: ev.step });
342
- }
343
- return null;
344
- }
345
- if (typeof ev.turn === "number" && typeof ev.step === "number") {
346
- return t("events.at", { t: ev.turn, s: ev.step });
347
- }
348
- return null;
349
- }
350
- return { eventLabel, eventAt };
351
- }
352
- function makeEventList(kit) {
353
- const { t, fmt: fmt3, fmtTime: fmtTime2, eventLabel, eventAt } = kit;
354
- return function EventList(props) {
355
- if (props.events.length === 0) {
356
- return /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("events.empty"));
357
- }
358
- const sorted = props.events.slice().reverse();
359
- return /* @__PURE__ */ React.createElement("div", { className: "lc-events" }, sorted.map((ev, i) => {
360
- const label = eventLabel(ev);
361
- const at = eventAt(ev);
362
- const glyph = ev.kind === "inject" ? /* @__PURE__ */ React.createElement(import_dsh_client_ui_primitives.IconPlusOutline16, null) : ev.kind === "model" ? /* @__PURE__ */ React.createElement(import_dsh_client_ui_primitives.IconBranchOutline16, null) : EVENT_ICONS[ev.kind] || "\u2022";
363
- return /* @__PURE__ */ React.createElement("div", { key: ev.seq + "-" + i, className: "lc-event" }, /* @__PURE__ */ React.createElement("span", { className: "lc-event-icon lc-event-" + ev.kind }, glyph), /* @__PURE__ */ React.createElement("span", { className: "lc-event-label", title: label }, label), at !== null ? /* @__PURE__ */ React.createElement("span", { className: "lc-event-at" }, at) : null, ev.tokens ? /* @__PURE__ */ React.createElement("span", { className: "lc-event-tokens" + (ev.kind === "inject" ? " lc-up" : " lc-down") }, (ev.kind === "inject" ? "+" : "\u2212") + fmt3(ev.tokens)) : null, /* @__PURE__ */ React.createElement("span", { className: "lc-event-time" }, fmtTime2(ev.time)));
364
- }));
365
- };
366
- }
367
-
368
- // src/client/components/nodes.tsx
369
- function makeNodeList(kit) {
370
- const { t, tr, fmt: fmt3, fmtTime: fmtTime2 } = kit;
371
- function nodeText(n) {
372
- if (n.cat === "tool") {
373
- return t("node.toolResult") + (n.tool ? " \u2190 " + n.tool : "") + (n.err ? " \u26A0" : "");
374
- }
375
- if (n.skill) return "Skill: " + n.skill;
376
- if (n.calls) return t("node.calls") + n.calls.join(", ");
377
- if (n.text) return n.form === "snapshot" ? t("node.snapshot") + n.text : n.text;
378
- if (n.cat === "assistant") return t("node.empty");
379
- if (n.cat === "inject") return t("form." + (n.form || "context"));
380
- return t("node.nonText");
381
- }
382
- return function NodeList(props) {
383
- if (props.nodes.length === 0) {
384
- return /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("nodes.empty"));
385
- }
386
- const catColor = {};
387
- for (const c of CATS) catColor[c.key] = c.color;
388
- const rows = props.nodes.slice().reverse();
389
- return /* @__PURE__ */ React.createElement("div", { className: "lc-nodes" }, props.dropped > 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-nodes-more" }, tr("nodes.more", { n: props.dropped })) : null, rows.map((n) => {
390
- const text = nodeText(n);
391
- return /* @__PURE__ */ React.createElement("div", { key: n.seq, className: "lc-node" }, /* @__PURE__ */ React.createElement("i", { style: { background: catColor[n.cat] || "#999" } }), /* @__PURE__ */ React.createElement("span", { className: "lc-node-preview", title: text }, text), typeof n.time === "number" ? /* @__PURE__ */ React.createElement("span", { className: "lc-node-time" }, fmtTime2(n.time)) : null, /* @__PURE__ */ React.createElement("span", { className: "lc-node-tokens" }, fmt3(n.tokens)));
392
- }));
393
- };
394
- }
395
-
396
- // src/client/components/requestDetail.tsx
397
- function makeRequestDetail(kit, StackedBar) {
398
- const { t, tr, fmt: fmt3, fmtTime: fmtTime2, catLabel, eventLabel, eventAt } = kit;
399
- return function RequestDetail(props) {
400
- const req = props.request;
401
- if (!req) return null;
402
- const isTurn = req.stepCount !== void 0 && req.stepCount > 1;
403
- 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 });
404
- const marker = props.marker ?? null;
405
- const markerAt = marker !== null ? eventAt(marker) : null;
406
- return /* @__PURE__ */ React.createElement("div", { className: "lc-detail" }, /* @__PURE__ */ React.createElement("div", { className: "lc-detail-head" }, /* @__PURE__ */ React.createElement("b", null, head), marker !== null && markerAt !== null ? /* @__PURE__ */ React.createElement("span", { className: "lc-detail-marker", title: eventLabel(marker) }, "\u2702 " + markerAt) : null, isTurn ? /* @__PURE__ */ React.createElement("span", { className: "lc-detail-tag" }, t("detail.lastStep")) : null, /* @__PURE__ */ React.createElement("span", null, fmtTime2(req.time)), /* @__PURE__ */ React.createElement("span", null, tr("detail.estTotal", { n: fmt3(req.total) })), req.prompt !== void 0 ? /* @__PURE__ */ React.createElement("span", { className: "lc-actual" }, tr("detail.actual", { n: fmt3(req.prompt) })) : null, req.output !== void 0 ? /* @__PURE__ */ React.createElement("span", null, tr("detail.output", { n: fmt3(req.output) })) : null), /* @__PURE__ */ React.createElement(StackedBar, { parts: partsOf(req), height: 10 }), /* @__PURE__ */ React.createElement("div", { className: "lc-detail-rows" }, CATS.map((c) => {
407
- const v = req[c.key] || 0;
408
- return /* @__PURE__ */ React.createElement("div", { key: c.key, className: "lc-detail-row" }, /* @__PURE__ */ React.createElement("i", { style: { background: c.color } }), /* @__PURE__ */ React.createElement("span", { className: "lc-detail-label" }, catLabel(c.key)), /* @__PURE__ */ React.createElement("span", { className: "lc-bar-track" }, /* @__PURE__ */ React.createElement("span", { className: "lc-bar-fill", style: { width: (req.total > 0 ? v / req.total * 100 : 0) + "%", background: c.color } })), /* @__PURE__ */ React.createElement("span", { className: "lc-detail-num" }, "\u2248" + fmt3(v)), /* @__PURE__ */ React.createElement("span", { className: "lc-detail-pct" }, req.total > 0 ? Math.round(v / req.total * 100) + "%" : ""));
409
- })));
410
- };
411
- }
412
-
413
- // src/client/components/statsBoard.tsx
414
- function makeStatsBoard(kit) {
415
- const { t, tr, fmt: fmt3 } = kit;
416
- return function StatsBoard(props) {
417
- const turns = /* @__PURE__ */ new Set();
418
- let steps = 0, compactions = 0, prunes = 0, injects = 0, switches = 0;
419
- let recycled = 0, est = 0, prompt = 0, output = 0;
420
- for (const req of props.requests) {
421
- turns.add(req.turn ?? 0);
422
- steps++;
423
- est += req.total;
424
- if (typeof req.prompt === "number") prompt += req.prompt;
425
- if (typeof req.output === "number") output += req.output;
426
- }
427
- for (const ev of props.events) {
428
- if (ev.kind === "compaction") {
429
- compactions++;
430
- recycled += ev.tokens || 0;
431
- } else if (ev.kind === "prune") {
432
- prunes++;
433
- recycled += ev.tokens || 0;
434
- } else if (ev.kind === "inject") injects++;
435
- else if (ev.kind === "model") switches++;
436
- }
437
- const cell = (label, value, sub) => /* @__PURE__ */ React.createElement("div", { className: "lc-stat" }, /* @__PURE__ */ React.createElement("span", { className: "lc-stat-label" }, label), /* @__PURE__ */ React.createElement("b", { className: "lc-stat-value" }, value), sub !== void 0 ? /* @__PURE__ */ React.createElement("span", { className: "lc-stat-sub" }, sub) : null);
438
- return /* @__PURE__ */ React.createElement("div", { className: "lc-card" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, t("stats.title"), /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("stats.hint"))), /* @__PURE__ */ React.createElement("div", { className: "lc-stats" }, cell(t("stats.turns"), fmt3(turns.size)), cell(t("stats.steps"), fmt3(steps)), cell(
439
- t("stats.recycled"),
440
- recycled > 0 ? "\u2212" + fmt3(recycled) : "0",
441
- tr("stats.recycleSub", { c: compactions, p: prunes })
442
- ), cell(t("stats.injects"), fmt3(injects)), cell(t("stats.switches"), fmt3(switches)), cell(t("stats.est"), "\u2248 " + fmt3(est)), cell(t("stats.actualPrompt"), fmt3(prompt)), cell(t("stats.output"), fmt3(output))));
443
- };
444
- }
445
-
446
- // src/client/components/stackedBar.tsx
447
- function makeStackedBar(kit) {
448
- const { t, tr, fmt: fmt3, catLabel } = kit;
449
- return function StackedBar(props) {
450
- let total = 0;
451
- for (const p of props.parts) total += p.value;
452
- const scale = props.max !== void 0 && props.max > total ? props.max : total;
453
- const free = props.max !== void 0 && props.max > total ? props.max - total : 0;
454
- const usedPct = scale > 0 ? total / scale * 100 : 0;
455
- const hovering = props.hoverKey !== null && props.hoverKey !== void 0;
456
- const showBox = free > 0 && hovering;
457
- let tip = null;
458
- if (props.hoverKey !== null && props.hoverKey !== void 0) {
459
- if (props.hoverKey === "free" && free > 0) {
460
- const pct = scale > 0 ? free / scale * 100 : 0;
461
- tip = {
462
- text: t("overview.free") + " " + fmt3(free) + " (" + Math.round(pct) + "%)",
463
- leftPct: Math.max(12, Math.min(total / scale * 100 + pct / 2, 88))
464
- };
465
- } else {
466
- let acc = 0;
467
- for (const p of props.parts) {
468
- const pct = scale > 0 ? p.value / scale * 100 : 0;
469
- if (p.key === props.hoverKey && p.value > 0) {
470
- tip = {
471
- // "(pct%)" is a share of the OCCUPIED total — the dashed box
472
- // that appears on hover frames exactly this reference region.
473
- text: catLabel(p.key) + " \u2248" + fmt3(p.value) + " (" + Math.round(p.value / total * 100) + "%) " + tr("overview.ofUsed"),
474
- leftPct: Math.max(12, Math.min(acc + pct / 2, 88))
475
- };
476
- break;
477
- }
478
- acc += pct;
479
- }
480
- }
481
- }
482
- return /* @__PURE__ */ React.createElement("div", { className: "lc-stacked-wrap" }, /* @__PURE__ */ React.createElement(
483
- "div",
484
- {
485
- className: "lc-stacked" + (hovering ? " lc-stacked-dim" : ""),
486
- style: { height: (props.height || 14) + "px" },
487
- onMouseLeave: () => {
488
- if (props.onHoverKey !== void 0) props.onHoverKey(null);
489
- }
490
- },
491
- total > 0 ? props.parts.map((p) => {
492
- if (!p.value) return null;
493
- const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
494
- return /* @__PURE__ */ React.createElement(
495
- "div",
496
- {
497
- key: p.key,
498
- className: "lc-stacked-seg" + (on ? " lc-stacked-seg-on" : ""),
499
- style: { width: p.value / scale * 100 + "%", background: p.color },
500
- onMouseEnter: () => {
501
- if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
502
- }
503
- }
504
- );
505
- }) : null,
506
- free > 0 ? /* @__PURE__ */ React.createElement(
507
- "div",
508
- {
509
- key: "free",
510
- className: "lc-stacked-free" + (props.hoverKey === "free" ? " lc-stacked-free-on" : ""),
511
- style: { width: free / scale * 100 + "%" },
512
- onMouseEnter: () => {
513
- if (props.onHoverKey !== void 0) props.onHoverKey("free");
514
- }
515
- }
516
- ) : null,
517
- showBox ? /* @__PURE__ */ React.createElement("div", { className: "lc-occupied-box", style: { width: usedPct + "%" } }) : null
518
- ), tip ? /* @__PURE__ */ React.createElement("div", { className: "lc-bar-tip", style: { left: tip.leftPct + "%" } }, tip.text) : null);
519
- };
520
- }
521
- function makeLegend(kit) {
522
- const { tr, fmt: fmt3, catLabel } = kit;
523
- return function Legend(props) {
524
- let total = 0;
525
- for (const p of props.parts) total += p.value;
526
- return /* @__PURE__ */ React.createElement("div", { className: "lc-legend" }, props.parts.map((p) => {
527
- const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
528
- return /* @__PURE__ */ React.createElement(
529
- "span",
530
- {
531
- key: p.key,
532
- className: "lc-chip" + (on ? " lc-chip-on" : ""),
533
- title: tr("overview.ofUsed"),
534
- onMouseEnter: () => {
535
- if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
536
- },
537
- onMouseLeave: () => {
538
- if (props.onHoverKey !== void 0) props.onHoverKey(null);
539
- }
540
- },
541
- /* @__PURE__ */ React.createElement("i", { style: { background: p.color } }),
542
- catLabel(p.key) + " \u2248" + fmt3(p.value),
543
- total > 0 ? /* @__PURE__ */ React.createElement("em", null, Math.round(p.value / total * 100) + "%") : null
544
- );
804
+ function eventAt(ev) {
805
+ if (ev.kind === "compaction" || ev.kind === "prune") {
806
+ if (typeof ev.turn === "number" && typeof ev.step === "number") {
807
+ if (typeof ev.fromTurn === "number" && typeof ev.fromStep === "number") {
808
+ if (ev.fromTurn === ev.turn) return t("events.range", { t: ev.turn, a: ev.fromStep, b: ev.step });
809
+ return t("events.rangeTo", { a: ev.fromTurn, as: ev.fromStep, b: ev.turn, bs: ev.step });
810
+ }
811
+ return t("events.at", { t: ev.turn, s: ev.step });
812
+ }
813
+ return null;
814
+ }
815
+ if (typeof ev.turn === "number" && typeof ev.step === "number") {
816
+ return t("events.at", { t: ev.turn, s: ev.step });
817
+ }
818
+ return null;
819
+ }
820
+ return { eventLabel, eventAt };
821
+ }
822
+ function makeEventList(kit) {
823
+ const { t, fmt: fmt3, fmtTime: fmtTime2, eventLabel, eventAt } = kit;
824
+ return function EventList(props) {
825
+ if (props.events.length === 0) {
826
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("events.empty"));
827
+ }
828
+ const sorted = props.events.slice().reverse();
829
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-events" }, sorted.map((ev, i) => {
830
+ const label = eventLabel(ev);
831
+ const at = eventAt(ev);
832
+ const glyph = ev.kind === "inject" ? /* @__PURE__ */ React.createElement(import_dsh_client_ui_primitives.IconPlusOutline16, null) : ev.kind === "model" ? /* @__PURE__ */ React.createElement(import_dsh_client_ui_primitives.IconBranchOutline16, null) : EVENT_ICONS[ev.kind] || "\u2022";
833
+ return /* @__PURE__ */ React.createElement("div", { key: ev.seq + "-" + i, className: "lc-event" }, /* @__PURE__ */ React.createElement("span", { className: "lc-event-icon lc-event-" + ev.kind }, glyph), /* @__PURE__ */ React.createElement("span", { className: "lc-event-label", title: label }, label), at !== null ? /* @__PURE__ */ React.createElement("span", { className: "lc-event-at" }, at) : null, ev.tokens ? /* @__PURE__ */ React.createElement("span", { className: "lc-event-tokens" + (ev.kind === "inject" ? " lc-up" : " lc-down") }, (ev.kind === "inject" ? "+" : "\u2212") + fmt3(ev.tokens)) : null, /* @__PURE__ */ React.createElement("span", { className: "lc-event-time" }, fmtTime2(ev.time)));
545
834
  }));
546
835
  };
547
836
  }
548
837
 
549
- // src/client/components/trendChart.tsx
550
- function aggregateByTurn(requests) {
551
- const out = [];
552
- let runSteps = 0;
553
- for (const req of requests) {
554
- const last = out.length > 0 ? out[out.length - 1] : null;
555
- if (last !== null && (last.turn ?? 0) === (req.turn ?? 0)) {
556
- runSteps++;
557
- out[out.length - 1] = { ...req, stepCount: runSteps };
558
- } else {
559
- runSteps = 1;
560
- out.push({ ...req, stepCount: 1 });
838
+ // src/client/components/nodes.tsx
839
+ function makeNodeList(kit) {
840
+ const { t, tr, fmt: fmt3, fmtTime: fmtTime2 } = kit;
841
+ function nodeText(n) {
842
+ if (n.cat === "tool") {
843
+ return t("node.toolResult") + (n.tool ? " \u2190 " + n.tool : "") + (n.err ? " \u26A0" : "");
561
844
  }
845
+ if (n.skill) return "Skill: " + n.skill;
846
+ if (n.calls) return t("node.calls") + n.calls.join(", ");
847
+ if (n.text) return n.form === "snapshot" ? t("node.snapshot") + n.text : n.text;
848
+ if (n.cat === "assistant") return t("node.empty");
849
+ if (n.cat === "inject") return t("form." + (n.form || "context"));
850
+ return t("node.nonText");
562
851
  }
563
- return out;
564
- }
565
- function attachMarkers(requests, events) {
566
- const markers = new Array(requests.length);
567
- for (const ev of events) {
568
- if (ev.kind !== "compaction" && ev.kind !== "prune") continue;
569
- for (let r = 0; r < requests.length; r++) {
570
- if (requests[r].seq >= ev.seq) {
571
- if (markers[r] === void 0) markers[r] = ev;
572
- break;
573
- }
852
+ return function NodeList(props) {
853
+ if (props.nodes.length === 0) {
854
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("nodes.empty"));
574
855
  }
575
- }
576
- return markers;
856
+ const catColor = {};
857
+ for (const c of CATS) catColor[c.key] = c.color;
858
+ const rows = props.nodes.slice().reverse();
859
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-nodes" }, props.dropped > 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-nodes-more" }, tr("nodes.more", { n: props.dropped })) : null, rows.map((n) => {
860
+ const text = nodeText(n);
861
+ return /* @__PURE__ */ React.createElement("div", { key: n.seq, className: "lc-node" }, /* @__PURE__ */ React.createElement("i", { style: { background: catColor[n.cat] || "#999" } }), /* @__PURE__ */ React.createElement("span", { className: "lc-node-preview", title: text }, text), typeof n.time === "number" ? /* @__PURE__ */ React.createElement("span", { className: "lc-node-time" }, fmtTime2(n.time)) : null, /* @__PURE__ */ React.createElement("span", { className: "lc-node-tokens" }, fmt3(n.tokens)));
862
+ }));
863
+ };
577
864
  }
578
- function makeTrendChart(kit) {
579
- const { t, tr, fmt: fmt3, fmtTime: fmtTime2, catLabel, eventLabel, eventAt } = kit;
580
- const CHART_H = 112;
581
- const BAR_W = 14;
582
- const BAR_GAP = 2;
583
- const TURN_COLORS = ["#6366f1", "#f59e0b", "#22c55e", "#a855f7", "#3b82f6", "#14b8a6", "#ef4444", "#ec4899"];
584
- return function TrendChart(props) {
585
- const requests = props.requests;
586
- const markers = props.markers;
587
- const anchorOf = (req) => typeof req.prompt === "number" && req.prompt > 0 && req.total > 0 ? req.prompt / req.total : 1;
588
- const barTotalOf = (req) => typeof req.prompt === "number" && req.prompt > 0 ? req.prompt : req.total;
589
- let maxTotal = 1;
590
- for (const req of requests) {
591
- const bt = barTotalOf(req);
592
- if (bt > maxTotal) maxTotal = bt;
865
+
866
+ // src/client/components/statsBoard.tsx
867
+ function makeStatsBoard(kit) {
868
+ const { t, tr, fmt: fmt3 } = kit;
869
+ return function StatsBoard(props) {
870
+ const turns = /* @__PURE__ */ new Set();
871
+ let steps = 0, compactions = 0, prunes = 0, injects = 0, switches = 0;
872
+ let recycled = 0, est = 0, prompt = 0, output = 0;
873
+ for (const req of props.requests) {
874
+ turns.add(req.turn ?? 0);
875
+ steps++;
876
+ est += req.total;
877
+ if (typeof req.prompt === "number") prompt += req.prompt;
878
+ if (typeof req.output === "number") output += req.output;
593
879
  }
594
- const groups = [];
595
- for (const req of requests) {
596
- let grp = groups.length > 0 ? groups[groups.length - 1] : null;
597
- if (grp === null || grp.turn !== (req.turn ?? 0)) {
598
- grp = { turn: req.turn ?? 0, count: 0, span: 0, agg: req.stepCount !== void 0 };
599
- groups.push(grp);
600
- }
601
- grp.count++;
602
- grp.span += req.stepCount ?? 1;
880
+ for (const ev of props.events) {
881
+ if (ev.kind === "compaction") {
882
+ compactions++;
883
+ recycled += ev.tokens || 0;
884
+ } else if (ev.kind === "prune") {
885
+ prunes++;
886
+ recycled += ev.tokens || 0;
887
+ } else if (ev.kind === "inject") injects++;
888
+ else if (ev.kind === "model") switches++;
603
889
  }
604
- const scrollRef = React.useRef(null);
605
- const scrolledOnce = React.useRef(false);
606
- const lastGranRef = React.useRef(props.granularity);
607
- const [edges, setEdges] = React.useState({ left: false, right: false });
608
- const edgesRef = React.useRef(edges);
609
- const updateEdges = (el) => {
610
- const left = el.scrollLeft > 4;
611
- const right = el.scrollLeft + el.clientWidth < el.scrollWidth - 4;
612
- const prev = edgesRef.current;
613
- if (prev.left === left && prev.right === right) return;
614
- edgesRef.current = { left, right };
615
- setEdges({ left, right });
616
- };
617
- React.useLayoutEffect(() => {
618
- const el = scrollRef.current;
619
- if (el === null) return;
620
- if (props.granularity !== lastGranRef.current) {
621
- lastGranRef.current = props.granularity;
622
- scrolledOnce.current = false;
623
- }
624
- if (!scrolledOnce.current) {
625
- scrolledOnce.current = true;
626
- el.scrollLeft = el.scrollWidth;
627
- } else if (el.scrollLeft + el.clientWidth >= el.scrollWidth - 24) {
628
- el.scrollLeft = el.scrollWidth;
629
- }
630
- updateEdges(el);
631
- });
632
- const tipOf = (req) => {
633
- 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 });
634
- 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) }) : "");
635
- };
636
- const hoveredIdx = props.hoveredSeq !== null ? requests.findIndex((r) => r.seq === props.hoveredSeq) : -1;
637
- const hoveredReq = hoveredIdx >= 0 ? requests[hoveredIdx] : null;
638
- return /* @__PURE__ */ React.createElement("div", { className: "lc-chartrow" }, /* @__PURE__ */ React.createElement("div", { className: "lc-axis" }, /* @__PURE__ */ React.createElement("span", { className: "lc-axis-top" }, fmt3(maxTotal)), /* @__PURE__ */ React.createElement("span", { className: "lc-axis-mid" }, fmt3(Math.round(maxTotal / 2))), /* @__PURE__ */ React.createElement("span", { className: "lc-axis-bot" }, "0")), /* @__PURE__ */ React.createElement(
639
- "div",
640
- {
641
- className: "lc-chart-scroll" + (props.activeTurn !== null ? " lc-chart-dim" : ""),
642
- ref: scrollRef,
643
- onScroll: (e) => {
644
- updateEdges(e.currentTarget);
645
- }
646
- },
647
- edges.left ? /* @__PURE__ */ React.createElement("div", { className: "lc-chart-fade lc-chart-fade-l" }) : null,
648
- /* @__PURE__ */ React.createElement(
649
- "div",
650
- {
651
- className: "lc-chart",
652
- onMouseLeave: () => {
653
- props.onHover(null);
654
- }
655
- },
656
- /* @__PURE__ */ React.createElement("div", { className: "lc-grid lc-grid-top" }),
657
- /* @__PURE__ */ React.createElement("div", { className: "lc-grid lc-grid-mid" }),
658
- requests.map((req, i) => {
659
- const marker = markers[i];
660
- const markerAt = marker !== void 0 ? eventAt(marker) : null;
661
- const selected = props.selectedSeq === req.seq;
662
- const hovered = props.hoveredSeq === req.seq;
663
- const inTurn = props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn;
664
- return /* @__PURE__ */ React.createElement(
665
- "div",
666
- {
667
- key: req.seq,
668
- className: "lc-bar" + (selected ? " lc-bar-selected" : "") + (hovered ? " lc-bar-hovered" : "") + (inTurn ? " lc-bar-in-turn" : ""),
669
- style: { width: BAR_W + "px" },
670
- onClick: () => {
671
- props.onSelect(selected ? null : req.seq);
672
- },
673
- onMouseEnter: () => {
674
- props.onHover(req.seq);
675
- }
676
- },
677
- marker !== void 0 ? /* @__PURE__ */ React.createElement(
678
- "span",
679
- {
680
- className: "lc-bar-marker",
681
- title: "\u2702 " + (markerAt !== null ? markerAt + " \u2014 " : "") + eventLabel(marker)
682
- },
683
- "\u2702"
684
- ) : null,
685
- /* @__PURE__ */ React.createElement("div", { className: "lc-bar-stack" }, CATS.map((c) => {
686
- const v = (req[c.key] || 0) * anchorOf(req);
687
- if (!v) return null;
688
- return /* @__PURE__ */ React.createElement("div", { key: c.key, style: { height: Math.max(1, Math.round(v / maxTotal * CHART_H)) + "px", background: c.color } });
689
- }))
690
- );
691
- })
692
- ),
693
- hoveredReq !== null ? /* @__PURE__ */ React.createElement(
694
- "div",
695
- {
696
- className: "lc-chart-tip",
697
- style: { left: hoveredIdx * (BAR_W + BAR_GAP) + BAR_W / 2 + "px" }
698
- },
699
- tipOf(hoveredReq)
700
- ) : null,
701
- /* @__PURE__ */ React.createElement("div", { className: "lc-turns", onMouseLeave: () => {
702
- props.onHoverTurn(null);
703
- } }, groups.map((grp, gi) => {
704
- const on = props.activeTurn === grp.turn;
705
- const blockW = grp.agg ? BAR_W : grp.span * (BAR_W + BAR_GAP) - BAR_GAP;
706
- return /* @__PURE__ */ React.createElement(
707
- "span",
708
- {
709
- key: "turn-" + gi,
710
- className: "lc-turn" + (on ? " lc-turn-on" : ""),
711
- style: {
712
- width: blockW + "px",
713
- background: TURN_COLORS[gi % TURN_COLORS.length]
714
- },
715
- title: "T" + grp.turn,
716
- onMouseEnter: () => {
717
- props.onHoverTurn(grp.turn);
718
- }
719
- },
720
- "T" + grp.turn
721
- );
722
- }))
723
- ), edges.right ? /* @__PURE__ */ React.createElement("div", { className: "lc-chart-fade lc-chart-fade-r" }) : null);
890
+ const cell = (label, value, sub) => /* @__PURE__ */ React.createElement("div", { className: "lc-stat" }, /* @__PURE__ */ React.createElement("span", { className: "lc-stat-label" }, label), /* @__PURE__ */ React.createElement("b", { className: "lc-stat-value" }, value), sub !== void 0 ? /* @__PURE__ */ React.createElement("span", { className: "lc-stat-sub" }, sub) : null);
891
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-card" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, t("stats.title"), /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("stats.hint"))), /* @__PURE__ */ React.createElement("div", { className: "lc-stats" }, cell(t("stats.turns"), fmt3(turns.size)), cell(t("stats.steps"), fmt3(steps)), cell(
892
+ t("stats.recycled"),
893
+ recycled > 0 ? "\u2212" + fmt3(recycled) : "0",
894
+ tr("stats.recycleSub", { c: compactions, p: prunes })
895
+ ), cell(t("stats.injects"), fmt3(injects)), cell(t("stats.switches"), fmt3(switches)), cell(t("stats.est"), "\u2248 " + fmt3(est)), cell(t("stats.actualPrompt"), fmt3(prompt)), cell(t("stats.output"), fmt3(output))));
724
896
  };
725
897
  }
726
898
 
@@ -773,7 +945,6 @@ function makeContextView(ctx, kit) {
773
945
  if (!data) {
774
946
  return /* @__PURE__ */ React.createElement("div", { className: "lc-root", ref: rootRef }, /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("loading")));
775
947
  }
776
- const current = data.current;
777
948
  const requests = data.requests || [];
778
949
  const events = data.events || [];
779
950
  const nodes = data.nodes || [];
@@ -798,16 +969,8 @@ function makeContextView(ctx, kit) {
798
969
  break;
799
970
  }
800
971
  }
801
- const occ = data.occupancy;
802
- const projected = occ !== void 0 && typeof occ.projectedTokens === "number" ? occ.projectedTokens : void 0;
803
- const lastReq = displayRequests.length > 0 ? displayRequests[displayRequests.length - 1] : null;
804
- const derived = lastReq !== null && typeof lastReq.prompt === "number" ? lastReq.prompt + (current.total - lastReq.total) : void 0;
805
- const occupancyTokens = projected ?? derived ?? null;
806
- const occupancyWindow = occ !== void 0 && typeof occ.contextWindow === "number" ? occ.contextWindow : data.contextWindow;
807
- const headline = occupancyTokens ?? current.total;
808
- const headlinePct = occupancyWindow ? Math.min(100, Math.round(headline / occupancyWindow * 100)) : null;
809
- const parts = anchoredParts(partsOf(current), occupancyTokens !== null && headline > 0 ? headline : null);
810
- return /* @__PURE__ */ React.createElement("div", { className: "lc-root", ref: rootRef }, /* @__PURE__ */ React.createElement(StatsBoard, { requests, events }), /* @__PURE__ */ React.createElement("div", { className: "lc-card" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, t("overview.title"), /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, (data.model ? data.model : "") + (data.provider ? " \xB7 " + data.provider : ""))), /* @__PURE__ */ React.createElement("div", { className: "lc-overview-num" }, /* @__PURE__ */ React.createElement("b", null, fmt3(headline)), /* @__PURE__ */ React.createElement("span", null, occupancyWindow ? " / " + fmt3(occupancyWindow) + " " + tr("overview.ofWindow", { p: headlinePct ?? 0 }) : " " + t("overview.estimate")), occupancyTokens !== null ? /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("overview.splitEst")) : null), /* @__PURE__ */ React.createElement(StackedBar, { parts, height: 16, max: occupancyWindow, hoverKey: hoverCat, onHoverKey: setHoverCat }), /* @__PURE__ */ React.createElement(Legend, { parts, hoverKey: hoverCat, onHoverKey: setHoverCat }), data.toolList && data.toolList.length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-tools" }, t("tools.top"), data.toolList.slice().sort((a, b) => b.tokens - a.tokens).slice(0, 5).map((tool) => {
972
+ const head = headlineOf(data);
973
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-root", ref: rootRef }, /* @__PURE__ */ React.createElement(StatsBoard, { requests, events }), /* @__PURE__ */ React.createElement("div", { className: "lc-card" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, t("overview.title"), /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, (data.model ? data.model : "") + (data.provider ? " \xB7 " + data.provider : ""))), /* @__PURE__ */ React.createElement("div", { className: "lc-overview-num" }, /* @__PURE__ */ React.createElement("b", null, fmt3(head.tokens)), /* @__PURE__ */ React.createElement("span", null, head.window ? " / " + fmt3(head.window) + " " + tr("overview.ofWindow", { p: head.pct ?? 0 }) : " " + t("overview.estimate")), !head.estimated ? /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("overview.splitEst")) : null), /* @__PURE__ */ React.createElement(StackedBar, { parts: head.parts, height: 16, max: head.window, hoverKey: hoverCat, onHoverKey: setHoverCat }), /* @__PURE__ */ React.createElement(Legend, { parts: head.parts, hoverKey: hoverCat, onHoverKey: setHoverCat }), data.toolList && data.toolList.length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-tools" }, t("tools.top"), data.toolList.slice().sort((a, b) => b.tokens - a.tokens).slice(0, 5).map((tool) => {
811
974
  return /* @__PURE__ */ React.createElement("span", { key: tool.name, className: "lc-tool-chip" }, tool.name + " " + fmt3(tool.tokens));
812
975
  }), data.toolList.length > 5 ? /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, " " + tr("tools.more", { n: data.toolList.length })) : null) : null), /* @__PURE__ */ React.createElement("div", { className: "lc-card" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, t("trend.title"), /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("trend.hint")), /* @__PURE__ */ React.createElement("div", { className: "lc-gran" }, /* @__PURE__ */ React.createElement(
813
976
  "button",
@@ -875,7 +1038,8 @@ function apply(ctx) {
875
1038
  if (tag.parentNode !== null) tag.parentNode.removeChild(tag);
876
1039
  };
877
1040
  }, "dsh-context: styles");
878
- const ContextView = makeContextView(ctx, makeViewKit(t));
1041
+ const kit = makeViewKit(t);
1042
+ const ContextView = makeContextView(ctx, kit);
879
1043
  ctx.slots.inject("conversation.view", () => {
880
1044
  return ctx.slots.register(
881
1045
  // order 20 renders right of Chat (0) and Trajectory (10); the locale
@@ -884,6 +1048,20 @@ function apply(ctx) {
884
1048
  (props) => h(ContextView, props)
885
1049
  );
886
1050
  });
1051
+ registerContextCommand(ctx, kit);
1052
+ const ContextModal = makeContextModal(ctx, kit);
1053
+ ctx.slots.inject("conversation.input.overlay", () => {
1054
+ return ctx.slots.register(
1055
+ {
1056
+ name: "conversation.input.overlay",
1057
+ id: "context-modal",
1058
+ order: 10,
1059
+ locale: NS,
1060
+ inject: (sessionId) => ({ hooks: { contextModal: modalStoreOf(sessionId) } })
1061
+ },
1062
+ (props) => h(ContextModal, props)
1063
+ );
1064
+ });
887
1065
  }
888
1066
  module.exports = {
889
1067
  name: "dsh-context",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-context",
3
- "version": "0.9.0",
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. Streamed through the harness's session-projection pipeline.",
3
+ "version": "0.10.1",
4
+ "description": "A DeepSeek Harness plugin for context dashboard and context command, for understanding how the context is made of, and how it evolves.",
5
5
  "author": "bowenliang123",
6
6
  "repository": {
7
7
  "type": "git",