dsh-context 0.16.0 → 0.17.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 (3) hide show
  1. package/README.md +3 -1
  2. package/lib/client.js +893 -815
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -20,6 +20,7 @@ var DICT_ZH = {
20
20
  "overview.free": "\u5269\u4F59\u7A97\u53E3",
21
21
  "overview.used": "\u4E0A\u4E0B\u6587\u5DF2\u7528",
22
22
  "overview.ofUsed": "\u5360\u5DF2\u7528\u4E0A\u4E0B\u6587",
23
+ "overview.compactReserve": "\u81EA\u52A8\u538B\u7F29\u9884\u7559\uFF1A\u5360\u7528\u8FBE {pct}% \u7A97\u53E3\u65F6\u89E6\u53D1\u538B\u7F29\uFF0C\u6B64\u533A\u57DF\u4E00\u822C\u4E0D\u5B9E\u9645\u5360\u7528",
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",
@@ -27,6 +28,7 @@ var DICT_ZH = {
27
28
  "stats.injects": "\u6CE8\u5165",
28
29
  "stats.compactions": "\u538B\u7F29",
29
30
  "stats.prunes": "\u526A\u679D",
31
+ "stats.cacheHit": "\u7F13\u5B58\u547D\u4E2D",
30
32
  "plugin.title": "\u63D2\u4EF6\u4FE1\u606F",
31
33
  "plugin.hint": "The best DSH context plugin \u2B50",
32
34
  "plugin.name": "\u63D2\u4EF6",
@@ -80,7 +82,7 @@ var DICT_ZH = {
80
82
  "node.empty": "(\u7A7A\u56DE\u590D)",
81
83
  "node.nonText": "(\u975E\u6587\u672C\u6D88\u606F)",
82
84
  "node.snapshot": "\u5FEB\u7167: ",
83
- "cmd.desc": "\u67E5\u770B\u4E0A\u4E0B\u6587\u7684\u6784\u6210\u548C\u53D8\u5316",
85
+ "cmd.desc": "\u67E5\u770B\u5F53\u524D\u4E0A\u4E0B\u6587\u6784\u6210\uFF0C\u6D4F\u89C8\u5404\u6B65\u9AA4\u7EC4\u6210",
84
86
  "cmd.close": "\u5173\u95ED",
85
87
  "browser.title": "\u4E0A\u4E0B\u6587\u6D4F\u89C8\u5668",
86
88
  "browser.live": "\u5F53\u524D\uFF08\u4E0B\u4E00\u6B21\u8BF7\u6C42\uFF09",
@@ -88,6 +90,7 @@ var DICT_ZH = {
88
90
  "browser.items": "{n} \u9879",
89
91
  "browser.missingLive": "\u2026 \u53E6\u6709 {n} \u6761\u66F4\u65E9\u7684\u6D88\u606F\u4E5F\u5728\u4E0A\u4E0B\u6587\u4E2D\uFF08\u8D85\u51FA\u5C55\u793A\u7A97\u53E3\uFF09",
90
92
  "browser.approx": "\u8BE5\u6B65\u9AA4\u6D89\u53CA\u7684\u90E8\u5206\u5DF2\u79FB\u9664\u6D88\u606F\u8D85\u51FA\u4FDD\u7559\u8303\u56F4\uFF0C\u4EE5\u4E0B\u4E3A\u8FD1\u4F3C\u6784\u6210",
93
+ "browser.deltaHint": "\u5BF9\u6BD4\u4E0A\u8F6E\u672B\u6B65\u7684\u53D8\u52A8",
91
94
  "browser.noHeader": "\u6B64\u6570\u636E\u6765\u81EA\u65E7\u7248\u63D2\u4EF6\uFF1A\u4EC5\u63D0\u4F9B token \u4F30\u7B97\uFF0C\u65E0\u5B9E\u9645\u5185\u5BB9",
92
95
  "browser.noEpoch": "\u8BE5\u6B65\u9AA4\u7684\u5934\u90E8\u5185\u5BB9\uFF08\u7CFB\u7EDF\u63D0\u793A\u8BCD / \u5DE5\u5177\u5B9A\u4E49\uFF09\u4E0D\u5728\u4FDD\u7559\u8303\u56F4\u5185",
93
96
  "browser.noContent": "\u5B8C\u6574\u5185\u5BB9\u4E0D\u5728\u5F53\u524D\u52A0\u8F7D\u7684\u6D88\u606F\u7A97\u53E3\u5185\uFF08\u5728\u804A\u5929\u9875\u52A0\u8F7D\u66F4\u65E9\u5386\u53F2\u540E\u53EF\u67E5\u770B\uFF09",
@@ -115,6 +118,7 @@ var DICT_EN = {
115
118
  "overview.free": "Free Window",
116
119
  "overview.used": "of context used",
117
120
  "overview.ofUsed": "of used context",
121
+ "overview.compactReserve": "Auto-compaction reserve: auto-compaction triggers at {pct}% of the window \u2014 this area is normally kept as headroom, not actually used",
118
122
  "stats.title": "Context Stats",
119
123
  "stats.hint": "Over the retained history window (same as the History chart)",
120
124
  "stats.turns": "Turns",
@@ -122,6 +126,7 @@ var DICT_EN = {
122
126
  "stats.injects": "Injections",
123
127
  "stats.compactions": "Compactions",
124
128
  "stats.prunes": "Prunes",
129
+ "stats.cacheHit": "Cache Hits",
125
130
  "plugin.title": "Plugin Info",
126
131
  "plugin.hint": "The best DSH context plugin \u2B50",
127
132
  "plugin.name": "Plugin",
@@ -175,7 +180,7 @@ var DICT_EN = {
175
180
  "node.empty": "(empty reply)",
176
181
  "node.nonText": "(non-text message)",
177
182
  "node.snapshot": "Snapshot: ",
178
- "cmd.desc": "View context makeup and how it evolves",
183
+ "cmd.desc": "View current context makeup, browse per-step composition",
179
184
  "cmd.close": "Close",
180
185
  "browser.title": "Context Browser",
181
186
  "browser.live": "Live (Next Request)",
@@ -183,6 +188,7 @@ var DICT_EN = {
183
188
  "browser.items": "{n} Items",
184
189
  "browser.missingLive": "\u2026 {n} earlier messages are also part of the context (outside the served window)",
185
190
  "browser.approx": "Some removed messages of this step exceed retention \u2014 the makeup below is approximate",
191
+ "browser.deltaHint": "vs previous turn",
186
192
  "browser.noHeader": "Served by an older plugin build: token estimates only, no content",
187
193
  "browser.noEpoch": "The header content (System Prompt / Tool Schemas) of this step is outside retention",
188
194
  "browser.noContent": "Full content is outside the loaded message window (load older history in Chat to view)",
@@ -310,751 +316,112 @@ function asRecord(value) {
310
316
  }
311
317
  var timelineOf = (value) => asRecord(value);
312
318
  var contextPressureOf = (value) => asRecord(value);
319
+ var tokenUsageOf = (value) => asRecord(value);
313
320
  function headersOf(value) {
314
321
  const headers = asRecord(value);
315
322
  return headers !== null && Array.isArray(headers.headers) ? headers : null;
316
323
  }
317
324
 
325
+ // src/client/assemble.ts
326
+ function headerAt(headers, seq) {
327
+ if (headers === null || headers.headers.length === 0) return null;
328
+ if (seq === null) return headers.headers[headers.headers.length - 1];
329
+ for (let i = headers.headers.length - 1; i >= 0; i--) {
330
+ if (headers.headers[i].seq < seq) return headers.headers[i];
331
+ }
332
+ return null;
333
+ }
334
+ function assemble(data, headers, seq) {
335
+ const live = seq === null;
336
+ let nodes;
337
+ if (live) {
338
+ nodes = data.nodes.slice();
339
+ } else {
340
+ const picked = [];
341
+ for (const n of data.nodes) {
342
+ if (n.seq < seq) picked.push(n);
343
+ }
344
+ for (const n of data.archive || []) {
345
+ if (n.seq < seq && n.gone !== void 0 && n.gone > seq) picked.push(n);
346
+ }
347
+ nodes = picked;
348
+ }
349
+ nodes.sort((a, b) => a.seq - b.seq);
350
+ let missingLive = 0;
351
+ if (data.droppedNodes > 0) {
352
+ if (live || data.surfaceFloor !== void 0 && seq > data.surfaceFloor) {
353
+ missingLive = data.droppedNodes;
354
+ }
355
+ }
356
+ const approximate = !live && data.archiveFloor !== void 0 && seq < data.archiveFloor;
357
+ return { live, header: headerAt(headers, seq), nodes, missingLive, approximate };
358
+ }
359
+
318
360
  // src/client/react.ts
319
361
  var React = require("react");
320
362
  var h = React.createElement;
321
363
 
322
- // src/client/components/requestDetail.tsx
323
- function makeRequestDetail(kit, StackedBar) {
324
- const { t, fmt: fmt3, fmtTime: fmtTime2, catLabel, eventLabel, eventAt } = kit;
325
- return function RequestDetail(props) {
326
- const req = props.request;
327
- if (!req) return null;
328
- const isTurn = req.stepCount !== void 0 && req.stepCount > 1;
329
- const head = isTurn ? t("detail.turn", { t: req.turn ?? 0, n: req.stepCount ?? 0 }) : t("detail.step", { t: req.turn ?? 0, s: req.step ?? 0 });
330
- const marker = props.marker ?? null;
331
- const markerAt = marker !== null ? eventAt(marker) : null;
332
- 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, t("detail.estTotal", { n: fmt3(req.total) })), req.prompt !== void 0 ? /* @__PURE__ */ React.createElement("span", { className: "lc-actual" }, t("detail.actual", { n: fmt3(req.prompt) })) : null, req.output !== void 0 ? /* @__PURE__ */ React.createElement("span", null, t("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) => {
333
- const v = req[c.key] || 0;
334
- 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) + "%" : ""));
335
- })));
336
- };
337
- }
338
-
339
- // src/client/components/stackedBar.tsx
340
- function makeStackedBar(kit) {
341
- const { t, fmt: fmt3, catLabel } = kit;
342
- return function StackedBar(props) {
343
- let total = 0;
344
- for (const p of props.parts) total += p.value;
345
- const scale = props.max !== void 0 && props.max > total ? props.max : total;
346
- const free = props.max !== void 0 && props.max > total ? props.max - total : 0;
347
- const usedPct = scale > 0 ? total / scale * 100 : 0;
348
- const hovering = props.hoverKey !== null && props.hoverKey !== void 0;
349
- const showBox = free > 0 && hovering;
350
- let tip = null;
351
- if (props.hoverKey !== null && props.hoverKey !== void 0) {
352
- if (props.hoverKey === "free" && free > 0) {
353
- const pct = scale > 0 ? free / scale * 100 : 0;
354
- tip = {
355
- text: t("overview.free") + " " + fmt3(free) + " (" + Math.round(pct) + "%)",
356
- leftPct: Math.max(12, Math.min(total / scale * 100 + pct / 2, 88))
357
- };
358
- } else {
359
- let acc = 0;
360
- for (const p of props.parts) {
361
- const pct = scale > 0 ? p.value / scale * 100 : 0;
362
- if (p.key === props.hoverKey && p.value > 0) {
363
- tip = {
364
- // "(pct%)" is a share of the OCCUPIED total — the dashed box
365
- // that appears on hover frames exactly this reference region.
366
- text: catLabel(p.key) + " \u2248" + fmt3(p.value) + " (" + Math.round(p.value / total * 100) + "%) " + t("overview.ofUsed"),
367
- leftPct: Math.max(12, Math.min(acc + pct / 2, 88))
368
- };
369
- break;
370
- }
371
- acc += pct;
372
- }
373
- }
364
+ // src/client/components/nodes.tsx
365
+ function makeNodeText(kit) {
366
+ const { t } = kit;
367
+ return function nodeText(n) {
368
+ if (n.cat === "tool") {
369
+ return t("node.toolResult") + (n.tool ? " \u2190 " + n.tool : "") + (n.err ? " \u26A0" : "");
374
370
  }
375
- return /* @__PURE__ */ React.createElement("div", { className: "lc-stacked-wrap" }, /* @__PURE__ */ React.createElement(
376
- "div",
377
- {
378
- className: "lc-stacked" + (hovering ? " lc-stacked-dim" : ""),
379
- style: { height: (props.height || 14) + "px" },
380
- onMouseLeave: () => {
381
- if (props.onHoverKey !== void 0) props.onHoverKey(null);
382
- }
383
- },
384
- total > 0 ? props.parts.map((p) => {
385
- if (!p.value) return null;
386
- const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
387
- return /* @__PURE__ */ React.createElement(
388
- "div",
389
- {
390
- key: p.key,
391
- className: "lc-stacked-seg" + (on ? " lc-stacked-seg-on" : ""),
392
- style: { width: p.value / scale * 100 + "%", background: p.color },
393
- onMouseEnter: () => {
394
- if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
395
- }
396
- }
397
- );
398
- }) : null,
399
- free > 0 ? /* @__PURE__ */ React.createElement(
400
- "div",
401
- {
402
- key: "free",
403
- className: "lc-stacked-free" + (props.hoverKey === "free" ? " lc-stacked-free-on" : ""),
404
- style: { width: free / scale * 100 + "%" },
405
- onMouseEnter: () => {
406
- if (props.onHoverKey !== void 0) props.onHoverKey("free");
407
- }
408
- }
409
- ) : null,
410
- /* @__PURE__ */ React.createElement(
411
- "div",
412
- {
413
- className: "lc-occupied-box" + (showBox ? " lc-occupied-box-on" : ""),
414
- style: { width: usedPct + "%" }
415
- }
416
- )
417
- ), props.tip !== false ? /* @__PURE__ */ React.createElement(
418
- "div",
419
- {
420
- className: "lc-bar-tip" + (tip ? " lc-bar-tip-on" : ""),
421
- style: { left: tip ? tip.leftPct + "%" : "50%" }
422
- },
423
- tip ? tip.text : ""
424
- ) : null);
371
+ if (n.skill) return "Skill: " + n.skill;
372
+ if (n.calls) return t("node.calls") + n.calls.join(", ");
373
+ if (n.text) return n.form === "snapshot" ? t("node.snapshot") + n.text : n.text;
374
+ if (n.cat === "assistant") return t("node.empty");
375
+ if (n.cat === "inject") return t("form." + (n.form || "context"));
376
+ return t("node.nonText");
425
377
  };
426
378
  }
427
- function makeLegend(kit) {
428
- const { t, fmt: fmt3, catLabel } = kit;
429
- return function Legend(props) {
430
- let total = 0;
431
- for (const p of props.parts) total += p.value;
432
- return /* @__PURE__ */ React.createElement("div", { className: "lc-legend" }, props.parts.map((p) => {
433
- const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
434
- return /* @__PURE__ */ React.createElement(
435
- "span",
436
- {
437
- key: p.key,
438
- className: "lc-chip" + (on ? " lc-chip-on" : ""),
439
- title: t("overview.ofUsed"),
440
- onMouseEnter: () => {
441
- if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
442
- },
443
- onMouseLeave: () => {
444
- if (props.onHoverKey !== void 0) props.onHoverKey(null);
445
- }
446
- },
447
- /* @__PURE__ */ React.createElement("i", { style: { background: p.color } }),
448
- catLabel(p.key) + " \u2248" + fmt3(p.value),
449
- total > 0 ? /* @__PURE__ */ React.createElement("em", null, Math.round(p.value / total * 100) + "%") : null
450
- );
379
+ function makeNodeList(kit) {
380
+ const { t, fmt: fmt3, fmtTime: fmtTime2 } = kit;
381
+ const nodeText = makeNodeText(kit);
382
+ return function NodeList(props) {
383
+ if (props.nodes.length === 0) {
384
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("nodes.empty"));
385
+ }
386
+ const catColor = {};
387
+ for (const c of CATS) catColor[c.key] = c.color;
388
+ const rows = props.nodes.slice().reverse();
389
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-nodes" }, props.dropped > 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-nodes-more" }, t("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)));
451
392
  }));
452
393
  };
453
394
  }
454
395
 
455
- // src/client/components/trendChart.tsx
456
- function aggregateByTurn(requests) {
457
- const out = [];
458
- let runSteps = 0;
459
- for (const req of requests) {
460
- const last = out.length > 0 ? out[out.length - 1] : null;
461
- if (last !== null && (last.turn ?? 0) === (req.turn ?? 0)) {
462
- runSteps++;
463
- out[out.length - 1] = { ...req, stepCount: runSteps };
464
- } else {
465
- runSteps = 1;
466
- out.push({ ...req, stepCount: 1 });
467
- }
396
+ // src/client/components/browser.tsx
397
+ function unionTypesOf(p) {
398
+ const branches = [];
399
+ if (Array.isArray(p.anyOf)) branches.push(...p.anyOf);
400
+ if (Array.isArray(p.oneOf)) branches.push(...p.oneOf);
401
+ if (branches.length === 0) return null;
402
+ const parts = [];
403
+ for (const b of branches) {
404
+ if (b !== null && typeof b === "object") parts.push(typeOf(b));
468
405
  }
469
- return out;
406
+ return parts.length > 0 ? parts.join(" | ") : null;
470
407
  }
471
- function attachMarkers(requests, events) {
472
- const markers = new Array(requests.length);
473
- for (const ev of events) {
474
- if (ev.kind !== "compaction" && ev.kind !== "prune") continue;
475
- for (let r = 0; r < requests.length; r++) {
476
- if (requests[r].seq >= ev.seq) {
477
- if (markers[r] === void 0) markers[r] = ev;
478
- break;
479
- }
408
+ function typeOf(p) {
409
+ const u = unionTypesOf(p);
410
+ if (u !== null) return u;
411
+ const t = p.type;
412
+ if (t === "array") {
413
+ const items = p.items;
414
+ if (items !== null && typeof items === "object") {
415
+ const inner = typeOf(items);
416
+ return "array<" + inner + ">";
480
417
  }
418
+ return "array";
481
419
  }
482
- return markers;
483
- }
484
- function makeTrendChart(kit) {
485
- const { t, fmt: fmt3, fmtTime: fmtTime2, catLabel, eventLabel, eventAt } = kit;
486
- const CHART_H = 112;
487
- const BAR_W = 14;
488
- const BAR_GAP = 2;
489
- const TURN_FILLS = ["rgba(128,128,128,0.12)", "rgba(128,128,128,0.26)"];
490
- return function TrendChart(props) {
491
- const requests = props.requests;
492
- const markers = props.markers;
493
- const anchorOf = (req) => typeof req.prompt === "number" && req.prompt > 0 && req.total > 0 ? req.prompt / req.total : 1;
494
- const barTotalOf = (req) => typeof req.prompt === "number" && req.prompt > 0 ? req.prompt : req.total;
495
- let maxTotal = 1;
496
- for (const req of requests) {
497
- const bt = barTotalOf(req);
498
- if (bt > maxTotal) maxTotal = bt;
499
- }
500
- const groups = [];
501
- for (const req of requests) {
502
- let grp = groups.length > 0 ? groups[groups.length - 1] : null;
503
- if (grp === null || grp.turn !== (req.turn ?? 0)) {
504
- grp = { turn: req.turn ?? 0, count: 0, span: 0, agg: req.stepCount !== void 0 };
505
- groups.push(grp);
506
- }
507
- grp.count++;
508
- grp.span += req.stepCount ?? 1;
509
- }
510
- const scrollRef = React.useRef(null);
511
- const scrolledOnce = React.useRef(false);
512
- const lastGranRef = React.useRef(props.granularity);
513
- const [edges, setEdges] = React.useState({ left: false, right: false });
514
- const edgesRef = React.useRef(edges);
515
- const updateEdges = (el) => {
516
- const left = el.scrollLeft > 4;
517
- const right = el.scrollLeft + el.clientWidth < el.scrollWidth - 4;
518
- const prev = edgesRef.current;
519
- if (prev.left === left && prev.right === right) return;
520
- edgesRef.current = { left, right };
521
- setEdges({ left, right });
522
- };
523
- React.useLayoutEffect(() => {
524
- const el = scrollRef.current;
525
- if (el === null) return;
526
- if (props.granularity !== lastGranRef.current) {
527
- lastGranRef.current = props.granularity;
528
- scrolledOnce.current = false;
529
- }
530
- if (!scrolledOnce.current) {
531
- scrolledOnce.current = true;
532
- el.scrollLeft = el.scrollWidth;
533
- } else if (el.scrollLeft + el.clientWidth >= el.scrollWidth - 24) {
534
- el.scrollLeft = el.scrollWidth;
535
- }
536
- updateEdges(el);
537
- });
538
- const tipOf = (req) => {
539
- const head = req.stepCount !== void 0 && req.stepCount > 1 ? t("tip.turn", { t: req.turn ?? 0, n: req.stepCount }) : t("tip.step", { t: req.turn ?? 0, s: req.step ?? 0 });
540
- return head + " \xB7 " + fmtTime2(req.time) + " \xB7 " + t("tip.total", { n: fmt3(req.total) }) + (req.prompt !== void 0 ? " \xB7 " + t("tip.actual", { n: fmt3(req.prompt) }) : "");
541
- };
542
- const hoveredIdx = props.hoveredSeq !== null ? requests.findIndex((r) => r.seq === props.hoveredSeq) : -1;
543
- const hoveredReq = hoveredIdx >= 0 ? requests[hoveredIdx] : null;
544
- 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(
545
- "div",
546
- {
547
- className: "lc-chart-scroll" + (props.activeTurn !== null ? " lc-chart-dim" : ""),
548
- ref: scrollRef,
549
- onScroll: (e) => {
550
- updateEdges(e.currentTarget);
551
- }
552
- },
553
- edges.left ? /* @__PURE__ */ React.createElement("div", { className: "lc-chart-fade lc-chart-fade-l" }) : null,
554
- /* @__PURE__ */ React.createElement(
555
- "div",
556
- {
557
- className: "lc-chart",
558
- onMouseLeave: () => {
559
- props.onHover(null);
560
- }
561
- },
562
- /* @__PURE__ */ React.createElement("div", { className: "lc-grid lc-grid-top" }),
563
- /* @__PURE__ */ React.createElement("div", { className: "lc-grid lc-grid-mid" }),
564
- requests.map((req, i) => {
565
- const marker = markers[i];
566
- const markerAt = marker !== void 0 ? eventAt(marker) : null;
567
- const selected = props.selectedSeq === req.seq;
568
- const hovered = props.hoveredSeq === req.seq;
569
- const inTurn = props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn;
570
- return /* @__PURE__ */ React.createElement(
571
- "div",
572
- {
573
- key: req.seq,
574
- className: "lc-bar" + (selected ? " lc-bar-selected" : "") + (hovered ? " lc-bar-hovered" : "") + (inTurn ? " lc-bar-in-turn" : ""),
575
- style: { width: BAR_W + "px" },
576
- onClick: () => {
577
- props.onSelect(selected ? null : req.seq);
578
- },
579
- onMouseEnter: () => {
580
- props.onHover(req.seq);
581
- }
582
- },
583
- marker !== void 0 ? /* @__PURE__ */ React.createElement(
584
- "span",
585
- {
586
- className: "lc-bar-marker",
587
- title: "\u2702 " + (markerAt !== null ? markerAt + " \u2014 " : "") + eventLabel(marker)
588
- },
589
- "\u2702"
590
- ) : null,
591
- /* @__PURE__ */ React.createElement("div", { className: "lc-bar-stack" }, CATS.map((c) => {
592
- const v = (req[c.key] || 0) * anchorOf(req);
593
- if (!v) return null;
594
- return /* @__PURE__ */ React.createElement("div", { key: c.key, style: { height: Math.max(1, Math.round(v / maxTotal * CHART_H)) + "px", background: c.color } });
595
- }))
596
- );
597
- })
598
- ),
599
- hoveredReq !== null ? /* @__PURE__ */ React.createElement(
600
- "div",
601
- {
602
- className: "lc-chart-tip",
603
- style: { left: hoveredIdx * (BAR_W + BAR_GAP) + BAR_W / 2 + "px" }
604
- },
605
- tipOf(hoveredReq)
606
- ) : null,
607
- /* @__PURE__ */ React.createElement("div", { className: "lc-turns", onMouseLeave: () => {
608
- props.onHoverTurn(null);
609
- } }, groups.map((grp, gi) => {
610
- const on = props.activeTurn === grp.turn;
611
- const blockW = grp.agg ? BAR_W : grp.span * (BAR_W + BAR_GAP) - BAR_GAP;
612
- return /* @__PURE__ */ React.createElement(
613
- "span",
614
- {
615
- key: "turn-" + gi,
616
- className: "lc-turn" + (on ? " lc-turn-on" : ""),
617
- style: {
618
- width: blockW + "px",
619
- background: TURN_FILLS[gi % TURN_FILLS.length]
620
- },
621
- title: "T" + grp.turn,
622
- onMouseEnter: () => {
623
- props.onHoverTurn(grp.turn);
624
- }
625
- },
626
- "T" + grp.turn
627
- );
628
- }))
629
- ), edges.right ? /* @__PURE__ */ React.createElement("div", { className: "lc-chart-fade lc-chart-fade-r" }) : null);
630
- };
631
- }
632
-
633
- // src/client/components/contextModal.tsx
634
- var TREND_TURNS = 10;
635
- function makeContextModal(ctx, kit) {
636
- const { t, fmt: fmt3 } = kit;
637
- const sessions = ctx.get("sessions");
638
- const StackedBar = makeStackedBar(kit);
639
- const Legend = makeLegend(kit);
640
- const TrendChart = makeTrendChart(kit);
641
- const RequestDetail = makeRequestDetail(kit, StackedBar);
642
- return function ContextModal(props) {
643
- const sessionId = typeof props.sessionId === "string" ? props.sessionId : "";
644
- const open = typeof props.useContextModal === "function" ? props.useContextModal((s) => s) : false;
645
- const data = typeof props.useProjection === "function" ? timelineOf(props.useProjection("contextTimeline")) : null;
646
- const pressure = typeof props.useProjection === "function" ? contextPressureOf(props.useProjection("contextPressure")) : null;
647
- const [selectedSeq, setSelectedSeq] = React.useState(null);
648
- const [hoveredSeq, setHoveredSeq] = React.useState(null);
649
- const [hoverCat, setHoverCat] = React.useState(null);
650
- const close = React.useCallback(() => {
651
- if (sessionId === "") return;
652
- modalStoreOf(sessionId).set(false);
653
- const guard = takePendingConsume(sessionId);
654
- if (guard === void 0 || sessions === void 0) return;
655
- const scope = sessions.scope(sessionId);
656
- if (scope !== void 0) scope.bail(scope, "slash/input-consume-token", { guard });
657
- }, [sessionId]);
658
- React.useEffect(() => {
659
- if (!open) return void 0;
660
- const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
661
- const onKey = (ev) => {
662
- if (ev.key !== "Escape") return;
663
- ev.preventDefault();
664
- ev.stopPropagation();
665
- close();
666
- };
667
- window.addEventListener("keydown", onKey, true);
668
- return () => {
669
- window.removeEventListener("keydown", onKey, true);
670
- if (previous !== null && document.contains(previous)) previous.focus();
671
- };
672
- }, [open, close]);
673
- if (!open) return null;
674
- const requests = data !== null ? data.requests || [] : [];
675
- const events = data !== null ? data.events || [] : [];
676
- const turns = aggregateByTurn(requests).slice(-TREND_TURNS);
677
- const markers = attachMarkers(turns, events);
678
- let pinnedReq = null;
679
- for (const req of turns) if (req.seq === selectedSeq) pinnedReq = req;
680
- let activeReq = null;
681
- if (hoveredSeq !== null) {
682
- for (const req of turns) if (req.seq === hoveredSeq) activeReq = req;
683
- }
684
- if (activeReq === null) activeReq = pinnedReq;
685
- if (activeReq === null && turns.length > 0) activeReq = turns[turns.length - 1];
686
- const markerOf = (req) => {
687
- const i = turns.indexOf(req);
688
- return i >= 0 ? markers[i] : void 0;
689
- };
690
- const head = data !== null ? headlineOf(data, pressure) : null;
691
- return /* @__PURE__ */ React.createElement("div", { className: "lc-modal-backdrop", onClick: close }, /* @__PURE__ */ React.createElement("div", { className: "lc-modal-card", onClick: (ev) => {
692
- ev.stopPropagation();
693
- } }, /* @__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) + " tokens" : " " + t("overview.estimate")), head.pct !== null ? /* @__PURE__ */ React.createElement("span", { className: "lc-overview-pct" }, /* @__PURE__ */ React.createElement("b", null, head.pct + "%"), t("overview.used")) : 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" }, /* @__PURE__ */ React.createElement("span", { className: "lc-card-title-text" }, t("trend.title"))), turns.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("trend.empty")) : /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(
694
- TrendChart,
695
- {
696
- requests: turns,
697
- markers,
698
- selectedSeq: pinnedReq !== null ? pinnedReq.seq : null,
699
- hoveredSeq,
700
- activeTurn: null,
701
- granularity: "turn",
702
- onSelect: setSelectedSeq,
703
- onHover: setHoveredSeq,
704
- onHoverTurn: () => {
705
- }
706
- }
707
- ), /* @__PURE__ */ React.createElement(RequestDetail, { request: activeReq, marker: activeReq !== null ? markerOf(activeReq) : void 0 })))));
708
- };
709
- }
710
-
711
- // src/client/styles.ts
712
- var STYLES = [
713
- ".lc-root { padding: 16px 20px 32px; overflow-y: auto; height: 100%; box-sizing: border-box; color: var(--dsw-alias-label-primary); font-size: 13px; }",
714
- ".lc-card { background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; padding: 14px 16px; margin-bottom: 14px; }",
715
- ".lc-card-title { font-weight: 600; margin-bottom: 10px; display: flex; align-items: baseline; gap: 8px; }",
716
- ".lc-card-title-text { flex: none; white-space: nowrap; }",
717
- ".lc-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(92px, 1fr)); gap: 8px; }",
718
- ".lc-stat { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 8px 10px; display: flex; flex-direction: column; gap: 2px; min-width: 0; }",
719
- ".lc-stat-label { color: var(--dsw-alias-label-secondary); font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
720
- ".lc-stat-value { color: var(--dsw-alias-label-primary); font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center; }",
721
- ".lc-stat-sub { color: var(--dsw-alias-label-secondary); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
722
- ".lc-card-sub { font-weight: 400; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
723
- ".lc-gran, .lc-kinds { margin-left: auto; display: flex; gap: 2px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 1px; }",
724
- ".lc-gran-btn { border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 1; padding: 3px 8px; border-radius: 5px; cursor: pointer; font-family: inherit; }",
725
- ".lc-gran-btn:hover { color: var(--dsw-alias-label-primary); }",
726
- ".lc-gran-on, .lc-gran-on:hover { background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-foreground); }",
727
- ".lc-overview-num { display: flex; align-items: baseline; gap: 6px; margin-bottom: 8px; }",
728
- ".lc-overview-num > b { font-size: 20px; }",
729
- ".lc-overview-num span { color: var(--dsw-alias-label-secondary); }",
730
- // The used-percentage figure is the emphasis of the line: pushed to the
731
- // right edge, large and primary-colored, with a small "used" caption.
732
- ".lc-overview-pct { margin-left: auto; font-size: 11px; }",
733
- ".lc-overview-pct b { font-size: 20px; color: var(--dsw-alias-label-primary); margin-right: 4px; }",
734
- ".lc-stacked-wrap { position: relative; width: 100%; }",
735
- ".lc-stacked { display: flex; width: 100%; border-radius: 5px; overflow: hidden; background: rgba(128,128,128,0.18); position: relative; }",
736
- // Hover reference frame around the OCCUPIED region of the composition bar:
737
- // dashes from the left edge to the used/window boundary, so the legend's
738
- // "share of used" percentages visibly map to the boxed part (the free track
739
- // sits outside it). pointer-events: none keeps hover on the segments/free.
740
- // Deliberately high-contrast (label-primary) and thick so the frame reads at
741
- // a glance, and the other parts dim underneath it (`.lc-stacked-dim`).
742
- ".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: 0; box-shadow: 0 0 0 1px var(--dsw-alias-bg-layer-2); transition: opacity 120ms ease; }",
743
- ".lc-occupied-box-on { opacity: 1; }",
744
- ".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; opacity: 0; transition: opacity 120ms ease; }",
745
- ".lc-bar-tip-on { opacity: 1; }",
746
- ".lc-stacked > div { height: 100%; }",
747
- // Hover eases: the shared composition hover (dim + segment brighten, on the
748
- // overview or the mirrored browser bar) glides in and out — same 120ms
749
- // rhythm as the rest of the view.
750
- ".lc-stacked-seg { transition: filter 120ms ease, opacity 120ms ease; }",
751
- ".lc-stacked-free { transition: box-shadow 120ms ease, opacity 120ms ease; }",
752
- ".lc-stacked-seg-on { filter: brightness(1.18); }",
753
- ".lc-stacked-free-on { box-shadow: inset 0 0 0 1px var(--dsw-alias-label-secondary); border-radius: 3px; }",
754
- // Hover focus: everything except the hovered part (segment, legend chip, or
755
- // free track) recedes, so the composition highlight and the occupied-region
756
- // frame read clearly. The selected segment/free keeps full opacity.
757
- ".lc-stacked-dim .lc-stacked-seg { opacity: 0.35; }",
758
- ".lc-stacked-dim .lc-stacked-seg-on { opacity: 1; }",
759
- ".lc-stacked-dim .lc-stacked-free { opacity: 0.35; }",
760
- ".lc-stacked-dim .lc-stacked-free-on { opacity: 1; }",
761
- ".lc-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 10px; }",
762
- ".lc-chip { display: inline-flex; align-items: center; gap: 5px; color: var(--dsw-alias-label-primary); padding: 1px 6px; border-radius: 6px; transition: background-color 120ms ease; }",
763
- ".lc-chip i, .lc-detail-row i, .lc-node i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }",
764
- ".lc-chip i { transition: box-shadow 120ms ease; }",
765
- ".lc-chip em { font-style: normal; color: var(--dsw-alias-label-secondary); }",
766
- // The selected chip glows with the shared hover tint (smoothly, like the
767
- // browser rows) instead of snapping its weight; the dot ring on top marks
768
- // the exact segment.
769
- ".lc-chip-on { font-weight: 600; background: var(--dsw-alias-interactive-bg-hover); }",
770
- ".lc-chip-on i { box-shadow: 0 0 0 1px var(--dsw-alias-brand-primary); }",
771
- ".lc-tools { margin-top: 10px; color: var(--dsw-alias-label-secondary); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }",
772
- // The overview tool list links into the Context browser: the "工具定义 Top"
773
- // label, its "等 N 个" overflow link, and every tool chip are buttons, and
774
- // hover underlines them (the chip already reads as a tag, so the underline
775
- // is the only affordance).
776
- ".lc-tools-label, .lc-tools-more { border: 0; background: transparent; padding: 0; color: var(--dsw-alias-label-secondary); font-family: inherit; font-size: 12px; cursor: pointer; }",
777
- ".lc-tools-label:hover, .lc-tools-more:hover { text-decoration: underline; }",
778
- ".lc-tool-chip { background: var(--dsw-alias-bg-layer-2); border: 0; border-radius: 4px; padding: 1px 7px; font: inherit; font-size: 12px; color: var(--dsw-alias-label-primary); cursor: pointer; }",
779
- ".lc-tool-chip:hover { text-decoration: underline; }",
780
- ".lc-chartrow { display: flex; gap: 6px; align-items: stretch; }",
781
- ".lc-axis { position: relative; width: 40px; height: 150px; padding-top: 18px; box-sizing: border-box; color: var(--dsw-alias-label-secondary); font-size: 11px; }",
782
- ".lc-axis span { position: absolute; right: 0; line-height: 1; }",
783
- ".lc-axis-top { top: 13px; }",
784
- ".lc-axis-mid { top: 69px; }",
785
- ".lc-axis-bot { top: 125px; }",
786
- ".lc-chart-scroll { position: relative; flex: 1; overflow-x: auto; overflow-y: hidden; min-width: 0; scrollbar-width: thin; }",
787
- ".lc-chart-fade { position: absolute; top: 0; bottom: 0; width: 26px; pointer-events: none; z-index: 2; }",
788
- ".lc-chart-fade-l { left: 0; background: linear-gradient(to right, var(--dsw-alias-bg-layer-1), transparent); }",
789
- ".lc-chart-fade-r { right: 0; background: linear-gradient(to left, var(--dsw-alias-bg-layer-1), transparent); }",
790
- ".lc-chart { position: relative; display: flex; align-items: flex-end; gap: 2px; height: 130px; padding-top: 18px; box-sizing: border-box; width: max-content; min-width: 100%; }",
791
- ".lc-grid { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--dsw-alias-border-l1); pointer-events: none; }",
792
- ".lc-grid-top { top: 18px; }",
793
- ".lc-grid-mid { top: 74px; }",
794
- ".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, background-color 120ms ease; }",
795
- // Turn-aware dimming: while a turn is focused, bars OUTSIDE the active
796
- // turn fade to 35% and the whole current turn stays fully opaque.
797
- ".lc-chart-dim .lc-bar { opacity: 0.35; }",
798
- ".lc-chart-dim .lc-bar-in-turn { opacity: 1; }",
799
- ".lc-chart-dim .lc-turn { opacity: 0.35; }",
800
- ".lc-chart-dim .lc-turn-on { opacity: 1; }",
801
- ".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; }",
802
- ".lc-bar:hover { background: var(--dsw-alias-bg-layer-2); }",
803
- ".lc-bar-selected { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }",
804
- ".lc-bar-hovered { outline: 1px dashed var(--dsw-alias-brand-primary); outline-offset: 1px; }",
805
- ".lc-bar-in-turn { background: rgba(128,128,128,0.14); }",
806
- ".lc-bar-stack { display: flex; flex-direction: column-reverse; width: 100%; }",
807
- ".lc-bar-stack > div { width: 100%; }",
808
- ".lc-bar-marker { position: absolute; top: -16px; left: 50%; transform: translateX(-50%); font-size: 11px; color: var(--dsw-alias-state-warn-primary); }",
809
- // Turn strip: NEUTRAL zebra fills (inline, see TURN_FILLS) with secondary
810
- // text — never the category palette, or the strip reads as a detached
811
- // bottom segment of the bars above it.
812
- ".lc-turns { display: flex; gap: 2px; width: max-content; min-width: 100%; margin-top: 4px; }",
813
- ".lc-turn { flex: none; box-sizing: border-box; text-align: center; font-size: 10px; line-height: 14px; font-weight: 600; color: var(--dsw-alias-label-secondary); border-radius: 3px; height: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; transition: filter 120ms, opacity 120ms; }",
814
- ".lc-turn-on { filter: brightness(1.35); color: var(--dsw-alias-label-primary); }",
815
- ".lc-detail { margin-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); padding-top: 12px; }",
816
- ".lc-detail-head { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 8px; color: var(--dsw-alias-label-secondary); }",
817
- ".lc-detail-head b { color: var(--dsw-alias-label-primary); }",
818
- ".lc-detail-marker { color: var(--dsw-alias-state-warn-primary); font-size: 11px; background: var(--dsw-alias-bg-layer-2); border-radius: 6px; padding: 1px 7px; }",
819
- ".lc-detail-head .lc-actual { color: var(--dsw-alias-state-success-primary); }",
820
- ".lc-detail-tag { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 4px; padding: 0 6px; font-size: 11px; color: var(--dsw-alias-label-secondary); }",
821
- ".lc-detail-rows { margin-top: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px 24px; }",
822
- ".lc-detail-row { display: flex; align-items: center; gap: 8px; }",
823
- ".lc-detail-label { min-width: 70px; white-space: nowrap; color: var(--dsw-alias-label-secondary); }",
824
- ".lc-bar-track { flex: 1; height: 5px; border-radius: 3px; background: rgba(128,128,128,0.18); overflow: hidden; display: block; }",
825
- ".lc-bar-fill { display: block; height: 100%; border-radius: 3px; }",
826
- ".lc-detail-num { width: 44px; text-align: right; }",
827
- ".lc-detail-pct { width: 34px; text-align: right; color: var(--dsw-alias-label-secondary); }",
828
- // Section rows share one 14px vertical rhythm, owned by the ROW (flex gap
829
- // covers the wrapped-row axis too), never by the cards inside: a card that
830
- // IS a column (head, events/messages) drops its own bottom margin, and a
831
- // card stacked inside a wrapper column (overview + trend) keeps its margin
832
- // as the inner gap but the last one drops it — so every section boundary
833
- // measures exactly one 14px, whichever way the flex wraps.
834
- ".lc-cols { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 14px; }",
835
- ".lc-col { flex: 1; min-width: 280px; }",
836
- ".lc-cols > .lc-card { margin-bottom: 0; }",
837
- ".lc-col > .lc-card:last-child { margin-bottom: 0; }",
838
- // Head row: stats board + plugin info sit side by side under the shared
839
- // `lc-cols` flex — stats takes ~7/10 of the row, plugin info ~3/10, both
840
- // wrap onto their own line when the available width falls below each card's
841
- // min-width (so a narrow viewport keeps both readable). Both children are
842
- // `.lc-card` themselves (rendered by StatsBoard / PluginInfo); the flex
843
- // sizing lives on the head row's direct children so the existing card
844
- // chrome doesn't change.
845
- ".lc-head > .lc-card:first-child { flex: 7 1 0; min-width: 360px; }",
846
- ".lc-head > .lc-card:last-child { flex: 3 1 0; min-width: 220px; }",
847
- // Plugin info: two full-width rows (Plugin / GitHub), each one horizontal
848
- // line with the label on the left and the value on the right — a compact
849
- // definition list rather than a multi-cell grid.
850
- ".lc-pi-grid { display: grid; grid-template-columns: 1fr; gap: 8px; }",
851
- // The row IS the link: an `<a>` with the label + value on one baseline.
852
- // Hovering a row underlines its value — the only hover affordance.
853
- ".lc-pi-row { display: flex; flex-direction: row; justify-content: space-between; align-items: baseline; gap: 12px; min-width: 0; text-decoration: none; color: inherit; }",
854
- ".lc-pi-row:hover .lc-pi-value { text-decoration: underline; }",
855
- // Upgrade chip appended to the Plugin value when the npm registry has a
856
- // newer version than the baked-in one.
857
- ".lc-pi-update { color: var(--dsw-alias-state-warn-primary); font-size: 11px; margin-left: 6px; white-space: nowrap; }",
858
- ".lc-pi-label { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
859
- // min-width: 0 lets the value shrink inside the flex row and truncate with
860
- // an ellipsis; without it a narrow card pushes the value over the label.
861
- ".lc-pi-value { min-width: 0; color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: right; }",
862
- ".lc-events, .lc-nodes { display: flex; flex-direction: column; gap: 2px; max-height: 320px; overflow-y: auto; }",
863
- ".lc-event { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
864
- ".lc-event-icon { width: 18px; text-align: center; color: var(--dsw-alias-state-warn-primary); }",
865
- ".lc-event-icon.lc-event-inject { color: #a855f7; }",
866
- ".lc-event-icon.lc-event-model { color: var(--dsw-alias-brand-primary); }",
867
- // Kind chip: the event classification at a glance; the tint matches the
868
- // impact direction (inject = adds context, compaction/prune = frees it,
869
- // model switch = neutral), mirroring the token sign colors below.
870
- ".lc-kind { flex: none; font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px; white-space: nowrap; }",
871
- ".lc-kind-inject { background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent); color: var(--dsw-alias-state-success-primary); }",
872
- ".lc-kind-compaction { background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 15%, transparent); color: var(--dsw-alias-state-error-primary); }",
873
- ".lc-kind-prune { background: color-mix(in srgb, #8b5cf6 15%, transparent); color: #8b5cf6; }",
874
- ".lc-kind-model { background: color-mix(in srgb, var(--dsw-alias-brand-primary) 15%, transparent); color: var(--dsw-alias-brand-primary); }",
875
- ".lc-event-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
876
- ".lc-event-at { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; white-space: nowrap; }",
877
- ".lc-event-tokens { color: var(--dsw-alias-state-success-primary); font-weight: 600; white-space: nowrap; }",
878
- ".lc-event-tokens.lc-up { color: var(--dsw-alias-state-warn-primary); }",
879
- ".lc-event-time { color: var(--dsw-alias-label-secondary); font-size: 12px; }",
880
- ".lc-node { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
881
- ".lc-node-preview { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); }",
882
- ".lc-node-time { color: var(--dsw-alias-label-secondary); font-size: 12px; min-width: 54px; text-align: right; }",
883
- ".lc-node-tokens { color: var(--dsw-alias-label-secondary); }",
884
- ".lc-nodes-more { color: var(--dsw-alias-label-secondary); padding: 3px 0; }",
885
- ".lc-empty { color: var(--dsw-alias-label-secondary); padding: 18px 0; text-align: center; }",
886
- ".lc-foot { color: var(--dsw-alias-label-secondary); font-size: 12px; margin-top: 4px; }",
887
- // ---- /context modal (centered dialog; escapes the composer anchor via fixed positioning) ----
888
- ".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; }",
889
- ".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; }",
890
- ".lc-modal-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 12px; }",
891
- ".lc-modal-title { font-weight: 600; font-size: 14px; }",
892
- ".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; }",
893
- ".lc-modal-close:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); }",
894
- ".lc-modal-trend { margin-top: 14px; }",
895
- // ---- Context browser card (progressive disclosure: category accordion ->
896
- // element rows -> per-kind content) ----
897
- ".lc-br-pick { margin-left: auto; font: inherit; font-size: 12px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 3px 6px; max-width: 240px; }",
898
- ".lc-br-meta { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 8px; color: var(--dsw-alias-label-secondary); }",
899
- ".lc-br-meta b { color: var(--dsw-alias-label-primary); }",
900
- ".lc-br-meta .lc-actual { color: var(--dsw-alias-state-success-primary); }",
901
- ".lc-br-note { margin-top: 8px; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
902
- ".lc-br-cats { margin-top: 10px; display: flex; flex-direction: column; gap: 4px; }",
903
- ".lc-br-cat { border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; background: var(--dsw-alias-bg-layer-2); overflow: hidden; }",
904
- ".lc-br-cat-empty { opacity: 0.55; }",
905
- ".lc-br-cat-row { display: flex; align-items: center; gap: 8px; width: 100%; border: 0; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; padding: 7px 10px; cursor: pointer; text-align: left; transition: background-color 120ms ease; }",
906
- ".lc-br-cat-row:hover { background: var(--dsw-alias-interactive-bg-hover); }",
907
- // Current-composition hover echo: the category row lit by the shared hover
908
- // link (hovering the Current Composition card's bar/legend) looks hovered
909
- // too — same tint as the physical hover, plus a ring on its color dot.
910
- ".lc-br-cat-on { background: var(--dsw-alias-interactive-bg-hover); }",
911
- ".lc-br-cat-on i { box-shadow: 0 0 0 1px var(--dsw-alias-brand-primary); }",
912
- ".lc-br-cat-row i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; flex: none; transition: box-shadow 120ms ease; }",
913
- ".lc-br-cat-label { font-weight: 600; }",
914
- ".lc-br-cat-count { color: var(--dsw-alias-label-secondary); font-size: 12px; flex: 1; }",
915
- ".lc-br-chev { flex: none; width: 12px; color: var(--dsw-alias-label-secondary); transition: transform 120ms ease; }",
916
- ".lc-br-chev-on { transform: rotate(90deg); }",
917
- ".lc-br-tokens { flex: none; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
918
- ".lc-br-pct { flex: none; width: 36px; text-align: right; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
919
- ".lc-br-body { border-top: 1px solid var(--dsw-alias-border-l1); padding: 4px 6px; display: flex; flex-direction: column; gap: 2px; }",
920
- ".lc-br-elem { border-radius: 6px; }",
921
- ".lc-br-elem-on { background: var(--dsw-alias-interactive-bg-active); }",
922
- ".lc-br-elem-row { display: flex; align-items: center; gap: 8px; width: 100%; border: 0; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; padding: 5px 6px; cursor: pointer; text-align: left; border-radius: 6px; transition: background-color 120ms ease; }",
923
- ".lc-br-elem-row:hover { background: var(--dsw-alias-interactive-bg-hover); }",
924
- ".lc-br-tag { flex: none; font-size: 11px; color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); border-radius: 4px; padding: 0 6px; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
925
- ".lc-br-preview { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
926
- ".lc-br-time { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; }",
927
- ".lc-br-content { padding: 2px 6px 8px 26px; display: flex; flex-direction: column; gap: 6px; }",
928
- ".lc-br-pre { margin: 0; padding: 8px 10px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 1.55; white-space: pre-wrap; word-break: break-word; max-height: 320px; overflow-y: auto; scrollbar-width: thin; }",
929
- ".lc-br-dim { color: var(--dsw-alias-label-secondary); }",
930
- ".lc-br-call { display: flex; flex-direction: column; gap: 4px; align-items: flex-start; }",
931
- // ---- Tool schema body (browser card, tools category). The description
932
- // and parameter table share one "card with a small title" chrome — a
933
- // labeled bordered block so a reader can scan three stacked sections
934
- // without losing track of which is which. The raw JSON sits below as a
935
- // collapsed toggle (no card chrome — it's a debug view, not a primary
936
- // section). ----
937
- ".lc-ts-card { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; overflow: hidden; }",
938
- ".lc-ts-card-head { display: flex; align-items: center; gap: 8px; padding: 6px 10px; font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-secondary); border-bottom: 1px solid var(--dsw-alias-border-l1); }",
939
- ".lc-ts-card-head b { color: var(--dsw-alias-label-primary); }",
940
- ".lc-ts-card-count { margin-left: auto; }",
941
- ".lc-ts-desc-body { margin: 0; padding: 8px 10px; color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 1.55; white-space: pre-wrap; word-break: break-word; max-height: 160px; overflow-y: auto; scrollbar-width: thin; }",
942
- // Grid: name + type + required chip + description. Columns wrap on narrow
943
- // rows; the description cell spans the full second row so a long blurb
944
- // never breaks the one-line rhythm of name/type/required.
945
- ".lc-ts-param-row { display: grid; grid-template-columns: 140px 90px 56px 1fr; column-gap: 10px; row-gap: 2px; padding: 6px 10px; border-top: 1px solid var(--dsw-alias-border-l1); font-size: 12px; align-items: baseline; }",
946
- ".lc-ts-param-row:first-of-type { border-top: 0; }",
947
- ".lc-ts-param-name { font-weight: 600; color: var(--dsw-alias-label-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
948
- ".lc-ts-param-type { color: var(--dsw-alias-label-secondary); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
949
- ".lc-ts-param-req { color: var(--dsw-alias-state-warn-primary); font-size: 11px; font-weight: 600; }",
950
- ".lc-ts-param-req-off { color: var(--dsw-alias-label-secondary); font-size: 11px; }",
951
- ".lc-ts-param-desc { grid-column: 1 / -1; color: var(--dsw-alias-label-secondary); line-height: 1.5; }",
952
- ".lc-ts-params-empty { padding: 8px 10px; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
953
- // Raw JSON toggle: a slim button that owns the open/closed state, sitting
954
- // just under the parameter table. The <pre> itself only mounts when open
955
- // (avoids stringifying the schema on every render and keeps the default
956
- // body compact).
957
- ".lc-ts-json { display: flex; flex-direction: column; gap: 4px; }",
958
- ".lc-ts-json-toggle { align-self: flex-start; border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 11px; padding: 0; cursor: pointer; }",
959
- ".lc-ts-json-toggle:hover { color: var(--dsw-alias-label-primary); text-decoration: underline; }"
960
- ].join("\n");
961
-
962
- // src/client/assemble.ts
963
- function headerAt(headers, seq) {
964
- if (headers === null || headers.headers.length === 0) return null;
965
- if (seq === null) return headers.headers[headers.headers.length - 1];
966
- for (let i = headers.headers.length - 1; i >= 0; i--) {
967
- if (headers.headers[i].seq < seq) return headers.headers[i];
968
- }
969
- return null;
970
- }
971
- function assemble(data, headers, seq) {
972
- const live = seq === null;
973
- let nodes;
974
- if (live) {
975
- nodes = data.nodes.slice();
976
- } else {
977
- const picked = [];
978
- for (const n of data.nodes) {
979
- if (n.seq < seq) picked.push(n);
980
- }
981
- for (const n of data.archive || []) {
982
- if (n.seq < seq && n.gone !== void 0 && n.gone > seq) picked.push(n);
983
- }
984
- nodes = picked;
985
- }
986
- nodes.sort((a, b) => a.seq - b.seq);
987
- let missingLive = 0;
988
- if (data.droppedNodes > 0) {
989
- if (live || data.surfaceFloor !== void 0 && seq > data.surfaceFloor) {
990
- missingLive = data.droppedNodes;
991
- }
992
- }
993
- const approximate = !live && data.archiveFloor !== void 0 && seq < data.archiveFloor;
994
- return { live, header: headerAt(headers, seq), nodes, missingLive, approximate };
995
- }
996
-
997
- // src/client/components/nodes.tsx
998
- function makeNodeText(kit) {
999
- const { t } = kit;
1000
- return function nodeText(n) {
1001
- if (n.cat === "tool") {
1002
- return t("node.toolResult") + (n.tool ? " \u2190 " + n.tool : "") + (n.err ? " \u26A0" : "");
1003
- }
1004
- if (n.skill) return "Skill: " + n.skill;
1005
- if (n.calls) return t("node.calls") + n.calls.join(", ");
1006
- if (n.text) return n.form === "snapshot" ? t("node.snapshot") + n.text : n.text;
1007
- if (n.cat === "assistant") return t("node.empty");
1008
- if (n.cat === "inject") return t("form." + (n.form || "context"));
1009
- return t("node.nonText");
1010
- };
1011
- }
1012
- function makeNodeList(kit) {
1013
- const { t, fmt: fmt3, fmtTime: fmtTime2 } = kit;
1014
- const nodeText = makeNodeText(kit);
1015
- return function NodeList(props) {
1016
- if (props.nodes.length === 0) {
1017
- return /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("nodes.empty"));
1018
- }
1019
- const catColor = {};
1020
- for (const c of CATS) catColor[c.key] = c.color;
1021
- const rows = props.nodes.slice().reverse();
1022
- return /* @__PURE__ */ React.createElement("div", { className: "lc-nodes" }, props.dropped > 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-nodes-more" }, t("nodes.more", { n: props.dropped })) : null, rows.map((n) => {
1023
- const text = nodeText(n);
1024
- 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)));
1025
- }));
1026
- };
1027
- }
1028
-
1029
- // src/client/components/browser.tsx
1030
- function unionTypesOf(p) {
1031
- const branches = [];
1032
- if (Array.isArray(p.anyOf)) branches.push(...p.anyOf);
1033
- if (Array.isArray(p.oneOf)) branches.push(...p.oneOf);
1034
- if (branches.length === 0) return null;
1035
- const parts = [];
1036
- for (const b of branches) {
1037
- if (b !== null && typeof b === "object") parts.push(typeOf(b));
1038
- }
1039
- return parts.length > 0 ? parts.join(" | ") : null;
1040
- }
1041
- function typeOf(p) {
1042
- const u = unionTypesOf(p);
1043
- if (u !== null) return u;
1044
- const t = p.type;
1045
- if (t === "array") {
1046
- const items = p.items;
1047
- if (items !== null && typeof items === "object") {
1048
- const inner = typeOf(items);
1049
- return "array<" + inner + ">";
1050
- }
1051
- return "array";
1052
- }
1053
- if (typeof t === "string") {
1054
- if (t === "object") {
1055
- const props = p.properties;
1056
- if (props !== null && typeof props === "object" && Object.keys(props).length > 0) {
1057
- return "object{" + Object.keys(props).length + "}";
420
+ if (typeof t === "string") {
421
+ if (t === "object") {
422
+ const props = p.properties;
423
+ if (props !== null && typeof props === "object" && Object.keys(props).length > 0) {
424
+ return "object{" + Object.keys(props).length + "}";
1058
425
  }
1059
426
  }
1060
427
  if (Array.isArray(p.enum) && p.enum.length > 0) {
@@ -1156,6 +523,21 @@ function NodeContent(props) {
1156
523
  }
1157
524
  return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, props.hint));
1158
525
  }
526
+ function byCatOf(asm) {
527
+ var _a;
528
+ const m = {};
529
+ for (const n of asm.nodes) (m[_a = n.cat] ?? (m[_a] = [])).push(n);
530
+ return m;
531
+ }
532
+ function countOf(asm, c) {
533
+ if (c === "system") return asm.header !== null && asm.header.system !== void 0 ? 1 : 0;
534
+ if (c === "tools") return asm.header !== null ? asm.header.tools.length : 0;
535
+ return byCatOf(asm)[c]?.length ?? 0;
536
+ }
537
+ function lastOfTurn(requests, turn) {
538
+ for (let i = requests.length - 1; i >= 0; i--) if ((requests[i].turn ?? 0) === turn) return requests[i];
539
+ return null;
540
+ }
1159
541
  function makeContextBrowser(kit, StackedBar) {
1160
542
  const { t, fmt: fmt3, fmtTime: fmtTime2, catLabel } = kit;
1161
543
  const nodeText = makeNodeText(kit);
@@ -1163,7 +545,6 @@ function makeContextBrowser(kit, StackedBar) {
1163
545
  for (const c of CATS) catColor[c.key] = c.color;
1164
546
  const MAX_AUTO_PAGES = 20;
1165
547
  return function ContextBrowser(props) {
1166
- var _a;
1167
548
  const { data, headers } = props;
1168
549
  const [sel, setSel] = React.useState("live");
1169
550
  const [openCat, setOpenCat] = React.useState(null);
@@ -1228,13 +609,10 @@ function makeContextBrowser(kit, StackedBar) {
1228
609
  setOpenCat(null);
1229
610
  setOpenElem(null);
1230
611
  };
1231
- const byCat = {};
1232
- for (const n of view.nodes) (byCat[_a = n.cat] ?? (byCat[_a] = [])).push(n);
1233
- const toolCount = (c) => {
1234
- if (c === "system") return view.header !== null && view.header.system !== void 0 ? 1 : 0;
1235
- if (c === "tools") return view.header !== null ? view.header.tools.length : 0;
1236
- return byCat[c]?.length ?? 0;
1237
- };
612
+ const refReq = req === null ? requests.length > 0 ? requests[requests.length - 1] : null : lastOfTurn(requests, (req.turn ?? 0) - 1);
613
+ const prevView = refReq !== null ? assemble(data, headers, refReq.seq) : null;
614
+ const byCat = byCatOf(view);
615
+ const toolCount = (c) => countOf(view, c);
1238
616
  const toggleCat = (c) => {
1239
617
  const openable = toolCount(c) > 0 || (c === "system" || c === "tools") && view.header === null;
1240
618
  if (!openable) return;
@@ -1312,57 +690,576 @@ function makeContextBrowser(kit, StackedBar) {
1312
690
  );
1313
691
  });
1314
692
  };
1315
- return /* @__PURE__ */ React.createElement("div", { className: "lc-card" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, /* @__PURE__ */ React.createElement("span", { className: "lc-card-title-text" }, t("browser.title")), /* @__PURE__ */ React.createElement(
693
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-card" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, /* @__PURE__ */ React.createElement("span", { className: "lc-card-title-text" }, t("browser.title")), /* @__PURE__ */ React.createElement("span", { className: "lc-br-hint" }, t("browser.deltaHint")), /* @__PURE__ */ React.createElement(
1316
694
  "select",
1317
695
  {
1318
- className: "lc-br-pick",
1319
- value: seq === null ? "live" : String(seq),
1320
- onChange: (e) => {
1321
- pick(e.target.value);
1322
- }
1323
- },
1324
- /* @__PURE__ */ React.createElement("option", { value: "live" }, t("browser.live")),
1325
- requests.slice().reverse().map((r) => /* @__PURE__ */ React.createElement("option", { key: r.seq, value: String(r.seq) }, t("detail.step", { t: r.turn ?? 0, s: r.step ?? 0 }) + " \xB7 " + fmtTime2(r.time)))
1326
- )), /* @__PURE__ */ React.createElement("div", { className: "lc-br-meta" }, /* @__PURE__ */ React.createElement("b", null, req !== null ? t("detail.step", { t: req.turn ?? 0, s: req.step ?? 0 }) : t("browser.liveNow")), req !== null ? /* @__PURE__ */ React.createElement("span", null, fmtTime2(req.time)) : null, hoverReq !== null ? /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("browser.preview")) : null, /* @__PURE__ */ React.createElement("span", null, t("detail.estTotal", { n: fmt3(total) })), req !== null && req.prompt !== void 0 ? /* @__PURE__ */ React.createElement("span", { className: "lc-actual" }, t("detail.actual", { n: fmt3(req.prompt) })) : null), /* @__PURE__ */ React.createElement("div", { className: "lc-br-bar" }, /* @__PURE__ */ React.createElement(
1327
- StackedBar,
696
+ className: "lc-br-pick",
697
+ value: seq === null ? "live" : String(seq),
698
+ onChange: (e) => {
699
+ pick(e.target.value);
700
+ }
701
+ },
702
+ /* @__PURE__ */ React.createElement("option", { value: "live" }, t("browser.live")),
703
+ requests.slice().reverse().map((r) => /* @__PURE__ */ React.createElement("option", { key: r.seq, value: String(r.seq) }, t("detail.step", { t: r.turn ?? 0, s: r.step ?? 0 }) + " \xB7 " + fmtTime2(r.time)))
704
+ )), /* @__PURE__ */ React.createElement("div", { className: "lc-br-meta" }, /* @__PURE__ */ React.createElement("b", null, req !== null ? t("detail.step", { t: req.turn ?? 0, s: req.step ?? 0 }) : t("browser.liveNow")), req !== null ? /* @__PURE__ */ React.createElement("span", null, fmtTime2(req.time)) : null, hoverReq !== null ? /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("browser.preview")) : null, /* @__PURE__ */ React.createElement("span", null, t("detail.estTotal", { n: fmt3(total) })), req !== null && req.prompt !== void 0 ? /* @__PURE__ */ React.createElement("span", { className: "lc-actual" }, t("detail.actual", { n: fmt3(req.prompt) })) : null), /* @__PURE__ */ React.createElement("div", { className: "lc-br-bar" }, /* @__PURE__ */ React.createElement(
705
+ StackedBar,
706
+ {
707
+ parts,
708
+ height: 10,
709
+ hoverKey: linked ? linkKey : void 0,
710
+ onHoverKey: linked ? props.onHoverKey : void 0,
711
+ tip: false
712
+ }
713
+ )), view.missingLive > 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, t("browser.missingLive", { n: view.missingLive })) : null, view.approximate ? /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, t("browser.approx")) : null, /* @__PURE__ */ React.createElement("div", { className: "lc-br-cats" }, CATS.map((c) => {
714
+ const count = toolCount(c.key);
715
+ const v = breakdown[c.key] || 0;
716
+ const prevCount = prevView !== null ? countOf(prevView, c.key) : null;
717
+ const countDelta = prevCount !== null ? count - prevCount : null;
718
+ const prevTokens = refReq !== null ? refReq[c.key] || 0 : null;
719
+ const tokenDelta = prevTokens !== null ? v - prevTokens : null;
720
+ const openable = count > 0 || (c.key === "system" || c.key === "tools") && view.header === null;
721
+ const open = openCat === c.key && openable;
722
+ return /* @__PURE__ */ React.createElement("div", { key: c.key, className: "lc-br-cat" + (openable ? "" : " lc-br-cat-empty") }, /* @__PURE__ */ React.createElement(
723
+ "button",
724
+ {
725
+ type: "button",
726
+ className: "lc-br-cat-row" + (linked && props.hoverKey === c.key ? " lc-br-cat-on" : ""),
727
+ onMouseEnter: linked ? () => {
728
+ if (props.onHoverKey !== void 0) props.onHoverKey(c.key);
729
+ } : void 0,
730
+ onMouseLeave: linked ? () => {
731
+ if (props.onHoverKey !== void 0) props.onHoverKey(null);
732
+ } : void 0,
733
+ onClick: () => {
734
+ toggleCat(c.key);
735
+ }
736
+ },
737
+ /* @__PURE__ */ React.createElement("span", { className: "lc-br-chev" + (open ? " lc-br-chev-on" : "") }, "\u25B8"),
738
+ /* @__PURE__ */ React.createElement("i", { style: { background: c.color } }),
739
+ /* @__PURE__ */ React.createElement("span", { className: "lc-br-cat-label" }, catLabel(c.key)),
740
+ /* @__PURE__ */ React.createElement("span", { className: "lc-br-count-grp" }, /* @__PURE__ */ React.createElement("span", { className: "lc-br-cat-count" }, t("browser.items", { n: count })), countDelta !== null && countDelta !== 0 ? /* @__PURE__ */ React.createElement("span", { className: "lc-br-delta lc-br-delta-" + (countDelta > 0 ? "up" : "down") }, (countDelta > 0 ? "+" : "") + countDelta) : null),
741
+ /* @__PURE__ */ React.createElement("span", { className: "lc-br-tokens-grp" }, tokenDelta !== null && tokenDelta !== 0 ? /* @__PURE__ */ React.createElement("span", { className: "lc-br-tdelta lc-br-tdelta-" + (tokenDelta > 0 ? "up" : "down") }, (tokenDelta > 0 ? "+" : "") + fmt3(tokenDelta)) : null, /* @__PURE__ */ React.createElement("span", { className: "lc-br-tokens" }, "\u2248" + fmt3(v))),
742
+ /* @__PURE__ */ React.createElement("span", { className: "lc-br-pct" }, total > 0 ? Math.round(v / total * 100) + "%" : "")
743
+ ), open ? /* @__PURE__ */ React.createElement("div", { className: "lc-br-body" }, catBody(c.key)) : null);
744
+ })));
745
+ };
746
+ }
747
+
748
+ // src/client/components/stackedBar.tsx
749
+ var AUTO_COMPACT_RATIO = 0.8;
750
+ function makeStackedBar(kit) {
751
+ const { t, fmt: fmt3, catLabel } = kit;
752
+ return function StackedBar(props) {
753
+ const [reserveOn, setReserveOn] = React.useState(false);
754
+ let total = 0;
755
+ for (const p of props.parts) total += p.value;
756
+ const scale = props.max !== void 0 && props.max > total ? props.max : total;
757
+ const free = props.max !== void 0 && props.max > total ? props.max - total : 0;
758
+ const usedPct = scale > 0 ? total / scale * 100 : 0;
759
+ const hovering = props.hoverKey !== null && props.hoverKey !== void 0;
760
+ const showBox = free > 0 && hovering;
761
+ const reserve = props.reserve !== void 0 && props.max !== void 0 && props.max > 0 ? props.reserve : null;
762
+ const reserveLeft = reserve !== null ? Math.round(props.max * reserve.ratio / scale * 1e3) / 10 : 0;
763
+ const reserveWidth = reserve !== null ? Math.round((1 - reserve.ratio) * props.max / scale * 1e3) / 10 : 0;
764
+ let tip = null;
765
+ if (reserveOn && reserve !== null) {
766
+ tip = {
767
+ text: reserve.label,
768
+ leftPct: Math.max(12, Math.min(reserveLeft + reserveWidth / 2, 88))
769
+ };
770
+ } else if (props.hoverKey !== null && props.hoverKey !== void 0) {
771
+ if (props.hoverKey === "free" && free > 0) {
772
+ const pct = scale > 0 ? free / scale * 100 : 0;
773
+ tip = {
774
+ text: t("overview.free") + " " + fmt3(free) + " (" + Math.round(pct) + "%)",
775
+ leftPct: Math.max(12, Math.min(total / scale * 100 + pct / 2, 88))
776
+ };
777
+ } else {
778
+ let acc = 0;
779
+ for (const p of props.parts) {
780
+ const pct = scale > 0 ? p.value / scale * 100 : 0;
781
+ if (p.key === props.hoverKey && p.value > 0) {
782
+ tip = {
783
+ // "(pct%)" is a share of the OCCUPIED total — the solid box
784
+ // that appears on hover frames exactly this reference region.
785
+ text: catLabel(p.key) + " \u2248" + fmt3(p.value) + " (" + Math.round(p.value / total * 100) + "%) " + t("overview.ofUsed"),
786
+ leftPct: Math.max(12, Math.min(acc + pct / 2, 88))
787
+ };
788
+ break;
789
+ }
790
+ acc += pct;
791
+ }
792
+ }
793
+ }
794
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-stacked-wrap" }, /* @__PURE__ */ React.createElement(
795
+ "div",
796
+ {
797
+ className: "lc-stacked" + (hovering ? " lc-stacked-dim" : ""),
798
+ style: { height: (props.height || 14) + "px" },
799
+ onMouseLeave: () => {
800
+ if (props.onHoverKey !== void 0) props.onHoverKey(null);
801
+ setReserveOn(false);
802
+ }
803
+ },
804
+ total > 0 ? props.parts.map((p) => {
805
+ if (!p.value) return null;
806
+ const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
807
+ return /* @__PURE__ */ React.createElement(
808
+ "div",
809
+ {
810
+ key: p.key,
811
+ className: "lc-stacked-seg" + (on ? " lc-stacked-seg-on" : ""),
812
+ style: { width: p.value / scale * 100 + "%", background: p.color },
813
+ onMouseEnter: () => {
814
+ if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
815
+ }
816
+ }
817
+ );
818
+ }) : null,
819
+ free > 0 ? /* @__PURE__ */ React.createElement(
820
+ "div",
821
+ {
822
+ key: "free",
823
+ className: "lc-stacked-free" + (props.hoverKey === "free" ? " lc-stacked-free-on" : ""),
824
+ style: { width: free / scale * 100 + "%" },
825
+ onMouseEnter: () => {
826
+ if (props.onHoverKey !== void 0) props.onHoverKey("free");
827
+ }
828
+ }
829
+ ) : null,
830
+ reserve !== null ? /* @__PURE__ */ React.createElement(
831
+ "div",
832
+ {
833
+ className: "lc-reserve",
834
+ style: { left: reserveLeft + "%", width: reserveWidth + "%" },
835
+ onMouseEnter: () => {
836
+ setReserveOn(true);
837
+ if (props.onHoverKey !== void 0) props.onHoverKey(null);
838
+ },
839
+ onMouseLeave: () => setReserveOn(false)
840
+ }
841
+ ) : null,
842
+ /* @__PURE__ */ React.createElement(
843
+ "div",
844
+ {
845
+ className: "lc-occupied-box" + (showBox ? " lc-occupied-box-on" : ""),
846
+ style: { width: usedPct + "%" }
847
+ }
848
+ )
849
+ ), props.tip !== false ? /* @__PURE__ */ React.createElement(
850
+ "div",
851
+ {
852
+ className: "lc-bar-tip" + (tip ? " lc-bar-tip-on" : ""),
853
+ style: { left: tip ? tip.leftPct + "%" : "50%" }
854
+ },
855
+ tip ? tip.text : ""
856
+ ) : null);
857
+ };
858
+ }
859
+ function makeLegend(kit) {
860
+ const { t, fmt: fmt3, catLabel } = kit;
861
+ return function Legend(props) {
862
+ let total = 0;
863
+ for (const p of props.parts) total += p.value;
864
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-legend" }, props.parts.map((p) => {
865
+ const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
866
+ return /* @__PURE__ */ React.createElement(
867
+ "span",
868
+ {
869
+ key: p.key,
870
+ className: "lc-chip" + (on ? " lc-chip-on" : ""),
871
+ title: t("overview.ofUsed"),
872
+ onMouseEnter: () => {
873
+ if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
874
+ },
875
+ onMouseLeave: () => {
876
+ if (props.onHoverKey !== void 0) props.onHoverKey(null);
877
+ }
878
+ },
879
+ /* @__PURE__ */ React.createElement("i", { style: { background: p.color } }),
880
+ /* @__PURE__ */ React.createElement("span", { className: "lc-chip-label" }, catLabel(p.key)),
881
+ /* @__PURE__ */ React.createElement("span", { className: "lc-chip-nums" }, "\u2248" + fmt3(p.value), total > 0 ? /* @__PURE__ */ React.createElement("em", null, Math.round(p.value / total * 100) + "%") : null)
882
+ );
883
+ }));
884
+ };
885
+ }
886
+
887
+ // src/client/components/currentComposition.tsx
888
+ function makeCurrentComposition(kit, StackedBar, Legend) {
889
+ const { t, fmt: fmt3 } = kit;
890
+ return function CurrentComposition(props) {
891
+ const head = props.head;
892
+ const reserve = head.window != null && head.window > 0 ? { ratio: AUTO_COMPACT_RATIO, label: t("overview.compactReserve", { pct: Math.round(AUTO_COMPACT_RATIO * 100) }) } : void 0;
893
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-card" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, /* @__PURE__ */ React.createElement("span", { className: "lc-card-title-text" }, t("overview.title")), props.subtitle !== void 0 && props.subtitle !== "" ? /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, props.subtitle) : 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) + " tokens" : " " + t("overview.estimate")), head.pct !== null ? /* @__PURE__ */ React.createElement("span", { className: "lc-overview-pct" }, /* @__PURE__ */ React.createElement("b", null, head.pct + "%"), t("overview.used")) : null), /* @__PURE__ */ React.createElement(StackedBar, { parts: head.parts, height: 16, max: head.window, hoverKey: props.hoverKey, onHoverKey: props.onHoverKey, reserve }), /* @__PURE__ */ React.createElement(Legend, { parts: head.parts, hoverKey: props.hoverKey, onHoverKey: props.onHoverKey }), props.tools !== void 0 && props.tools.length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-tools" }, /* @__PURE__ */ React.createElement("button", { type: "button", className: "lc-tools-label", onClick: () => {
894
+ if (props.onToolFocus !== void 0) props.onToolFocus({});
895
+ } }, t("tools.top")), props.tools.slice().sort((a, b) => b.tokens - a.tokens).slice(0, 5).map((tool) => /* @__PURE__ */ React.createElement("button", { key: tool.name, type: "button", className: "lc-tool-chip", onClick: () => {
896
+ if (props.onToolFocus !== void 0) props.onToolFocus({ tool: tool.name });
897
+ } }, tool.name + " " + fmt3(tool.tokens))), props.tools.length > 5 ? /* @__PURE__ */ React.createElement("button", { type: "button", className: "lc-tools-more", onClick: () => {
898
+ if (props.onToolFocus !== void 0) props.onToolFocus({});
899
+ } }, t("tools.more", { n: props.tools.length })) : null) : null);
900
+ };
901
+ }
902
+
903
+ // src/client/components/contextModal.tsx
904
+ function makeContextModal(ctx, kit) {
905
+ const { t } = kit;
906
+ const sessions = ctx.get("sessions");
907
+ const StackedBar = makeStackedBar(kit);
908
+ const Legend = makeLegend(kit);
909
+ const CurrentComposition = makeCurrentComposition(kit, StackedBar, Legend);
910
+ const ContextBrowser = makeContextBrowser(kit, StackedBar);
911
+ return function ContextModal(props) {
912
+ const sessionId = typeof props.sessionId === "string" ? props.sessionId : "";
913
+ const open = typeof props.useContextModal === "function" ? props.useContextModal((s) => s) : false;
914
+ const data = typeof props.useProjection === "function" ? timelineOf(props.useProjection("contextTimeline")) : null;
915
+ const pressure = typeof props.useProjection === "function" ? contextPressureOf(props.useProjection("contextPressure")) : null;
916
+ const headers = typeof props.useProjection === "function" ? headersOf(props.useProjection("contextHeaders")) : null;
917
+ const [hoverCat, setHoverCat] = React.useState(null);
918
+ const [toolFocus, setToolFocus] = React.useState(null);
919
+ const close = React.useCallback(() => {
920
+ if (sessionId === "") return;
921
+ modalStoreOf(sessionId).set(false);
922
+ const guard = takePendingConsume(sessionId);
923
+ if (guard === void 0 || sessions === void 0) return;
924
+ const scope = sessions.scope(sessionId);
925
+ if (scope !== void 0) scope.bail(scope, "slash/input-consume-token", { guard });
926
+ }, [sessionId]);
927
+ React.useEffect(() => {
928
+ if (!open) return void 0;
929
+ const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
930
+ const onKey = (ev) => {
931
+ if (ev.key !== "Escape") return;
932
+ ev.preventDefault();
933
+ ev.stopPropagation();
934
+ close();
935
+ };
936
+ window.addEventListener("keydown", onKey, true);
937
+ return () => {
938
+ window.removeEventListener("keydown", onKey, true);
939
+ if (previous !== null && document.contains(previous)) previous.focus();
940
+ };
941
+ }, [open, close]);
942
+ if (!open) return null;
943
+ const head = data !== null ? headlineOf(data, pressure) : null;
944
+ const subtitle = data !== null ? (data.model ? data.model : "") + (data.provider ? " \xB7 " + data.provider : "") : "";
945
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-modal-backdrop", onClick: close }, /* @__PURE__ */ React.createElement("div", { className: "lc-modal-card", onClick: (ev) => {
946
+ ev.stopPropagation();
947
+ } }, /* @__PURE__ */ React.createElement("div", { className: "lc-modal-head" }, /* @__PURE__ */ React.createElement("span", { className: "lc-modal-title" }, t("tab")), /* @__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(
948
+ CurrentComposition,
949
+ {
950
+ head,
951
+ subtitle,
952
+ hoverKey: hoverCat,
953
+ onHoverKey: setHoverCat,
954
+ tools: data.toolList,
955
+ onToolFocus: setToolFocus
956
+ }
957
+ ), /* @__PURE__ */ React.createElement(
958
+ ContextBrowser,
1328
959
  {
1329
- parts,
1330
- height: 10,
1331
- hoverKey: linked ? linkKey : void 0,
1332
- onHoverKey: linked ? props.onHoverKey : void 0,
1333
- tip: false
960
+ data,
961
+ headers,
962
+ useSession: props.useSession,
963
+ loadOlderHistory: props.loadOlderHistory,
964
+ hoverKey: hoverCat,
965
+ onHoverKey: setHoverCat,
966
+ toolFocus,
967
+ onToolFocusHandled: () => {
968
+ setToolFocus(null);
969
+ }
1334
970
  }
1335
- )), view.missingLive > 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, t("browser.missingLive", { n: view.missingLive })) : null, view.approximate ? /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, t("browser.approx")) : null, /* @__PURE__ */ React.createElement("div", { className: "lc-br-cats" }, CATS.map((c) => {
1336
- const count = toolCount(c.key);
1337
- const v = breakdown[c.key] || 0;
1338
- const openable = count > 0 || (c.key === "system" || c.key === "tools") && view.header === null;
1339
- const open = openCat === c.key && openable;
1340
- return /* @__PURE__ */ React.createElement("div", { key: c.key, className: "lc-br-cat" + (openable ? "" : " lc-br-cat-empty") }, /* @__PURE__ */ React.createElement(
1341
- "button",
1342
- {
1343
- type: "button",
1344
- className: "lc-br-cat-row" + (linked && props.hoverKey === c.key ? " lc-br-cat-on" : ""),
1345
- onMouseEnter: linked ? () => {
1346
- if (props.onHoverKey !== void 0) props.onHoverKey(c.key);
1347
- } : void 0,
1348
- onMouseLeave: linked ? () => {
1349
- if (props.onHoverKey !== void 0) props.onHoverKey(null);
1350
- } : void 0,
1351
- onClick: () => {
1352
- toggleCat(c.key);
1353
- }
1354
- },
1355
- /* @__PURE__ */ React.createElement("span", { className: "lc-br-chev" + (open ? " lc-br-chev-on" : "") }, "\u25B8"),
1356
- /* @__PURE__ */ React.createElement("i", { style: { background: c.color } }),
1357
- /* @__PURE__ */ React.createElement("span", { className: "lc-br-cat-label" }, catLabel(c.key)),
1358
- /* @__PURE__ */ React.createElement("span", { className: "lc-br-cat-count" }, t("browser.items", { n: count })),
1359
- /* @__PURE__ */ React.createElement("span", { className: "lc-br-tokens" }, "\u2248" + fmt3(v)),
1360
- /* @__PURE__ */ React.createElement("span", { className: "lc-br-pct" }, total > 0 ? Math.round(v / total * 100) + "%" : "")
1361
- ), open ? /* @__PURE__ */ React.createElement("div", { className: "lc-br-body" }, catBody(c.key)) : null);
1362
- })));
971
+ ))));
1363
972
  };
