dsh-context 0.8.0 → 0.10.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.
Files changed (4) hide show
  1. package/README.md +19 -8
  2. package/lib/client.js +611 -646
  3. package/lib/index.js +269 -210
  4. package/package.json +9 -2
package/lib/client.js CHANGED
@@ -20,6 +20,7 @@ var DICT_ZH = {
20
20
  "overview.estimate": "tokens\uFF08\u4F30\u7B97\uFF09",
21
21
  "overview.free": "\u5269\u4F59\u7A97\u53E3",
22
22
  "overview.splitEst": "\xB7 \u5206\u7C7B\u6309\u4F30\u7B97\u6BD4\u4F8B\u5206\u914D",
23
+ "overview.ofUsed": "\u5360\u5DF2\u7528\u4E0A\u4E0B\u6587",
23
24
  "stats.title": "\u4E0A\u4E0B\u6587\u7EDF\u8BA1",
24
25
  "stats.hint": "\u7EDF\u8BA1\u5F53\u524D\u4FDD\u7559\u7684\u5386\u53F2\u7A97\u53E3\uFF08\u4E0E\u8D8B\u52BF\u56FE\u4E00\u81F4\uFF09",
25
26
  "stats.turns": "\u8F6E\u6B21",
@@ -75,7 +76,9 @@ var DICT_ZH = {
75
76
  "node.calls": "\u8C03\u7528 ",
76
77
  "node.empty": "(\u7A7A\u56DE\u590D)",
77
78
  "node.nonText": "(\u975E\u6587\u672C\u6D88\u606F)",
78
- "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"
79
82
  };
80
83
  var DICT_EN = {
81
84
  "tab": "Context",
@@ -90,6 +93,7 @@ var DICT_EN = {
90
93
  "overview.estimate": "tokens (estimated)",
91
94
  "overview.free": "Free window",
92
95
  "overview.splitEst": "\xB7 category split is estimated",
96
+ "overview.ofUsed": "of used context",
93
97
  "stats.title": "Context stats",
94
98
  "stats.hint": "Over the retained history window (same as the History chart)",
95
99
  "stats.turns": "Turns",
@@ -145,9 +149,502 @@ var DICT_EN = {
145
149
  "node.calls": "calls ",
146
150
  "node.empty": "(empty reply)",
147
151
  "node.nonText": "(non-text message)",
148
- "node.snapshot": "snapshot: "
152
+ "node.snapshot": "snapshot: ",
153
+ "cmd.desc": "View context makeup and how it evolves",
154
+ "cmd.close": "Close"
149
155
  };
150
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
+
151
648
  // src/client/styles.ts
152
649
  var STYLES = [
153
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; }",
@@ -167,11 +664,25 @@ var STYLES = [
167
664
  ".lc-overview-num b { font-size: 20px; }",
168
665
  ".lc-overview-num span { color: var(--dsw-alias-label-secondary); }",
169
666
  ".lc-stacked-wrap { position: relative; width: 100%; }",
170
- ".lc-stacked { display: flex; width: 100%; border-radius: 5px; overflow: hidden; background: rgba(128,128,128,0.18); }",
667
+ ".lc-stacked { display: flex; width: 100%; border-radius: 5px; overflow: hidden; background: rgba(128,128,128,0.18); position: relative; }",
668
+ // Hover reference frame around the OCCUPIED region of the composition bar:
669
+ // dashes from the left edge to the used/window boundary, so the legend's
670
+ // "share of used" percentages visibly map to the boxed part (the free track
671
+ // sits outside it). pointer-events: none keeps hover on the segments/free.
672
+ // Deliberately high-contrast (label-primary) and thick so the frame reads at
673
+ // a glance, and the other parts dim underneath it (`.lc-stacked-dim`).
674
+ ".lc-occupied-box { position: absolute; top: 0; bottom: 0; left: 0; border: 2px dashed var(--dsw-alias-label-primary); border-radius: 5px; box-sizing: border-box; pointer-events: none; opacity: 1; box-shadow: 0 0 0 1px var(--dsw-alias-bg-layer-2); }",
171
675
  ".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; }",
172
676
  ".lc-stacked > div { height: 100%; }",
173
677
  ".lc-stacked-seg-on { filter: brightness(1.18); }",
174
678
  ".lc-stacked-free-on { box-shadow: inset 0 0 0 1px var(--dsw-alias-label-secondary); border-radius: 3px; }",
679
+ // Hover focus: everything except the hovered part (segment, legend chip, or
680
+ // free track) recedes, so the composition highlight and the occupied-region
681
+ // frame read clearly. The selected segment/free keeps full opacity.
682
+ ".lc-stacked-dim .lc-stacked-seg { opacity: 0.35; }",
683
+ ".lc-stacked-dim .lc-stacked-seg-on { opacity: 1; }",
684
+ ".lc-stacked-dim .lc-stacked-free { opacity: 0.35; }",
685
+ ".lc-stacked-dim .lc-stacked-free-on { opacity: 1; }",
175
686
  ".lc-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 10px; }",
176
687
  ".lc-chip { display: inline-flex; align-items: center; gap: 5px; color: var(--dsw-alias-label-primary); }",
177
688
  ".lc-chip i, .lc-detail-row i, .lc-node i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }",
@@ -247,45 +758,19 @@ var STYLES = [
247
758
  ".lc-node-tokens { color: var(--dsw-alias-label-secondary); }",
248
759
  ".lc-nodes-more { color: var(--dsw-alias-label-secondary); padding: 3px 0; }",
249
760
  ".lc-empty { color: var(--dsw-alias-label-secondary); padding: 18px 0; text-align: center; }",
250
- ".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; }"
251
770
  ].join("\n");
252
771
 
253
- // src/client/cache.ts
254
- var MAX_CACHED_SESSIONS = 10;
255
- var sessionCache = /* @__PURE__ */ new Map();
256
- function cacheGet(sessionId) {
257
- return sessionCache.get(sessionId);
258
- }
259
- function cachePut(sessionId, snapshot) {
260
- sessionCache.set(sessionId, snapshot);
261
- if (sessionCache.size > MAX_CACHED_SESSIONS) {
262
- const oldest = sessionCache.keys().next().value;
263
- if (oldest !== void 0) sessionCache.delete(oldest);
264
- }
265
- }
266
-
267
- // src/client/categories.ts
268
- var CATS = [
269
- { key: "system", color: "#6366f1" },
270
- { key: "tools", color: "#f59e0b" },
271
- { key: "user", color: "#22c55e" },
272
- { key: "inject", color: "#a855f7" },
273
- { key: "assistant", color: "#3b82f6" },
274
- { key: "tool", color: "#14b8a6" }
275
- ];
276
- function partsOf(breakdown) {
277
- return CATS.map((c) => {
278
- return { key: c.key, color: c.color, value: breakdown[c.key] || 0 };
279
- });
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
- }
772
+ // src/client/components/events.tsx
773
+ var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
289
774
 
290
775
  // src/client/format.ts
291
776
  function fmt(n) {
@@ -302,11 +787,7 @@ function fmtTime(t) {
302
787
  return p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds());
303
788
  }
304
789
 
305
- // src/client/react.ts
306
- var React = require("react");
307
- var h = React.createElement;
308
-
309
- // src/client/components/events.ts
790
+ // src/client/components/events.tsx
310
791
  var EVENT_ICONS = { compaction: "\u2702", prune: "\u2702", inject: "\uFF0B", model: "\u21C4" };
311
792
  function makeEventText(t, tr) {
312
793
  function eventLabel(ev) {
@@ -341,35 +822,20 @@ function makeEventText(t, tr) {
341
822
  function makeEventList(kit) {
342
823
  const { t, fmt: fmt3, fmtTime: fmtTime2, eventLabel, eventAt } = kit;
343
824
  return function EventList(props) {
344
- if (props.events.length === 0) {
345
- return h("div", { className: "lc-empty" }, t("events.empty"));
346
- }
347
- const sorted = props.events.slice().reverse();
348
- return h(
349
- "div",
350
- { className: "lc-events" },
351
- sorted.map((ev, i) => {
352
- const label = eventLabel(ev);
353
- const at = eventAt(ev);
354
- return h(
355
- "div",
356
- { key: ev.seq + "-" + i, className: "lc-event" },
357
- h("span", { className: "lc-event-icon lc-event-" + ev.kind }, EVENT_ICONS[ev.kind] || "\u2022"),
358
- h("span", { className: "lc-event-label", title: label }, label),
359
- at !== null ? h("span", { className: "lc-event-at" }, at) : null,
360
- ev.tokens ? h(
361
- "span",
362
- { className: "lc-event-tokens" + (ev.kind === "inject" ? " lc-up" : " lc-down") },
363
- (ev.kind === "inject" ? "+" : "\u2212") + fmt3(ev.tokens)
364
- ) : null,
365
- h("span", { className: "lc-event-time" }, fmtTime2(ev.time))
366
- );
367
- })
368
- );
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)));
834
+ }));
369
835
  };
370
836
  }
371
837
 
372
- // src/client/components/nodes.ts
838
+ // src/client/components/nodes.tsx
373
839
  function makeNodeList(kit) {
374
840
  const { t, tr, fmt: fmt3, fmtTime: fmtTime2 } = kit;
375
841
  function nodeText(n) {
@@ -385,81 +851,19 @@ function makeNodeList(kit) {
385
851
  }
386
852
  return function NodeList(props) {
387
853
  if (props.nodes.length === 0) {
388
- return h("div", { className: "lc-empty" }, t("nodes.empty"));
854
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("nodes.empty"));
389
855
  }
390
856
  const catColor = {};
391
857
  for (const c of CATS) catColor[c.key] = c.color;
392
858
  const rows = props.nodes.slice().reverse();
393
- return h(
394
- "div",
395
- { className: "lc-nodes" },
396
- props.dropped > 0 ? h("div", { className: "lc-nodes-more" }, tr("nodes.more", { n: props.dropped })) : null,
397
- rows.map((n) => {
398
- const text = nodeText(n);
399
- return h(
400
- "div",
401
- { key: n.seq, className: "lc-node" },
402
- h("i", { style: { background: catColor[n.cat] || "#999" } }),
403
- h("span", { className: "lc-node-preview", title: text }, text),
404
- // Timestamp when the host event carried one.
405
- typeof n.time === "number" ? h("span", { className: "lc-node-time" }, fmtTime2(n.time)) : null,
406
- h("span", { className: "lc-node-tokens" }, fmt3(n.tokens))
407
- );
408
- })
409
- );
410
- };
411
- }
412
-
413
- // src/client/components/requestDetail.ts
414
- function makeRequestDetail(kit, StackedBar) {
415
- const { t, tr, fmt: fmt3, fmtTime: fmtTime2, catLabel, eventLabel, eventAt } = kit;
416
- return function RequestDetail(props) {
417
- const req = props.request;
418
- if (!req) return null;
419
- const isTurn = req.stepCount !== void 0 && req.stepCount > 1;
420
- 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 });
421
- const marker = props.marker ?? null;
422
- const markerAt = marker !== null ? eventAt(marker) : null;
423
- return h(
424
- "div",
425
- { className: "lc-detail" },
426
- h(
427
- "div",
428
- { className: "lc-detail-head" },
429
- h("b", null, head),
430
- marker !== null && markerAt !== null ? h("span", { className: "lc-detail-marker", title: eventLabel(marker) }, "\u2702 " + markerAt) : null,
431
- isTurn ? h("span", { className: "lc-detail-tag" }, t("detail.lastStep")) : null,
432
- h("span", null, fmtTime2(req.time)),
433
- h("span", null, tr("detail.estTotal", { n: fmt3(req.total) })),
434
- req.prompt !== void 0 ? h("span", { className: "lc-actual" }, tr("detail.actual", { n: fmt3(req.prompt) })) : null,
435
- req.output !== void 0 ? h("span", null, tr("detail.output", { n: fmt3(req.output) })) : null
436
- ),
437
- h(StackedBar, { parts: partsOf(req), height: 10 }),
438
- h(
439
- "div",
440
- { className: "lc-detail-rows" },
441
- CATS.map((c) => {
442
- const v = req[c.key] || 0;
443
- return h(
444
- "div",
445
- { key: c.key, className: "lc-detail-row" },
446
- h("i", { style: { background: c.color } }),
447
- h("span", { className: "lc-detail-label" }, catLabel(c.key)),
448
- h(
449
- "span",
450
- { className: "lc-bar-track" },
451
- h("span", { className: "lc-bar-fill", style: { width: (req.total > 0 ? v / req.total * 100 : 0) + "%", background: c.color } })
452
- ),
453
- h("span", { className: "lc-detail-num" }, "\u2248" + fmt3(v)),
454
- h("span", { className: "lc-detail-pct" }, req.total > 0 ? Math.round(v / req.total * 100) + "%" : "")
455
- );
456
- })
457
- )
458
- );
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
+ }));
459
863
  };
