dsh-context 0.7.3 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -19,6 +19,8 @@ var DICT_ZH = {
19
19
  "overview.ofWindow": "tokens\uFF08\u7EA6 {p}%\uFF09",
20
20
  "overview.estimate": "tokens\uFF08\u4F30\u7B97\uFF09",
21
21
  "overview.free": "\u5269\u4F59\u7A97\u53E3",
22
+ "overview.splitEst": "\xB7 \u5206\u7C7B\u6309\u4F30\u7B97\u6BD4\u4F8B\u5206\u914D",
23
+ "overview.ofUsed": "\u5360\u5DF2\u7528\u4E0A\u4E0B\u6587",
22
24
  "stats.title": "\u4E0A\u4E0B\u6587\u7EDF\u8BA1",
23
25
  "stats.hint": "\u7EDF\u8BA1\u5F53\u524D\u4FDD\u7559\u7684\u5386\u53F2\u7A97\u53E3\uFF08\u4E0E\u8D8B\u52BF\u56FE\u4E00\u81F4\uFF09",
24
26
  "stats.turns": "\u8F6E\u6B21",
@@ -88,6 +90,8 @@ var DICT_EN = {
88
90
  "overview.ofWindow": "tokens (~{p}%)",
89
91
  "overview.estimate": "tokens (estimated)",
90
92
  "overview.free": "Free window",
93
+ "overview.splitEst": "\xB7 category split is estimated",
94
+ "overview.ofUsed": "of used context",
91
95
  "stats.title": "Context stats",
92
96
  "stats.hint": "Over the retained history window (same as the History chart)",
93
97
  "stats.turns": "Turns",
@@ -165,11 +169,25 @@ var STYLES = [
165
169
  ".lc-overview-num b { font-size: 20px; }",
166
170
  ".lc-overview-num span { color: var(--dsw-alias-label-secondary); }",
167
171
  ".lc-stacked-wrap { position: relative; width: 100%; }",
168
- ".lc-stacked { display: flex; width: 100%; border-radius: 5px; overflow: hidden; background: rgba(128,128,128,0.18); }",
172
+ ".lc-stacked { display: flex; width: 100%; border-radius: 5px; overflow: hidden; background: rgba(128,128,128,0.18); position: relative; }",
173
+ // Hover reference frame around the OCCUPIED region of the composition bar:
174
+ // dashes from the left edge to the used/window boundary, so the legend's
175
+ // "share of used" percentages visibly map to the boxed part (the free track
176
+ // sits outside it). pointer-events: none keeps hover on the segments/free.
177
+ // Deliberately high-contrast (label-primary) and thick so the frame reads at
178
+ // a glance, and the other parts dim underneath it (`.lc-stacked-dim`).
179
+ ".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); }",
169
180
  ".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; }",
170
181
  ".lc-stacked > div { height: 100%; }",
171
182
  ".lc-stacked-seg-on { filter: brightness(1.18); }",
172
183
  ".lc-stacked-free-on { box-shadow: inset 0 0 0 1px var(--dsw-alias-label-secondary); border-radius: 3px; }",
184
+ // Hover focus: everything except the hovered part (segment, legend chip, or
185
+ // free track) recedes, so the composition highlight and the occupied-region
186
+ // frame read clearly. The selected segment/free keeps full opacity.
187
+ ".lc-stacked-dim .lc-stacked-seg { opacity: 0.35; }",
188
+ ".lc-stacked-dim .lc-stacked-seg-on { opacity: 1; }",
189
+ ".lc-stacked-dim .lc-stacked-free { opacity: 0.35; }",
190
+ ".lc-stacked-dim .lc-stacked-free-on { opacity: 1; }",
173
191
  ".lc-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 10px; }",
174
192
  ".lc-chip { display: inline-flex; align-items: center; gap: 5px; color: var(--dsw-alias-label-primary); }",
175
193
  ".lc-chip i, .lc-detail-row i, .lc-node i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }",
@@ -196,7 +214,14 @@ var STYLES = [
196
214
  ".lc-grid { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--dsw-alias-border-l1); pointer-events: none; }",
197
215
  ".lc-grid-top { top: 18px; }",
198
216
  ".lc-grid-mid { top: 74px; }",
199
- ".lc-bar { position: relative; width: 14px; flex: none; height: 100%; display: flex; align-items: flex-end; cursor: pointer; border-radius: 2px; }",
217
+ ".lc-bar { position: relative; width: 14px; flex: none; height: 100%; display: flex; align-items: flex-end; cursor: pointer; border-radius: 2px; transition: opacity 120ms ease; }",
218
+ // Turn-aware dimming: while a turn is focused, bars OUTSIDE the active
219
+ // turn fade to 35% and the whole current turn stays fully opaque.
220
+ ".lc-chart-dim .lc-bar { opacity: 0.35; }",
221
+ ".lc-chart-dim .lc-bar-in-turn { opacity: 1; }",
222
+ ".lc-chart-dim .lc-turn { opacity: 0.35; }",
223
+ ".lc-chart-dim .lc-turn-on { opacity: 1; }",
224
+ ".lc-chart-tip { position: absolute; top: 0; transform: translateX(-50%); z-index: 5; white-space: nowrap; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 3px 8px; font-size: 12px; color: var(--dsw-alias-label-primary); box-shadow: 0 2px 8px rgba(0,0,0,0.18); pointer-events: none; }",
200
225
  ".lc-bar:hover { background: var(--dsw-alias-bg-layer-2); }",
201
226
  ".lc-bar-selected { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }",
202
227
  ".lc-bar-hovered { outline: 1px dashed var(--dsw-alias-brand-primary); outline-offset: 1px; }",
@@ -205,7 +230,7 @@ var STYLES = [
205
230
  ".lc-bar-stack > div { width: 100%; }",
206
231
  ".lc-bar-marker { position: absolute; top: -16px; left: 50%; transform: translateX(-50%); font-size: 11px; color: var(--dsw-alias-state-warn-primary); }",
207
232
  ".lc-turns { display: flex; gap: 2px; width: max-content; min-width: 100%; margin-top: 4px; }",
208
- ".lc-turn { flex: none; box-sizing: border-box; text-align: center; font-size: 10px; line-height: 14px; font-weight: 600; color: #fff; border-radius: 3px; height: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; transition: filter 120ms; }",
233
+ ".lc-turn { flex: none; box-sizing: border-box; text-align: center; font-size: 10px; line-height: 14px; font-weight: 600; color: #fff; border-radius: 3px; height: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; transition: filter 120ms, opacity 120ms; }",
209
234
  ".lc-turn-on { filter: brightness(1.35); box-shadow: 0 0 0 1px rgba(255,255,255,0.4); }",
210
235
  ".lc-detail { margin-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); padding-top: 12px; }",
211
236
  ".lc-detail-head { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 8px; color: var(--dsw-alias-label-secondary); }",
@@ -241,20 +266,6 @@ var STYLES = [
241
266
  ".lc-foot { color: var(--dsw-alias-label-secondary); font-size: 12px; margin-top: 4px; }"
242
267
  ].join("\n");
243
268
 
244
- // src/client/cache.ts
245
- var MAX_CACHED_SESSIONS = 10;
246
- var sessionCache = /* @__PURE__ */ new Map();
247
- function cacheGet(sessionId) {
248
- return sessionCache.get(sessionId);
249
- }
250
- function cachePut(sessionId, snapshot) {
251
- sessionCache.set(sessionId, snapshot);
252
- if (sessionCache.size > MAX_CACHED_SESSIONS) {
253
- const oldest = sessionCache.keys().next().value;
254
- if (oldest !== void 0) sessionCache.delete(oldest);
255
- }
256
- }
257
-
258
269
  // src/client/categories.ts
259
270
  var CATS = [
260
271
  { key: "system", color: "#6366f1" },
@@ -269,6 +280,23 @@ function partsOf(breakdown) {
269
280
  return { key: c.key, color: c.color, value: breakdown[c.key] || 0 };
270
281
  });
271
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
+ // src/client/components/events.tsx
299
+ var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
272
300
 
273
301
  // src/client/format.ts
274
302
  function fmt(n) {
@@ -289,7 +317,7 @@ function fmtTime(t) {
289
317
  var React = require("react");
290
318
  var h = React.createElement;
291
319
 
292
- // src/client/components/events.ts
320
+ // src/client/components/events.tsx
293
321
  var EVENT_ICONS = { compaction: "\u2702", prune: "\u2702", inject: "\uFF0B", model: "\u21C4" };
294
322
  function makeEventText(t, tr) {
295
323
  function eventLabel(ev) {
@@ -325,34 +353,19 @@ function makeEventList(kit) {
325
353
  const { t, fmt: fmt3, fmtTime: fmtTime2, eventLabel, eventAt } = kit;
326
354
  return function EventList(props) {
327
355
  if (props.events.length === 0) {
328
- return h("div", { className: "lc-empty" }, t("events.empty"));
356
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("events.empty"));
329
357
  }
330
358
  const sorted = props.events.slice().reverse();
331
- return h(
332
- "div",
333
- { className: "lc-events" },
334
- sorted.map((ev, i) => {
335
- const label = eventLabel(ev);
336
- const at = eventAt(ev);
337
- return h(
338
- "div",
339
- { key: ev.seq + "-" + i, className: "lc-event" },
340
- h("span", { className: "lc-event-icon lc-event-" + ev.kind }, EVENT_ICONS[ev.kind] || "\u2022"),
341
- h("span", { className: "lc-event-label", title: label }, label),
342
- at !== null ? h("span", { className: "lc-event-at" }, at) : null,
343
- ev.tokens ? h(
344
- "span",
345
- { className: "lc-event-tokens" + (ev.kind === "inject" ? " lc-up" : " lc-down") },
346
- (ev.kind === "inject" ? "+" : "\u2212") + fmt3(ev.tokens)
347
- ) : null,
348
- h("span", { className: "lc-event-time" }, fmtTime2(ev.time))
349
- );
350
- })
351
- );
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
+ }));
352
365
  };
353
366
  }
354
367
 
355
- // src/client/components/nodes.ts
368
+ // src/client/components/nodes.tsx
356
369
  function makeNodeList(kit) {
357
370
  const { t, tr, fmt: fmt3, fmtTime: fmtTime2 } = kit;
358
371
  function nodeText(n) {
@@ -368,32 +381,19 @@ function makeNodeList(kit) {
368
381
  }
369
382
  return function NodeList(props) {
370
383
  if (props.nodes.length === 0) {
371
- return h("div", { className: "lc-empty" }, t("nodes.empty"));
384
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("nodes.empty"));
372
385
  }
373
386
  const catColor = {};
374
387
  for (const c of CATS) catColor[c.key] = c.color;
375
388
  const rows = props.nodes.slice().reverse();
376
- return h(
377
- "div",
378
- { className: "lc-nodes" },
379
- props.dropped > 0 ? h("div", { className: "lc-nodes-more" }, tr("nodes.more", { n: props.dropped })) : null,
380
- rows.map((n) => {
381
- const text = nodeText(n);
382
- return h(
383
- "div",
384
- { key: n.seq, className: "lc-node" },
385
- h("i", { style: { background: catColor[n.cat] || "#999" } }),
386
- h("span", { className: "lc-node-preview", title: text }, text),
387
- // Timestamp when the host event carried one.
388
- typeof n.time === "number" ? h("span", { className: "lc-node-time" }, fmtTime2(n.time)) : null,
389
- h("span", { className: "lc-node-tokens" }, fmt3(n.tokens))
390
- );
391
- })
392
- );
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
393
  };
394
394
  }
395
395
 
396
- // src/client/components/requestDetail.ts
396
+ // src/client/components/requestDetail.tsx
397
397
  function makeRequestDetail(kit, StackedBar) {
398
398
  const { t, tr, fmt: fmt3, fmtTime: fmtTime2, catLabel, eventLabel, eventAt } = kit;
399
399
  return function RequestDetail(props) {
@@ -403,46 +403,14 @@ function makeRequestDetail(kit, StackedBar) {
403
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
404
  const marker = props.marker ?? null;
405
405
  const markerAt = marker !== null ? eventAt(marker) : null;
406
- return h(
407
- "div",
408
- { className: "lc-detail" },
409
- h(
410
- "div",
411
- { className: "lc-detail-head" },
412
- h("b", null, head),
413
- marker !== null && markerAt !== null ? h("span", { className: "lc-detail-marker", title: eventLabel(marker) }, "\u2702 " + markerAt) : null,
414
- isTurn ? h("span", { className: "lc-detail-tag" }, t("detail.lastStep")) : null,
415
- h("span", null, fmtTime2(req.time)),
416
- h("span", null, tr("detail.estTotal", { n: fmt3(req.total) })),
417
- req.prompt !== void 0 ? h("span", { className: "lc-actual" }, tr("detail.actual", { n: fmt3(req.prompt) })) : null,
418
- req.output !== void 0 ? h("span", null, tr("detail.output", { n: fmt3(req.output) })) : null
419
- ),
420
- h(StackedBar, { parts: partsOf(req), height: 10 }),
421
- h(
422
- "div",
423
- { className: "lc-detail-rows" },
424
- CATS.map((c) => {
425
- const v = req[c.key] || 0;
426
- return h(
427
- "div",
428
- { key: c.key, className: "lc-detail-row" },
429
- h("i", { style: { background: c.color } }),
430
- h("span", { className: "lc-detail-label" }, catLabel(c.key)),
431
- h(
432
- "span",
433
- { className: "lc-bar-track" },
434
- h("span", { className: "lc-bar-fill", style: { width: (req.total > 0 ? v / req.total * 100 : 0) + "%", background: c.color } })
435
- ),
436
- h("span", { className: "lc-detail-num" }, fmt3(v)),
437
- h("span", { className: "lc-detail-pct" }, req.total > 0 ? Math.round(v / req.total * 100) + "%" : "")
438
- );
439
- })
440
- )
441
- );
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
+ })));
442
410
  };
443
411
  }
444
412
 
445
- // src/client/components/statsBoard.ts
413
+ // src/client/components/statsBoard.tsx
446
414
  function makeStatsBoard(kit) {
447
415
  const { t, tr, fmt: fmt3 } = kit;
448
416
  return function StatsBoard(props) {
@@ -466,50 +434,26 @@ function makeStatsBoard(kit) {
466
434
  } else if (ev.kind === "inject") injects++;
467
435
  else if (ev.kind === "model") switches++;
468
436
  }
469
- const cell = (label, value, sub) => h(
470
- "div",
471
- { className: "lc-stat" },
472
- h("span", { className: "lc-stat-label" }, label),
473
- h("b", { className: "lc-stat-value" }, value),
474
- sub !== void 0 ? h("span", { className: "lc-stat-sub" }, sub) : null
475
- );
476
- return h(
477
- "div",
478
- { className: "lc-card" },
479
- h(
480
- "div",
481
- { className: "lc-card-title" },
482
- t("stats.title"),
483
- h("span", { className: "lc-card-sub" }, t("stats.hint"))
484
- ),
485
- h(
486
- "div",
487
- { className: "lc-stats" },
488
- cell(t("stats.turns"), fmt3(turns.size)),
489
- cell(t("stats.steps"), fmt3(steps)),
490
- cell(
491
- t("stats.recycled"),
492
- recycled > 0 ? "\u2212" + fmt3(recycled) : "0",
493
- tr("stats.recycleSub", { c: compactions, p: prunes })
494
- ),
495
- cell(t("stats.injects"), fmt3(injects)),
496
- cell(t("stats.switches"), fmt3(switches)),
497
- cell(t("stats.est"), "\u2248 " + fmt3(est)),
498
- cell(t("stats.actualPrompt"), fmt3(prompt)),
499
- cell(t("stats.output"), fmt3(output))
500
- )
501
- );
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))));
502
443
  };
