dsh-context 0.15.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.
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,11 +90,20 @@ 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",
94
97
  "browser.loading": "\u6B63\u5728\u4ECE\u66F4\u65E9\u7684\u4F1A\u8BDD\u5386\u53F2\u52A0\u8F7D\u5B8C\u6574\u5185\u5BB9\u2026",
95
- "browser.preview": "\u9884\u89C8"
98
+ "browser.preview": "\u9884\u89C8",
99
+ // Tool schema browser card: parsed parameter table sits above the (collapsed
100
+ // by default) raw JSON so the common case — skimming arguments — stays
101
+ // scannable; the JSON is still one click away for the full payload.
102
+ "tool.desc": "\u63CF\u8FF0",
103
+ "tool.params": "\u53C2\u6570",
104
+ "tool.paramsEmpty": "\uFF08\u65E0\u53C2\u6570\uFF09",
105
+ "tool.jsonToggle": "\u67E5\u770B\u539F\u59CB JSON",
106
+ "tool.jsonHide": "\u6536\u8D77"
96
107
  };
97
108
  var DICT_EN = {
98
109
  "tab": "Context",
@@ -107,6 +118,7 @@ var DICT_EN = {
107
118
  "overview.free": "Free Window",
108
119
  "overview.used": "of context used",
109
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",
110
122
  "stats.title": "Context Stats",
111
123
  "stats.hint": "Over the retained history window (same as the History chart)",
112
124
  "stats.turns": "Turns",
@@ -114,6 +126,7 @@ var DICT_EN = {
114
126
  "stats.injects": "Injections",
115
127
  "stats.compactions": "Compactions",
116
128
  "stats.prunes": "Prunes",
129
+ "stats.cacheHit": "Cache Hits",
117
130
  "plugin.title": "Plugin Info",
118
131
  "plugin.hint": "The best DSH context plugin \u2B50",
119
132
  "plugin.name": "Plugin",
@@ -167,7 +180,7 @@ var DICT_EN = {
167
180
  "node.empty": "(empty reply)",
168
181
  "node.nonText": "(non-text message)",
169
182
  "node.snapshot": "Snapshot: ",
170
- "cmd.desc": "View context makeup and how it evolves",
183
+ "cmd.desc": "View current context makeup, browse per-step composition",
171
184
  "cmd.close": "Close",
172
185
  "browser.title": "Context Browser",
173
186
  "browser.live": "Live (Next Request)",
@@ -175,11 +188,20 @@ var DICT_EN = {
175
188
  "browser.items": "{n} Items",
176
189
  "browser.missingLive": "\u2026 {n} earlier messages are also part of the context (outside the served window)",
177
190
  "browser.approx": "Some removed messages of this step exceed retention \u2014 the makeup below is approximate",
191
+ "browser.deltaHint": "vs previous turn",
178
192
  "browser.noHeader": "Served by an older plugin build: token estimates only, no content",
179
193
  "browser.noEpoch": "The header content (System Prompt / Tool Schemas) of this step is outside retention",
180
194
  "browser.noContent": "Full content is outside the loaded message window (load older history in Chat to view)",
181
195
  "browser.loading": "Loading full content from older session history\u2026",
182
- "browser.preview": "Preview"
196
+ "browser.preview": "Preview",
197
+ // Tool schema browser card: parsed parameter table sits above the (collapsed
198
+ // by default) raw JSON so the common case — skimming arguments — stays
199
+ // scannable; the JSON is still one click away for the full payload.
200
+ "tool.desc": "Description",
201
+ "tool.params": "Parameters",
202
+ "tool.paramsEmpty": "(no parameters)",
203
+ "tool.jsonToggle": "View Raw JSON",
204
+ "tool.jsonHide": "Collapse"
183
205
  };
184
206
 
185
207
  // src/client/modalStore.ts
@@ -294,343 +316,606 @@ function asRecord(value) {
294
316
  }
295
317
  var timelineOf = (value) => asRecord(value);
296
318
  var contextPressureOf = (value) => asRecord(value);
319
+ var tokenUsageOf = (value) => asRecord(value);
297
320
  function headersOf(value) {
298
321
  const headers = asRecord(value);
299
322
  return headers !== null && Array.isArray(headers.headers) ? headers : null;
300
323
  }
301
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
+
302
360
  // src/client/react.ts
303
361
  var React = require("react");
304
362
  var h = React.createElement;
305
363
 
306
- // src/client/components/requestDetail.tsx
307
- function makeRequestDetail(kit, StackedBar) {
308
- const { t, fmt: fmt3, fmtTime: fmtTime2, catLabel, eventLabel, eventAt } = kit;
309
- return function RequestDetail(props) {
310
- const req = props.request;
311
- if (!req) return null;
312
- const isTurn = req.stepCount !== void 0 && req.stepCount > 1;
313
- 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 });
314
- const marker = props.marker ?? null;
315
- const markerAt = marker !== null ? eventAt(marker) : null;
316
- 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) => {
317
- const v = req[c.key] || 0;
318
- 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) + "%" : ""));
319
- })));
320
- };
321
- }
322
-
323
- // src/client/components/stackedBar.tsx
324
- function makeStackedBar(kit) {
325
- const { t, fmt: fmt3, catLabel } = kit;
326
- return function StackedBar(props) {
327
- let total = 0;
328
- for (const p of props.parts) total += p.value;
329
- const scale = props.max !== void 0 && props.max > total ? props.max : total;
330
- const free = props.max !== void 0 && props.max > total ? props.max - total : 0;
331
- const usedPct = scale > 0 ? total / scale * 100 : 0;
332
- const hovering = props.hoverKey !== null && props.hoverKey !== void 0;
333
- const showBox = free > 0 && hovering;
334
- let tip = null;
335
- if (props.hoverKey !== null && props.hoverKey !== void 0) {
336
- if (props.hoverKey === "free" && free > 0) {
337
- const pct = scale > 0 ? free / scale * 100 : 0;
338
- tip = {
339
- text: t("overview.free") + " " + fmt3(free) + " (" + Math.round(pct) + "%)",
340
- leftPct: Math.max(12, Math.min(total / scale * 100 + pct / 2, 88))
341
- };
342
- } else {
343
- let acc = 0;
344
- for (const p of props.parts) {
345
- const pct = scale > 0 ? p.value / scale * 100 : 0;
346
- if (p.key === props.hoverKey && p.value > 0) {
347
- tip = {
348
- // "(pct%)" is a share of the OCCUPIED total — the dashed box
349
- // that appears on hover frames exactly this reference region.
350
- text: catLabel(p.key) + " \u2248" + fmt3(p.value) + " (" + Math.round(p.value / total * 100) + "%) " + t("overview.ofUsed"),
351
- leftPct: Math.max(12, Math.min(acc + pct / 2, 88))
352
- };
353
- break;
354
- }
355
- acc += pct;
356
- }
357
- }
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" : "");
358
370
  }
359
- return /* @__PURE__ */ React.createElement("div", { className: "lc-stacked-wrap" }, /* @__PURE__ */ React.createElement(
360
- "div",
361
- {
362
- className: "lc-stacked" + (hovering ? " lc-stacked-dim" : ""),
363
- style: { height: (props.height || 14) + "px" },
364
- onMouseLeave: () => {
365
- if (props.onHoverKey !== void 0) props.onHoverKey(null);
366
- }
367
- },
368
- total > 0 ? props.parts.map((p) => {
369
- if (!p.value) return null;
370
- const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
371
- return /* @__PURE__ */ React.createElement(
372
- "div",
373
- {
374
- key: p.key,
375
- className: "lc-stacked-seg" + (on ? " lc-stacked-seg-on" : ""),
376
- style: { width: p.value / scale * 100 + "%", background: p.color },
377
- onMouseEnter: () => {
378
- if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
379
- }
380
- }
381
- );
382
- }) : null,
383
- free > 0 ? /* @__PURE__ */ React.createElement(
384
- "div",
385
- {
386
- key: "free",
387
- className: "lc-stacked-free" + (props.hoverKey === "free" ? " lc-stacked-free-on" : ""),
388
- style: { width: free / scale * 100 + "%" },
389
- onMouseEnter: () => {
390
- if (props.onHoverKey !== void 0) props.onHoverKey("free");
391
- }
392
- }
393
- ) : null,
394
- /* @__PURE__ */ React.createElement(
395
- "div",
396
- {
397
- className: "lc-occupied-box" + (showBox ? " lc-occupied-box-on" : ""),
398
- style: { width: usedPct + "%" }
399
- }
400
- )
401
- ), props.tip !== false ? /* @__PURE__ */ React.createElement(
402
- "div",
403
- {
404
- className: "lc-bar-tip" + (tip ? " lc-bar-tip-on" : ""),
405
- style: { left: tip ? tip.leftPct + "%" : "50%" }
406
- },
407
- tip ? tip.text : ""
408
- ) : 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");
409
377
  };
410
378
  }
411
- function makeLegend(kit) {
412
- const { t, fmt: fmt3, catLabel } = kit;
413
- return function Legend(props) {
414
- let total = 0;
415
- for (const p of props.parts) total += p.value;
416
- return /* @__PURE__ */ React.createElement("div", { className: "lc-legend" }, props.parts.map((p) => {
417
- const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
418
- return /* @__PURE__ */ React.createElement(
419
- "span",
420
- {
421
- key: p.key,
422
- className: "lc-chip" + (on ? " lc-chip-on" : ""),
423
- title: t("overview.ofUsed"),
424
- onMouseEnter: () => {
425
- if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
426
- },
427
- onMouseLeave: () => {
428
- if (props.onHoverKey !== void 0) props.onHoverKey(null);
429
- }
430
- },
431
- /* @__PURE__ */ React.createElement("i", { style: { background: p.color } }),
432
- catLabel(p.key) + " \u2248" + fmt3(p.value),
433
- total > 0 ? /* @__PURE__ */ React.createElement("em", null, Math.round(p.value / total * 100) + "%") : null
434
- );
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)));
435
392
  }));