460
864
  }
461
865
 
462
- // src/client/components/statsBoard.ts
866
+ // src/client/components/statsBoard.tsx
463
867
  function makeStatsBoard(kit) {
464
868
  const { t, tr, fmt: fmt3 } = kit;
465
869
  return function StatsBoard(props) {
@@ -483,345 +887,16 @@ function makeStatsBoard(kit) {
483
887
  } else if (ev.kind === "inject") injects++;
484
888
  else if (ev.kind === "model") switches++;
485
889
  }
486
- const cell = (label, value, sub) => h(
487
- "div",
488
- { className: "lc-stat" },
489
- h("span", { className: "lc-stat-label" }, label),
490
- h("b", { className: "lc-stat-value" }, value),
491
- sub !== void 0 ? h("span", { className: "lc-stat-sub" }, sub) : null
492
- );
493
- return h(
494
- "div",
495
- { className: "lc-card" },
496
- h(
497
- "div",
498
- { className: "lc-card-title" },
499
- t("stats.title"),
500
- h("span", { className: "lc-card-sub" }, t("stats.hint"))
501
- ),
502
- h(
503
- "div",
504
- { className: "lc-stats" },
505
- cell(t("stats.turns"), fmt3(turns.size)),
506
- cell(t("stats.steps"), fmt3(steps)),
507
- cell(
508
- t("stats.recycled"),
509
- recycled > 0 ? "\u2212" + fmt3(recycled) : "0",
510
- tr("stats.recycleSub", { c: compactions, p: prunes })
511
- ),
512
- cell(t("stats.injects"), fmt3(injects)),
513
- cell(t("stats.switches"), fmt3(switches)),
514
- cell(t("stats.est"), "\u2248 " + fmt3(est)),
515
- cell(t("stats.actualPrompt"), fmt3(prompt)),
516
- cell(t("stats.output"), fmt3(output))
517
- )
518
- );
519
- };
520
- }
521
-
522
- // src/client/components/stackedBar.ts
523
- function makeStackedBar(kit) {
524
- const { t, fmt: fmt3, catLabel } = kit;
525
- return function StackedBar(props) {
526
- let total = 0;
527
- for (const p of props.parts) total += p.value;
528
- const scale = props.max !== void 0 && props.max > total ? props.max : total;
529
- const free = props.max !== void 0 && props.max > total ? props.max - total : 0;
530
- let tip = null;
531
- if (props.hoverKey !== null && props.hoverKey !== void 0) {
532
- if (props.hoverKey === "free" && free > 0) {
533
- const pct = scale > 0 ? free / scale * 100 : 0;
534
- tip = {
535
- text: t("overview.free") + " " + fmt3(free) + " (" + Math.round(pct) + "%)",
536
- leftPct: Math.max(12, Math.min(total / scale * 100 + pct / 2, 88))
537
- };
538
- } else {
539
- let acc = 0;
540
- for (const p of props.parts) {
541
- const pct = scale > 0 ? p.value / scale * 100 : 0;
542
- if (p.key === props.hoverKey && p.value > 0) {
543
- tip = {
544
- text: catLabel(p.key) + " \u2248" + fmt3(p.value) + " (" + Math.round(p.value / total * 100) + "%)",
545
- leftPct: Math.max(12, Math.min(acc + pct / 2, 88))
546
- };
547
- break;
548
- }
549
- acc += pct;
550
- }
551
- }
552
- }
553
- return h(
554
- "div",
555
- { className: "lc-stacked-wrap" },
556
- h(
557
- "div",
558
- {
559
- className: "lc-stacked",
560
- style: { height: (props.height || 14) + "px" },
561
- onMouseLeave: () => {
562
- if (props.onHoverKey !== void 0) props.onHoverKey(null);
563
- }
564
- },
565
- total > 0 ? props.parts.map((p) => {
566
- if (!p.value) return null;
567
- const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
568
- return h("div", {
569
- key: p.key,
570
- className: "lc-stacked-seg" + (on ? " lc-stacked-seg-on" : ""),
571
- style: { width: p.value / scale * 100 + "%", background: p.color },
572
- onMouseEnter: () => {
573
- if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
574
- }
575
- });
576
- }) : null,
577
- free > 0 ? h("div", {
578
- key: "free",
579
- className: "lc-stacked-free" + (props.hoverKey === "free" ? " lc-stacked-free-on" : ""),
580
- style: { width: free / scale * 100 + "%" },
581
- onMouseEnter: () => {
582
- if (props.onHoverKey !== void 0) props.onHoverKey("free");
583
- }
584
- }) : null
585
- ),
586
- tip ? h("div", { className: "lc-bar-tip", style: { left: tip.leftPct + "%" } }, tip.text) : null
587
- );
588
- };
589
- }
590
- function makeLegend(kit) {
591
- const { fmt: fmt3, catLabel } = kit;
592
- return function Legend(props) {
593
- let total = 0;
594
- for (const p of props.parts) total += p.value;
595
- return h(
596
- "div",
597
- { className: "lc-legend" },
598
- props.parts.map((p) => {
599
- const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
600
- return h(
601
- "span",
602
- {
603
- key: p.key,
604
- className: "lc-chip" + (on ? " lc-chip-on" : ""),
605
- onMouseEnter: () => {
606
- if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
607
- },
608
- onMouseLeave: () => {
609
- if (props.onHoverKey !== void 0) props.onHoverKey(null);
610
- }
611
- },
612
- h("i", { style: { background: p.color } }),
613
- catLabel(p.key) + " \u2248" + fmt3(p.value),
614
- total > 0 ? h("em", null, Math.round(p.value / total * 100) + "%") : null
615
- );
616
- })
617
- );
618
- };
619
- }
620
-
621
- // src/client/components/trendChart.ts
622
- function aggregateByTurn(requests) {
623
- const out = [];
624
- let runSteps = 0;
625
- for (const req of requests) {
626
- const last = out.length > 0 ? out[out.length - 1] : null;
627
- if (last !== null && (last.turn ?? 0) === (req.turn ?? 0)) {
628
- runSteps++;
629
- out[out.length - 1] = { ...req, stepCount: runSteps };
630
- } else {
631
- runSteps = 1;
632
- out.push({ ...req, stepCount: 1 });
633
- }
634
- }
635
- return out;
636
- }
637
- function attachMarkers(requests, events) {
638
- const markers = new Array(requests.length);
639
- for (const ev of events) {
640
- if (ev.kind !== "compaction" && ev.kind !== "prune") continue;
641
- for (let r = 0; r < requests.length; r++) {
642
- if (requests[r].seq >= ev.seq) {
643
- if (markers[r] === void 0) markers[r] = ev;
644
- break;
645
- }
646
- }
647
- }
648
- return markers;
649
- }
650
- function makeTrendChart(kit) {
651
- const { t, tr, fmt: fmt3, fmtTime: fmtTime2, catLabel, eventLabel, eventAt } = kit;
652
- const CHART_H = 112;
653
- const BAR_W = 14;
654
- const BAR_GAP = 2;
655
- const TURN_COLORS = ["#6366f1", "#f59e0b", "#22c55e", "#a855f7", "#3b82f6", "#14b8a6", "#ef4444", "#ec4899"];
656
- return function TrendChart(props) {
657
- const requests = props.requests;
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;
661
- let maxTotal = 1;
662
- for (const req of requests) {
663
- const bt = barTotalOf(req);
664
- if (bt > maxTotal) maxTotal = bt;
665
- }
666
- const groups = [];
667
- for (const req of requests) {
668
- let grp = groups.length > 0 ? groups[groups.length - 1] : null;
669
- if (grp === null || grp.turn !== (req.turn ?? 0)) {
670
- grp = { turn: req.turn ?? 0, count: 0, span: 0, agg: req.stepCount !== void 0 };
671
- groups.push(grp);
672
- }
673
- grp.count++;
674
- grp.span += req.stepCount ?? 1;
675
- }
676
- const scrollRef = React.useRef(null);
677
- const scrolledOnce = React.useRef(false);
678
- const lastGranRef = React.useRef(props.granularity);
679
- const [edges, setEdges] = React.useState({ left: false, right: false });
680
- const edgesRef = React.useRef(edges);
681
- const updateEdges = (el) => {
682
- const left = el.scrollLeft > 4;
683
- const right = el.scrollLeft + el.clientWidth < el.scrollWidth - 4;
684
- const prev = edgesRef.current;
685
- if (prev.left === left && prev.right === right) return;
686
- edgesRef.current = { left, right };
687
- setEdges({ left, right });
688
- };
689
- React.useLayoutEffect(() => {
690
- const el = scrollRef.current;
691
- if (el === null) return;
692
- if (props.granularity !== lastGranRef.current) {
693
- lastGranRef.current = props.granularity;
694
- scrolledOnce.current = false;
695
- }
696
- if (!scrolledOnce.current) {
697
- scrolledOnce.current = true;
698
- el.scrollLeft = el.scrollWidth;
699
- } else if (el.scrollLeft + el.clientWidth >= el.scrollWidth - 24) {
700
- el.scrollLeft = el.scrollWidth;
701
- }
702
- updateEdges(el);
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;
710
- return h(
711
- "div",
712
- { className: "lc-chartrow" },
713
- h(
714
- "div",
715
- { className: "lc-axis" },
716
- h("span", { className: "lc-axis-top" }, fmt3(maxTotal)),
717
- h("span", { className: "lc-axis-mid" }, fmt3(Math.round(maxTotal / 2))),
718
- h("span", { className: "lc-axis-bot" }, "0")
719
- ),
720
- h(
721
- "div",
722
- {
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" : ""),
726
- ref: scrollRef,
727
- onScroll: (e) => {
728
- updateEdges(e.currentTarget);
729
- }
730
- },
731
- // Edge fades: visible whenever more history sits beyond the viewport,
732
- // so the horizontal scroll affordance is obvious.
733
- edges.left ? h("div", { className: "lc-chart-fade lc-chart-fade-l" }) : null,
734
- // Hovering a bar previews it in the detail below; leaving the plot
735
- // clears the preview (a pinned selection, if any, takes over again).
736
- h(
737
- "div",
738
- {
739
- className: "lc-chart",
740
- onMouseLeave: () => {
741
- props.onHover(null);
742
- }
743
- },
744
- h("div", { className: "lc-grid lc-grid-top" }),
745
- h("div", { className: "lc-grid lc-grid-mid" }),
746
- requests.map((req, i) => {
747
- const marker = markers[i];
748
- const markerAt = marker !== void 0 ? eventAt(marker) : null;
749
- const selected = props.selectedSeq === req.seq;
750
- const hovered = props.hoveredSeq === req.seq;
751
- const inTurn = props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn;
752
- return h(
753
- "div",
754
- {
755
- key: req.seq,
756
- // Uniform column width in BOTH granularities: turn aggregates
757
- // keep the same fixed width as step bars.
758
- className: "lc-bar" + (selected ? " lc-bar-selected" : "") + (hovered ? " lc-bar-hovered" : "") + (inTurn ? " lc-bar-in-turn" : ""),
759
- style: { width: BAR_W + "px" },
760
- onClick: () => {
761
- props.onSelect(selected ? null : req.seq);
762
- },
763
- onMouseEnter: () => {
764
- props.onHover(req.seq);
765
- }
766
- },
767
- // The ✂ tooltip names the event AND where it happened: the
768
- // gap between the request before and the request after.
769
- marker !== void 0 ? h("span", {
770
- className: "lc-bar-marker",
771
- title: "\u2702 " + (markerAt !== null ? markerAt + " \u2014 " : "") + eventLabel(marker)
772
- }, "\u2702") : null,
773
- h(
774
- "div",
775
- { className: "lc-bar-stack" },
776
- CATS.map((c) => {
777
- const v = (req[c.key] || 0) * anchorOf(req);
778
- if (!v) return null;
779
- return h("div", { key: c.key, style: { height: Math.max(1, Math.round(v / maxTotal * CHART_H)) + "px", background: c.color } });
780
- })
781
- )
782
- );
783
- })
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,
791
- // Turn strip: one COLOR BLOCK per turn, spanning exactly its bars'
792
- // columns, so the partition reads at a glance and lines up with the
793
- // steps above. Hovering a block highlights that turn's bars in the
794
- // chart (and hovering a bar highlights its block — the active turn
795
- // is shared hover-only state).
796
- h(
797
- "div",
798
- { className: "lc-turns", onMouseLeave: () => {
799
- props.onHoverTurn(null);
800
- } },
801
- groups.map((grp, gi) => {
802
- const on = props.activeTurn === grp.turn;
803
- const blockW = grp.agg ? BAR_W : grp.span * (BAR_W + BAR_GAP) - BAR_GAP;
804
- return h("span", {
805
- key: "turn-" + gi,
806
- className: "lc-turn" + (on ? " lc-turn-on" : ""),
807
- style: {
808
- width: blockW + "px",
809
- background: TURN_COLORS[gi % TURN_COLORS.length]
810
- },
811
- title: "T" + grp.turn,
812
- onMouseEnter: () => {
813
- props.onHoverTurn(grp.turn);
814
- }
815
- }, "T" + grp.turn);
816
- })
817
- )
818
- ),
819
- edges.right ? h("div", { className: "lc-chart-fade lc-chart-fade-r" }) : null
820
- );
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))));
821
896
  };