1364
973
  }
1365
974
 
975
+ // src/client/styles.ts
976
+ var STYLES = [
977
+ ".lc-root { padding: 16px 20px 32px; overflow-y: auto; height: 100%; box-sizing: border-box; color: var(--dsw-alias-label-primary); font-size: 13px; }",
978
+ ".lc-card { background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; padding: 14px 16px; margin-bottom: 14px; }",
979
+ ".lc-card-title { font-weight: 600; margin-bottom: 10px; display: flex; align-items: baseline; gap: 8px; }",
980
+ ".lc-card-title-text { flex: none; white-space: nowrap; }",
981
+ ".lc-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(92px, 1fr)); gap: 8px; }",
982
+ ".lc-stat { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 8px 10px; display: flex; flex-direction: column; gap: 2px; min-width: 0; }",
983
+ ".lc-stat-label { color: var(--dsw-alias-label-secondary); font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
984
+ ".lc-stat-value { color: var(--dsw-alias-label-primary); font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center; }",
985
+ ".lc-stat-sub { color: var(--dsw-alias-label-secondary); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
986
+ ".lc-card-sub { font-weight: 400; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
987
+ ".lc-gran, .lc-kinds { margin-left: auto; display: flex; gap: 2px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 1px; }",
988
+ ".lc-gran-btn { border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 1; padding: 3px 8px; border-radius: 5px; cursor: pointer; font-family: inherit; }",
989
+ ".lc-gran-btn:hover { color: var(--dsw-alias-label-primary); }",
990
+ ".lc-gran-on, .lc-gran-on:hover { background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-foreground); }",
991
+ ".lc-overview-num { display: flex; align-items: baseline; gap: 6px; margin-bottom: 8px; }",
992
+ ".lc-overview-num > b { font-size: 20px; }",
993
+ ".lc-overview-num span { color: var(--dsw-alias-label-secondary); }",
994
+ // The used-percentage figure is the emphasis of the line: pushed to the
995
+ // right edge, large and primary-colored, with a small "used" caption.
996
+ ".lc-overview-pct { margin-left: auto; font-size: 11px; }",
997
+ ".lc-overview-pct b { font-size: 20px; color: var(--dsw-alias-label-primary); margin-right: 4px; }",
998
+ ".lc-stacked-wrap { position: relative; width: 100%; }",
999
+ ".lc-stacked { display: flex; width: 100%; border-radius: 5px; overflow: hidden; background: rgba(128,128,128,0.18); position: relative; }",
1000
+ // Hover reference frame around the OCCUPIED region of the composition bar:
1001
+ // a SOLID box from the left edge to the used/window boundary, so the
1002
+ // legend's "share of used" percentages visibly map to the boxed part (the
1003
+ // free track sits outside it). pointer-events: none keeps hover on the
1004
+ // segments/free. A soft LIGHT-GRAY stroke — a gentle guide rather than a
1005
+ // hard black box — that still reads through its 2px thickness against the
1006
+ // dimmed parts underneath it (`.lc-stacked-dim`). The free track echoes
1007
+ // the same weight with a dashed box, so the two read as one system.
1008
+ ".lc-occupied-box { position: absolute; top: 0; bottom: 0; left: 0; border: 2px solid var(--dsw-alias-label-tertiary); border-radius: 5px; box-sizing: border-box; pointer-events: none; opacity: 0; box-shadow: 0 0 0 1px var(--dsw-alias-bg-layer-2); transition: opacity 120ms ease; }",
1009
+ ".lc-occupied-box-on { opacity: 1; }",
1010
+ ".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; opacity: 0; transition: opacity 120ms ease; }",
1011
+ ".lc-bar-tip-on { opacity: 1; }",
1012
+ ".lc-stacked > div { height: 100%; }",
1013
+ // Hover eases: the shared composition hover (dim + segment brighten, on the
1014
+ // overview or the mirrored browser bar) glides in and out — same 120ms
1015
+ // rhythm as the rest of the view.
1016
+ ".lc-stacked-seg { transition: filter 120ms ease, opacity 120ms ease; }",
1017
+ ".lc-stacked-free { box-sizing: border-box; transition: box-shadow 120ms ease, opacity 120ms ease; }",
1018
+ ".lc-stacked-seg-on { filter: brightness(1.18); }",
1019
+ // The free track mirrors the occupied frame: a DASHED 2px box (the occupied
1020
+ // frame is solid) at the same weight — border, not the old inset shadow, so
1021
+ // the dash pattern renders; the base box-sizing keeps the width from shifting.
1022
+ // Auto-compaction reserve band: the rightmost (1−ratio) of the window,
1023
+ // striped diagonally like a warning plate so it reads as "headroom, not
1024
+ // real usage" rather than plain free space. Sits above the track/segments
1025
+ // (overlay visible even past the threshold) with its own pointer so the
1026
+ // hover explains it (help cursor signals "more info").
1027
+ ".lc-reserve { position: absolute; top: 0; bottom: 0; z-index: 1; pointer-events: auto; cursor: help; background: repeating-linear-gradient(45deg, color-mix(in srgb, var(--dsw-alias-state-warn-primary) 24%, transparent) 0 5px, transparent 5px 10px); }",
1028
+ ".lc-stacked-free-on { border: 2px dashed var(--dsw-alias-label-secondary); border-radius: 3px; }",
1029
+ // Hover focus: everything except the hovered part (segment, legend chip, or
1030
+ // free track) recedes, so the composition highlight and the occupied-region
1031
+ // frame read clearly. The selected segment/free keeps full opacity.
1032
+ ".lc-stacked-dim .lc-stacked-seg { opacity: 0.35; }",
1033
+ ".lc-stacked-dim .lc-stacked-seg-on { opacity: 1; }",
1034
+ ".lc-stacked-dim .lc-stacked-free { opacity: 0.35; }",
1035
+ ".lc-stacked-dim .lc-stacked-free-on { opacity: 1; }",
1036
+ // Current-composition legend: an auto-flowing grid (as many columns as the
1037
+ // card fits, narrowing flows to fewer) whose cells are label-left /
1038
+ // numbers-right rows — each category a tidy "● 系统提示词 ≈2.0k 2%".
1039
+ ".lc-legend { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 6px 14px; margin-top: 10px; }",
1040
+ ".lc-chip { display: flex; align-items: center; gap: 5px; min-width: 0; color: var(--dsw-alias-label-primary); padding: 1px 6px; border-radius: 6px; cursor: pointer; transition: background-color 120ms ease; }",
1041
+ // The category name is the row's emphasis: bold at rest (the chip is a
1042
+ // legend label, not inline prose), ellipsized if a narrow cell squeezes it
1043
+ // while the right-aligned figures stay intact.
1044
+ ".lc-chip-label { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }",
1045
+ // The numeric group (≈tokens + percent) pins to the cell's right edge while
1046
+ // the category label stays left; a baseline acts as the shared vertical
1047
+ // rhythm for the two figures.
1048
+ ".lc-chip-nums { margin-left: auto; flex: none; display: inline-flex; align-items: baseline; gap: 6px; white-space: nowrap; }",
1049
+ ".lc-chip i, .lc-detail-row i, .lc-node i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }",
1050
+ ".lc-chip i { transition: box-shadow 120ms ease; }",
1051
+ ".lc-chip em { font-style: normal; color: var(--dsw-alias-label-secondary); }",
1052
+ // The selected chip glows with the shared hover tint (smoothly, like the
1053
+ // browser rows) instead of snapping its weight; the dot ring on top marks
1054
+ // the exact segment.
1055
+ ".lc-chip-on { font-weight: 600; background: var(--dsw-alias-interactive-bg-hover); }",
1056
+ ".lc-chip-on i { box-shadow: 0 0 0 1px var(--dsw-alias-brand-primary); }",
1057
+ ".lc-tools { margin-top: 10px; color: var(--dsw-alias-label-secondary); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }",
1058
+ // The overview tool list links into the Context browser: the "工具定义 Top"
1059
+ // label, its "等 N 个" overflow link, and every tool chip are buttons, and
1060
+ // hover underlines them (the chip already reads as a tag, so the underline
1061
+ // is the only affordance).
1062
+ ".lc-tools-label, .lc-tools-more { border: 0; background: transparent; padding: 0; color: var(--dsw-alias-label-secondary); font-family: inherit; font-size: 12px; cursor: pointer; }",
1063
+ ".lc-tools-label:hover, .lc-tools-more:hover { text-decoration: underline; }",
1064
+ ".lc-tool-chip { background: var(--dsw-alias-bg-layer-2); border: 0; border-radius: 4px; padding: 1px 7px; font: inherit; font-size: 12px; color: var(--dsw-alias-label-primary); cursor: pointer; }",
1065
+ ".lc-tool-chip:hover { text-decoration: underline; }",
1066
+ ".lc-chartrow { display: flex; gap: 6px; align-items: stretch; }",
1067
+ ".lc-axis { position: relative; width: 40px; height: 150px; padding-top: 18px; box-sizing: border-box; color: var(--dsw-alias-label-secondary); font-size: 11px; }",
1068
+ ".lc-axis span { position: absolute; right: 0; line-height: 1; }",
1069
+ ".lc-axis-top { top: 13px; }",
1070
+ ".lc-axis-mid { top: 69px; }",
1071
+ ".lc-axis-bot { top: 125px; }",
1072
+ ".lc-chart-scroll { position: relative; flex: 1; overflow-x: auto; overflow-y: hidden; min-width: 0; scrollbar-width: thin; }",
1073
+ ".lc-chart-fade { position: absolute; top: 0; bottom: 0; width: 26px; pointer-events: none; z-index: 2; }",
1074
+ ".lc-chart-fade-l { left: 0; background: linear-gradient(to right, var(--dsw-alias-bg-layer-1), transparent); }",
1075
+ ".lc-chart-fade-r { right: 0; background: linear-gradient(to left, var(--dsw-alias-bg-layer-1), transparent); }",
1076
+ ".lc-chart { position: relative; display: flex; align-items: flex-end; gap: 2px; height: 130px; padding-top: 18px; box-sizing: border-box; width: max-content; min-width: 100%; }",
1077
+ ".lc-grid { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--dsw-alias-border-l1); pointer-events: none; }",
1078
+ ".lc-grid-top { top: 18px; }",
1079
+ ".lc-grid-mid { top: 74px; }",
1080
+ ".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, background-color 120ms ease; }",
1081
+ // Turn-aware dimming: while a turn is focused, bars OUTSIDE the active
1082
+ // turn fade to 35% and the whole current turn stays fully opaque.
1083
+ ".lc-chart-dim .lc-bar { opacity: 0.35; }",
1084
+ ".lc-chart-dim .lc-bar-in-turn { opacity: 1; }",
1085
+ ".lc-chart-dim .lc-turn { opacity: 0.35; }",
1086
+ ".lc-chart-dim .lc-turn-on { opacity: 1; }",
1087
+ ".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; }",
1088
+ ".lc-bar:hover { background: var(--dsw-alias-bg-layer-2); }",
1089
+ ".lc-bar-selected { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }",
1090
+ ".lc-bar-hovered { outline: 1px dashed var(--dsw-alias-brand-primary); outline-offset: 1px; }",
1091
+ ".lc-bar-in-turn { background: rgba(128,128,128,0.14); }",
1092
+ ".lc-bar-stack { display: flex; flex-direction: column-reverse; width: 100%; }",
1093
+ ".lc-bar-stack > div { width: 100%; }",
1094
+ ".lc-bar-marker { position: absolute; top: -16px; left: 50%; transform: translateX(-50%); font-size: 11px; color: var(--dsw-alias-state-warn-primary); }",
1095
+ // Turn strip: NEUTRAL zebra fills (inline, see TURN_FILLS) with secondary
1096
+ // text — never the category palette, or the strip reads as a detached
1097
+ // bottom segment of the bars above it.
1098
+ ".lc-turns { display: flex; gap: 2px; width: max-content; min-width: 100%; margin-top: 4px; }",
1099
+ ".lc-turn { flex: none; box-sizing: border-box; text-align: center; font-size: 10px; line-height: 14px; font-weight: 600; color: var(--dsw-alias-label-secondary); border-radius: 3px; height: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; transition: filter 120ms, opacity 120ms; }",
1100
+ ".lc-turn-on { filter: brightness(1.35); color: var(--dsw-alias-label-primary); }",
1101
+ ".lc-detail { margin-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); padding-top: 12px; }",
1102
+ ".lc-detail-head { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 8px; color: var(--dsw-alias-label-secondary); }",
1103
+ ".lc-detail-head b { color: var(--dsw-alias-label-primary); }",
1104
+ ".lc-detail-marker { color: var(--dsw-alias-state-warn-primary); font-size: 11px; background: var(--dsw-alias-bg-layer-2); border-radius: 6px; padding: 1px 7px; }",
1105
+ ".lc-detail-head .lc-actual { color: var(--dsw-alias-state-success-primary); }",
1106
+ ".lc-detail-tag { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 4px; padding: 0 6px; font-size: 11px; color: var(--dsw-alias-label-secondary); }",
1107
+ ".lc-detail-rows { margin-top: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px 24px; }",
1108
+ ".lc-detail-row { display: flex; align-items: center; gap: 8px; }",
1109
+ ".lc-detail-label { min-width: 70px; white-space: nowrap; color: var(--dsw-alias-label-secondary); }",
1110
+ ".lc-bar-track { flex: 1; height: 5px; border-radius: 3px; background: rgba(128,128,128,0.18); overflow: hidden; display: block; }",
1111
+ ".lc-bar-fill { display: block; height: 100%; border-radius: 3px; }",
1112
+ ".lc-detail-num { width: 44px; text-align: right; }",
1113
+ ".lc-detail-pct { width: 34px; text-align: right; color: var(--dsw-alias-label-secondary); }",
1114
+ // Section rows share one 14px vertical rhythm, owned by the ROW (flex gap
1115
+ // covers the wrapped-row axis too), never by the cards inside: a card that
1116
+ // IS a column (head, events/messages) drops its own bottom margin, and a
1117
+ // card stacked inside a wrapper column (overview + trend) keeps its margin
1118
+ // as the inner gap but the last one drops it — so every section boundary
1119
+ // measures exactly one 14px, whichever way the flex wraps.
1120
+ ".lc-cols { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 14px; }",
1121
+ ".lc-col { flex: 1; min-width: 280px; }",
1122
+ ".lc-cols > .lc-card { margin-bottom: 0; }",
1123
+ ".lc-col > .lc-card:last-child { margin-bottom: 0; }",
1124
+ // Head row: stats board + plugin info sit side by side under the shared
1125
+ // `lc-cols` flex — stats takes ~7/10 of the row, plugin info ~3/10, both
1126
+ // wrap onto their own line when the available width falls below each card's
1127
+ // min-width (so a narrow viewport keeps both readable). Both children are
1128
+ // `.lc-card` themselves (rendered by StatsBoard / PluginInfo); the flex
1129
+ // sizing lives on the head row's direct children so the existing card
1130
+ // chrome doesn't change.
1131
+ ".lc-head > .lc-card:first-child { flex: 7 1 0; min-width: 360px; }",
1132
+ ".lc-head > .lc-card:last-child { flex: 3 1 0; min-width: 220px; }",
1133
+ // Plugin info: two full-width rows (Plugin / GitHub), each one horizontal
1134
+ // line with the label on the left and the value on the right — a compact
1135
+ // definition list rather than a multi-cell grid.
1136
+ ".lc-pi-grid { display: grid; grid-template-columns: 1fr; gap: 8px; }",
1137
+ // The row IS the link: an `<a>` with the label + value on one baseline.
1138
+ // Hovering a row underlines its value — the only hover affordance.
1139
+ ".lc-pi-row { display: flex; flex-direction: row; justify-content: space-between; align-items: baseline; gap: 12px; min-width: 0; text-decoration: none; color: inherit; }",
1140
+ ".lc-pi-row:hover .lc-pi-value { text-decoration: underline; }",
1141
+ // Upgrade chip appended to the Plugin value when the npm registry has a
1142
+ // newer version than the baked-in one.
1143
+ ".lc-pi-update { color: var(--dsw-alias-state-warn-primary); font-size: 11px; margin-left: 6px; white-space: nowrap; }",
1144
+ ".lc-pi-label { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
1145
+ // min-width: 0 lets the value shrink inside the flex row and truncate with
1146
+ // an ellipsis; without it a narrow card pushes the value over the label.
1147
+ ".lc-pi-value { min-width: 0; color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: right; }",
1148
+ ".lc-events, .lc-nodes { display: flex; flex-direction: column; gap: 2px; max-height: 320px; overflow-y: auto; }",
1149
+ ".lc-event { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
1150
+ ".lc-event-icon { width: 18px; text-align: center; color: var(--dsw-alias-state-warn-primary); }",
1151
+ ".lc-event-icon.lc-event-inject { color: #a855f7; }",
1152
+ ".lc-event-icon.lc-event-model { color: var(--dsw-alias-brand-primary); }",
1153
+ // Kind chip: the event classification at a glance; the tint matches the
1154
+ // impact direction (inject = adds context, compaction/prune = frees it,
1155
+ // model switch = neutral), mirroring the token sign colors below.
1156
+ ".lc-kind { flex: none; font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px; white-space: nowrap; }",
1157
+ ".lc-kind-inject { background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent); color: var(--dsw-alias-state-success-primary); }",
1158
+ ".lc-kind-compaction { background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 15%, transparent); color: var(--dsw-alias-state-error-primary); }",
1159
+ ".lc-kind-prune { background: color-mix(in srgb, #8b5cf6 15%, transparent); color: #8b5cf6; }",
1160
+ ".lc-kind-model { background: color-mix(in srgb, var(--dsw-alias-brand-primary) 15%, transparent); color: var(--dsw-alias-brand-primary); }",
1161
+ ".lc-event-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
1162
+ ".lc-event-at { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; white-space: nowrap; }",
1163
+ ".lc-event-tokens { color: var(--dsw-alias-state-success-primary); font-weight: 600; white-space: nowrap; }",
1164
+ ".lc-event-tokens.lc-up { color: var(--dsw-alias-state-warn-primary); }",
1165
+ ".lc-event-time { color: var(--dsw-alias-label-secondary); font-size: 12px; }",
1166
+ ".lc-node { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
1167
+ ".lc-node-preview { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); }",
1168
+ ".lc-node-time { color: var(--dsw-alias-label-secondary); font-size: 12px; min-width: 54px; text-align: right; }",
1169
+ ".lc-node-tokens { color: var(--dsw-alias-label-secondary); }",
1170
+ ".lc-nodes-more { color: var(--dsw-alias-label-secondary); padding: 3px 0; }",
1171
+ ".lc-empty { color: var(--dsw-alias-label-secondary); padding: 18px 0; text-align: center; }",
1172
+ ".lc-foot { color: var(--dsw-alias-label-secondary); font-size: 12px; margin-top: 4px; }",
1173
+ // ---- /context modal (centered dialog; escapes the composer anchor via fixed positioning) ----
1174
+ ".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; }",
1175
+ ".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; }",
1176
+ ".lc-modal-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 12px; }",
1177
+ ".lc-modal-title { font-weight: 600; font-size: 14px; }",
1178
+ ".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; }",
1179
+ ".lc-modal-close:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); }",
1180
+ // ---- Context browser card (progressive disclosure: category accordion ->
1181
+ // element rows -> per-kind content) ----
1182
+ // The δ-baseline caption sits at the card title's right edge, just left of
1183
+ // the step picker (it takes the row's auto margin the picker used to own).
1184
+ ".lc-br-hint { margin-left: auto; color: var(--dsw-alias-label-secondary); font-size: 11px; font-weight: 400; white-space: nowrap; }",
1185
+ ".lc-br-pick { font: inherit; font-size: 12px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 3px 6px; max-width: 240px; }",
1186
+ ".lc-br-meta { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 8px; color: var(--dsw-alias-label-secondary); }",
1187
+ ".lc-br-meta b { color: var(--dsw-alias-label-primary); }",
1188
+ ".lc-br-meta .lc-actual { color: var(--dsw-alias-state-success-primary); }",
1189
+ ".lc-br-note { margin-top: 8px; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
1190
+ ".lc-br-cats { margin-top: 10px; display: flex; flex-direction: column; gap: 4px; }",
1191
+ ".lc-br-cat { border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; background: var(--dsw-alias-bg-layer-2); overflow: hidden; }",
1192
+ ".lc-br-cat-empty { opacity: 0.55; }",
1193
+ ".lc-br-cat-row { display: flex; align-items: center; gap: 8px; width: 100%; border: 0; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; padding: 7px 10px; cursor: pointer; text-align: left; transition: background-color 120ms ease; }",
1194
+ ".lc-br-cat-row:hover { background: var(--dsw-alias-interactive-bg-hover); }",
1195
+ // Current-composition hover echo: the category row lit by the shared hover
1196
+ // link (hovering the Current Composition card's bar/legend) looks hovered
1197
+ // too — same tint as the physical hover, plus a ring on its color dot.
1198
+ ".lc-br-cat-on { background: var(--dsw-alias-interactive-bg-hover); }",
1199
+ ".lc-br-cat-on i { box-shadow: 0 0 0 1px var(--dsw-alias-brand-primary); }",
1200
+ ".lc-br-cat-row i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; flex: none; transition: box-shadow 120ms ease; }",
1201
+ ".lc-br-cat-label { font-weight: 600; }",
1202
+ ".lc-br-cat-count { color: var(--dsw-alias-label-secondary); font-size: 12px; white-space: nowrap; }",
1203
+ // Count + Δ pill form one attached group: the group absorbs the row's free
1204
+ // space (tokens/percent stay right-aligned) with a tight 4px inner gap so
1205
+ // the pill hugs the item count instead of standing at the row's 8px rhythm.
1206
+ ".lc-br-count-grp { flex: 1; min-width: 0; display: inline-flex; align-items: center; gap: 4px; }",
1207
+ ".lc-br-chev { flex: none; width: 12px; color: var(--dsw-alias-label-secondary); transition: transform 120ms ease; }",
1208
+ ".lc-br-chev-on { transform: rotate(90deg); }",
1209
+ ".lc-br-tokens { flex: none; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
1210
+ // Δ pills on a category row: count and token swings vs the previous turn's
1211
+ // last step, tinted by direction (added = success, freed = error — the same
1212
+ // language as the events card's kind chips).
1213
+ ".lc-br-delta, .lc-br-tdelta { flex: none; font-size: 11px; font-weight: 600; padding: 0 5px; border-radius: 4px; white-space: nowrap; }",
1214
+ ".lc-br-delta-up, .lc-br-tdelta-up { color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent); }",
1215
+ ".lc-br-delta-down, .lc-br-tdelta-down { color: var(--dsw-alias-state-error-primary); background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 15%, transparent); }",
1216
+ // The token Δ pill hugs the LEFT of the token figure (tight 4px gap, like
1217
+ // the count group) while the figure itself keeps its right-aligned slot.
1218
+ ".lc-br-tokens-grp { flex: none; min-width: 0; display: inline-flex; align-items: center; gap: 4px; }",
1219
+ ".lc-br-pct { flex: none; width: 36px; text-align: right; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
1220
+ ".lc-br-body { border-top: 1px solid var(--dsw-alias-border-l1); padding: 4px 6px; display: flex; flex-direction: column; gap: 2px; }",
1221
+ ".lc-br-elem { border-radius: 6px; }",
1222
+ ".lc-br-elem-on { background: var(--dsw-alias-interactive-bg-active); }",
1223
+ ".lc-br-elem-row { display: flex; align-items: center; gap: 8px; width: 100%; border: 0; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; padding: 5px 6px; cursor: pointer; text-align: left; border-radius: 6px; transition: background-color 120ms ease; }",
1224
+ ".lc-br-elem-row:hover { background: var(--dsw-alias-interactive-bg-hover); }",
1225
+ ".lc-br-tag { flex: none; font-size: 11px; color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); border-radius: 4px; padding: 0 6px; max-width: 220px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
1226
+ ".lc-br-preview { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
1227
+ ".lc-br-time { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; }",
1228
+ ".lc-br-content { padding: 2px 6px 8px 26px; display: flex; flex-direction: column; gap: 6px; }",
1229
+ ".lc-br-pre { margin: 0; padding: 8px 10px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 1.55; white-space: pre-wrap; word-break: break-word; max-height: 320px; overflow-y: auto; scrollbar-width: thin; }",
1230
+ ".lc-br-dim { color: var(--dsw-alias-label-secondary); }",
1231
+ ".lc-br-call { display: flex; flex-direction: column; gap: 4px; align-items: flex-start; }",
1232
+ // ---- Tool schema body (browser card, tools category). The description
1233
+ // and parameter table share one "card with a small title" chrome — a
1234
+ // labeled bordered block so a reader can scan three stacked sections
1235
+ // without losing track of which is which. The raw JSON sits below as a
1236
+ // collapsed toggle (no card chrome — it's a debug view, not a primary
1237
+ // section). ----
1238
+ ".lc-ts-card { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; overflow: hidden; }",
1239
+ ".lc-ts-card-head { display: flex; align-items: center; gap: 8px; padding: 6px 10px; font-size: 11px; font-weight: 600; color: var(--dsw-alias-label-secondary); border-bottom: 1px solid var(--dsw-alias-border-l1); }",
1240
+ ".lc-ts-card-head b { color: var(--dsw-alias-label-primary); }",
1241
+ ".lc-ts-card-count { margin-left: auto; }",
1242
+ ".lc-ts-desc-body { margin: 0; padding: 8px 10px; color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 1.55; white-space: pre-wrap; word-break: break-word; max-height: 160px; overflow-y: auto; scrollbar-width: thin; }",
1243
+ // Grid: name + type + required chip + description. Columns wrap on narrow
1244
+ // rows; the description cell spans the full second row so a long blurb
1245
+ // never breaks the one-line rhythm of name/type/required.
1246
+ ".lc-ts-param-row { display: grid; grid-template-columns: 140px 90px 56px 1fr; column-gap: 10px; row-gap: 2px; padding: 6px 10px; border-top: 1px solid var(--dsw-alias-border-l1); font-size: 12px; align-items: baseline; }",
1247
+ ".lc-ts-param-row:first-of-type { border-top: 0; }",
1248
+ ".lc-ts-param-name { font-weight: 600; color: var(--dsw-alias-label-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
1249
+ ".lc-ts-param-type { color: var(--dsw-alias-label-secondary); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
1250
+ ".lc-ts-param-req { color: var(--dsw-alias-state-warn-primary); font-size: 11px; font-weight: 600; }",
1251
+ ".lc-ts-param-req-off { color: var(--dsw-alias-label-secondary); font-size: 11px; }",
1252
+ ".lc-ts-param-desc { grid-column: 1 / -1; color: var(--dsw-alias-label-secondary); line-height: 1.5; }",
1253
+ ".lc-ts-params-empty { padding: 8px 10px; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
1254
+ // Raw JSON toggle: a slim button that owns the open/closed state, sitting
1255
+ // just under the parameter table. The <pre> itself only mounts when open
1256
+ // (avoids stringifying the schema on every render and keeps the default
1257
+ // body compact).
1258
+ ".lc-ts-json { display: flex; flex-direction: column; gap: 4px; }",
1259
+ ".lc-ts-json-toggle { align-self: flex-start; border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 11px; padding: 0; cursor: pointer; }",
1260
+ ".lc-ts-json-toggle:hover { color: var(--dsw-alias-label-primary); text-decoration: underline; }"
1261
+ ].join("\n");
1262
+
1366
1263
  // src/client/components/events.tsx