503
444
  }
504
445
 
505
- // src/client/components/stackedBar.ts
446
+ // src/client/components/stackedBar.tsx
506
447
  function makeStackedBar(kit) {
507
- const { t, fmt: fmt3, catLabel } = kit;
448
+ const { t, tr, fmt: fmt3, catLabel } = kit;
508
449
  return function StackedBar(props) {
509
450
  let total = 0;
510
451
  for (const p of props.parts) total += p.value;
511
452
  const scale = props.max !== void 0 && props.max > total ? props.max : total;
512
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;
513
457
  let tip = null;
514
458
  if (props.hoverKey !== null && props.hoverKey !== void 0) {
515
459
  if (props.hoverKey === "free" && free > 0) {
@@ -524,7 +468,9 @@ function makeStackedBar(kit) {
524
468
  const pct = scale > 0 ? p.value / scale * 100 : 0;
525
469
  if (p.key === props.hoverKey && p.value > 0) {
526
470
  tip = {
527
- text: catLabel(p.key) + " " + fmt3(p.value) + " (" + Math.round(p.value / total * 100) + "%)",
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"),
528
474
  leftPct: Math.max(12, Math.min(acc + pct / 2, 88))
529
475
  };
530
476
  break;
@@ -533,75 +479,74 @@ function makeStackedBar(kit) {
533
479
  }
534
480
  }
535
481
  }
536
- return h(
482
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-stacked-wrap" }, /* @__PURE__ */ React.createElement(
537
483
  "div",
538
- { className: "lc-stacked-wrap" },
539
- h(
540
- "div",
541
- {
542
- className: "lc-stacked",
543
- style: { height: (props.height || 14) + "px" },
544
- onMouseLeave: () => {
545
- if (props.onHoverKey !== void 0) props.onHoverKey(null);
546
- }
547
- },
548
- total > 0 ? props.parts.map((p) => {
549
- if (!p.value) return null;
550
- const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
551
- return h("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
+ {
552
497
  key: p.key,
553
498
  className: "lc-stacked-seg" + (on ? " lc-stacked-seg-on" : ""),
554
499
  style: { width: p.value / scale * 100 + "%", background: p.color },
555
500
  onMouseEnter: () => {
556
501
  if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
557
502
  }
558
- });
559
- }) : null,
560
- free > 0 ? h("div", {
503
+ }
504
+ );
505
+ }) : null,
506
+ free > 0 ? /* @__PURE__ */ React.createElement(
507
+ "div",
508
+ {
561
509
  key: "free",
562
510
  className: "lc-stacked-free" + (props.hoverKey === "free" ? " lc-stacked-free-on" : ""),
563
511
  style: { width: free / scale * 100 + "%" },
564
512
  onMouseEnter: () => {
565
513
  if (props.onHoverKey !== void 0) props.onHoverKey("free");
566
514
  }
567
- }) : null
568
- ),
569
- tip ? h("div", { className: "lc-bar-tip", style: { left: tip.leftPct + "%" } }, tip.text) : null
570
- );
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);
571
519
  };