436
393
  };
437
394
  }
438
395
 
439
- // src/client/components/trendChart.tsx
440
- function aggregateByTurn(requests) {
441
- const out = [];
442
- let runSteps = 0;
443
- for (const req of requests) {
444
- const last = out.length > 0 ? out[out.length - 1] : null;
445
- if (last !== null && (last.turn ?? 0) === (req.turn ?? 0)) {
446
- runSteps++;
447
- out[out.length - 1] = { ...req, stepCount: runSteps };
448
- } else {
449
- runSteps = 1;
450
- out.push({ ...req, stepCount: 1 });
451
- }
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));
452
405
  }
453
- return out;
406
+ return parts.length > 0 ? parts.join(" | ") : null;
454
407
  }
455
- function attachMarkers(requests, events) {
456
- const markers = new Array(requests.length);
457
- for (const ev of events) {
458
- if (ev.kind !== "compaction" && ev.kind !== "prune") continue;
459
- for (let r = 0; r < requests.length; r++) {
460
- if (requests[r].seq >= ev.seq) {
461
- if (markers[r] === void 0) markers[r] = ev;
462
- break;
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 + ">";
417
+ }
418
+ return "array";
419
+ }
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 + "}";
463
425
  }
464
426
  }
427
+ if (Array.isArray(p.enum) && p.enum.length > 0) {
428
+ return t + " (enum)";
429
+ }
430
+ return t;
465
431
  }
466
- return markers;
432
+ if (Array.isArray(p.enum) && p.enum.length > 0) return "(enum)";
433
+ return "unknown";
467
434
  }