1367
1264
  var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
1368
1265
 
@@ -1440,7 +1337,7 @@ function makeEventList(kit) {
1440
1337
 
1441
1338
  // src/client/meta.ts
1442
1339
  var PLUGIN_NAME = "dsh-context";
1443
- var PLUGIN_VERSION = true ? "0.16.0" : "0.0.0-dev";
1340
+ var PLUGIN_VERSION = true ? "0.17.0" : "0.0.0-dev";
1444
1341
  var PLUGIN_REPO = true ? "https://github.com/bowenliang123/dsh-context" : "https://github.com/bowenliang123/dsh-context";
1445
1342
  var PLUGIN_REPO_SHORT = PLUGIN_REPO.replace(/^https?:\/\/github\.com\//, "");
1446
1343
 
@@ -1492,7 +1389,30 @@ function makePluginInfo(kit) {
1492
1389
  };
1493
1390
  }
1494
1391
 
1392
+ // src/client/components/requestDetail.tsx
1393
+ function makeRequestDetail(kit, StackedBar) {
1394
+ const { t, fmt: fmt3, fmtTime: fmtTime2, catLabel, eventLabel, eventAt } = kit;
1395
+ return function RequestDetail(props) {
1396
+ const req = props.request;
1397
+ if (!req) return null;
1398
+ const isTurn = req.stepCount !== void 0 && req.stepCount > 1;
1399
+ const head = isTurn ? t("detail.turn", { t: req.turn ?? 0, n: req.stepCount ?? 0 }) : t("detail.step", { t: req.turn ?? 0, s: req.step ?? 0 });
1400
+ const marker = props.marker ?? null;
1401
+ const markerAt = marker !== null ? eventAt(marker) : null;
1402
+ 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, t("detail.estTotal", { n: fmt3(req.total) })), req.prompt !== void 0 ? /* @__PURE__ */ React.createElement("span", { className: "lc-actual" }, t("detail.actual", { n: fmt3(req.prompt) })) : null, req.output !== void 0 ? /* @__PURE__ */ React.createElement("span", null, t("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) => {
1403
+ const v = req[c.key] || 0;
1404
+ 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) + "%" : ""));
1405
+ })));
1406
+ };
1407
+ }
1408
+
1495
1409
  // src/client/components/statsBoard.tsx