822
897
  }
823
898
 
824
- // src/client/components/contextView.ts
899
+ // src/client/components/contextView.tsx
825
900
  var viewScroll = /* @__PURE__ */ new Map();
826
901
  function makeContextView(ctx, kit) {
827
902
  const { t, tr, fmt: fmt3, fmtTime: fmtTime2, catLabel } = kit;
@@ -834,19 +909,13 @@ function makeContextView(ctx, kit) {
834
909
  const StatsBoard = makeStatsBoard(kit);
835
910
  return function ContextView(props) {
836
911
  const sessionId = props.sessionId;
837
- const initial = typeof sessionId === "string" && sessionId !== "" ? cacheGet(sessionId) ?? null : null;
838
- const [data, setData] = React.useState(initial);
839
- const [error, setError] = React.useState(null);
912
+ const data = typeof props.useProjection === "function" ? timelineOf(props.useProjection("contextTimeline")) : null;
840
913
  const [selectedSeq, setSelectedSeq] = React.useState(null);
841
914
  const [hoveredSeq, setHoveredSeq] = React.useState(null);
842
915
  const [hoverTurn, setHoverTurn] = React.useState(null);
843
916
  const [tick, setTick] = React.useState(0);
844
917
  const [granularity, setGranularity] = React.useState("step");
845
918
  const [hoverCat, setHoverCat] = React.useState(null);
846
- const dataRef = React.useRef(initial);
847
- React.useEffect(() => {
848
- dataRef.current = data;
849
- }, [data]);
850
919
  const rootRef = React.useRef(null);
851
920
  const scrollerRef = React.useRef(null);
852
921
  const restoredRef = React.useRef(null);
@@ -867,53 +936,15 @@ function makeContextView(ctx, kit) {
867
936
  viewScroll.set(sessionId, scroller.scrollTop);
868
937
  };
869
938
  }, [sessionId]);
870
- React.useEffect(() => {
871
- if (typeof sessionId !== "string" || sessionId === "") return void 0;
872
- let alive = true;
873
- const load = () => {
874
- ctx.connection.rpc.call("/dsh-context", "snapshot", { sessionId }).then((res) => {
875
- if (!alive) return;
876
- if (res && res.ok) {
877
- const snap = res.value;
878
- cachePut(sessionId, snap);
879
- setData(snap);
880
- setError(null);
881
- } else if (dataRef.current === null) {
882
- setError(res && res.error ? String(res.error.message || res.error.code) : "failed");
883
- }
884
- }, (err) => {
885
- if (alive && dataRef.current === null) {
886
- setError(String(err instanceof Error ? err.message : err));
887
- }
888
- });
889
- };
890
- load();
891
- const timerId = setInterval(() => {
892
- if (document.visibilityState !== "hidden") load();
893
- }, 2e3);
894
- const onVisible = () => {
895
- if (document.visibilityState === "visible") load();
896
- };
897
- document.addEventListener("visibilitychange", onVisible);
898
- return () => {
899
- alive = false;
900
- clearInterval(timerId);
901
- document.removeEventListener("visibilitychange", onVisible);
902
- };
903
- }, [sessionId]);
904
939
  React.useEffect(() => {
905
940
  const localeSvc = ctx.get("locale");
906
941
  if (!localeSvc) return void 0;
907
942
  return localeSvc.subscribe(() => setTick((x) => x + 1));
908
943
  }, []);
909
944
  void tick;
910
- if (error) {
911
- return h("div", { className: "lc-root", ref: rootRef }, h("div", { className: "lc-empty" }, t("error") + error));
912
- }
913
945
  if (!data) {
914
- return h("div", { className: "lc-root", ref: rootRef }, h("div", { className: "lc-empty" }, t("loading")));
946
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-root", ref: rootRef }, /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("loading")));
915
947
  }
916
- const current = data.current;
917
948
  const requests = data.requests || [];
918
949
  const events = data.events || [];
919
950
  const nodes = data.nodes || [];
@@ -938,125 +969,42 @@ function makeContextView(ctx, kit) {
938
969
  break;
939
970
  }
940
971
  }
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);
950
- return h(
951
- "div",
952
- { className: "lc-root", ref: rootRef },
953
- // ---- session context stats (over the retained window) ----
954
- h(StatsBoard, { requests, events }),
955
- // ---- overview ----
956
- h(
957
- "div",
958
- { className: "lc-card" },
959
- h(
960
- "div",
961
- { className: "lc-card-title" },
962
- t("overview.title"),
963
- h(
964
- "span",
965
- { className: "lc-card-sub" },
966
- (data.model ? data.model : "") + (data.provider ? " \xB7 " + data.provider : "")
967
- )
968
- ),
969
- h(
970
- "div",
971
- { className: "lc-overview-num" },
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
975
- ),
976
- h(StackedBar, { parts, height: 16, max: occupancyWindow, hoverKey: hoverCat, onHoverKey: setHoverCat }),
977
- h(Legend, { parts, hoverKey: hoverCat, onHoverKey: setHoverCat }),
978
- data.toolList && data.toolList.length > 0 ? h(
979
- "div",
980
- { className: "lc-tools" },
981
- t("tools.top"),
982
- data.toolList.slice().sort((a, b) => b.tokens - a.tokens).slice(0, 5).map((tool) => {
983
- return h("span", { key: tool.name, className: "lc-tool-chip" }, tool.name + " " + fmt3(tool.tokens));
984
- }),
985
- data.toolList.length > 5 ? h("span", { className: "lc-card-sub" }, " " + tr("tools.more", { n: data.toolList.length })) : null
986
- ) : null
987
- ),
988
- // ---- trend ----
989
- h(
990
- "div",
991
- { className: "lc-card" },
992
- h(
993
- "div",
994
- { className: "lc-card-title" },
995
- t("trend.title"),
996
- h("span", { className: "lc-card-sub" }, t("trend.hint")),
997
- h(
998
- "div",
999
- { className: "lc-gran" },
1000
- h("button", {
1001
- className: "lc-gran-btn" + (granularity === "step" ? " lc-gran-on" : ""),
1002
- onClick: () => {
1003
- setGranularity("step");
1004
- }
1005
- }, t("gran.step")),
1006
- h("button", {
1007
- className: "lc-gran-btn" + (granularity === "turn" ? " lc-gran-on" : ""),
1008
- onClick: () => {
1009
- setGranularity("turn");
1010
- }
1011
- }, t("gran.turn"))
1012
- )
1013
- ),
1014
- displayRequests.length === 0 ? h("div", { className: "lc-empty" }, t("trend.empty")) : h(
1015
- "div",
1016
- null,
1017
- h(TrendChart, {
1018
- // Remount per session: switching sessions re-anchors the chart
1019
- // at the newest bars instead of inheriting stale scroll state.
1020
- key: sessionId,
1021
- // The host caps the log at 160 requests; render them ALL so
1022
- // earlier turns/steps stay reachable via horizontal scroll.
1023
- requests: displayRequests,
1024
- markers,
1025
- selectedSeq: pinnedReq ? pinnedReq.seq : null,
1026
- hoveredSeq,
1027
- activeTurn,
1028
- granularity,
1029
- onSelect: setSelectedSeq,
1030
- onHover: setHoveredSeq,
1031
- onHoverTurn: setHoverTurn
1032
- }),
1033
- h(RequestDetail, { request: activeReq, marker: activeReq !== null ? markerOf(activeReq) : void 0 })
1034
- )
1035
- ),
1036
- // ---- events + messages ----
1037
- h(
1038
- "div",
1039
- { className: "lc-cols" },
1040
- h(
1041
- "div",
1042
- { className: "lc-card lc-col" },
1043
- h("div", { className: "lc-card-title" }, t("events.title")),
1044
- h(EventList, { events })
1045
- ),
1046
- h(
1047
- "div",
1048
- { className: "lc-card lc-col" },
1049
- h(
1050
- "div",
1051
- { className: "lc-card-title" },
1052
- t("nodes.title"),
1053
- h("span", { className: "lc-card-sub" }, t("nodes.hint"))
1054
- ),
1055
- h(NodeList, { nodes, dropped: data.droppedNodes || 0 })
1056
- )
1057
- ),
1058
- h("div", { className: "lc-foot" }, t("footer"))
1059
- );
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) => {
974
+ return /* @__PURE__ */ React.createElement("span", { key: tool.name, className: "lc-tool-chip" }, tool.name + " " + fmt3(tool.tokens));
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(
976
+ "button",
977
+ {
978
+ className: "lc-gran-btn" + (granularity === "step" ? " lc-gran-on" : ""),
979
+ onClick: () => {
980
+ setGranularity("step");
981
+ }
982
+ },
983
+ t("gran.step")
984
+ ), /* @__PURE__ */ React.createElement(
985
+ "button",
986
+ {
987
+ className: "lc-gran-btn" + (granularity === "turn" ? " lc-gran-on" : ""),
988
+ onClick: () => {
989
+ setGranularity("turn");
990
+ }
991
+ },
992
+ t("gran.turn")
993
+ ))), displayRequests.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("trend.empty")) : /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(
994
+ TrendChart,
995
+ {
996
+ key: sessionId,
997
+ requests: displayRequests,
998
+ markers,
999
+ selectedSeq: pinnedReq ? pinnedReq.seq : null,
1000
+ hoveredSeq,
1001
+ activeTurn,
1002
+ granularity,
1003
+ onSelect: setSelectedSeq,
1004
+ onHover: setHoveredSeq,
1005
+ onHoverTurn: setHoverTurn
1006
+ }
1007
+ ), /* @__PURE__ */ React.createElement(RequestDetail, { request: activeReq, marker: activeReq !== null ? markerOf(activeReq) : void 0 }))), /* @__PURE__ */ React.createElement("div", { className: "lc-cols" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card lc-col" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, t("events.title")), /* @__PURE__ */ React.createElement(EventList, { events })), /* @__PURE__ */ React.createElement("div", { className: "lc-card lc-col" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, t("nodes.title"), /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("nodes.hint"))), /* @__PURE__ */ React.createElement(NodeList, { nodes, dropped: data.droppedNodes || 0 }))), /* @__PURE__ */ React.createElement("div", { className: "lc-foot" }, t("footer")));
1060
1008
  };
1061
1009
  }