468
- function makeTrendChart(kit) {
469
- const { t, fmt: fmt3, fmtTime: fmtTime2, catLabel, eventLabel, eventAt } = kit;
470
- const CHART_H = 112;
471
- const BAR_W = 14;
472
- const BAR_GAP = 2;
473
- const TURN_FILLS = ["rgba(128,128,128,0.12)", "rgba(128,128,128,0.26)"];
474
- return function TrendChart(props) {
475
- const requests = props.requests;
476
- const markers = props.markers;
477
- const anchorOf = (req) => typeof req.prompt === "number" && req.prompt > 0 && req.total > 0 ? req.prompt / req.total : 1;
478
- const barTotalOf = (req) => typeof req.prompt === "number" && req.prompt > 0 ? req.prompt : req.total;
479
- let maxTotal = 1;
480
- for (const req of requests) {
481
- const bt = barTotalOf(req);
482
- if (bt > maxTotal) maxTotal = bt;
435
+ function paramsOf(schema) {
436
+ if (schema === null || typeof schema !== "object") return null;
437
+ const s = schema;
438
+ const candidate = (v) => v !== null && typeof v === "object" ? v : null;
439
+ const nested = candidate(s.parameters) ?? candidate(s.input_schema) ?? candidate(s.inputSchema);
440
+ if (nested !== null) return nested;
441
+ if (s.type === "object" && s.properties !== void 0 && typeof s.properties === "object") {
442
+ return s;
443
+ }
444
+ return null;
445
+ }
446
+ function ParamRow(props) {
447
+ const typeLabel = typeOf(props.schema);
448
+ const desc = props.schema.description;
449
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-ts-param-row" }, /* @__PURE__ */ React.createElement("span", { className: "lc-ts-param-name" }, props.name), /* @__PURE__ */ React.createElement("span", { className: "lc-ts-param-type" }, typeLabel), /* @__PURE__ */ React.createElement("span", { className: props.required ? "lc-ts-param-req" : "lc-ts-param-req-off" }, props.required ? "\u2713" : "\xB7"), typeof desc === "string" && desc !== "" ? /* @__PURE__ */ React.createElement("span", { className: "lc-ts-param-desc" }, desc) : null);
450
+ }
451
+ function ToolSchema(props) {
452
+ const [jsonOpen, setJsonOpen] = React.useState(false);
453
+ const params = React.useMemo(() => paramsOf(props.schema), [props.schema]);
454
+ const rows = React.useMemo(() => {
455
+ if (params === null) return [];
456
+ const props2 = params.properties;
457
+ if (props2 === null || typeof props2 !== "object") return [];
458
+ const req = Array.isArray(params.required) ? new Set(params.required.filter((x) => typeof x === "string")) : /* @__PURE__ */ new Set();
459
+ const out = [];
460
+ for (const k of Object.keys(props2)) {
461
+ const v = props2[k];
462
+ if (v === null || typeof v !== "object") continue;
463
+ out.push({ name: k, schema: v, required: req.has(k) });
483
464
  }
484
- const groups = [];
485
- for (const req of requests) {
486
- let grp = groups.length > 0 ? groups[groups.length - 1] : null;
487
- if (grp === null || grp.turn !== (req.turn ?? 0)) {
488
- grp = { turn: req.turn ?? 0, count: 0, span: 0, agg: req.stepCount !== void 0 };
489
- groups.push(grp);
465
+ return out;
466
+ }, [params]);
467
+ const schemaJson = React.useMemo(
468
+ () => props.schema !== void 0 ? JSON.stringify(props.schema, null, 2) : "",
469
+ [props.schema]
470
+ );
471
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, props.description !== void 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-ts-card" }, /* @__PURE__ */ React.createElement("div", { className: "lc-ts-card-head" }, /* @__PURE__ */ React.createElement("b", null, props.labels.desc)), /* @__PURE__ */ React.createElement("pre", { className: "lc-ts-desc-body" }, props.description)) : null, params !== null && rows.length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-ts-card" }, /* @__PURE__ */ React.createElement("div", { className: "lc-ts-card-head" }, /* @__PURE__ */ React.createElement("b", null, props.labels.title), /* @__PURE__ */ React.createElement("span", { className: "lc-ts-card-count" }, rows.length)), rows.map((r) => /* @__PURE__ */ React.createElement(ParamRow, { key: r.name, name: r.name, schema: r.schema, required: r.required }))) : params !== null ? /* @__PURE__ */ React.createElement("div", { className: "lc-ts-params-empty" }, props.labels.empty) : null, schemaJson !== "" ? /* @__PURE__ */ React.createElement("div", { className: "lc-ts-json" }, /* @__PURE__ */ React.createElement(
472
+ "button",
473
+ {
474
+ type: "button",
475
+ className: "lc-ts-json-toggle",
476
+ onClick: () => {
477
+ setJsonOpen((o) => !o);
490
478
  }
491
- grp.count++;
492
- grp.span += req.stepCount ?? 1;
479
+ },
480
+ (jsonOpen ? "\u25BE " : "\u25B8 ") + (jsonOpen ? props.labels.hide : props.labels.show)
481
+ ), jsonOpen ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre lc-br-dim" }, schemaJson) : null) : null);
482
+ }
483
+ function RawBlocks(props) {
484
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, props.blocks.map((b, i) => {
485
+ const blk = b;
486
+ if (blk !== null && typeof blk === "object" && (blk.type === "text" || blk.type === "reasoning") && typeof blk.text === "string") {
487
+ return /* @__PURE__ */ React.createElement("pre", { key: i, className: "lc-br-pre" + (blk.type === "reasoning" ? " lc-br-dim" : "") }, blk.text);
493
488
  }
494
- const scrollRef = React.useRef(null);
495
- const scrolledOnce = React.useRef(false);
496
- const lastGranRef = React.useRef(props.granularity);
497
- const [edges, setEdges] = React.useState({ left: false, right: false });
498
- const edgesRef = React.useRef(edges);
499
- const updateEdges = (el) => {
500
- const left = el.scrollLeft > 4;
501
- const right = el.scrollLeft + el.clientWidth < el.scrollWidth - 4;
502
- const prev = edgesRef.current;
503
- if (prev.left === left && prev.right === right) return;
504
- edgesRef.current = { left, right };
505
- setEdges({ left, right });
506
- };
507
- React.useLayoutEffect(() => {
508
- const el = scrollRef.current;
509
- if (el === null) return;
510
- if (props.granularity !== lastGranRef.current) {
511
- lastGranRef.current = props.granularity;
512
- scrolledOnce.current = false;
489
+ if (blk !== null && typeof blk === "object" && blk.type === "tool-result" && Array.isArray(blk.content)) {
490
+ return /* @__PURE__ */ React.createElement(RawBlocks, { key: i, blocks: blk.content });
491
+ }
492
+ return /* @__PURE__ */ React.createElement("pre", { key: i, className: "lc-br-pre lc-br-dim" }, JSON.stringify(b, null, 2));
493
+ }));
494
+ }
495
+ function NodeContent(props) {
496
+ const { node, conv } = props;
497
+ if (conv === void 0) {
498
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, node.text !== void 0 && node.text !== "" ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre" }, node.text) : null, /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, props.hint));
499
+ }
500
+ if (conv.kind === "assistant" && Array.isArray(conv.blocks)) {
501
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, conv.blocks.map((b, i) => {
502
+ const blk = b;
503
+ if (blk.kind === "text" && typeof blk.text === "string") {
504
+ return /* @__PURE__ */ React.createElement("pre", { key: i, className: "lc-br-pre" }, blk.text);
513
505
  }
514
- if (!scrolledOnce.current) {
515
- scrolledOnce.current = true;
516
- el.scrollLeft = el.scrollWidth;
517
- } else if (el.scrollLeft + el.clientWidth >= el.scrollWidth - 24) {
518
- el.scrollLeft = el.scrollWidth;
506
+ if (blk.kind === "reasoning" && typeof blk.text === "string") {
507
+ return /* @__PURE__ */ React.createElement("pre", { key: i, className: "lc-br-pre lc-br-dim" }, blk.text);
519
508
  }
520
- updateEdges(el);
521
- });
522
- const tipOf = (req) => {
523
- 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 });
524
- 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) }) : "");
509
+ if (blk.kind === "tool-call") {
510
+ return /* @__PURE__ */ React.createElement("div", { key: i, className: "lc-br-call" }, /* @__PURE__ */ React.createElement("span", { className: "lc-br-tag" }, "\u2192 " + String(blk.name ?? "?")), typeof blk.argsRaw === "string" && blk.argsRaw !== "" ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre lc-br-dim" }, blk.argsRaw) : null);
511
+ }
512
+ return null;
513
+ }));
514
+ }
515
+ if (conv.kind === "tool-result") {
516
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, conv.call != null ? /* @__PURE__ */ React.createElement("div", { className: "lc-br-call" }, /* @__PURE__ */ React.createElement("span", { className: "lc-br-tag" }, "\u2190 " + conv.call.name), conv.call.argsRaw !== "" ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre lc-br-dim" }, conv.call.argsRaw) : null) : null, Array.isArray(conv.content) ? /* @__PURE__ */ React.createElement(RawBlocks, { blocks: conv.content }) : null);
517
+ }
518
+ if (conv.kind === "compaction") {
519
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, typeof conv.summary === "string" && conv.summary !== "" ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre" }, conv.summary) : null);
520
+ }
521
+ if (Array.isArray(conv.content)) {
522
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, /* @__PURE__ */ React.createElement(RawBlocks, { blocks: conv.content }));
523
+ }
524
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, props.hint));
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
+ }
541
+ function makeContextBrowser(kit, StackedBar) {
542
+ const { t, fmt: fmt3, fmtTime: fmtTime2, catLabel } = kit;
543
+ const nodeText = makeNodeText(kit);
544
+ const catColor = {};
545
+ for (const c of CATS) catColor[c.key] = c.color;
546
+ const MAX_AUTO_PAGES = 20;
547
+ return function ContextBrowser(props) {
548
+ const { data, headers } = props;
549
+ const [sel, setSel] = React.useState("live");
550
+ const [openCat, setOpenCat] = React.useState(null);
551
+ const [openElem, setOpenElem] = React.useState(null);
552
+ const convNodes = typeof props.useSession === "function" ? props.useSession((s) => s.nodes) : void 0;
553
+ const bySeq = React.useMemo(() => {
554
+ const m = /* @__PURE__ */ new Map();
555
+ for (const n of convNodes ?? []) m.set(n.seq, n);
556
+ return m;
557
+ }, [convNodes]);
558
+ const hasMore = typeof props.useSession === "function" ? props.useSession((s) => s.hasMore === true) : false;
559
+ const loadingOlder = typeof props.useSession === "function" ? props.useSession((s) => s.loadingOlder === true) : false;
560
+ const openSeq = openElem !== null && openElem.startsWith("n") ? Number(openElem.slice(1)) : null;
561
+ const missingSeq = openSeq !== null && !bySeq.has(openSeq) ? openSeq : null;
562
+ const [exhausted, setExhausted] = React.useState(false);
563
+ const pagesRef = React.useRef(0);
564
+ React.useEffect(() => {
565
+ pagesRef.current = 0;
566
+ setExhausted(false);
567
+ }, [openElem]);
568
+ const loadOlderHistory = props.loadOlderHistory;
569
+ React.useEffect(() => {
570
+ if (missingSeq === null || !hasMore || loadingOlder || exhausted) return;
571
+ if (loadOlderHistory === void 0) return;
572
+ if (pagesRef.current >= MAX_AUTO_PAGES) {
573
+ setExhausted(true);
574
+ return;
575
+ }
576
+ pagesRef.current += 1;
577
+ void loadOlderHistory();
578
+ }, [missingSeq, hasMore, loadingOlder, exhausted, bySeq, loadOlderHistory]);
579
+ React.useEffect(() => {
580
+ if (!hasMore && missingSeq !== null && !exhausted) setExhausted(true);
581
+ }, [hasMore, missingSeq, exhausted]);
582
+ const pinSeq = props.pinSeq;
583
+ React.useEffect(() => {
584
+ setSel(pinSeq === null || pinSeq === void 0 ? "live" : pinSeq);
585
+ setOpenCat(null);
586
+ setOpenElem(null);
587
+ }, [pinSeq]);
588
+ const toolFocus = props.toolFocus;
589
+ React.useEffect(() => {
590
+ if (toolFocus === null || toolFocus === void 0) return;
591
+ setSel("live");
592
+ setOpenCat("tools");
593
+ setOpenElem(toolFocus.tool !== void 0 ? "tool:" + toolFocus.tool : null);
594
+ if (props.onToolFocusHandled !== void 0) props.onToolFocusHandled();
595
+ }, [toolFocus, props.onToolFocusHandled]);
596
+ const awaiting = missingSeq !== null && !exhausted && loadOlderHistory !== void 0 && hasMore;
597
+ const requests = data.requests || [];
598
+ const hoverReq = props.previewSeq !== null && props.previewSeq !== void 0 ? requests.find((r) => r.seq === props.previewSeq) ?? null : null;
599
+ const req = hoverReq ?? (sel === "live" ? null : requests.find((r) => r.seq === sel) ?? null);
600
+ const seq = req !== null ? req.seq : null;
601
+ const linked = req === null && props.onHoverKey !== void 0;
602
+ const linkKey = linked && props.hoverKey !== null && props.hoverKey !== "free" ? props.hoverKey : null;
603
+ const view = assemble(data, headers, seq);
604
+ const breakdown = req !== null ? req : data.current;
605
+ const parts = partsOf(breakdown);
606
+ const total = breakdown.total;
607
+ const pick = (v) => {
608
+ setSel(v === "live" ? "live" : Number(v));
609
+ setOpenCat(null);
610
+ setOpenElem(null);
525
611
  };
526
- const hoveredIdx = props.hoveredSeq !== null ? requests.findIndex((r) => r.seq === props.hoveredSeq) : -1;
527
- const hoveredReq = hoveredIdx >= 0 ? requests[hoveredIdx] : null;
528
- 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(
529
- "div",
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);
616
+ const toggleCat = (c) => {
617
+ const openable = toolCount(c) > 0 || (c === "system" || c === "tools") && view.header === null;
618
+ if (!openable) return;
619
+ setOpenCat(openCat === c ? null : c);
620
+ setOpenElem(null);
621
+ };
622
+ const toggleElem = (key) => setOpenElem(openElem === key ? null : key);
623
+ const elemRow = (key, tag, preview, tokens, time, body) => {
624
+ const open = openElem === key;
625
+ return /* @__PURE__ */ React.createElement("div", { key, className: "lc-br-elem" + (open ? " lc-br-elem-on" : "") }, /* @__PURE__ */ React.createElement("button", { type: "button", className: "lc-br-elem-row", onClick: () => {
626
+ toggleElem(key);
627
+ } }, /* @__PURE__ */ React.createElement("span", { className: "lc-br-chev" + (open ? " lc-br-chev-on" : "") }, "\u25B8"), tag !== null ? /* @__PURE__ */ React.createElement("span", { className: "lc-br-tag" }, tag) : null, /* @__PURE__ */ React.createElement("span", { className: "lc-br-preview" }, preview), time !== void 0 ? /* @__PURE__ */ React.createElement("span", { className: "lc-br-time" }, fmtTime2(time)) : null, /* @__PURE__ */ React.createElement("span", { className: "lc-br-tokens" }, "\u2248" + fmt3(tokens))), open ? body : null);
628
+ };
629
+ const catBody = (c) => {
630
+ if (c === "system") {
631
+ if (view.header === null) return /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, t(headers === null ? "browser.noHeader" : "browser.noEpoch"));
632
+ const system = view.header.system;
633
+ if (system === void 0) return null;
634
+ return elemRow(
635
+ "sys",
636
+ null,
637
+ system.replace(/\s+/g, " ").trim().slice(0, 80),
638
+ breakdown.system,
639
+ void 0,
640
+ /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre" }, system))
641
+ );
642
+ }
643
+ if (c === "tools") {
644
+ if (view.header === null) return /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, t(headers === null ? "browser.noHeader" : "browser.noEpoch"));
645
+ const labels = {
646
+ desc: t("tool.desc"),
647
+ title: t("tool.params"),
648
+ empty: t("tool.paramsEmpty"),
649
+ show: t("tool.jsonToggle"),
650
+ hide: t("tool.jsonHide")
651
+ };
652
+ return view.header.tools.slice().sort((a, b) => b.tokens - a.tokens).map((tool) => {
653
+ return elemRow(
654
+ "tool:" + tool.name,
655
+ null,
656
+ tool.name,
657
+ tool.tokens,
658
+ void 0,
659
+ /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, /* @__PURE__ */ React.createElement(ToolSchema, { description: tool.description, schema: tool.schema, labels }))
660
+ );
661
+ });
662
+ }
663
+ const nodes = (byCat[c] ?? []).slice().reverse();
664
+ return nodes.map((n) => {
665
+ let tag = null;
666
+ let preview = nodeText(n);
667
+ if (n.cat === "tool") {
668
+ tag = (n.tool ?? "?") + (n.err ? " \u26A0" : "");
669
+ preview = t("node.toolResult") + (n.err ? " \u26A0" : "");
670
+ } else if (n.cat === "inject" && !n.skill) {
671
+ tag = t("form." + (n.form || "context"));
672
+ if (n.text !== void 0 && n.text !== "") {
673
+ preview = n.form === "snapshot" ? t("node.snapshot") + n.text : n.text;
674
+ }
675
+ }
676
+ return elemRow(
677
+ "n" + n.seq,
678
+ tag,
679
+ preview,
680
+ n.tokens,
681
+ n.time,
682
+ /* @__PURE__ */ React.createElement(
683
+ NodeContent,
684
+ {
685
+ node: n,
686
+ conv: bySeq.get(n.seq),
687
+ hint: bySeq.get(n.seq) === void 0 && awaiting ? t("browser.loading") : t("browser.noContent")
688
+ }
689
+ )
690
+ );
691
+ });
692
+ };
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(
694
+ "select",
530
695
  {
531
- className: "lc-chart-scroll" + (props.activeTurn !== null ? " lc-chart-dim" : ""),
532
- ref: scrollRef,
533
- onScroll: (e) => {
534
- updateEdges(e.currentTarget);
696
+ className: "lc-br-pick",
697
+ value: seq === null ? "live" : String(seq),
698
+ onChange: (e) => {
699
+ pick(e.target.value);
535
700
  }
536
701
  },
537
- edges.left ? /* @__PURE__ */ React.createElement("div", { className: "lc-chart-fade lc-chart-fade-l" }) : null,
538
- /* @__PURE__ */ React.createElement(
539
- "div",
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",
540
724
  {
541
- className: "lc-chart",
542
- onMouseLeave: () => {
543
- props.onHover(null);
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);
544
735
  }
545
736
  },
546
- /* @__PURE__ */ React.createElement("div", { className: "lc-grid lc-grid-top" }),
547
- /* @__PURE__ */ React.createElement("div", { className: "lc-grid lc-grid-mid" }),
548
- requests.map((req, i) => {
549
- const marker = markers[i];
550
- const markerAt = marker !== void 0 ? eventAt(marker) : null;
551
- const selected = props.selectedSeq === req.seq;
552
- const hovered = props.hoveredSeq === req.seq;
553
- const inTurn = props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn;
554
- return /* @__PURE__ */ React.createElement(
555
- "div",
556
- {
557
- key: req.seq,
558
- className: "lc-bar" + (selected ? " lc-bar-selected" : "") + (hovered ? " lc-bar-hovered" : "") + (inTurn ? " lc-bar-in-turn" : ""),
559
- style: { width: BAR_W + "px" },
560
- onClick: () => {
561
- props.onSelect(selected ? null : req.seq);
562
- },
563
- onMouseEnter: () => {
564
- props.onHover(req.seq);
565
- }
566
- },
567
- marker !== void 0 ? /* @__PURE__ */ React.createElement(
568
- "span",
569
- {
570
- className: "lc-bar-marker",
571
- title: "\u2702 " + (markerAt !== null ? markerAt + " \u2014 " : "") + eventLabel(marker)
572
- },
573
- "\u2702"
574
- ) : null,
575
- /* @__PURE__ */ React.createElement("div", { className: "lc-bar-stack" }, CATS.map((c) => {
576
- const v = (req[c.key] || 0) * anchorOf(req);
577
- if (!v) return null;
578
- return /* @__PURE__ */ React.createElement("div", { key: c.key, style: { height: Math.max(1, Math.round(v / maxTotal * CHART_H)) + "px", background: c.color } });
579
- }))
580
- );
581
- })
582
- ),
583
- hoveredReq !== null ? /* @__PURE__ */ React.createElement(
584
- "div",
585
- {
586
- className: "lc-chart-tip",
587
- style: { left: hoveredIdx * (BAR_W + BAR_GAP) + BAR_W / 2 + "px" }
588
- },
589
- tipOf(hoveredReq)
590
- ) : null,
591
- /* @__PURE__ */ React.createElement("div", { className: "lc-turns", onMouseLeave: () => {
592
- props.onHoverTurn(null);
593
- } }, groups.map((grp, gi) => {
594
- const on = props.activeTurn === grp.turn;
595
- const blockW = grp.agg ? BAR_W : grp.span * (BAR_W + BAR_GAP) - BAR_GAP;
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;
596
807
  return /* @__PURE__ */ React.createElement(
597
- "span",
808
+ "div",
598
809
  {
599
- key: "turn-" + gi,
600
- className: "lc-turn" + (on ? " lc-turn-on" : ""),
601
- style: {
602
- width: blockW + "px",
603
- background: TURN_FILLS[gi % TURN_FILLS.length]
604
- },
605
- title: "T" + grp.turn,
810
+ key: p.key,
811
+ className: "lc-stacked-seg" + (on ? " lc-stacked-seg-on" : ""),
812
+ style: { width: p.value / scale * 100 + "%", background: p.color },
606
813
  onMouseEnter: () => {
607
- props.onHoverTurn(grp.turn);
814
+ if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
608
815
  }
609
- },
610
- "T" + grp.turn
816
+ }
611
817
  );
612
- }))
613
- ), edges.right ? /* @__PURE__ */ React.createElement("div", { className: "lc-chart-fade lc-chart-fade-r" }) : null);
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);
614
900
  };