572
520
  }
573
521
  function makeLegend(kit) {
574
- const { fmt: fmt3, catLabel } = kit;
522
+ const { tr, fmt: fmt3, catLabel } = kit;
575
523
  return function Legend(props) {
576
524
  let total = 0;
577
525
  for (const p of props.parts) total += p.value;
578
- return h(
579
- "div",
580
- { className: "lc-legend" },
581
- props.parts.map((p) => {
582
- const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
583
- return h(
584
- "span",
585
- {
586
- key: p.key,
587
- className: "lc-chip" + (on ? " lc-chip-on" : ""),
588
- onMouseEnter: () => {
589
- if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
590
- },
591
- onMouseLeave: () => {
592
- if (props.onHoverKey !== void 0) props.onHoverKey(null);
593
- }
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);
594
536
  },
595
- h("i", { style: { background: p.color } }),
596
- catLabel(p.key) + " " + fmt3(p.value),
597
- total > 0 ? h("em", null, Math.round(p.value / total * 100) + "%") : null
598
- );
599
- })
600
- );
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
+ );
545
+ }));
601
546
  };
602
547
  }
603
548
 
604
- // src/client/components/trendChart.ts
549
+ // src/client/components/trendChart.tsx
605
550
  function aggregateByTurn(requests) {
606
551
  const out = [];
607
552
  let runSteps = 0;
@@ -639,8 +584,13 @@ function makeTrendChart(kit) {
639
584
  return function TrendChart(props) {
640
585
  const requests = props.requests;
641
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;
642
589
  let maxTotal = 1;
643
- for (const req of requests) if (req.total > maxTotal) maxTotal = req.total;
590
+ for (const req of requests) {
591
+ const bt = barTotalOf(req);
592
+ if (bt > maxTotal) maxTotal = bt;
593
+ }
644
594
  const groups = [];
645
595
  for (const req of requests) {
646
596
  let grp = groups.length > 0 ? groups[groups.length - 1] : null;
@@ -679,112 +629,103 @@ function makeTrendChart(kit) {
679
629
  }
680
630
  updateEdges(el);
681
631
  });
682
- return h(
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(
683
639
  "div",
684
- { className: "lc-chartrow" },
685
- h(
686
- "div",
687
- { className: "lc-axis" },
688
- h("span", { className: "lc-axis-top" }, fmt3(maxTotal)),
689
- h("span", { className: "lc-axis-mid" }, fmt3(Math.round(maxTotal / 2))),
690
- h("span", { className: "lc-axis-bot" }, "0")
691
- ),
692
- h(
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(
693
649
  "div",
694
650
  {
695
- className: "lc-chart-scroll",
696
- ref: scrollRef,
697
- onScroll: (e) => {
698
- updateEdges(e.currentTarget);
651
+ className: "lc-chart",
652
+ onMouseLeave: () => {
653
+ props.onHover(null);
699
654
  }
700
655
  },
701
- // Edge fades: visible whenever more history sits beyond the viewport,
702
- // so the horizontal scroll affordance is obvious.
703
- edges.left ? h("div", { className: "lc-chart-fade lc-chart-fade-l" }) : null,
704
- // Hovering a bar previews it in the detail below; leaving the plot
705
- // clears the preview (a pinned selection, if any, takes over again).
706
- h(
707
- "div",
708
- { className: "lc-chart", onMouseLeave: () => {
709
- props.onHover(null);
710
- } },
711
- h("div", { className: "lc-grid lc-grid-top" }),
712
- h("div", { className: "lc-grid lc-grid-mid" }),
713
- requests.map((req, i) => {
714
- const marker = markers[i];
715
- const markerAt = marker !== void 0 ? eventAt(marker) : null;
716
- const selected = props.selectedSeq === req.seq;
717
- const hovered = props.hoveredSeq === req.seq;
718
- const inTurn = props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn;
719
- const tip = (req.stepCount !== void 0 && req.stepCount > 1 ? tr("tip.turn", { t: req.turn ?? 0, n: req.stepCount }) : tr("tip.step", { t: req.turn ?? 0, s: req.step ?? 0 })) + " \xB7 " + fmtTime2(req.time) + "\n" + tr("tip.total", { n: fmt3(req.total) }) + (req.prompt !== void 0 ? tr("tip.actual", { n: fmt3(req.prompt) }) : "") + "\n" + CATS.map((c) => catLabel(c.key) + " " + fmt3(req[c.key] || 0)).join(" / ");
720
- return h(
721
- "div",
722
- {
723
- key: req.seq,
724
- // Uniform column width in BOTH granularities: turn aggregates
725
- // keep the same fixed width as step bars.
726
- className: "lc-bar" + (selected ? " lc-bar-selected" : "") + (hovered ? " lc-bar-hovered" : "") + (inTurn ? " lc-bar-in-turn" : ""),
727
- style: { width: BAR_W + "px" },
728
- title: tip,
729
- onClick: () => {
730
- props.onSelect(selected ? null : req.seq);
731
- },
732
- onMouseEnter: () => {
733
- props.onHover(req.seq);
734
- }
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);
735
672
  },
736
- // The tooltip names the event AND where it happened: the
737
- // gap between the request before and the request after.
738
- marker !== void 0 ? h("span", {
673
+ onMouseEnter: () => {
674
+ props.onHover(req.seq);
675
+ }
676
+ },
677
+ marker !== void 0 ? /* @__PURE__ */ React.createElement(
678
+ "span",
679
+ {
739
680
  className: "lc-bar-marker",
740
681
  title: "\u2702 " + (markerAt !== null ? markerAt + " \u2014 " : "") + eventLabel(marker)
741
- }, "\u2702") : null,
742
- h(
743
- "div",
744
- { className: "lc-bar-stack" },
745
- CATS.map((c) => {
746
- const v = req[c.key] || 0;
747
- if (!v) return null;
748
- return h("div", { key: c.key, style: { height: Math.max(1, Math.round(v / maxTotal * CHART_H)) + "px", background: c.color } });
749
- })
750
- )
751
- );
752
- })
753
- ),
754
- // Turn strip: one COLOR BLOCK per turn, spanning exactly its bars'
755
- // columns, so the partition reads at a glance and lines up with the
756
- // steps above. Hovering a block highlights that turn's bars in the
757
- // chart (and hovering a bar highlights its block — the active turn
758
- // is shared hover-only state).
759
- h(
760
- "div",
761
- { className: "lc-turns", onMouseLeave: () => {
762
- props.onHoverTurn(null);
763
- } },
764
- groups.map((grp, gi) => {
765
- const on = props.activeTurn === grp.turn;
766
- const blockW = grp.agg ? BAR_W : grp.span * (BAR_W + BAR_GAP) - BAR_GAP;
767
- return h("span", {
768
- key: "turn-" + gi,
769
- className: "lc-turn" + (on ? " lc-turn-on" : ""),
770
- style: {
771
- width: blockW + "px",
772
- background: TURN_COLORS[gi % TURN_COLORS.length]
773
682
  },
774
- title: "T" + grp.turn,
775
- onMouseEnter: () => {
776
- props.onHoverTurn(grp.turn);
777
- }
778
- }, "T" + grp.turn);
779
- })
780
- )
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
+ })
781
692
  ),
782
- edges.right ? h("div", { className: "lc-chart-fade lc-chart-fade-r" }) : null
783
- );
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);
784
724
  };
785
725
  }