1062
1010
 
@@ -1075,11 +1023,12 @@ function makeViewKit(t) {
1075
1023
  }
1076
1024
 
1077
1025
  // src/client/index.ts
1026
+ var NS = "dsh-context";
1078
1027
  function apply(ctx) {
1079
1028
  ctx.effect(() => {
1080
- return ctx.locale.register("dsh-context", { zh: DICT_ZH, en: DICT_EN });
1029
+ return ctx.locale.register(NS, { zh: DICT_ZH, en: DICT_EN });
1081
1030
  }, "dsh-context: dictionaries");
1082
- const t = ctx.locale.bind("dsh-context");
1031
+ const t = ctx.locale.bind(NS);
1083
1032
  ctx.effect(() => {
1084
1033
  const tag = document.createElement("style");
1085
1034
  tag.setAttribute("data-plugin", "dsh-context");
@@ -1089,18 +1038,34 @@ function apply(ctx) {
1089
1038
  if (tag.parentNode !== null) tag.parentNode.removeChild(tag);
1090
1039
  };
1091
1040
  }, "dsh-context: styles");
1092
- const ContextView = makeContextView(ctx, makeViewKit(t));
1041
+ const kit = makeViewKit(t);
1042
+ const ContextView = makeContextView(ctx, kit);
1093
1043
  ctx.slots.inject("conversation.view", () => {
1094
1044
  return ctx.slots.register(
1095
- // order 20 renders right of Chat (0) and Trajectory (10).
1096
- { name: "conversation.view", id: "context", order: 20, label: () => t("tab") },
1045
+ // order 20 renders right of Chat (0) and Trajectory (10); the locale
1046
+ // namespace put the framework `t` seat on the component's props too.
1047
+ { name: "conversation.view", id: "context", order: 20, locale: NS, label: () => t("tab") },
1097
1048
  (props) => h(ContextView, props)
1098
1049
  );
1099
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
+ });
1100
1065
  }
1101
1066
  module.exports = {
1102
1067
  name: "dsh-context",
1103
- inject: ["connection", "slots", "locale"],
1068
+ inject: ["slots", "locale"],
1104
1069
  apply
1105
1070
  };
1106
1071