615
901
  }
616
902
 
617
903
  // src/client/components/contextModal.tsx
618
- var TREND_TURNS = 10;
619
904
  function makeContextModal(ctx, kit) {
620
- const { t, fmt: fmt3 } = kit;
905
+ const { t } = kit;
621
906
  const sessions = ctx.get("sessions");
622
907
  const StackedBar = makeStackedBar(kit);
623
908
  const Legend = makeLegend(kit);
624
- const TrendChart = makeTrendChart(kit);
625
- const RequestDetail = makeRequestDetail(kit, StackedBar);
909
+ const CurrentComposition = makeCurrentComposition(kit, StackedBar, Legend);
910
+ const ContextBrowser = makeContextBrowser(kit, StackedBar);
626
911
  return function ContextModal(props) {
627
912
  const sessionId = typeof props.sessionId === "string" ? props.sessionId : "";
628
913
  const open = typeof props.useContextModal === "function" ? props.useContextModal((s) => s) : false;
629
914
  const data = typeof props.useProjection === "function" ? timelineOf(props.useProjection("contextTimeline")) : null;
630
915
  const pressure = typeof props.useProjection === "function" ? contextPressureOf(props.useProjection("contextPressure")) : null;
631
- const [selectedSeq, setSelectedSeq] = React.useState(null);
632
- const [hoveredSeq, setHoveredSeq] = React.useState(null);
916
+ const headers = typeof props.useProjection === "function" ? headersOf(props.useProjection("contextHeaders")) : null;
633
917
  const [hoverCat, setHoverCat] = React.useState(null);
918
+ const [toolFocus, setToolFocus] = React.useState(null);
634
919
  const close = React.useCallback(() => {
635
920
  if (sessionId === "") return;
636
921
  modalStoreOf(sessionId).set(false);
@@ -655,40 +940,35 @@ function makeContextModal(ctx, kit) {
655
940
  };
656
941
  }, [open, close]);
657
942
  if (!open) return null;
658
- const requests = data !== null ? data.requests || [] : [];
659
- const events = data !== null ? data.events || [] : [];
660
- const turns = aggregateByTurn(requests).slice(-TREND_TURNS);
661
- const markers = attachMarkers(turns, events);
662
- let pinnedReq = null;
663
- for (const req of turns) if (req.seq === selectedSeq) pinnedReq = req;
664
- let activeReq = null;
665
- if (hoveredSeq !== null) {
666
- for (const req of turns) if (req.seq === hoveredSeq) activeReq = req;
667
- }
668
- if (activeReq === null) activeReq = pinnedReq;
669
- if (activeReq === null && turns.length > 0) activeReq = turns[turns.length - 1];
670
- const markerOf = (req) => {
671
- const i = turns.indexOf(req);
672
- return i >= 0 ? markers[i] : void 0;
673
- };
674
943
  const head = data !== null ? headlineOf(data, pressure) : null;
944
+ const subtitle = data !== null ? (data.model ? data.model : "") + (data.provider ? " \xB7 " + data.provider : "") : "";
675
945
  return /* @__PURE__ */ React.createElement("div", { className: "lc-modal-backdrop", onClick: close }, /* @__PURE__ */ React.createElement("div", { className: "lc-modal-card", onClick: (ev) => {
676
946
  ev.stopPropagation();
677
- } }, /* @__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(
678
- TrendChart,
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,
679
949
  {
680
- requests: turns,
681
- markers,
682
- selectedSeq: pinnedReq !== null ? pinnedReq.seq : null,
683
- hoveredSeq,
684
- activeTurn: null,
685
- granularity: "turn",
686
- onSelect: setSelectedSeq,
687
- onHover: setHoveredSeq,
688
- onHoverTurn: () => {
950
+ head,
951
+ subtitle,
952
+ hoverKey: hoverCat,
953
+ onHoverKey: setHoverCat,
954
+ tools: data.toolList,
955
+ onToolFocus: setToolFocus
956
+ }
957
+ ), /* @__PURE__ */ React.createElement(
958
+ ContextBrowser,
959
+ {
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);
689
969
  }
690
970
  }
691
- ), /* @__PURE__ */ React.createElement(RequestDetail, { request: activeReq, marker: activeReq !== null ? markerOf(activeReq) : void 0 })))));
971
+ ))));
692
972
  };