786
726
 
787
- // src/client/components/contextView.ts
727
+ // src/client/components/contextView.tsx
728
+ var viewScroll = /* @__PURE__ */ new Map();
788
729
  function makeContextView(ctx, kit) {
789
730
  const { t, tr, fmt: fmt3, fmtTime: fmtTime2, catLabel } = kit;
790
731
  const StackedBar = makeStackedBar(kit);
@@ -796,51 +737,31 @@ function makeContextView(ctx, kit) {
796
737
  const StatsBoard = makeStatsBoard(kit);
797
738
  return function ContextView(props) {
798
739
  const sessionId = props.sessionId;
799
- const initial = typeof sessionId === "string" && sessionId !== "" ? cacheGet(sessionId) ?? null : null;
800
- const [data, setData] = React.useState(initial);
801
- const [error, setError] = React.useState(null);
740
+ const data = typeof props.useProjection === "function" ? timelineOf(props.useProjection("contextTimeline")) : null;
802
741
  const [selectedSeq, setSelectedSeq] = React.useState(null);
803
742
  const [hoveredSeq, setHoveredSeq] = React.useState(null);
804
743
  const [hoverTurn, setHoverTurn] = React.useState(null);
805
744
  const [tick, setTick] = React.useState(0);
806
745
  const [granularity, setGranularity] = React.useState("step");
807
746
  const [hoverCat, setHoverCat] = React.useState(null);
808
- const dataRef = React.useRef(initial);
809
- React.useEffect(() => {
810
- dataRef.current = data;
811
- }, [data]);
812
- React.useEffect(() => {
813
- if (typeof sessionId !== "string" || sessionId === "") return void 0;
814
- let alive = true;
815
- const load = () => {
816
- ctx.connection.rpc.call("/dsh-context", "snapshot", { sessionId }).then((res) => {
817
- if (!alive) return;
818
- if (res && res.ok) {
819
- const snap = res.value;
820
- cachePut(sessionId, snap);
821
- setData(snap);
822
- setError(null);
823
- } else if (dataRef.current === null) {
824
- setError(res && res.error ? String(res.error.message || res.error.code) : "failed");
825
- }
826
- }, (err) => {
827
- if (alive && dataRef.current === null) {
828
- setError(String(err instanceof Error ? err.message : err));
829
- }
830
- });
831
- };
832
- load();
833
- const timerId = setInterval(() => {
834
- if (document.visibilityState !== "hidden") load();
835
- }, 2e3);
836
- const onVisible = () => {
837
- if (document.visibilityState === "visible") load();
838
- };
839
- document.addEventListener("visibilitychange", onVisible);
747
+ const rootRef = React.useRef(null);
748
+ const scrollerRef = React.useRef(null);
749
+ const restoredRef = React.useRef(null);
750
+ React.useLayoutEffect(() => {
751
+ if (typeof sessionId !== "string" || sessionId === "" || data === null) return;
752
+ if (restoredRef.current === sessionId) return;
753
+ restoredRef.current = sessionId;
754
+ const scroller = rootRef.current !== null ? rootRef.current.closest("[data-conversation-scroll]") : null;
755
+ if (scroller === null) return;
756
+ scrollerRef.current = scroller;
757
+ scroller.scrollTop = viewScroll.get(sessionId) ?? 0;
758
+ }, [sessionId, data]);
759
+ React.useLayoutEffect(() => {
840
760
  return () => {
841
- alive = false;
842
- clearInterval(timerId);
843
- document.removeEventListener("visibilitychange", onVisible);
761
+ if (typeof sessionId !== "string" || sessionId === "") return;
762
+ const scroller = scrollerRef.current;
763
+ if (scroller === null) return;
764
+ viewScroll.set(sessionId, scroller.scrollTop);
844
765
  };
845
766
  }, [sessionId]);
846
767
  React.useEffect(() => {
@@ -849,11 +770,8 @@ function makeContextView(ctx, kit) {
849
770
  return localeSvc.subscribe(() => setTick((x) => x + 1));
850
771
  }, []);
851
772
  void tick;
852
- if (error) {
853
- return h("div", { className: "lc-root" }, h("div", { className: "lc-empty" }, t("error") + error));
854
- }
855
773
  if (!data) {
856
- return h("div", { className: "lc-root" }, h("div", { className: "lc-empty" }, t("loading")));
774
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-root", ref: rootRef }, /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("loading")));
857
775
  }
858
776
  const current = data.current;
859
777
  const requests = data.requests || [];
@@ -880,125 +798,50 @@ function makeContextView(ctx, kit) {
880
798
  break;
881
799
  }
882
800
  }
883
- const windowPct = data.contextWindow ? Math.min(100, Math.round(current.total / data.contextWindow * 100)) : null;
884
- return h(
885
- "div",
886
- { className: "lc-root" },
887
- // ---- session context stats (over the retained window) ----
888
- h(StatsBoard, { requests, events }),
889
- // ---- overview ----
890
- h(
891
- "div",
892
- { className: "lc-card" },
893
- h(
894
- "div",
895
- { className: "lc-card-title" },
896
- t("overview.title"),
897
- h(
898
- "span",
899
- { className: "lc-card-sub" },
900
- (data.model ? data.model : "") + (data.provider ? " \xB7 " + data.provider : "")
901
- )
902
- ),
903
- h(
904
- "div",
905
- { className: "lc-overview-num" },
906
- h("b", null, fmt3(current.total)),
907
- h("span", null, data.contextWindow ? " / " + fmt3(data.contextWindow) + " " + tr("overview.ofWindow", { p: windowPct ?? 0 }) : " " + t("overview.estimate")),
908
- // The provider-reported prompt of the last request is the best
909
- // ground truth for what the model actually received; the fixed
910
- // density heuristic can undercount CJK-heavy content, so show the
911
- // real number alongside the estimate.
912
- displayRequests.length > 0 && displayRequests[displayRequests.length - 1].prompt !== void 0 ? h(
913
- "span",
914
- { className: "lc-actual" },
915
- tr("overview.actual", { n: fmt3(displayRequests[displayRequests.length - 1].prompt ?? 0) })
916
- ) : null
917
- ),
918
- h(StackedBar, { parts: partsOf(current), height: 16, max: data.contextWindow, hoverKey: hoverCat, onHoverKey: setHoverCat }),
919
- h(Legend, { parts: partsOf(current), hoverKey: hoverCat, onHoverKey: setHoverCat }),
920
- data.toolList && data.toolList.length > 0 ? h(
921
- "div",
922
- { className: "lc-tools" },
923
- t("tools.top"),
924
- data.toolList.slice().sort((a, b) => b.tokens - a.tokens).slice(0, 5).map((tool) => {
925
- return h("span", { key: tool.name, className: "lc-tool-chip" }, tool.name + " " + fmt3(tool.tokens));
926
- }),
927
- data.toolList.length > 5 ? h("span", { className: "lc-card-sub" }, " " + tr("tools.more", { n: data.toolList.length })) : null
928
- ) : null
929
- ),
930
- // ---- trend ----
931
- h(
932
- "div",
933
- { className: "lc-card" },
934
- h(
935
- "div",
936
- { className: "lc-card-title" },
937
- t("trend.title"),
938
- h("span", { className: "lc-card-sub" }, t("trend.hint")),
939
- h(
940
- "div",
941
- { className: "lc-gran" },
942
- h("button", {
943
- className: "lc-gran-btn" + (granularity === "step" ? " lc-gran-on" : ""),
944
- onClick: () => {
945
- setGranularity("step");
946
- }
947
- }, t("gran.step")),
948
- h("button", {
949
- className: "lc-gran-btn" + (granularity === "turn" ? " lc-gran-on" : ""),
950
- onClick: () => {
951
- setGranularity("turn");
952
- }
953
- }, t("gran.turn"))
954
- )
955
- ),
956
- displayRequests.length === 0 ? h("div", { className: "lc-empty" }, t("trend.empty")) : h(
957
- "div",
958
- null,
959
- h(TrendChart, {
960
- // Remount per session: switching sessions re-anchors the chart
961
- // at the newest bars instead of inheriting stale scroll state.
962
- key: sessionId,
963
- // The host caps the log at 160 requests; render them ALL so
964
- // earlier turns/steps stay reachable via horizontal scroll.
965
- requests: displayRequests,
966
- markers,
967
- selectedSeq: pinnedReq ? pinnedReq.seq : null,
968
- hoveredSeq,
969
- activeTurn,
970
- granularity,
971
- onSelect: setSelectedSeq,
972
- onHover: setHoveredSeq,
973
- onHoverTurn: setHoverTurn
974
- }),
975
- h(RequestDetail, { request: activeReq, marker: activeReq !== null ? markerOf(activeReq) : void 0 })
976
- )
977
- ),
978
- // ---- events + messages ----
979
- h(
980
- "div",
981
- { className: "lc-cols" },
982
- h(
983
- "div",
984
- { className: "lc-card lc-col" },
985
- h("div", { className: "lc-card-title" }, t("events.title")),
986
- h(EventList, { events })
987
- ),
988
- h(
989
- "div",
990
- { className: "lc-card lc-col" },
991
- h(
992
- "div",
993
- { className: "lc-card-title" },
994
- t("nodes.title"),
995
- h("span", { className: "lc-card-sub" }, t("nodes.hint"))
996
- ),
997
- h(NodeList, { nodes, dropped: data.droppedNodes || 0 })
998
- )
999
- ),
1000
- h("div", { className: "lc-foot" }, t("footer"))
1001
- );
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) => {
811
+ return /* @__PURE__ */ React.createElement("span", { key: tool.name, className: "lc-tool-chip" }, tool.name + " " + fmt3(tool.tokens));
812
+ }), 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
+ "button",
814
+ {
815
+ className: "lc-gran-btn" + (granularity === "step" ? " lc-gran-on" : ""),
816
+ onClick: () => {
817
+ setGranularity("step");
818
+ }
819
+ },
820
+ t("gran.step")
821
+ ), /* @__PURE__ */ React.createElement(
822
+ "button",
823
+ {
824
+ className: "lc-gran-btn" + (granularity === "turn" ? " lc-gran-on" : ""),
825
+ onClick: () => {
826
+ setGranularity("turn");
827
+ }
828
+ },
829
+ t("gran.turn")
830
+ ))), displayRequests.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("trend.empty")) : /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(
831
+ TrendChart,
832
+ {
833
+ key: sessionId,
834
+ requests: displayRequests,
835
+ markers,
836
+ selectedSeq: pinnedReq ? pinnedReq.seq : null,
837
+ hoveredSeq,
838
+ activeTurn,
839
+ granularity,
840
+ onSelect: setSelectedSeq,
841
+ onHover: setHoveredSeq,
842
+ onHoverTurn: setHoverTurn
843
+ }
844
+ ), /* @__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")));
1002
845
  };
1003
846
  }
1004
847
 
@@ -1017,11 +860,12 @@ function makeViewKit(t) {
1017
860
  }
1018
861
 
1019
862
  // src/client/index.ts
863
+ var NS = "dsh-context";
1020
864
  function apply(ctx) {
1021
865
  ctx.effect(() => {
1022
- return ctx.locale.register("dsh-context", { zh: DICT_ZH, en: DICT_EN });
866
+ return ctx.locale.register(NS, { zh: DICT_ZH, en: DICT_EN });
1023
867
  }, "dsh-context: dictionaries");
1024
- const t = ctx.locale.bind("dsh-context");
868
+ const t = ctx.locale.bind(NS);
1025
869
  ctx.effect(() => {
1026
870
  const tag = document.createElement("style");
1027
871
  tag.setAttribute("data-plugin", "dsh-context");
@@ -1034,15 +878,16 @@ function apply(ctx) {
1034
878
  const ContextView = makeContextView(ctx, makeViewKit(t));
1035
879
  ctx.slots.inject("conversation.view", () => {
1036
880
  return ctx.slots.register(
1037
- // order 20 renders right of Chat (0) and Trajectory (10).
1038
- { name: "conversation.view", id: "context", order: 20, label: () => t("tab") },
881
+ // order 20 renders right of Chat (0) and Trajectory (10); the locale
882
+ // namespace put the framework `t` seat on the component's props too.
883
+ { name: "conversation.view", id: "context", order: 20, locale: NS, label: () => t("tab") },
1039
884
  (props) => h(ContextView, props)
1040
885
  );
1041
886
  });
1042
887
  }
1043
888
  module.exports = {
1044
889
  name: "dsh-context",
1045
- inject: ["connection", "slots", "locale"],
890
+ inject: ["slots", "locale"],
1046
891
  apply
1047
892
  };
1048
893