1410
+ function cacheHitPercent(usage) {
1411
+ const billed = usage.uncachedInputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
1412
+ if (billed === 0) return null;
1413
+ const hundredths = Math.trunc(usage.cacheReadTokens / billed * 1e4 + 1e-9);
1414
+ return `${Math.floor(hundredths / 100)}.${String(hundredths % 100).padStart(2, "0")}`;
1415
+ }
1496
1416
  function makeStatsBoard(kit) {
1497
1417
  const { t, fmt: fmt3 } = kit;
1498
1418
  return function StatsBoard(props) {
@@ -1507,8 +1427,187 @@ function makeStatsBoard(kit) {
1507
1427
  else if (ev.kind === "prune") prunes++;
1508
1428
  else if (ev.kind === "inject") injects++;
1509
1429
  }
1430
+ const hit = props.usage !== null ? cacheHitPercent(props.usage) : null;
1510
1431
  const cell = (label, value) => /* @__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));
1511
- return /* @__PURE__ */ React.createElement("div", { className: "lc-card lc-col lc-col-stats" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, /* @__PURE__ */ React.createElement("span", { className: "lc-card-title-text" }, 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(t("stats.injects"), fmt3(injects)), cell(t("stats.compactions"), fmt3(compactions)), cell(t("stats.prunes"), fmt3(prunes))));
1432
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-card lc-col lc-col-stats" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, /* @__PURE__ */ React.createElement("span", { className: "lc-card-title-text" }, 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(t("stats.injects"), fmt3(injects)), cell(t("stats.compactions"), fmt3(compactions)), cell(t("stats.prunes"), fmt3(prunes)), cell(t("stats.cacheHit"), hit === null ? "\u2014" : hit + "%")));
1433
+ };
1434
+ }
1435
+
1436
+ // src/client/components/trendChart.tsx
1437
+ function aggregateByTurn(requests) {
1438
+ const out = [];
1439
+ let runSteps = 0;
1440
+ for (const req of requests) {
1441
+ const last = out.length > 0 ? out[out.length - 1] : null;
1442
+ if (last !== null && (last.turn ?? 0) === (req.turn ?? 0)) {
1443
+ runSteps++;
1444
+ out[out.length - 1] = { ...req, stepCount: runSteps };
1445
+ } else {
1446
+ runSteps = 1;
1447
+ out.push({ ...req, stepCount: 1 });
1448
+ }
1449
+ }
1450
+ return out;
1451
+ }
1452
+ function attachMarkers(requests, events) {
1453
+ const markers = new Array(requests.length);
1454
+ for (const ev of events) {
1455
+ if (ev.kind !== "compaction" && ev.kind !== "prune") continue;
1456
+ for (let r = 0; r < requests.length; r++) {
1457
+ if (requests[r].seq >= ev.seq) {
1458
+ if (markers[r] === void 0) markers[r] = ev;
1459
+ break;
1460
+ }
1461
+ }
1462
+ }
1463
+ return markers;
1464
+ }
1465
+ function makeTrendChart(kit) {
1466
+ const { t, fmt: fmt3, fmtTime: fmtTime2, catLabel, eventLabel, eventAt } = kit;
1467
+ const CHART_H = 112;
1468
+ const BAR_W = 14;
1469
+ const BAR_GAP = 2;
1470
+ const TURN_FILLS = ["rgba(128,128,128,0.12)", "rgba(128,128,128,0.26)"];
1471
+ return function TrendChart(props) {
1472
+ const requests = props.requests;
1473
+ const markers = props.markers;
1474
+ const anchorOf = (req) => typeof req.prompt === "number" && req.prompt > 0 && req.total > 0 ? req.prompt / req.total : 1;
1475
+ const barTotalOf = (req) => typeof req.prompt === "number" && req.prompt > 0 ? req.prompt : req.total;
1476
+ let maxTotal = 1;
1477
+ for (const req of requests) {
1478
+ const bt = barTotalOf(req);
1479
+ if (bt > maxTotal) maxTotal = bt;
1480
+ }
1481
+ const groups = [];
1482
+ for (const req of requests) {
1483
+ let grp = groups.length > 0 ? groups[groups.length - 1] : null;
1484
+ if (grp === null || grp.turn !== (req.turn ?? 0)) {
1485
+ grp = { turn: req.turn ?? 0, count: 0, span: 0, agg: req.stepCount !== void 0 };
1486
+ groups.push(grp);
1487
+ }
1488
+ grp.count++;
1489
+ grp.span += req.stepCount ?? 1;
1490
+ }
1491
+ const scrollRef = React.useRef(null);
1492
+ const scrolledOnce = React.useRef(false);
1493
+ const lastGranRef = React.useRef(props.granularity);
1494
+ const [edges, setEdges] = React.useState({ left: false, right: false });
1495
+ const edgesRef = React.useRef(edges);
1496
+ const updateEdges = (el) => {
1497
+ const left = el.scrollLeft > 4;
1498
+ const right = el.scrollLeft + el.clientWidth < el.scrollWidth - 4;
1499
+ const prev = edgesRef.current;
1500
+ if (prev.left === left && prev.right === right) return;
1501
+ edgesRef.current = { left, right };
1502
+ setEdges({ left, right });
1503
+ };
1504
+ React.useLayoutEffect(() => {
1505
+ const el = scrollRef.current;
1506
+ if (el === null) return;
1507
+ if (props.granularity !== lastGranRef.current) {
1508
+ lastGranRef.current = props.granularity;
1509
+ scrolledOnce.current = false;
1510
+ }
1511
+ if (!scrolledOnce.current) {
1512
+ scrolledOnce.current = true;
1513
+ el.scrollLeft = el.scrollWidth;
1514
+ } else if (el.scrollLeft + el.clientWidth >= el.scrollWidth - 24) {
1515
+ el.scrollLeft = el.scrollWidth;
1516
+ }
1517
+ updateEdges(el);
1518
+ });
1519
+ const tipOf = (req) => {
1520
+ const head = req.stepCount !== void 0 && req.stepCount > 1 ? t("tip.turn", { t: req.turn ?? 0, n: req.stepCount }) : t("tip.step", { t: req.turn ?? 0, s: req.step ?? 0 });
1521
+ return head + " \xB7 " + fmtTime2(req.time) + " \xB7 " + t("tip.total", { n: fmt3(req.total) }) + (req.prompt !== void 0 ? " \xB7 " + t("tip.actual", { n: fmt3(req.prompt) }) : "");
1522
+ };
1523
+ const hoveredIdx = props.hoveredSeq !== null ? requests.findIndex((r) => r.seq === props.hoveredSeq) : -1;
1524
+ const hoveredReq = hoveredIdx >= 0 ? requests[hoveredIdx] : null;
1525
+ 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(
1526
+ "div",
1527
+ {
1528
+ className: "lc-chart-scroll" + (props.activeTurn !== null ? " lc-chart-dim" : ""),
1529
+ ref: scrollRef,
1530
+ onScroll: (e) => {
1531
+ updateEdges(e.currentTarget);
1532
+ }
1533
+ },
1534
+ edges.left ? /* @__PURE__ */ React.createElement("div", { className: "lc-chart-fade lc-chart-fade-l" }) : null,
1535
+ /* @__PURE__ */ React.createElement(
1536
+ "div",
1537
+ {
1538
+ className: "lc-chart",
1539
+ onMouseLeave: () => {
1540
+ props.onHover(null);
1541
+ }
1542
+ },
1543
+ /* @__PURE__ */ React.createElement("div", { className: "lc-grid lc-grid-top" }),
1544
+ /* @__PURE__ */ React.createElement("div", { className: "lc-grid lc-grid-mid" }),
1545
+ requests.map((req, i) => {
1546
+ const marker = markers[i];
1547
+ const markerAt = marker !== void 0 ? eventAt(marker) : null;
1548
+ const selected = props.selectedSeq === req.seq;
1549
+ const hovered = props.hoveredSeq === req.seq;
1550
+ const inTurn = props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn;
1551
+ return /* @__PURE__ */ React.createElement(
1552
+ "div",
1553
+ {
1554
+ key: req.seq,
1555
+ className: "lc-bar" + (selected ? " lc-bar-selected" : "") + (hovered ? " lc-bar-hovered" : "") + (inTurn ? " lc-bar-in-turn" : ""),
1556
+ style: { width: BAR_W + "px" },
1557
+ onClick: () => {
1558
+ props.onSelect(selected ? null : req.seq);
1559
+ },
1560
+ onMouseEnter: () => {
1561
+ props.onHover(req.seq);
1562
+ }
1563
+ },
1564
+ marker !== void 0 ? /* @__PURE__ */ React.createElement(
1565
+ "span",
1566
+ {
1567
+ className: "lc-bar-marker",
1568
+ title: "\u2702 " + (markerAt !== null ? markerAt + " \u2014 " : "") + eventLabel(marker)
1569
+ },
1570
+ "\u2702"
1571
+ ) : null,
1572
+ /* @__PURE__ */ React.createElement("div", { className: "lc-bar-stack" }, CATS.map((c) => {
1573
+ const v = (req[c.key] || 0) * anchorOf(req);
1574
+ if (!v) return null;
1575
+ return /* @__PURE__ */ React.createElement("div", { key: c.key, style: { height: Math.max(1, Math.round(v / maxTotal * CHART_H)) + "px", background: c.color } });
1576
+ }))
1577
+ );
1578
+ })
1579
+ ),
1580
+ hoveredReq !== null ? /* @__PURE__ */ React.createElement(
1581
+ "div",
1582
+ {
1583
+ className: "lc-chart-tip",
1584
+ style: { left: hoveredIdx * (BAR_W + BAR_GAP) + BAR_W / 2 + "px" }
1585
+ },
1586
+ tipOf(hoveredReq)
1587
+ ) : null,
1588
+ /* @__PURE__ */ React.createElement("div", { className: "lc-turns", onMouseLeave: () => {
1589
+ props.onHoverTurn(null);
1590
+ } }, groups.map((grp, gi) => {
1591
+ const on = props.activeTurn === grp.turn;
1592
+ const blockW = grp.agg ? BAR_W : grp.span * (BAR_W + BAR_GAP) - BAR_GAP;
1593
+ return /* @__PURE__ */ React.createElement(
1594
+ "span",
1595
+ {
1596
+ key: "turn-" + gi,
1597
+ className: "lc-turn" + (on ? " lc-turn-on" : ""),
1598
+ style: {
1599
+ width: blockW + "px",
1600
+ background: TURN_FILLS[gi % TURN_FILLS.length]
1601
+ },
1602
+ title: "T" + grp.turn,
1603
+ onMouseEnter: () => {
1604
+ props.onHoverTurn(grp.turn);
1605
+ }
1606
+ },
1607
+ "T" + grp.turn
1608
+ );
1609
+ }))
1610
+ ), edges.right ? /* @__PURE__ */ React.createElement("div", { className: "lc-chart-fade lc-chart-fade-r" }) : null);
1512
1611
  };
1513
1612
  }