693
973
  }
694
974
 
@@ -718,12 +998,14 @@ var STYLES = [
718
998
  ".lc-stacked-wrap { position: relative; width: 100%; }",
719
999
  ".lc-stacked { display: flex; width: 100%; border-radius: 5px; overflow: hidden; background: rgba(128,128,128,0.18); position: relative; }",
720
1000
  // Hover reference frame around the OCCUPIED region of the composition bar:
721
- // dashes from the left edge to the used/window boundary, so the legend's
722
- // "share of used" percentages visibly map to the boxed part (the free track
723
- // sits outside it). pointer-events: none keeps hover on the segments/free.
724
- // Deliberately high-contrast (label-primary) and thick so the frame reads at
725
- // a glance, and the other parts dim underneath it (`.lc-stacked-dim`).
726
- ".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; }",
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; }",
727
1009
  ".lc-occupied-box-on { opacity: 1; }",
728
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; }",
729
1011
  ".lc-bar-tip-on { opacity: 1; }",
@@ -732,9 +1014,18 @@ var STYLES = [
732
1014
  // overview or the mirrored browser bar) glides in and out — same 120ms
733
1015
  // rhythm as the rest of the view.
734
1016
  ".lc-stacked-seg { transition: filter 120ms ease, opacity 120ms ease; }",
735
- ".lc-stacked-free { transition: box-shadow 120ms ease, opacity 120ms ease; }",
1017
+ ".lc-stacked-free { box-sizing: border-box; transition: box-shadow 120ms ease, opacity 120ms ease; }",
736
1018
  ".lc-stacked-seg-on { filter: brightness(1.18); }",
737
- ".lc-stacked-free-on { box-shadow: inset 0 0 0 1px var(--dsw-alias-label-secondary); border-radius: 3px; }",
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; }",
738
1029
  // Hover focus: everything except the hovered part (segment, legend chip, or
739
1030
  // free track) recedes, so the composition highlight and the occupied-region
740
1031
  // frame read clearly. The selected segment/free keeps full opacity.
@@ -742,8 +1033,19 @@ var STYLES = [
742
1033
  ".lc-stacked-dim .lc-stacked-seg-on { opacity: 1; }",
743
1034
  ".lc-stacked-dim .lc-stacked-free { opacity: 0.35; }",
744
1035
  ".lc-stacked-dim .lc-stacked-free-on { opacity: 1; }",
745
- ".lc-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 10px; }",
746
- ".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; }",
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; }",
747
1049
  ".lc-chip i, .lc-detail-row i, .lc-node i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }",
748
1050
  ".lc-chip i { transition: box-shadow 120ms ease; }",
749
1051
  ".lc-chip em { font-style: normal; color: var(--dsw-alias-label-secondary); }",
@@ -753,7 +1055,14 @@ var STYLES = [
753
1055
  ".lc-chip-on { font-weight: 600; background: var(--dsw-alias-interactive-bg-hover); }",
754
1056
  ".lc-chip-on i { box-shadow: 0 0 0 1px var(--dsw-alias-brand-primary); }",
755
1057
  ".lc-tools { margin-top: 10px; color: var(--dsw-alias-label-secondary); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }",
756
- ".lc-tool-chip { background: var(--dsw-alias-bg-layer-2); border-radius: 4px; padding: 1px 7px; font-size: 12px; color: var(--dsw-alias-label-primary); }",
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; }",
757
1066
  ".lc-chartrow { display: flex; gap: 6px; align-items: stretch; }",
758
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; }",
759
1068
  ".lc-axis span { position: absolute; right: 0; line-height: 1; }",