1514
1613
 
@@ -1516,9 +1615,10 @@ function makeStatsBoard(kit) {
1516
1615
  var viewScroll = /* @__PURE__ */ new Map();
1517
1616
  var EVENT_KINDS = ["inject", "compaction", "prune", "model"];
1518
1617
  function makeContextView(ctx, kit) {
1519
- const { t, fmt: fmt3, fmtTime: fmtTime2, catLabel } = kit;
1618
+ const { t } = kit;
1520
1619
  const StackedBar = makeStackedBar(kit);
1521
1620
  const Legend = makeLegend(kit);
1621
+ const CurrentComposition = makeCurrentComposition(kit, StackedBar, Legend);
1522
1622
  const TrendChart = makeTrendChart(kit);
1523
1623
  const RequestDetail = makeRequestDetail(kit, StackedBar);
1524
1624
  const EventList = makeEventList(kit);
@@ -1530,6 +1630,7 @@ function makeContextView(ctx, kit) {
1530
1630
  const sessionId = props.sessionId;
1531
1631
  const data = typeof props.useProjection === "function" ? timelineOf(props.useProjection("contextTimeline")) : null;
1532
1632
  const pressure = typeof props.useProjection === "function" ? contextPressureOf(props.useProjection("contextPressure")) : null;
1633
+ const usage = typeof props.useProjection === "function" ? tokenUsageOf(props.useProjection("tokenUsage")) : null;
1533
1634
  const headers = typeof props.useProjection === "function" ? headersOf(props.useProjection("contextHeaders")) : null;
1534
1635
  const [selectedSeq, setSelectedSeq] = React.useState(null);
1535
1636
  const [hoveredSeq, setHoveredSeq] = React.useState(null);
@@ -1604,40 +1705,17 @@ function makeContextView(ctx, kit) {
1604
1705
  }
1605
1706
  }
1606
1707
  const head = headlineOf(data, pressure);
1607
- return /* @__PURE__ */ React.createElement("div", { className: "lc-root", ref: rootRef }, /* @__PURE__ */ React.createElement("div", { className: "lc-cols lc-head" }, /* @__PURE__ */ React.createElement(StatsBoard, { requests, events }), /* @__PURE__ */ React.createElement(PluginInfo, null)), /* @__PURE__ */ React.createElement("div", { className: "lc-cols" }, /* @__PURE__ */ React.createElement("div", { className: "lc-col" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, /* @__PURE__ */ React.createElement("span", { className: "lc-card-title-text" }, 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) + " tokens" : " " + t("overview.estimate")), head.pct !== null ? /* @__PURE__ */ React.createElement("span", { className: "lc-overview-pct" }, /* @__PURE__ */ React.createElement("b", null, head.pct + "%"), t("overview.used")) : 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" }, /* @__PURE__ */ React.createElement(
1608
- "button",
1609
- {
1610
- type: "button",
1611
- className: "lc-tools-label",
1612
- onClick: () => {
1613
- setToolFocus({});
1614
- }
1615
- },
1616
- t("tools.top")
1617
- ), data.toolList.slice().sort((a, b) => b.tokens - a.tokens).slice(0, 5).map((tool) => {
1618
- return /* @__PURE__ */ React.createElement(
1619
- "button",
1620
- {
1621
- key: tool.name,
1622
- type: "button",
1623
- className: "lc-tool-chip",
1624
- onClick: () => {
1625
- setToolFocus({ tool: tool.name });
1626
- }
1627
- },
1628
- tool.name + " " + fmt3(tool.tokens)
1629
- );
1630
- }), data.toolList.length > 5 ? /* @__PURE__ */ React.createElement(
1631
- "button",
1708
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-root", ref: rootRef }, /* @__PURE__ */ React.createElement("div", { className: "lc-cols lc-head" }, /* @__PURE__ */ React.createElement(StatsBoard, { requests, events, usage }), /* @__PURE__ */ React.createElement(PluginInfo, null)), /* @__PURE__ */ React.createElement("div", { className: "lc-cols" }, /* @__PURE__ */ React.createElement("div", { className: "lc-col" }, /* @__PURE__ */ React.createElement(
1709
+ CurrentComposition,
1632
1710
  {
1633
- type: "button",
1634
- className: "lc-tools-more",
1635
- onClick: () => {
1636
- setToolFocus({});
1637
- }
1638
- },
1639
- t("tools.more", { n: data.toolList.length })
1640
- ) : null) : null), /* @__PURE__ */ React.createElement("div", { className: "lc-card" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, /* @__PURE__ */ React.createElement("span", { className: "lc-card-title-text" }, t("trend.title")), /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("trend.hint")), /* @__PURE__ */ React.createElement("div", { className: "lc-gran" }, /* @__PURE__ */ React.createElement(
1711
+ head,
1712
+ subtitle: (data.model ? data.model : "") + (data.provider ? " \xB7 " + data.provider : ""),
1713
+ hoverKey: hoverCat,
1714
+ onHoverKey: setHoverCat,
1715
+ tools: data.toolList,
1716
+ onToolFocus: setToolFocus
1717
+ }
1718
+ ), /* @__PURE__ */ React.createElement("div", { className: "lc-card" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, /* @__PURE__ */ React.createElement("span", { className: "lc-card-title-text" }, t("trend.title")), /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("trend.hint")), /* @__PURE__ */ React.createElement("div", { className: "lc-gran" }, /* @__PURE__ */ React.createElement(
1641
1719
  "button",
1642
1720
  {
1643
1721
  className: "lc-gran-btn" + (granularity === "step" ? " lc-gran-on" : ""),