@@ -848,368 +1157,108 @@ var STYLES = [
848
1157
  ".lc-kind-inject { background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent); color: var(--dsw-alias-state-success-primary); }",
849
1158
  ".lc-kind-compaction { background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 15%, transparent); color: var(--dsw-alias-state-error-primary); }",
850
1159
  ".lc-kind-prune { background: color-mix(in srgb, #8b5cf6 15%, transparent); color: #8b5cf6; }",
851
- ".lc-kind-model { background: color-mix(in srgb, var(--dsw-alias-brand-primary) 15%, transparent); color: var(--dsw-alias-brand-primary); }",
852
- ".lc-event-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
853
- ".lc-event-at { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; white-space: nowrap; }",
854
- ".lc-event-tokens { color: var(--dsw-alias-state-success-primary); font-weight: 600; white-space: nowrap; }",
855
- ".lc-event-tokens.lc-up { color: var(--dsw-alias-state-warn-primary); }",
856
- ".lc-event-time { color: var(--dsw-alias-label-secondary); font-size: 12px; }",
857
- ".lc-node { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
858
- ".lc-node-preview { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); }",
859
- ".lc-node-time { color: var(--dsw-alias-label-secondary); font-size: 12px; min-width: 54px; text-align: right; }",
860
- ".lc-node-tokens { color: var(--dsw-alias-label-secondary); }",
861
- ".lc-nodes-more { color: var(--dsw-alias-label-secondary); padding: 3px 0; }",
862
- ".lc-empty { color: var(--dsw-alias-label-secondary); padding: 18px 0; text-align: center; }",
863
- ".lc-foot { color: var(--dsw-alias-label-secondary); font-size: 12px; margin-top: 4px; }",
864
- // ---- /context modal (centered dialog; escapes the composer anchor via fixed positioning) ----
865
- ".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; }",
866
- ".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; }",
867
- ".lc-modal-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 12px; }",
868
- ".lc-modal-title { font-weight: 600; font-size: 14px; }",
869
- ".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; }",
870
- ".lc-modal-close:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); }",
871
- ".lc-modal-trend { margin-top: 14px; }",
872
- // ---- Context browser card (progressive disclosure: category accordion ->
873
- // element rows -> per-kind content) ----
874
- ".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; }",
875
- ".lc-br-meta { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 8px; color: var(--dsw-alias-label-secondary); }",
876
- ".lc-br-meta b { color: var(--dsw-alias-label-primary); }",
877
- ".lc-br-meta .lc-actual { color: var(--dsw-alias-state-success-primary); }",
878
- ".lc-br-note { margin-top: 8px; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
879
- ".lc-br-cats { margin-top: 10px; display: flex; flex-direction: column; gap: 4px; }",
880
- ".lc-br-cat { border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; background: var(--dsw-alias-bg-layer-2); overflow: hidden; }",
881
- ".lc-br-cat-empty { opacity: 0.55; }",
882
- ".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; }",
883
- ".lc-br-cat-row:hover { background: var(--dsw-alias-interactive-bg-hover); }",
884
- // Current-composition hover echo: the category row lit by the shared hover
885
- // link (hovering the Current Composition card's bar/legend) looks hovered
886
- // too same tint as the physical hover, plus a ring on its color dot.
887
- ".lc-br-cat-on { background: var(--dsw-alias-interactive-bg-hover); }",
888
- ".lc-br-cat-on i { box-shadow: 0 0 0 1px var(--dsw-alias-brand-primary); }",
889
- ".lc-br-cat-row i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; flex: none; transition: box-shadow 120ms ease; }",
890
- ".lc-br-cat-label { font-weight: 600; }",
891
- ".lc-br-cat-count { color: var(--dsw-alias-label-secondary); font-size: 12px; flex: 1; }",
892
- ".lc-br-chev { flex: none; width: 12px; color: var(--dsw-alias-label-secondary); transition: transform 120ms ease; }",
893
- ".lc-br-chev-on { transform: rotate(90deg); }",
894
- ".lc-br-tokens { flex: none; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
895
- ".lc-br-pct { flex: none; width: 36px; text-align: right; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
896
- ".lc-br-body { border-top: 1px solid var(--dsw-alias-border-l1); padding: 4px 6px; display: flex; flex-direction: column; gap: 2px; }",
897
- ".lc-br-elem { border-radius: 6px; }",
898
- ".lc-br-elem-on { background: var(--dsw-alias-interactive-bg-active); }",
899
- ".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; }",
900
- ".lc-br-elem-row:hover { background: var(--dsw-alias-interactive-bg-hover); }",
901
- ".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; }",
902
- ".lc-br-preview { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
903
- ".lc-br-time { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; }",
904
- ".lc-br-content { padding: 2px 6px 8px 26px; display: flex; flex-direction: column; gap: 6px; }",
905
- ".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; }",
906
- ".lc-br-dim { color: var(--dsw-alias-label-secondary); }",
907
- ".lc-br-call { display: flex; flex-direction: column; gap: 4px; align-items: flex-start; }"
908
- ].join("\n");
909
-
910
- // src/client/assemble.ts
911
- function headerAt(headers, seq) {
912
- if (headers === null || headers.headers.length === 0) return null;
913
- if (seq === null) return headers.headers[headers.headers.length - 1];
914
- for (let i = headers.headers.length - 1; i >= 0; i--) {
915
- if (headers.headers[i].seq < seq) return headers.headers[i];
916
- }
917
- return null;
918
- }
919
- function assemble(data, headers, seq) {
920
- const live = seq === null;
921
- let nodes;
922
- if (live) {
923
- nodes = data.nodes.slice();
924
- } else {
925
- const picked = [];
926
- for (const n of data.nodes) {
927
- if (n.seq < seq) picked.push(n);
928
- }
929
- for (const n of data.archive || []) {
930
- if (n.seq < seq && n.gone !== void 0 && n.gone > seq) picked.push(n);
931
- }
932
- nodes = picked;
933
- }
934
- nodes.sort((a, b) => a.seq - b.seq);
935
- let missingLive = 0;
936
- if (data.droppedNodes > 0) {
937
- if (live || data.surfaceFloor !== void 0 && seq > data.surfaceFloor) {
938
- missingLive = data.droppedNodes;
939
- }
940
- }
941
- const approximate = !live && data.archiveFloor !== void 0 && seq < data.archiveFloor;
942
- return { live, header: headerAt(headers, seq), nodes, missingLive, approximate };
943
- }
944
-
945
- // src/client/components/nodes.tsx
946
- function makeNodeText(kit) {
947
- const { t } = kit;
948
- return function nodeText(n) {
949
- if (n.cat === "tool") {
950
- return t("node.toolResult") + (n.tool ? " \u2190 " + n.tool : "") + (n.err ? " \u26A0" : "");
951
- }
952
- if (n.skill) return "Skill: " + n.skill;
953
- if (n.calls) return t("node.calls") + n.calls.join(", ");
954
- if (n.text) return n.form === "snapshot" ? t("node.snapshot") + n.text : n.text;
955
- if (n.cat === "assistant") return t("node.empty");
956
- if (n.cat === "inject") return t("form." + (n.form || "context"));
957
- return t("node.nonText");
958
- };
959
- }
960
- function makeNodeList(kit) {
961
- const { t, fmt: fmt3, fmtTime: fmtTime2 } = kit;
962
- const nodeText = makeNodeText(kit);
963
- return function NodeList(props) {
964
- if (props.nodes.length === 0) {
965
- return /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("nodes.empty"));
966
- }
967
- const catColor = {};
968
- for (const c of CATS) catColor[c.key] = c.color;
969
- const rows = props.nodes.slice().reverse();
970
- 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) => {
971
- const text = nodeText(n);
972
- 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)));
973
- }));
974
- };
975
- }
976
-
977
- // src/client/components/browser.tsx
978
- function RawBlocks(props) {
979
- return /* @__PURE__ */ React.createElement(React.Fragment, null, props.blocks.map((b, i) => {
980
- const blk = b;
981
- if (blk !== null && typeof blk === "object" && (blk.type === "text" || blk.type === "reasoning") && typeof blk.text === "string") {
982
- return /* @__PURE__ */ React.createElement("pre", { key: i, className: "lc-br-pre" + (blk.type === "reasoning" ? " lc-br-dim" : "") }, blk.text);
983
- }
984
- if (blk !== null && typeof blk === "object" && blk.type === "tool-result" && Array.isArray(blk.content)) {
985
- return /* @__PURE__ */ React.createElement(RawBlocks, { key: i, blocks: blk.content });
986
- }
987
- return /* @__PURE__ */ React.createElement("pre", { key: i, className: "lc-br-pre lc-br-dim" }, JSON.stringify(b, null, 2));
988
- }));
989
- }
990
- function NodeContent(props) {
991
- const { node, conv } = props;
992
- if (conv === void 0) {
993
- return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, node.text !== void 0 && node.text !== "" ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre" }, node.text) : null, /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, props.hint));
994
- }
995
- if (conv.kind === "assistant" && Array.isArray(conv.blocks)) {
996
- return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, conv.blocks.map((b, i) => {
997
- const blk = b;
998
- if (blk.kind === "text" && typeof blk.text === "string") {
999
- return /* @__PURE__ */ React.createElement("pre", { key: i, className: "lc-br-pre" }, blk.text);
1000
- }
1001
- if (blk.kind === "reasoning" && typeof blk.text === "string") {
1002
- return /* @__PURE__ */ React.createElement("pre", { key: i, className: "lc-br-pre lc-br-dim" }, blk.text);
1003
- }
1004
- if (blk.kind === "tool-call") {
1005
- return /* @__PURE__ */ React.createElement("div", { key: i, className: "lc-br-call" }, /* @__PURE__ */ React.createElement("span", { className: "lc-br-tag" }, "\u2192 " + String(blk.name ?? "?")), typeof blk.argsRaw === "string" && blk.argsRaw !== "" ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre lc-br-dim" }, blk.argsRaw) : null);
1006
- }
1007
- return null;
1008
- }));
1009
- }
1010
- if (conv.kind === "tool-result") {
1011
- return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, conv.call != null ? /* @__PURE__ */ React.createElement("div", { className: "lc-br-call" }, /* @__PURE__ */ React.createElement("span", { className: "lc-br-tag" }, "\u2190 " + conv.call.name), conv.call.argsRaw !== "" ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre lc-br-dim" }, conv.call.argsRaw) : null) : null, Array.isArray(conv.content) ? /* @__PURE__ */ React.createElement(RawBlocks, { blocks: conv.content }) : null);
1012
- }
1013
- if (conv.kind === "compaction") {
1014
- return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, typeof conv.summary === "string" && conv.summary !== "" ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre" }, conv.summary) : null);
1015
- }
1016
- if (Array.isArray(conv.content)) {
1017
- return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, /* @__PURE__ */ React.createElement(RawBlocks, { blocks: conv.content }));
1018
- }
1019
- return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, props.hint));
1020
- }
1021
- function makeContextBrowser(kit, StackedBar) {
1022
- const { t, fmt: fmt3, fmtTime: fmtTime2, catLabel } = kit;
1023
- const nodeText = makeNodeText(kit);
1024
- const catColor = {};
1025
- for (const c of CATS) catColor[c.key] = c.color;
1026
- const MAX_AUTO_PAGES = 20;
1027
- return function ContextBrowser(props) {
1028
- var _a;
1029
- const { data, headers } = props;
1030
- const [sel, setSel] = React.useState("live");
1031
- const [openCat, setOpenCat] = React.useState(null);
1032
- const [openElem, setOpenElem] = React.useState(null);
1033
- const convNodes = typeof props.useSession === "function" ? props.useSession((s) => s.nodes) : void 0;
1034
- const bySeq = React.useMemo(() => {
1035
- const m = /* @__PURE__ */ new Map();
1036
- for (const n of convNodes ?? []) m.set(n.seq, n);
1037
- return m;
1038
- }, [convNodes]);
1039
- const hasMore = typeof props.useSession === "function" ? props.useSession((s) => s.hasMore === true) : false;
1040
- const loadingOlder = typeof props.useSession === "function" ? props.useSession((s) => s.loadingOlder === true) : false;
1041
- const openSeq = openElem !== null && openElem.startsWith("n") ? Number(openElem.slice(1)) : null;
1042
- const missingSeq = openSeq !== null && !bySeq.has(openSeq) ? openSeq : null;
1043
- const [exhausted, setExhausted] = React.useState(false);
1044
- const pagesRef = React.useRef(0);
1045
- React.useEffect(() => {
1046
- pagesRef.current = 0;
1047
- setExhausted(false);
1048
- }, [openElem]);
1049
- const loadOlderHistory = props.loadOlderHistory;
1050
- React.useEffect(() => {
1051
- if (missingSeq === null || !hasMore || loadingOlder || exhausted) return;
1052
- if (loadOlderHistory === void 0) return;
1053
- if (pagesRef.current >= MAX_AUTO_PAGES) {
1054
- setExhausted(true);
1055
- return;
1056
- }
1057
- pagesRef.current += 1;
1058
- void loadOlderHistory();
1059
- }, [missingSeq, hasMore, loadingOlder, exhausted, bySeq, loadOlderHistory]);
1060
- React.useEffect(() => {
1061
- if (!hasMore && missingSeq !== null && !exhausted) setExhausted(true);
1062
- }, [hasMore, missingSeq, exhausted]);
1063
- const pinSeq = props.pinSeq;
1064
- React.useEffect(() => {
1065
- setSel(pinSeq === null || pinSeq === void 0 ? "live" : pinSeq);
1066
- setOpenCat(null);
1067
- setOpenElem(null);
1068
- }, [pinSeq]);
1069
- const awaiting = missingSeq !== null && !exhausted && loadOlderHistory !== void 0 && hasMore;
1070
- const requests = data.requests || [];
1071
- const hoverReq = props.previewSeq !== null && props.previewSeq !== void 0 ? requests.find((r) => r.seq === props.previewSeq) ?? null : null;
1072
- const req = hoverReq ?? (sel === "live" ? null : requests.find((r) => r.seq === sel) ?? null);
1073
- const seq = req !== null ? req.seq : null;
1074
- const linked = req === null && props.onHoverKey !== void 0;
1075
- const linkKey = linked && props.hoverKey !== null && props.hoverKey !== "free" ? props.hoverKey : null;
1076
- const view = assemble(data, headers, seq);
1077
- const breakdown = req !== null ? req : data.current;
1078
- const parts = partsOf(breakdown);
1079
- const total = breakdown.total;
1080
- const pick = (v) => {
1081
- setSel(v === "live" ? "live" : Number(v));
1082
- setOpenCat(null);
1083
- setOpenElem(null);
1084
- };
1085
- const byCat = {};
1086
- for (const n of view.nodes) (byCat[_a = n.cat] ?? (byCat[_a] = [])).push(n);
1087
- const toolCount = (c) => {
1088
- if (c === "system") return view.header !== null && view.header.system !== void 0 ? 1 : 0;
1089
- if (c === "tools") return view.header !== null ? view.header.tools.length : 0;
1090
- return byCat[c]?.length ?? 0;
1091
- };
1092
- const toggleCat = (c) => {
1093
- const openable = toolCount(c) > 0 || (c === "system" || c === "tools") && view.header === null;
1094
- if (!openable) return;
1095
- setOpenCat(openCat === c ? null : c);
1096
- setOpenElem(null);
1097
- };
1098
- const toggleElem = (key) => setOpenElem(openElem === key ? null : key);
1099
- const elemRow = (key, tag, preview, tokens, time, body) => {
1100
- const open = openElem === key;
1101
- return /* @__PURE__ */ React.createElement("div", { key, className: "lc-br-elem" + (open ? " lc-br-elem-on" : "") }, /* @__PURE__ */ React.createElement("button", { type: "button", className: "lc-br-elem-row", onClick: () => {
1102
- toggleElem(key);
1103
- } }, /* @__PURE__ */ React.createElement("span", { className: "lc-br-chev" + (open ? " lc-br-chev-on" : "") }, "\u25B8"), tag !== null ? /* @__PURE__ */ React.createElement("span", { className: "lc-br-tag" }, tag) : null, /* @__PURE__ */ React.createElement("span", { className: "lc-br-preview" }, preview), time !== void 0 ? /* @__PURE__ */ React.createElement("span", { className: "lc-br-time" }, fmtTime2(time)) : null, /* @__PURE__ */ React.createElement("span", { className: "lc-br-tokens" }, "\u2248" + fmt3(tokens))), open ? body : null);
1104
- };
1105
- const catBody = (c) => {
1106
- if (c === "system") {
1107
- if (view.header === null) return /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, t(headers === null ? "browser.noHeader" : "browser.noEpoch"));
1108
- const system = view.header.system;
1109
- if (system === void 0) return null;
1110
- return elemRow(
1111
- "sys",
1112
- null,
1113
- system.replace(/\s+/g, " ").trim().slice(0, 80),
1114
- breakdown.system,
1115
- void 0,
1116
- /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre" }, system))
1117
- );
1118
- }
1119
- if (c === "tools") {
1120
- if (view.header === null) return /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, t(headers === null ? "browser.noHeader" : "browser.noEpoch"));
1121
- return view.header.tools.map((tool) => {
1122
- const schema = tool.schema !== void 0 ? JSON.stringify(tool.schema, null, 2) : "";
1123
- return elemRow(
1124
- "tool:" + tool.name,
1125
- null,
1126
- tool.name,
1127
- tool.tokens,
1128
- void 0,
1129
- /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, tool.description !== void 0 ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre" }, tool.description) : null, schema !== "" ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre lc-br-dim" }, schema) : null)
1130
- );
1131
- });
1132
- }
1133
- const nodes = byCat[c] ?? [];
1134
- return nodes.map((n) => {
1135
- let tag = null;
1136
- let preview = nodeText(n);
1137
- if (n.cat === "tool") {
1138
- tag = (n.tool ?? "?") + (n.err ? " \u26A0" : "");
1139
- preview = t("node.toolResult") + (n.err ? " \u26A0" : "");
1140
- } else if (n.cat === "inject" && !n.skill) {
1141
- tag = t("form." + (n.form || "context"));
1142
- if (n.text !== void 0 && n.text !== "") {
1143
- preview = n.form === "snapshot" ? t("node.snapshot") + n.text : n.text;
1144
- }
1145
- }
1146
- return elemRow(
1147
- "n" + n.seq,
1148
- tag,
1149
- preview,
1150
- n.tokens,
1151
- n.time,
1152
- /* @__PURE__ */ React.createElement(
1153
- NodeContent,
1154
- {
1155
- node: n,
1156
- conv: bySeq.get(n.seq),
1157
- hint: bySeq.get(n.seq) === void 0 && awaiting ? t("browser.loading") : t("browser.noContent")
1158
- }
1159
- )
1160
- );
1161
- });
1162
- };
1163
- 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(
1164
- "select",
1165
- {
1166
- className: "lc-br-pick",
1167
- value: seq === null ? "live" : String(seq),
1168
- onChange: (e) => {
1169
- pick(e.target.value);
1170
- }
1171
- },
1172
- /* @__PURE__ */ React.createElement("option", { value: "live" }, t("browser.live")),
1173
- 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)))
1174
- )), /* @__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(
1175
- StackedBar,
1176
- {
1177
- parts,
1178
- height: 10,
1179
- hoverKey: linked ? linkKey : void 0,
1180
- onHoverKey: linked ? props.onHoverKey : void 0,
1181
- tip: false
1182
- }
1183
- )), 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) => {
1184
- const count = toolCount(c.key);
1185
- const v = breakdown[c.key] || 0;
1186
- const openable = count > 0 || (c.key === "system" || c.key === "tools") && view.header === null;
1187
- const open = openCat === c.key && openable;
1188
- return /* @__PURE__ */ React.createElement("div", { key: c.key, className: "lc-br-cat" + (openable ? "" : " lc-br-cat-empty") }, /* @__PURE__ */ React.createElement(
1189
- "button",
1190
- {
1191
- type: "button",
1192
- className: "lc-br-cat-row" + (linked && props.hoverKey === c.key ? " lc-br-cat-on" : ""),
1193
- onMouseEnter: linked ? () => {
1194
- if (props.onHoverKey !== void 0) props.onHoverKey(c.key);
1195
- } : void 0,
1196
- onMouseLeave: linked ? () => {
1197
- if (props.onHoverKey !== void 0) props.onHoverKey(null);
1198
- } : void 0,
1199
- onClick: () => {
1200
- toggleCat(c.key);
1201
- }
1202
- },
1203
- /* @__PURE__ */ React.createElement("span", { className: "lc-br-chev" + (open ? " lc-br-chev-on" : "") }, "\u25B8"),
1204
- /* @__PURE__ */ React.createElement("i", { style: { background: c.color } }),
1205
- /* @__PURE__ */ React.createElement("span", { className: "lc-br-cat-label" }, catLabel(c.key)),
1206
- /* @__PURE__ */ React.createElement("span", { className: "lc-br-cat-count" }, t("browser.items", { n: count })),
1207
- /* @__PURE__ */ React.createElement("span", { className: "lc-br-tokens" }, "\u2248" + fmt3(v)),
1208
- /* @__PURE__ */ React.createElement("span", { className: "lc-br-pct" }, total > 0 ? Math.round(v / total * 100) + "%" : "")
1209
- ), open ? /* @__PURE__ */ React.createElement("div", { className: "lc-br-body" }, catBody(c.key)) : null);
1210
- })));
1211
- };
1212
- }
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");
1213
1262
 
1214
1263
  // src/client/components/events.tsx
1215
1264
  var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
@@ -1288,7 +1337,7 @@ function makeEventList(kit) {
1288
1337
 
1289
1338
  // src/client/meta.ts
1290
1339
  var PLUGIN_NAME = "dsh-context";
1291
- var PLUGIN_VERSION = true ? "0.15.0" : "0.0.0-dev";
1340
+ var PLUGIN_VERSION = true ? "0.17.0" : "0.0.0-dev";
1292
1341
  var PLUGIN_REPO = true ? "https://github.com/bowenliang123/dsh-context" : "https://github.com/bowenliang123/dsh-context";
1293
1342
  var PLUGIN_REPO_SHORT = PLUGIN_REPO.replace(/^https?:\/\/github\.com\//, "");
1294
1343
 
@@ -1340,7 +1389,30 @@ function makePluginInfo(kit) {
1340
1389
  };
1341
1390
  }
1342
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
+
1343
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
+ }
1344
1416
  function makeStatsBoard(kit) {
1345
1417
  const { t, fmt: fmt3 } = kit;
1346
1418
  return function StatsBoard(props) {
@@ -1355,8 +1427,187 @@ function makeStatsBoard(kit) {
1355
1427
  else if (ev.kind === "prune") prunes++;
1356
1428
  else if (ev.kind === "inject") injects++;
1357
1429
  }
1430
+ const hit = props.usage !== null ? cacheHitPercent(props.usage) : null;
1358
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));
1359
- 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);
1360
1611
  };
1361
1612
  }
1362
1613
 
@@ -1364,9 +1615,10 @@ function makeStatsBoard(kit) {
1364
1615
  var viewScroll = /* @__PURE__ */ new Map();
1365
1616
  var EVENT_KINDS = ["inject", "compaction", "prune", "model"];
1366
1617
  function makeContextView(ctx, kit) {
1367
- const { t, fmt: fmt3, fmtTime: fmtTime2, catLabel } = kit;
1618
+ const { t } = kit;
1368
1619
  const StackedBar = makeStackedBar(kit);
1369
1620
  const Legend = makeLegend(kit);
1621
+ const CurrentComposition = makeCurrentComposition(kit, StackedBar, Legend);
1370
1622
  const TrendChart = makeTrendChart(kit);
1371
1623
  const RequestDetail = makeRequestDetail(kit, StackedBar);
1372
1624
  const EventList = makeEventList(kit);
@@ -1378,6 +1630,7 @@ function makeContextView(ctx, kit) {
1378
1630
  const sessionId = props.sessionId;
1379
1631
  const data = typeof props.useProjection === "function" ? timelineOf(props.useProjection("contextTimeline")) : null;
1380
1632
  const pressure = typeof props.useProjection === "function" ? contextPressureOf(props.useProjection("contextPressure")) : null;
1633
+ const usage = typeof props.useProjection === "function" ? tokenUsageOf(props.useProjection("tokenUsage")) : null;
1381
1634
  const headers = typeof props.useProjection === "function" ? headersOf(props.useProjection("contextHeaders")) : null;
1382
1635
  const [selectedSeq, setSelectedSeq] = React.useState(null);
1383
1636
  const [hoveredSeq, setHoveredSeq] = React.useState(null);
@@ -1393,6 +1646,10 @@ function makeContextView(ctx, kit) {
1393
1646
  return p.length === 1 ? [...EVENT_KINDS] : p.filter((x) => x !== k);
1394
1647
  });
1395
1648
  };
1649
+ const [toolFocus, setToolFocus] = React.useState(null);
1650
+ const clearToolFocus = React.useCallback(() => {
1651
+ setToolFocus(null);
1652
+ }, []);
1396
1653
  const rootRef = React.useRef(null);
1397
1654
  const scrollerRef = React.useRef(null);
1398
1655
  const restoredRef = React.useRef(null);
@@ -1448,9 +1705,17 @@ function makeContextView(ctx, kit) {
1448
1705
  }
1449
1706
  }
1450
1707
  const head = headlineOf(data, pressure);
1451
- 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" }, t("tools.top"), data.toolList.slice().sort((a, b) => b.tokens - a.tokens).slice(0, 5).map((tool) => {
1452
- return /* @__PURE__ */ React.createElement("span", { key: tool.name, className: "lc-tool-chip" }, tool.name + " " + fmt3(tool.tokens));
1453
- }), data.toolList.length > 5 ? /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, " " + t("tools.more", { n: data.toolList.length })) : 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(
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,
1710
+ {
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(
1454
1719
  "button",
1455
1720
  {
1456
1721
  className: "lc-gran-btn" + (granularity === "step" ? " lc-gran-on" : ""),
@@ -1492,7 +1757,9 @@ function makeContextView(ctx, kit) {
1492
1757
  previewSeq: hoveredSeq,
1493
1758
  pinSeq: pinnedReq !== null ? pinnedReq.seq : null,
1494
1759
  hoverKey: hoverCat,
1495
- onHoverKey: setHoverCat
1760
+ onHoverKey: setHoverCat,
1761
+ toolFocus,
1762
+ onToolFocusHandled: clearToolFocus
1496
1763
  }
1497
1764
  ))), /* @__PURE__ */ React.createElement("div", { className: "lc-cols" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card lc-col" }, /* @__PURE__ */ React.createElement("div", { className: "lc-card-title" }, /* @__PURE__ */ React.createElement("span", { className: "lc-card-title-text" }, t("events.title")), /* @__PURE__ */ React.createElement("div", { className: "lc-kinds" }, EVENT_KINDS.map((k) => /* @__PURE__ */ React.createElement(
1498
1765
  "button",