dsh-context 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/LICENSE +201 -21
  2. package/README.md +12 -6
  3. package/lib/client.js +648 -497
  4. package/lib/index.js +239 -279
  5. package/package.json +11 -3
package/lib/client.js CHANGED
@@ -1,562 +1,713 @@
1
- /* dsh-context client bundle — generated by scripts/build.mjs from src/client.js */
1
+ /* dsh-context client bundle — generated by scripts/build.mjs from src/client.ts */
2
2
  window.__ModuleLoader__.load({
3
3
  id: "dsh-context",
4
4
  factory: (require) => {
5
5
  var module = { exports: {} };
6
6
  var exports = module.exports;
7
- /**
8
- * dsh-context — Client half (installed package bundle).
9
- *
10
- * Registers a "上下文/Context" tab in the conversation view ring
11
- * (`conversation.view` slot, beside Chat/Trajectory) and renders the
12
- * context-composition timeline served by the Host half over the generic
13
- * Connection RPC channel `/dsh-context`: current makeup, per-request
14
- * stacked-bar history, context events, and the live message list.
15
- *
16
- * This file is the body of the package's `./client` bundle: build.mjs wraps
17
- * it into the web boot handoff (`window.__ModuleLoader__.load({id, factory})`),
18
- * so it runs inside the browser module table. React arrives via the injected
19
- * `require` (a platform seed word), UI text is bilingual (zh/en) through the
20
- * client `locale` service, and the Host sends structured event/node records
21
- * which this half localizes.
22
- */
23
-
24
- var React = require('react')
25
- var h = React.createElement
26
-
27
- var DICT_ZH = {
28
- 'tab': '上下文',
29
- 'cat.system': '系统提示', 'cat.tools': '工具定义', 'cat.user': '用户消息',
30
- 'cat.inject': '注入上下文', 'cat.assistant': '助手回复', 'cat.tool': '工具结果',
31
- 'overview.title': '当前构成',
32
- 'overview.ofWindow': 'tokens(约 {p}%)',
33
- 'overview.estimate': 'tokens(估算)',
34
- 'tools.top': '工具定义 Top:',
35
- 'tools.more': '等 {n} 个',
36
- 'trend.title': '历史趋势',
37
- 'trend.hint': '每次模型请求一段;点击柱子查看详情,✂ 表示压缩/剪枝',
38
- 'trend.empty': '发起一轮对话后,这里会展示每次模型请求的上下文构成',
39
- 'detail.step': 'T{t} · 第 {s} 步',
40
- 'detail.estTotal': '估算合计 {n}',
41
- 'detail.actual': '实际 prompt {n}',
42
- 'detail.output': '输出 {n}',
43
- 'events.title': '上下文事件',
44
- 'events.empty': '暂无上下文事件(压缩、注入、模型切换会出现在这里)',
45
- 'nodes.title': '消息构成',
46
- 'nodes.hint': '当前模型可见的消息,最新在前',
47
- 'nodes.more': '… 更早的 {n} 条消息已省略',
48
- 'nodes.empty': '当前没有模型可见的消息',
49
- 'loading': '正在读取会话日志…',
50
- 'error': '上下文数据读取失败:',
51
- 'footer': '估算口径:与 dsh 内置 tokenMeter 相同的固定密度启发式(约 4 字符 ≈ 1 token);「实际」为供应商上报用量。',
52
- 'tip.step': 'T{t} · 第{s}步',
53
- 'tip.total': '合计 ≈ {n}',
54
- 'tip.actual': '(实际 {n})',
55
- 'ev.compaction': '压缩上下文(摘要替换 {n} 条消息)',
56
- 'ev.prune': '剪枝工具输出',
57
- 'ev.skill': 'Skill 注入({name})',
58
- 'ev.model': '模型切换:{a} → {b}',
59
- 'form.instructions': '指令注入', 'form.catalog': '目录更新', 'form.snapshot': '状态快照',
60
- 'form.notice': '通知', 'form.relay': '代理转发', 'form.recall': '历史召回', 'form.context': '上下文注入',
61
- 'node.toolResult': '工具结果',
62
- 'node.calls': '调用 ',
63
- 'node.empty': '(空回复)',
64
- 'node.nonText': '(非文本消息)',
65
- 'node.snapshot': '快照: ',
66
- }
67
-
68
- var DICT_EN = {
69
- 'tab': 'Context',
70
- 'cat.system': 'System', 'cat.tools': 'Tool schemas', 'cat.user': 'User',
71
- 'cat.inject': 'Injected', 'cat.assistant': 'Assistant', 'cat.tool': 'Tool results',
72
- 'overview.title': 'Current composition',
73
- 'overview.ofWindow': 'tokens (~{p}%)',
74
- 'overview.estimate': 'tokens (estimated)',
75
- 'tools.top': 'Top tool schemas:',
76
- 'tools.more': 'of {n}',
77
- 'trend.title': 'History',
78
- 'trend.hint': 'one bar per model request; click a bar for details, ✂ marks compaction/prune',
79
- 'trend.empty': 'Send a message and each model request’s context makeup shows up here',
80
- 'detail.step': 'T{t} · step {s}',
81
- 'detail.estTotal': 'estimated {n}',
82
- 'detail.actual': 'actual prompt {n}',
83
- 'detail.output': 'output {n}',
84
- 'events.title': 'Context events',
85
- 'events.empty': 'No context events yet (compaction, injections, model switches appear here)',
86
- 'nodes.title': 'Messages',
87
- 'nodes.hint': 'currently model-visible, newest first',
88
- 'nodes.more': '… {n} earlier messages omitted',
89
- 'nodes.empty': 'No model-visible messages right now',
90
- 'loading': 'Reading the session log…',
91
- 'error': 'Failed to read context data: ',
92
- 'footer': 'Estimate: same fixed-density heuristic as dsh’s built-in tokenMeter (~4 chars ≈ 1 token); “actual” is provider-reported usage.',
93
- 'tip.step': 'T{t} · step {s}',
94
- 'tip.total': 'total {n}',
95
- 'tip.actual': ' (actual {n})',
96
- 'ev.compaction': 'Context compacted (summary replaced {n} messages)',
97
- 'ev.prune': 'Tool output pruned',
98
- 'ev.skill': 'Skill injected ({name})',
99
- 'ev.model': 'Model switched: {a} → {b}',
100
- 'form.instructions': 'Instructions', 'form.catalog': 'Catalog update', 'form.snapshot': 'State snapshot',
101
- 'form.notice': 'Notice', 'form.relay': 'Agent relay', 'form.recall': 'Recall', 'form.context': 'Context injection',
102
- 'node.toolResult': 'Tool result',
103
- 'node.calls': 'calls ',
104
- 'node.empty': '(empty reply)',
105
- 'node.nonText': '(non-text message)',
106
- 'node.snapshot': 'snapshot: ',
107
- }
108
-
109
- var EVENT_ICONS = { compaction: '✂', prune: '✂', inject: '+', model: '⇄' }
110
-
7
+ "use strict";
8
+ const React = require("react");
9
+ const h = React.createElement;
10
+ const DICT_ZH = {
11
+ "tab": "\u4E0A\u4E0B\u6587",
12
+ "cat.system": "\u7CFB\u7EDF\u63D0\u793A",
13
+ "cat.tools": "\u5DE5\u5177\u5B9A\u4E49",
14
+ "cat.user": "\u7528\u6237\u6D88\u606F",
15
+ "cat.inject": "\u6CE8\u5165\u4E0A\u4E0B\u6587",
16
+ "cat.assistant": "\u52A9\u624B\u56DE\u590D",
17
+ "cat.tool": "\u5DE5\u5177\u7ED3\u679C",
18
+ "overview.title": "\u5F53\u524D\u6784\u6210",
19
+ "overview.ofWindow": "tokens\uFF08\u7EA6 {p}%\uFF09",
20
+ "overview.estimate": "tokens\uFF08\u4F30\u7B97\uFF09",
21
+ "tools.top": "\u5DE5\u5177\u5B9A\u4E49 Top\uFF1A",
22
+ "tools.more": "\u7B49 {n} \u4E2A",
23
+ "trend.title": "\u5386\u53F2\u8D8B\u52BF",
24
+ "gran.step": "\u6B65\u9AA4",
25
+ "gran.turn": "\u8F6E\u6B21",
26
+ "trend.hint": "\u6BCF\u6B21\u6A21\u578B\u8BF7\u6C42\u4E00\u6BB5\uFF1B\u70B9\u51FB\u67F1\u5B50\u67E5\u770B\u8BE6\u60C5\uFF0C\u2702 \u8868\u793A\u538B\u7F29/\u526A\u679D",
27
+ "trend.empty": "\u53D1\u8D77\u4E00\u8F6E\u5BF9\u8BDD\u540E\uFF0C\u8FD9\u91CC\u4F1A\u5C55\u793A\u6BCF\u6B21\u6A21\u578B\u8BF7\u6C42\u7684\u4E0A\u4E0B\u6587\u6784\u6210",
28
+ "detail.step": "T{t} \xB7 \u7B2C {s} \u6B65",
29
+ "detail.estTotal": "\u4F30\u7B97\u5408\u8BA1 \u2248 {n}",
30
+ "detail.actual": "\u5B9E\u9645 prompt {n}",
31
+ "detail.output": "\u8F93\u51FA {n}",
32
+ "events.title": "\u4E0A\u4E0B\u6587\u4E8B\u4EF6",
33
+ "events.empty": "\u6682\u65E0\u4E0A\u4E0B\u6587\u4E8B\u4EF6\uFF08\u538B\u7F29\u3001\u6CE8\u5165\u3001\u6A21\u578B\u5207\u6362\u4F1A\u51FA\u73B0\u5728\u8FD9\u91CC\uFF09",
34
+ "nodes.title": "\u6D88\u606F\u6784\u6210",
35
+ "nodes.hint": "\u5F53\u524D\u6A21\u578B\u53EF\u89C1\u7684\u6D88\u606F\uFF0C\u6700\u65B0\u5728\u524D",
36
+ "nodes.more": "\u2026 \u66F4\u65E9\u7684 {n} \u6761\u6D88\u606F\u5DF2\u7701\u7565",
37
+ "nodes.empty": "\u5F53\u524D\u6CA1\u6709\u6A21\u578B\u53EF\u89C1\u7684\u6D88\u606F",
38
+ "loading": "\u6B63\u5728\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7\u2026",
39
+ "error": "\u4E0A\u4E0B\u6587\u6570\u636E\u8BFB\u53D6\u5931\u8D25\uFF1A",
40
+ "footer": "\u4F30\u7B97\u53E3\u5F84\uFF1A\u4E0E dsh \u5185\u7F6E tokenMeter \u76F8\u540C\u7684\u56FA\u5B9A\u5BC6\u5EA6\u542F\u53D1\u5F0F\uFF08\u7EA6 4 \u5B57\u7B26 \u2248 1 token\uFF09\uFF1B\u300C\u5B9E\u9645\u300D\u4E3A\u4F9B\u5E94\u5546\u4E0A\u62A5\u7528\u91CF\u3002",
41
+ "tip.step": "T{t} \xB7 \u7B2C{s}\u6B65",
42
+ "tip.total": "\u5408\u8BA1 \u2248 {n}",
43
+ "tip.actual": "\uFF08\u5B9E\u9645 {n}\uFF09",
44
+ "ev.compaction": "\u538B\u7F29\u4E0A\u4E0B\u6587\uFF08\u6458\u8981\u66FF\u6362 {n} \u6761\u6D88\u606F\uFF09",
45
+ "ev.prune": "\u526A\u679D\u5DE5\u5177\u8F93\u51FA",
46
+ "ev.skill": "Skill \u6CE8\u5165\uFF08{name}\uFF09",
47
+ "ev.model": "\u6A21\u578B\u5207\u6362\uFF1A{a} \u2192 {b}",
48
+ "form.instructions": "\u6307\u4EE4\u6CE8\u5165",
49
+ "form.catalog": "\u76EE\u5F55\u66F4\u65B0",
50
+ "form.snapshot": "\u72B6\u6001\u5FEB\u7167",
51
+ "form.notice": "\u901A\u77E5",
52
+ "form.relay": "\u4EE3\u7406\u8F6C\u53D1",
53
+ "form.recall": "\u5386\u53F2\u53EC\u56DE",
54
+ "form.context": "\u4E0A\u4E0B\u6587\u6CE8\u5165",
55
+ "node.toolResult": "\u5DE5\u5177\u7ED3\u679C",
56
+ "node.calls": "\u8C03\u7528 ",
57
+ "node.empty": "(\u7A7A\u56DE\u590D)",
58
+ "node.nonText": "(\u975E\u6587\u672C\u6D88\u606F)",
59
+ "node.snapshot": "\u5FEB\u7167: "
60
+ };
61
+ const DICT_EN = {
62
+ "tab": "Context",
63
+ "cat.system": "System",
64
+ "cat.tools": "Tool schemas",
65
+ "cat.user": "User",
66
+ "cat.inject": "Injected",
67
+ "cat.assistant": "Assistant",
68
+ "cat.tool": "Tool results",
69
+ "overview.title": "Current composition",
70
+ "overview.ofWindow": "tokens (~{p}%)",
71
+ "overview.estimate": "tokens (estimated)",
72
+ "tools.top": "Top tool schemas:",
73
+ "tools.more": "of {n}",
74
+ "trend.title": "History",
75
+ "gran.step": "Step",
76
+ "gran.turn": "Turn",
77
+ "trend.hint": "one bar per model request; click a bar for details, \u2702 marks compaction/prune",
78
+ "trend.empty": "Send a message and each model request\u2019s context makeup shows up here",
79
+ "detail.step": "T{t} \xB7 step {s}",
80
+ "detail.estTotal": "estimated \u2248 {n}",
81
+ "detail.actual": "actual prompt {n}",
82
+ "detail.output": "output {n}",
83
+ "events.title": "Context events",
84
+ "events.empty": "No context events yet (compaction, injections, model switches appear here)",
85
+ "nodes.title": "Messages",
86
+ "nodes.hint": "currently model-visible, newest first",
87
+ "nodes.more": "\u2026 {n} earlier messages omitted",
88
+ "nodes.empty": "No model-visible messages right now",
89
+ "loading": "Reading the session log\u2026",
90
+ "error": "Failed to read context data: ",
91
+ "footer": "Estimate: same fixed-density heuristic as dsh\u2019s built-in tokenMeter (~4 chars \u2248 1 token); \u201Cactual\u201D is provider-reported usage.",
92
+ "tip.step": "T{t} \xB7 step {s}",
93
+ "tip.total": "total \u2248 {n}",
94
+ "tip.actual": " (actual {n})",
95
+ "ev.compaction": "Context compacted (summary replaced {n} messages)",
96
+ "ev.prune": "Tool output pruned",
97
+ "ev.skill": "Skill injected ({name})",
98
+ "ev.model": "Model switched: {a} \u2192 {b}",
99
+ "form.instructions": "Instructions",
100
+ "form.catalog": "Catalog update",
101
+ "form.snapshot": "State snapshot",
102
+ "form.notice": "Notice",
103
+ "form.relay": "Agent relay",
104
+ "form.recall": "Recall",
105
+ "form.context": "Context injection",
106
+ "node.toolResult": "Tool result",
107
+ "node.calls": "calls ",
108
+ "node.empty": "(empty reply)",
109
+ "node.nonText": "(non-text message)",
110
+ "node.snapshot": "snapshot: "
111
+ };
112
+ const EVENT_ICONS = { compaction: "\u2702", prune: "\u2702", inject: "\uFF0B", model: "\u21C4" };
111
113
  function fmt(n) {
112
- if (n === undefined || n === null || isNaN(n)) return '—'
113
- if (n >= 1000) return (n / 1000).toFixed(1) + 'k'
114
- return String(Math.round(n))
114
+ if (n === void 0 || n === null || isNaN(n)) return "\u2014";
115
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
116
+ return String(Math.round(n));
115
117
  }
116
-
117
118
  function fmtTime(t) {
118
- var d = new Date(t)
119
- function p(x) { return (x < 10 ? '0' : '') + x }
120
- return p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())
119
+ const d = new Date(t);
120
+ function p(x) {
121
+ return (x < 10 ? "0" : "") + x;
122
+ }
123
+ return p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds());
124
+ }
125
+ function aggregateByTurn(requests) {
126
+ const out = [];
127
+ for (const req of requests) {
128
+ const last = out.length > 0 ? out[out.length - 1] : null;
129
+ if (last !== null && (last.turn ?? 0) === (req.turn ?? 0)) out[out.length - 1] = req;
130
+ else out.push(req);
131
+ }
132
+ return out;
121
133
  }
122
-
123
134
  function makeView(ctx, t) {
124
135
  function tr(key, vars) {
125
- return t(key, vars)
136
+ return t(key, vars);
137
+ }
138
+ const CATS = [
139
+ { key: "system", color: "#6366f1" },
140
+ { key: "tools", color: "#f59e0b" },
141
+ { key: "user", color: "#22c55e" },
142
+ { key: "inject", color: "#a855f7" },
143
+ { key: "assistant", color: "#3b82f6" },
144
+ { key: "tool", color: "#14b8a6" }
145
+ ];
146
+ function catLabel(key) {
147
+ return t("cat." + key);
126
148
  }
127
-
128
- var CATS = [
129
- { key: 'system', color: '#6366f1' },
130
- { key: 'tools', color: '#f59e0b' },
131
- { key: 'user', color: '#22c55e' },
132
- { key: 'inject', color: '#a855f7' },
133
- { key: 'assistant', color: '#3b82f6' },
134
- { key: 'tool', color: '#14b8a6' },
135
- ]
136
-
137
- function catLabel(key) { return t('cat.' + key) }
138
-
139
149
  function eventLabel(ev) {
140
- if (ev.kind === 'compaction') return tr('ev.compaction', { n: ev.count || 0 })
141
- if (ev.kind === 'prune') return t('ev.prune')
142
- if (ev.kind === 'model') return tr('ev.model', { a: ev.from || '?', b: ev.to || '?' })
143
- if (ev.kind === 'inject') {
144
- if (ev.sub === 'skill') return tr('ev.skill', { name: ev.name || '?' })
145
- var base = t('form.' + (ev.form || 'context'))
146
- return ev.name ? base + ' · ' + ev.name : base
150
+ if (ev.kind === "compaction") return tr("ev.compaction", { n: ev.count || 0 });
151
+ if (ev.kind === "prune") return t("ev.prune");
152
+ if (ev.kind === "model") return tr("ev.model", { a: ev.from || "?", b: ev.to || "?" });
153
+ if (ev.kind === "inject") {
154
+ if (ev.sub === "skill") return tr("ev.skill", { name: ev.name || "?" });
155
+ const base = t("form." + (ev.form || "context"));
156
+ return ev.name ? base + " \xB7 " + ev.name : base;
147
157
  }
148
- return ev.kind
158
+ return ev.kind;
149
159
  }
150
-
151
160
  function nodeText(n) {
152
- if (n.cat === 'tool') {
153
- return t('node.toolResult') + (n.tool ? ' ' + n.tool : '') + (n.err ? ' ⚠' : '')
161
+ if (n.cat === "tool") {
162
+ return t("node.toolResult") + (n.tool ? " \u2190 " + n.tool : "") + (n.err ? " \u26A0" : "");
154
163
  }
155
- if (n.skill) return 'Skill: ' + n.skill
156
- if (n.calls) return t('node.calls') + n.calls.join(', ')
157
- if (n.text) return n.form === 'snapshot' ? t('node.snapshot') + n.text : n.text
158
- if (n.cat === 'assistant') return t('node.empty')
159
- if (n.cat === 'inject') return t('form.' + (n.form || 'context'))
160
- return t('node.nonText')
164
+ if (n.skill) return "Skill: " + n.skill;
165
+ if (n.calls) return t("node.calls") + n.calls.join(", ");
166
+ if (n.text) return n.form === "snapshot" ? t("node.snapshot") + n.text : n.text;
167
+ if (n.cat === "assistant") return t("node.empty");
168
+ if (n.cat === "inject") return t("form." + (n.form || "context"));
169
+ return t("node.nonText");
161
170
  }
162
-
163
171
  function StackedBar(props) {
164
- // props.parts: [{key,color,value}]; optional props.max: when max exceeds
165
- // the parts' total, the remainder shows as empty track.
166
- var total = 0
167
- for (var i = 0; i < props.parts.length; i++) total += props.parts[i].value
168
- var scale = props.max !== undefined && props.max > total ? props.max : total
169
- return h('div', { className: 'lc-stacked', style: { height: (props.height || 14) + 'px' } },
170
- total <= 0
171
- ? null
172
- : props.parts.map(function (p) {
173
- if (!p.value) return null
174
- return h('div', {
172
+ let total = 0;
173
+ for (const p of props.parts) total += p.value;
174
+ const scale = props.max !== void 0 && props.max > total ? props.max : total;
175
+ const wrapRef = React.useRef(null);
176
+ const [tip, setTip] = React.useState(null);
177
+ const showTip = (e, p) => {
178
+ const rect = wrapRef.current?.getBoundingClientRect();
179
+ const left = rect ? e.clientX - rect.left : 50;
180
+ const width = rect?.width ?? 200;
181
+ setTip({
182
+ key: p.key,
183
+ text: catLabel(p.key) + " " + fmt(p.value) + " (" + Math.round(p.value / total * 100) + "%)",
184
+ left: Math.max(48, Math.min(left, width - 48))
185
+ });
186
+ };
187
+ return h(
188
+ "div",
189
+ { className: "lc-stacked-wrap" },
190
+ h(
191
+ "div",
192
+ {
193
+ className: "lc-stacked",
194
+ style: { height: (props.height || 14) + "px" },
195
+ ref: wrapRef,
196
+ onMouseLeave: () => {
197
+ setTip(null);
198
+ }
199
+ },
200
+ total <= 0 ? null : props.parts.map((p) => {
201
+ if (!p.value) return null;
202
+ return h("div", {
175
203
  key: p.key,
176
- title: catLabel(p.key) + ' ' + fmt(p.value) + ' (' + Math.round(p.value / total * 100) + '%)',
177
- style: { width: (p.value / scale * 100) + '%', background: p.color },
178
- })
179
- }))
204
+ style: { width: p.value / scale * 100 + "%", background: p.color },
205
+ onMouseEnter: (e) => {
206
+ showTip(e, p);
207
+ },
208
+ onMouseMove: (e) => {
209
+ showTip(e, p);
210
+ }
211
+ });
212
+ })
213
+ ),
214
+ tip ? h("div", { className: "lc-bar-tip", style: { left: tip.left + "px" } }, tip.text) : null
215
+ );
180
216
  }
181
-
182
217
  function Legend(props) {
183
- var total = 0
184
- for (var i = 0; i < props.parts.length; i++) total += props.parts[i].value
185
- return h('div', { className: 'lc-legend' },
186
- props.parts.map(function (p) {
187
- return h('span', { key: p.key, className: 'lc-chip' },
188
- h('i', { style: { background: p.color } }),
189
- catLabel(p.key) + ' ' + fmt(p.value),
190
- total > 0 ? h('em', null, Math.round(p.value / total * 100) + '%') : null)
191
- }))
218
+ let total = 0;
219
+ for (const p of props.parts) total += p.value;
220
+ return h(
221
+ "div",
222
+ { className: "lc-legend" },
223
+ props.parts.map((p) => {
224
+ return h(
225
+ "span",
226
+ { key: p.key, className: "lc-chip" },
227
+ h("i", { style: { background: p.color } }),
228
+ catLabel(p.key) + " " + fmt(p.value),
229
+ total > 0 ? h("em", null, Math.round(p.value / total * 100) + "%") : null
230
+ );
231
+ })
232
+ );
192
233
  }
193
-
194
234
  function partsOf(breakdown) {
195
- return CATS.map(function (c) {
196
- return { key: c.key, color: c.color, value: breakdown[c.key] || 0 }
197
- })
235
+ return CATS.map((c) => {
236
+ return { key: c.key, color: c.color, value: breakdown[c.key] || 0 };
237
+ });
198
238
  }
199
-
200
- // Plot height in px (the marker lane above it is 18px).
201
- var CHART_H = 112
202
- // Fixed column geometry: constant bar width keeps sparse histories from
203
- // stretching bars, and dense histories scroll horizontally instead of
204
- // compressing. The turn tick row below mirrors the same column grid.
205
- var BAR_W = 14
206
- var BAR_GAP = 2
207
-
239
+ const CHART_H = 112;
240
+ const BAR_W = 14;
241
+ const BAR_GAP = 2;
242
+ const TURN_COLORS = ["#6366f1", "#f59e0b", "#22c55e", "#a855f7", "#3b82f6", "#14b8a6", "#ef4444", "#ec4899"];
208
243
  function TrendChart(props) {
209
- var requests = props.requests
210
- var maxTotal = 1
211
- for (var i = 0; i < requests.length; i++) if (requests[i].total > maxTotal) maxTotal = requests[i].total
212
-
213
- // Compaction/prune markers: attach each to the first request logged after it.
214
- var markers = {}
215
- for (var m = 0; m < props.events.length; m++) {
216
- var ev = props.events[m]
217
- if (ev.kind !== 'compaction' && ev.kind !== 'prune') continue
218
- for (var r = 0; r < requests.length; r++) {
244
+ const requests = props.requests;
245
+ let maxTotal = 1;
246
+ for (const req of requests) if (req.total > maxTotal) maxTotal = req.total;
247
+ const markers = {};
248
+ for (const ev of props.events) {
249
+ if (ev.kind !== "compaction" && ev.kind !== "prune") continue;
250
+ for (let r = 0; r < requests.length; r++) {
219
251
  if (requests[r].seq >= ev.seq) {
220
- if (!markers[r]) markers[r] = ev
221
- break
252
+ if (!markers[r]) markers[r] = ev;
253
+ break;
222
254
  }
223
255
  }
224
256
  }
225
-
226
- // Consecutive requests of the same turn collapse into one labeled range.
227
- var groups = []
228
- for (var g = 0; g < requests.length; g++) {
229
- var grp = groups.length > 0 ? groups[groups.length - 1] : null
230
- if (grp === null || grp.turn !== requests[g].turn) {
231
- grp = { turn: requests[g].turn, count: 0 }
232
- groups.push(grp)
257
+ const groups = [];
258
+ for (const req of requests) {
259
+ let grp = groups.length > 0 ? groups[groups.length - 1] : null;
260
+ if (grp === null || grp.turn !== req.turn) {
261
+ grp = { turn: req.turn ?? 0, count: 0 };
262
+ groups.push(grp);
233
263
  }
234
- grp.count++
264
+ grp.count++;
235
265
  }
236
-
237
- // Stick to the newest bars unless the user scrolled away from the end.
238
- var scrollRef = React.useRef(null)
239
- React.useEffect(function () {
240
- var el = scrollRef.current
241
- if (el === null) return
242
- if (el.scrollLeft + el.clientWidth >= el.scrollWidth - 24) el.scrollLeft = el.scrollWidth
243
- })
244
-
245
- return h('div', { className: 'lc-chartrow' },
246
- h('div', { className: 'lc-axis' },
247
- h('span', { className: 'lc-axis-top' }, fmt(maxTotal)),
248
- h('span', { className: 'lc-axis-mid' }, fmt(Math.round(maxTotal / 2))),
249
- h('span', { className: 'lc-axis-bot' }, '0')),
250
- h('div', { className: 'lc-chart-scroll', ref: scrollRef },
251
- h('div', { className: 'lc-chart' },
252
- h('div', { className: 'lc-grid lc-grid-top' }),
253
- h('div', { className: 'lc-grid lc-grid-mid' }),
254
- requests.map(function (req, i) {
255
- var selected = props.selectedSeq === req.seq
256
- var tip = tr('tip.step', { t: req.turn, s: req.step }) + ' · ' + fmtTime(req.time) + '\n'
257
- + tr('tip.total', { n: fmt(req.total) })
258
- + (req.prompt !== undefined ? tr('tip.actual', { n: fmt(req.prompt) }) : '') + '\n'
259
- + CATS.map(function (c) { return catLabel(c.key) + ' ' + fmt(req[c.key] || 0) }).join(' / ')
260
- return h('div', {
261
- key: req.seq,
262
- className: 'lc-bar' + (selected ? ' lc-bar-selected' : ''),
263
- title: tip,
264
- onClick: function () { props.onSelect(selected ? null : req.seq) },
265
- },
266
- markers[i] ? h('span', { className: 'lc-bar-marker', title: eventLabel(markers[i]) }, '✂') : null,
267
- h('div', { className: 'lc-bar-stack' },
268
- CATS.map(function (c) {
269
- var v = req[c.key] || 0
270
- if (!v) return null
271
- // px heights: the stack's height is content-driven, so
272
- // percentage heights would collapse against an indefinite base.
273
- return h('div', { key: c.key, style: { height: Math.max(1, Math.round(v / maxTotal * CHART_H)) + 'px', background: c.color } })
274
- })))
275
- })),
276
- h('div', { className: 'lc-turns' },
277
- groups.map(function (grp, gi) {
278
- // One column per bar plus the shared gap; the flex gap between
279
- // ticks restores the inter-group gap, so tick spans line up with
280
- // their bars exactly.
281
- return h('span', {
282
- key: 'turn-' + gi,
283
- className: 'lc-turn',
284
- style: { width: (grp.count * (BAR_W + BAR_GAP) - BAR_GAP) + 'px' },
285
- }, 'T' + grp.turn)
286
- }))))
266
+ const scrollRef = React.useRef(null);
267
+ React.useEffect(() => {
268
+ const el = scrollRef.current;
269
+ if (el === null) return;
270
+ if (el.scrollLeft + el.clientWidth >= el.scrollWidth - 24) el.scrollLeft = el.scrollWidth;
271
+ });
272
+ return h(
273
+ "div",
274
+ { className: "lc-chartrow" },
275
+ h(
276
+ "div",
277
+ { className: "lc-axis" },
278
+ h("span", { className: "lc-axis-top" }, fmt(maxTotal)),
279
+ h("span", { className: "lc-axis-mid" }, fmt(Math.round(maxTotal / 2))),
280
+ h("span", { className: "lc-axis-bot" }, "0")
281
+ ),
282
+ h(
283
+ "div",
284
+ { className: "lc-chart-scroll", ref: scrollRef },
285
+ // Hovering a bar previews it in the detail below; leaving the plot
286
+ // clears the preview (a pinned selection, if any, takes over again).
287
+ h(
288
+ "div",
289
+ { className: "lc-chart", onMouseLeave: () => {
290
+ props.onHover(null);
291
+ } },
292
+ h("div", { className: "lc-grid lc-grid-top" }),
293
+ h("div", { className: "lc-grid lc-grid-mid" }),
294
+ requests.map((req, i) => {
295
+ const selected = props.selectedSeq === req.seq;
296
+ const hovered = props.hoveredSeq === req.seq;
297
+ const inTurn = props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn;
298
+ const tip = tr("tip.step", { t: req.turn ?? 0, s: req.step ?? 0 }) + " \xB7 " + fmtTime(req.time) + "\n" + tr("tip.total", { n: fmt(req.total) }) + (req.prompt !== void 0 ? tr("tip.actual", { n: fmt(req.prompt) }) : "") + "\n" + CATS.map((c) => catLabel(c.key) + " " + fmt(req[c.key] || 0)).join(" / ");
299
+ return h(
300
+ "div",
301
+ {
302
+ key: req.seq,
303
+ className: "lc-bar" + (selected ? " lc-bar-selected" : "") + (hovered ? " lc-bar-hovered" : "") + (inTurn ? " lc-bar-in-turn" : ""),
304
+ title: tip,
305
+ onClick: () => {
306
+ props.onSelect(selected ? null : req.seq);
307
+ },
308
+ onMouseEnter: () => {
309
+ props.onHover(req.seq);
310
+ }
311
+ },
312
+ markers[i] ? h("span", { className: "lc-bar-marker", title: eventLabel(markers[i]) }, "\u2702") : null,
313
+ h(
314
+ "div",
315
+ { className: "lc-bar-stack" },
316
+ CATS.map((c) => {
317
+ const v = req[c.key] || 0;
318
+ if (!v) return null;
319
+ return h("div", { key: c.key, style: { height: Math.max(1, Math.round(v / maxTotal * CHART_H)) + "px", background: c.color } });
320
+ })
321
+ )
322
+ );
323
+ })
324
+ ),
325
+ // Turn strip: one COLOR BLOCK per turn, spanning exactly its bars'
326
+ // columns, so the partition reads at a glance and lines up with the
327
+ // steps above. Hovering a block highlights that turn's bars in the
328
+ // chart (and hovering a bar highlights its block — the active turn
329
+ // is shared hover-only state).
330
+ h(
331
+ "div",
332
+ { className: "lc-turns", onMouseLeave: () => {
333
+ props.onHoverTurn(null);
334
+ } },
335
+ groups.map((grp, gi) => {
336
+ const on = props.activeTurn === grp.turn;
337
+ return h("span", {
338
+ key: "turn-" + gi,
339
+ className: "lc-turn" + (on ? " lc-turn-on" : ""),
340
+ style: {
341
+ width: grp.count * (BAR_W + BAR_GAP) - BAR_GAP + "px",
342
+ background: TURN_COLORS[gi % TURN_COLORS.length]
343
+ },
344
+ title: "T" + grp.turn,
345
+ onMouseEnter: () => {
346
+ props.onHoverTurn(grp.turn);
347
+ }
348
+ }, "T" + grp.turn);
349
+ })
350
+ )
351
+ )
352
+ );
287
353
  }
288
-
289
354
  function RequestDetail(props) {
290
- var req = props.request
291
- if (!req) return null
292
- return h('div', { className: 'lc-detail' },
293
- h('div', { className: 'lc-detail-head' },
294
- h('b', null, tr('detail.step', { t: req.turn, s: req.step })),
295
- h('span', null, fmtTime(req.time)),
296
- h('span', null, tr('detail.estTotal', { n: fmt(req.total) })),
297
- req.prompt !== undefined ? h('span', { className: 'lc-actual' }, tr('detail.actual', { n: fmt(req.prompt) })) : null,
298
- req.output !== undefined ? h('span', null, tr('detail.output', { n: fmt(req.output) })) : null),
355
+ const req = props.request;
356
+ if (!req) return null;
357
+ return h(
358
+ "div",
359
+ { className: "lc-detail" },
360
+ h(
361
+ "div",
362
+ { className: "lc-detail-head" },
363
+ h("b", null, tr("detail.step", { t: req.turn ?? 0, s: req.step ?? 0 })),
364
+ h("span", null, fmtTime(req.time)),
365
+ h("span", null, tr("detail.estTotal", { n: fmt(req.total) })),
366
+ req.prompt !== void 0 ? h("span", { className: "lc-actual" }, tr("detail.actual", { n: fmt(req.prompt) })) : null,
367
+ req.output !== void 0 ? h("span", null, tr("detail.output", { n: fmt(req.output) })) : null
368
+ ),
299
369
  h(StackedBar, { parts: partsOf(req), height: 10 }),
300
- h('div', { className: 'lc-detail-rows' },
301
- CATS.map(function (c) {
302
- var v = req[c.key] || 0
303
- return h('div', { key: c.key, className: 'lc-detail-row' },
304
- h('i', { style: { background: c.color } }),
305
- h('span', { className: 'lc-detail-label' }, catLabel(c.key)),
306
- h('span', { className: 'lc-bar-track' },
307
- h('span', { className: 'lc-bar-fill', style: { width: (req.total > 0 ? v / req.total * 100 : 0) + '%', background: c.color } })),
308
- h('span', { className: 'lc-detail-num' }, fmt(v)),
309
- h('span', { className: 'lc-detail-pct' }, req.total > 0 ? Math.round(v / req.total * 100) + '%' : ''))
310
- })))
370
+ h(
371
+ "div",
372
+ { className: "lc-detail-rows" },
373
+ CATS.map((c) => {
374
+ const v = req[c.key] || 0;
375
+ return h(
376
+ "div",
377
+ { key: c.key, className: "lc-detail-row" },
378
+ h("i", { style: { background: c.color } }),
379
+ h("span", { className: "lc-detail-label" }, catLabel(c.key)),
380
+ h(
381
+ "span",
382
+ { className: "lc-bar-track" },
383
+ h("span", { className: "lc-bar-fill", style: { width: (req.total > 0 ? v / req.total * 100 : 0) + "%", background: c.color } })
384
+ ),
385
+ h("span", { className: "lc-detail-num" }, fmt(v)),
386
+ h("span", { className: "lc-detail-pct" }, req.total > 0 ? Math.round(v / req.total * 100) + "%" : "")
387
+ );
388
+ })
389
+ )
390
+ );
311
391
  }
312
-
313
392
  function EventList(props) {
314
393
  if (props.events.length === 0) {
315
- return h('div', { className: 'lc-empty' }, t('events.empty'))
394
+ return h("div", { className: "lc-empty" }, t("events.empty"));
316
395
  }
317
- var sorted = props.events.slice().reverse()
318
- return h('div', { className: 'lc-events' },
319
- sorted.map(function (ev, i) {
320
- var label = eventLabel(ev)
321
- return h('div', { key: ev.seq + '-' + i, className: 'lc-event' },
322
- h('span', { className: 'lc-event-icon lc-event-' + ev.kind }, EVENT_ICONS[ev.kind] || '•'),
323
- h('span', { className: 'lc-event-label', title: label }, label),
324
- ev.tokens ? h('span', { className: 'lc-event-tokens' + (ev.kind === 'inject' ? ' lc-up' : ' lc-down') },
325
- (ev.kind === 'inject' ? '+' : '−') + fmt(ev.tokens)) : null,
326
- h('span', { className: 'lc-event-time' }, fmtTime(ev.time)))
327
- }))
396
+ const sorted = props.events.slice().reverse();
397
+ return h(
398
+ "div",
399
+ { className: "lc-events" },
400
+ sorted.map((ev, i) => {
401
+ const label = eventLabel(ev);
402
+ return h(
403
+ "div",
404
+ { key: ev.seq + "-" + i, className: "lc-event" },
405
+ h("span", { className: "lc-event-icon lc-event-" + ev.kind }, EVENT_ICONS[ev.kind] || "\u2022"),
406
+ h("span", { className: "lc-event-label", title: label }, label),
407
+ ev.tokens ? h(
408
+ "span",
409
+ { className: "lc-event-tokens" + (ev.kind === "inject" ? " lc-up" : " lc-down") },
410
+ (ev.kind === "inject" ? "+" : "\u2212") + fmt(ev.tokens)
411
+ ) : null,
412
+ h("span", { className: "lc-event-time" }, fmtTime(ev.time))
413
+ );
414
+ })
415
+ );
328
416
  }
329
-
330
417
  function NodeList(props) {
331
418
  if (props.nodes.length === 0) {
332
- return h('div', { className: 'lc-empty' }, t('nodes.empty'))
419
+ return h("div", { className: "lc-empty" }, t("nodes.empty"));
333
420
  }
334
- var catColor = {}
335
- CATS.forEach(function (c) { catColor[c.key] = c.color })
336
- var rows = props.nodes.slice().reverse()
337
- return h('div', { className: 'lc-nodes' },
338
- props.dropped > 0 ? h('div', { className: 'lc-nodes-more' }, tr('nodes.more', { n: props.dropped })) : null,
339
- rows.map(function (n) {
340
- var text = nodeText(n)
341
- return h('div', { key: n.seq, className: 'lc-node' },
342
- h('i', { style: { background: catColor[n.cat] || '#999' } }),
343
- h('span', { className: 'lc-node-preview', title: text }, text),
344
- h('span', { className: 'lc-node-tokens' }, fmt(n.tokens)))
345
- }))
421
+ const catColor = {};
422
+ for (const c of CATS) catColor[c.key] = c.color;
423
+ const rows = props.nodes.slice().reverse();
424
+ return h(
425
+ "div",
426
+ { className: "lc-nodes" },
427
+ props.dropped > 0 ? h("div", { className: "lc-nodes-more" }, tr("nodes.more", { n: props.dropped })) : null,
428
+ rows.map((n) => {
429
+ const text = nodeText(n);
430
+ return h(
431
+ "div",
432
+ { key: n.seq, className: "lc-node" },
433
+ h("i", { style: { background: catColor[n.cat] || "#999" } }),
434
+ h("span", { className: "lc-node-preview", title: text }, text),
435
+ h("span", { className: "lc-node-tokens" }, fmt(n.tokens))
436
+ );
437
+ })
438
+ );
346
439
  }
347
-
348
440
  function ContextView(props) {
349
- var sessionId = props.sessionId
350
- var state = React.useState(null)
351
- var data = state[0]
352
- var setData = state[1]
353
- var errState = React.useState(null)
354
- var error = errState[0]
355
- var setError = errState[1]
356
- var selState = React.useState(null)
357
- var selectedSeq = selState[0]
358
- var setSelectedSeq = selState[1]
359
- var tickState = React.useState(0)
360
- var setTick = tickState[1]
361
-
362
- React.useEffect(function () {
363
- if (typeof sessionId !== 'string' || sessionId === '') return undefined
364
- var alive = true
365
- var load = function () {
366
- // Generic Connection RPC channel served by the Host half.
367
- ctx.connection.rpc.call('/dsh-context', 'snapshot', { sessionId: sessionId }).then(function (res) {
368
- if (!alive) return
369
- if (res && res.ok) { setData(res.value); setError(null) }
370
- else setError(res && res.error ? String(res.error.message || res.error.code) : 'failed')
371
- }, function (err) {
372
- if (alive) setError(String(err && err.message ? err.message : err))
373
- })
374
- }
375
- load()
376
- var timerId = setInterval(load, 2000)
377
- return function () { alive = false; clearInterval(timerId) }
378
- }, [sessionId])
379
-
380
- // Re-render on locale switch.
381
- React.useEffect(function () {
382
- var localeSvc = ctx.get('locale')
383
- if (!localeSvc) return undefined
384
- return localeSvc.subscribe(function () { setTick(function (x) { return x + 1 }) })
385
- }, [])
386
-
441
+ const sessionId = props.sessionId;
442
+ const [data, setData] = React.useState(null);
443
+ const [error, setError] = React.useState(null);
444
+ const [selectedSeq, setSelectedSeq] = React.useState(null);
445
+ const [hoveredSeq, setHoveredSeq] = React.useState(null);
446
+ const [hoverTurn, setHoverTurn] = React.useState(null);
447
+ const [tick, setTick] = React.useState(0);
448
+ const [granularity, setGranularity] = React.useState("step");
449
+ React.useEffect(() => {
450
+ if (typeof sessionId !== "string" || sessionId === "") return void 0;
451
+ let alive = true;
452
+ const load = () => {
453
+ ctx.connection.rpc.call("/dsh-context", "snapshot", { sessionId }).then((res) => {
454
+ if (!alive) return;
455
+ if (res && res.ok) {
456
+ setData(res.value);
457
+ setError(null);
458
+ } else setError(res && res.error ? String(res.error.message || res.error.code) : "failed");
459
+ }, (err) => {
460
+ if (alive) setError(String(err instanceof Error ? err.message : err));
461
+ });
462
+ };
463
+ load();
464
+ const timerId = setInterval(load, 2e3);
465
+ return () => {
466
+ alive = false;
467
+ clearInterval(timerId);
468
+ };
469
+ }, [sessionId]);
470
+ React.useEffect(() => {
471
+ const localeSvc = ctx.get("locale");
472
+ if (!localeSvc) return void 0;
473
+ return localeSvc.subscribe(() => setTick((x) => x + 1));
474
+ }, []);
475
+ void tick;
387
476
  if (error) {
388
- return h('div', { className: 'lc-root' }, h('div', { className: 'lc-empty' }, t('error') + error))
477
+ return h("div", { className: "lc-root" }, h("div", { className: "lc-empty" }, t("error") + error));
389
478
  }
390
479
  if (!data) {
391
- return h('div', { className: 'lc-root' }, h('div', { className: 'lc-empty' }, t('loading')))
480
+ return h("div", { className: "lc-root" }, h("div", { className: "lc-empty" }, t("loading")));
392
481
  }
393
-
394
- var current = data.current
395
- var requests = data.requests || []
396
- var events = data.events || []
397
- var nodes = data.nodes || []
398
-
399
- var selReq = null
400
- for (var i = 0; i < requests.length; i++) if (requests[i].seq === selectedSeq) selReq = requests[i]
401
- if (!selReq && requests.length > 0) selReq = requests[requests.length - 1]
402
-
403
- var windowPct = data.contextWindow ? Math.min(100, Math.round(current.total / data.contextWindow * 100)) : null
404
-
405
- return h('div', { className: 'lc-root' },
406
-
482
+ const current = data.current;
483
+ const requests = data.requests || [];
484
+ const events = data.events || [];
485
+ const nodes = data.nodes || [];
486
+ const displayRequests = granularity === "turn" ? aggregateByTurn(requests) : requests;
487
+ let pinnedReq = null;
488
+ for (const req of displayRequests) if (req.seq === selectedSeq) pinnedReq = req;
489
+ let activeReq = null;
490
+ if (hoveredSeq !== null) {
491
+ for (const req of displayRequests) if (req.seq === hoveredSeq) activeReq = req;
492
+ }
493
+ if (activeReq === null) activeReq = pinnedReq;
494
+ if (activeReq === null && displayRequests.length > 0) activeReq = displayRequests[displayRequests.length - 1];
495
+ let activeTurn = hoverTurn;
496
+ if (activeTurn === null && hoveredSeq !== null) {
497
+ for (const req of displayRequests) if (req.seq === hoveredSeq) {
498
+ activeTurn = req.turn ?? null;
499
+ break;
500
+ }
501
+ }
502
+ const windowPct = data.contextWindow ? Math.min(100, Math.round(current.total / data.contextWindow * 100)) : null;
503
+ return h(
504
+ "div",
505
+ { className: "lc-root" },
407
506
  // ---- overview ----
408
- h('div', { className: 'lc-card' },
409
- h('div', { className: 'lc-card-title' },
410
- t('overview.title'),
411
- h('span', { className: 'lc-card-sub' },
412
- (data.model ? data.model : '') + (data.provider ? ' · ' + data.provider : ''))),
413
- h('div', { className: 'lc-overview-num' },
414
- h('b', null, fmt(current.total)),
415
- h('span', null, data.contextWindow
416
- ? ' / ' + fmt(data.contextWindow) + ' ' + tr('overview.ofWindow', { p: windowPct })
417
- : ' ' + t('overview.estimate'))),
507
+ h(
508
+ "div",
509
+ { className: "lc-card" },
510
+ h(
511
+ "div",
512
+ { className: "lc-card-title" },
513
+ t("overview.title"),
514
+ h(
515
+ "span",
516
+ { className: "lc-card-sub" },
517
+ (data.model ? data.model : "") + (data.provider ? " \xB7 " + data.provider : "")
518
+ )
519
+ ),
520
+ h(
521
+ "div",
522
+ { className: "lc-overview-num" },
523
+ h("b", null, fmt(current.total)),
524
+ h("span", null, data.contextWindow ? " / " + fmt(data.contextWindow) + " " + tr("overview.ofWindow", { p: windowPct ?? 0 }) : " " + t("overview.estimate"))
525
+ ),
418
526
  h(StackedBar, { parts: partsOf(current), height: 16, max: data.contextWindow }),
419
527
  h(Legend, { parts: partsOf(current) }),
420
- (data.toolList && data.toolList.length > 0) ? h('div', { className: 'lc-tools' },
421
- t('tools.top'),
422
- data.toolList.slice().sort(function (a, b) { return b.tokens - a.tokens }).slice(0, 5).map(function (tool) {
423
- return h('span', { key: tool.name, className: 'lc-tool-chip' }, tool.name + ' ' + fmt(tool.tokens))
528
+ data.toolList && data.toolList.length > 0 ? h(
529
+ "div",
530
+ { className: "lc-tools" },
531
+ t("tools.top"),
532
+ data.toolList.slice().sort((a, b) => b.tokens - a.tokens).slice(0, 5).map((tool) => {
533
+ return h("span", { key: tool.name, className: "lc-tool-chip" }, tool.name + " " + fmt(tool.tokens));
424
534
  }),
425
- data.toolList.length > 5 ? h('span', { className: 'lc-card-sub' }, ' ' + tr('tools.more', { n: data.toolList.length })) : null) : null),
426
-
535
+ data.toolList.length > 5 ? h("span", { className: "lc-card-sub" }, " " + tr("tools.more", { n: data.toolList.length })) : null
536
+ ) : null
537
+ ),
427
538
  // ---- trend ----
428
- h('div', { className: 'lc-card' },
429
- h('div', { className: 'lc-card-title' },
430
- t('trend.title'),
431
- h('span', { className: 'lc-card-sub' }, t('trend.hint'))),
432
- requests.length === 0
433
- ? h('div', { className: 'lc-empty' }, t('trend.empty'))
434
- : h('div', null,
435
- h(TrendChart, { requests: requests.slice(-80), events: events, selectedSeq: selReq ? selReq.seq : null, onSelect: setSelectedSeq }),
436
- h(RequestDetail, { request: selReq }))),
437
-
539
+ h(
540
+ "div",
541
+ { className: "lc-card" },
542
+ h(
543
+ "div",
544
+ { className: "lc-card-title" },
545
+ t("trend.title"),
546
+ h("span", { className: "lc-card-sub" }, t("trend.hint")),
547
+ h(
548
+ "div",
549
+ { className: "lc-gran" },
550
+ h("button", {
551
+ className: "lc-gran-btn" + (granularity === "step" ? " lc-gran-on" : ""),
552
+ onClick: () => {
553
+ setGranularity("step");
554
+ }
555
+ }, t("gran.step")),
556
+ h("button", {
557
+ className: "lc-gran-btn" + (granularity === "turn" ? " lc-gran-on" : ""),
558
+ onClick: () => {
559
+ setGranularity("turn");
560
+ }
561
+ }, t("gran.turn"))
562
+ )
563
+ ),
564
+ displayRequests.length === 0 ? h("div", { className: "lc-empty" }, t("trend.empty")) : h(
565
+ "div",
566
+ null,
567
+ h(TrendChart, {
568
+ requests: displayRequests.slice(-80),
569
+ events,
570
+ selectedSeq: pinnedReq ? pinnedReq.seq : null,
571
+ hoveredSeq,
572
+ activeTurn,
573
+ onSelect: setSelectedSeq,
574
+ onHover: setHoveredSeq,
575
+ onHoverTurn: setHoverTurn
576
+ }),
577
+ h(RequestDetail, { request: activeReq })
578
+ )
579
+ ),
438
580
  // ---- events + messages ----
439
- h('div', { className: 'lc-cols' },
440
- h('div', { className: 'lc-card lc-col' },
441
- h('div', { className: 'lc-card-title' }, t('events.title')),
442
- h(EventList, { events: events })),
443
- h('div', { className: 'lc-card lc-col' },
444
- h('div', { className: 'lc-card-title' },
445
- t('nodes.title'),
446
- h('span', { className: 'lc-card-sub' }, t('nodes.hint'))),
447
- h(NodeList, { nodes: nodes, dropped: data.droppedNodes || 0 }))),
448
-
449
- h('div', { className: 'lc-foot' }, t('footer')))
581
+ h(
582
+ "div",
583
+ { className: "lc-cols" },
584
+ h(
585
+ "div",
586
+ { className: "lc-card lc-col" },
587
+ h("div", { className: "lc-card-title" }, t("events.title")),
588
+ h(EventList, { events })
589
+ ),
590
+ h(
591
+ "div",
592
+ { className: "lc-card lc-col" },
593
+ h(
594
+ "div",
595
+ { className: "lc-card-title" },
596
+ t("nodes.title"),
597
+ h("span", { className: "lc-card-sub" }, t("nodes.hint"))
598
+ ),
599
+ h(NodeList, { nodes, dropped: data.droppedNodes || 0 })
600
+ )
601
+ ),
602
+ h("div", { className: "lc-foot" }, t("footer"))
603
+ );
450
604
  }
451
-
452
- return ContextView
605
+ return ContextView;
453
606
  }
454
-
455
- var STYLES = [
456
- '.lc-root { padding: 16px 20px 32px; overflow-y: auto; height: 100%; box-sizing: border-box; color: var(--dsw-alias-label-primary); font-size: 13px; }',
457
- '.lc-card { background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; padding: 14px 16px; margin-bottom: 14px; }',
458
- '.lc-card-title { font-weight: 600; margin-bottom: 10px; display: flex; align-items: baseline; gap: 8px; }',
459
- '.lc-card-sub { font-weight: 400; color: var(--dsw-alias-label-secondary); font-size: 12px; }',
460
- '.lc-overview-num { margin-bottom: 8px; }',
461
- '.lc-overview-num b { font-size: 20px; }',
462
- '.lc-overview-num span { color: var(--dsw-alias-label-secondary); }',
463
- '.lc-stacked { display: flex; width: 100%; border-radius: 5px; overflow: hidden; background: rgba(128,128,128,0.18); }',
464
- '.lc-stacked > div { height: 100%; }',
465
- '.lc-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 10px; }',
466
- '.lc-chip { display: inline-flex; align-items: center; gap: 5px; color: var(--dsw-alias-label-primary); }',
467
- '.lc-chip i, .lc-detail-row i, .lc-node i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }',
468
- '.lc-chip em { font-style: normal; color: var(--dsw-alias-label-secondary); }',
469
- '.lc-tools { margin-top: 10px; color: var(--dsw-alias-label-secondary); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }',
470
- '.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); }',
471
- '.lc-chartrow { display: flex; gap: 6px; align-items: stretch; }',
472
- '.lc-axis { position: relative; width: 40px; height: 150px; padding-top: 18px; box-sizing: border-box; color: var(--dsw-alias-label-secondary); font-size: 11px; }',
473
- '.lc-axis span { position: absolute; right: 0; line-height: 1; }',
474
- '.lc-axis-top { top: 13px; }',
475
- '.lc-axis-mid { top: 69px; }',
476
- '.lc-axis-bot { top: 125px; }',
477
- '.lc-chart-scroll { flex: 1; overflow-x: auto; overflow-y: hidden; min-width: 0; scrollbar-width: thin; }',
478
- '.lc-chart-scroll::-webkit-scrollbar { height: 6px; }',
479
- '.lc-chart-scroll::-webkit-scrollbar-thumb { background: var(--dsw-alias-border-l1); border-radius: 3px; }',
480
- '.lc-chart-scroll::-webkit-scrollbar-track { background: transparent; }',
481
- '.lc-chart { position: relative; display: flex; align-items: flex-end; gap: 2px; height: 130px; padding-top: 18px; box-sizing: border-box; width: max-content; min-width: 100%; }',
482
- '.lc-grid { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--dsw-alias-border-l1); pointer-events: none; }',
483
- '.lc-grid-top { top: 18px; }',
484
- '.lc-grid-mid { top: 74px; }',
485
- '.lc-bar { position: relative; width: 14px; flex: none; height: 100%; display: flex; align-items: flex-end; cursor: pointer; border-radius: 2px; }',
486
- '.lc-bar:hover { background: var(--dsw-alias-bg-layer-2); }',
487
- '.lc-bar-selected { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }',
488
- '.lc-bar-stack { display: flex; flex-direction: column-reverse; width: 100%; }',
489
- '.lc-bar-stack > div { width: 100%; }',
490
- '.lc-bar-marker { position: absolute; top: -16px; left: 50%; transform: translateX(-50%); font-size: 11px; color: var(--dsw-alias-state-warn-primary); }',
491
- '.lc-turns { display: flex; gap: 2px; width: max-content; min-width: 100%; }',
492
- '.lc-turn { flex: none; min-width: 24px; box-sizing: border-box; text-align: center; font-size: 11px; line-height: 18px; color: var(--dsw-alias-label-secondary); border-top: 1px solid var(--dsw-alias-border-l1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }',
493
- '.lc-detail { margin-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); padding-top: 12px; }',
494
- '.lc-detail-head { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 8px; color: var(--dsw-alias-label-secondary); }',
495
- '.lc-detail-head b { color: var(--dsw-alias-label-primary); }',
496
- '.lc-detail-head .lc-actual { color: var(--dsw-alias-state-success-primary); }',
497
- '.lc-detail-rows { margin-top: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px 24px; }',
498
- '.lc-detail-row { display: flex; align-items: center; gap: 8px; }',
499
- '.lc-detail-label { min-width: 70px; white-space: nowrap; color: var(--dsw-alias-label-secondary); }',
500
- '.lc-bar-track { flex: 1; height: 5px; border-radius: 3px; background: rgba(128,128,128,0.18); overflow: hidden; display: block; }',
501
- '.lc-bar-fill { display: block; height: 100%; border-radius: 3px; }',
502
- '.lc-detail-num { width: 44px; text-align: right; }',
503
- '.lc-detail-pct { width: 34px; text-align: right; color: var(--dsw-alias-label-secondary); }',
504
- '.lc-cols { display: flex; gap: 14px; flex-wrap: wrap; }',
505
- '.lc-col { flex: 1; min-width: 280px; }',
506
- '.lc-events, .lc-nodes { display: flex; flex-direction: column; gap: 2px; max-height: 320px; overflow-y: auto; }',
507
- '.lc-event { display: flex; align-items: center; gap: 8px; padding: 3px 0; }',
508
- '.lc-event-icon { width: 18px; text-align: center; color: var(--dsw-alias-state-warn-primary); }',
509
- '.lc-event-icon.lc-event-inject { color: #a855f7; }',
510
- '.lc-event-icon.lc-event-model { color: var(--dsw-alias-brand-primary); }',
511
- '.lc-event-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
512
- '.lc-event-tokens { color: var(--dsw-alias-state-success-primary); }',
513
- '.lc-event-tokens.lc-up { color: var(--dsw-alias-state-warn-primary); }',
514
- '.lc-event-time { color: var(--dsw-alias-label-secondary); font-size: 12px; }',
515
- '.lc-node { display: flex; align-items: center; gap: 8px; padding: 3px 0; }',
516
- '.lc-node-preview { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); }',
517
- '.lc-node-tokens { color: var(--dsw-alias-label-secondary); }',
518
- '.lc-nodes-more { color: var(--dsw-alias-label-secondary); padding: 3px 0; }',
519
- '.lc-empty { color: var(--dsw-alias-label-secondary); padding: 18px 0; text-align: center; }',
520
- '.lc-foot { color: var(--dsw-alias-label-secondary); font-size: 12px; margin-top: 4px; }',
521
- ].join('\n')
522
-
607
+ const STYLES = [
608
+ ".lc-root { padding: 16px 20px 32px; overflow-y: auto; height: 100%; box-sizing: border-box; color: var(--dsw-alias-label-primary); font-size: 13px; }",
609
+ ".lc-card { background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; padding: 14px 16px; margin-bottom: 14px; }",
610
+ ".lc-card-title { font-weight: 600; margin-bottom: 10px; display: flex; align-items: baseline; gap: 8px; }",
611
+ ".lc-card-sub { font-weight: 400; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
612
+ ".lc-gran { margin-left: auto; display: flex; gap: 2px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 1px; }",
613
+ ".lc-gran-btn { border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 1; padding: 3px 8px; border-radius: 5px; cursor: pointer; font-family: inherit; }",
614
+ ".lc-gran-btn:hover { color: var(--dsw-alias-label-primary); }",
615
+ ".lc-gran-on, .lc-gran-on:hover { background: var(--dsw-alias-brand-primary); color: #fff; }",
616
+ ".lc-overview-num { margin-bottom: 8px; }",
617
+ ".lc-overview-num b { font-size: 20px; }",
618
+ ".lc-overview-num span { color: var(--dsw-alias-label-secondary); }",
619
+ ".lc-stacked-wrap { position: relative; width: 100%; }",
620
+ ".lc-stacked { display: flex; width: 100%; border-radius: 5px; overflow: hidden; background: rgba(128,128,128,0.18); }",
621
+ ".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; }",
622
+ ".lc-stacked > div { height: 100%; }",
623
+ ".lc-legend { display: flex; flex-wrap: wrap; gap: 6px 14px; margin-top: 10px; }",
624
+ ".lc-chip { display: inline-flex; align-items: center; gap: 5px; color: var(--dsw-alias-label-primary); }",
625
+ ".lc-chip i, .lc-detail-row i, .lc-node i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }",
626
+ ".lc-chip em { font-style: normal; color: var(--dsw-alias-label-secondary); }",
627
+ ".lc-tools { margin-top: 10px; color: var(--dsw-alias-label-secondary); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }",
628
+ ".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); }",
629
+ ".lc-chartrow { display: flex; gap: 6px; align-items: stretch; }",
630
+ ".lc-axis { position: relative; width: 40px; height: 150px; padding-top: 18px; box-sizing: border-box; color: var(--dsw-alias-label-secondary); font-size: 11px; }",
631
+ ".lc-axis span { position: absolute; right: 0; line-height: 1; }",
632
+ ".lc-axis-top { top: 13px; }",
633
+ ".lc-axis-mid { top: 69px; }",
634
+ ".lc-axis-bot { top: 125px; }",
635
+ ".lc-chart-scroll { flex: 1; overflow-x: auto; overflow-y: hidden; min-width: 0; scrollbar-width: thin; }",
636
+ ".lc-chart-scroll::-webkit-scrollbar { height: 6px; }",
637
+ ".lc-chart-scroll::-webkit-scrollbar-thumb { background: var(--dsw-alias-border-l1); border-radius: 3px; }",
638
+ ".lc-chart-scroll::-webkit-scrollbar-track { background: transparent; }",
639
+ ".lc-chart { position: relative; display: flex; align-items: flex-end; gap: 2px; height: 130px; padding-top: 18px; box-sizing: border-box; width: max-content; min-width: 100%; }",
640
+ ".lc-grid { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--dsw-alias-border-l1); pointer-events: none; }",
641
+ ".lc-grid-top { top: 18px; }",
642
+ ".lc-grid-mid { top: 74px; }",
643
+ ".lc-bar { position: relative; width: 14px; flex: none; height: 100%; display: flex; align-items: flex-end; cursor: pointer; border-radius: 2px; }",
644
+ ".lc-bar:hover { background: var(--dsw-alias-bg-layer-2); }",
645
+ ".lc-bar-selected { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }",
646
+ ".lc-bar-hovered { outline: 1px dashed var(--dsw-alias-brand-primary); outline-offset: 1px; }",
647
+ ".lc-bar-in-turn { background: rgba(99,102,241,0.14); }",
648
+ ".lc-bar-stack { display: flex; flex-direction: column-reverse; width: 100%; }",
649
+ ".lc-bar-stack > div { width: 100%; }",
650
+ ".lc-bar-marker { position: absolute; top: -16px; left: 50%; transform: translateX(-50%); font-size: 11px; color: var(--dsw-alias-state-warn-primary); }",
651
+ ".lc-turns { display: flex; gap: 2px; width: max-content; min-width: 100%; margin-top: 4px; }",
652
+ ".lc-turn { flex: none; min-width: 16px; box-sizing: border-box; text-align: center; font-size: 10px; line-height: 14px; font-weight: 600; color: #fff; border-radius: 3px; height: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; transition: filter 120ms; }",
653
+ ".lc-turn-on { filter: brightness(1.35); box-shadow: 0 0 0 1px rgba(255,255,255,0.4); }",
654
+ ".lc-detail { margin-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); padding-top: 12px; }",
655
+ ".lc-detail-head { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 8px; color: var(--dsw-alias-label-secondary); }",
656
+ ".lc-detail-head b { color: var(--dsw-alias-label-primary); }",
657
+ ".lc-detail-head .lc-actual { color: var(--dsw-alias-state-success-primary); }",
658
+ ".lc-detail-rows { margin-top: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px 24px; }",
659
+ ".lc-detail-row { display: flex; align-items: center; gap: 8px; }",
660
+ ".lc-detail-label { min-width: 70px; white-space: nowrap; color: var(--dsw-alias-label-secondary); }",
661
+ ".lc-bar-track { flex: 1; height: 5px; border-radius: 3px; background: rgba(128,128,128,0.18); overflow: hidden; display: block; }",
662
+ ".lc-bar-fill { display: block; height: 100%; border-radius: 3px; }",
663
+ ".lc-detail-num { width: 44px; text-align: right; }",
664
+ ".lc-detail-pct { width: 34px; text-align: right; color: var(--dsw-alias-label-secondary); }",
665
+ ".lc-cols { display: flex; gap: 14px; flex-wrap: wrap; }",
666
+ ".lc-col { flex: 1; min-width: 280px; }",
667
+ ".lc-events, .lc-nodes { display: flex; flex-direction: column; gap: 2px; max-height: 320px; overflow-y: auto; }",
668
+ ".lc-event { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
669
+ ".lc-event-icon { width: 18px; text-align: center; color: var(--dsw-alias-state-warn-primary); }",
670
+ ".lc-event-icon.lc-event-inject { color: #a855f7; }",
671
+ ".lc-event-icon.lc-event-model { color: var(--dsw-alias-brand-primary); }",
672
+ ".lc-event-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
673
+ ".lc-event-tokens { color: var(--dsw-alias-state-success-primary); }",
674
+ ".lc-event-tokens.lc-up { color: var(--dsw-alias-state-warn-primary); }",
675
+ ".lc-event-time { color: var(--dsw-alias-label-secondary); font-size: 12px; }",
676
+ ".lc-node { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
677
+ ".lc-node-preview { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); }",
678
+ ".lc-node-tokens { color: var(--dsw-alias-label-secondary); }",
679
+ ".lc-nodes-more { color: var(--dsw-alias-label-secondary); padding: 3px 0; }",
680
+ ".lc-empty { color: var(--dsw-alias-label-secondary); padding: 18px 0; text-align: center; }",
681
+ ".lc-foot { color: var(--dsw-alias-label-secondary); font-size: 12px; margin-top: 4px; }"
682
+ ].join("\n");
523
683
  function apply(ctx) {
524
- // Bilingual dictionaries; the tab label thunk and all UI text follow the
525
- // active locale through the bound translate (missing keys fall back to
526
- // zh, then the key itself). The registration rides ctx.effect, so a stop
527
- // or HMR reload disposes it.
528
- ctx.effect(function () {
529
- return ctx.locale.register('dsh-context', { zh: DICT_ZH, en: DICT_EN })
530
- }, 'dsh-context: dictionaries')
531
- var t = ctx.locale.bind('dsh-context')
532
-
533
- // Theme-native styles, injected as a plugin-owned <style> tag (the web
534
- // boot loader claims and removes tags carrying data-plugin on unload).
535
- ctx.effect(function () {
536
- var tag = document.createElement('style')
537
- tag.setAttribute('data-plugin', 'dsh-context')
538
- tag.textContent = STYLES
539
- document.head.appendChild(tag)
540
- return function () {
541
- if (tag.parentNode !== null) tag.parentNode.removeChild(tag)
542
- }
543
- }, 'dsh-context: styles')
544
-
545
- var ContextView = makeView(ctx, t)
546
- ctx.slots.inject('conversation.view', function () {
684
+ ctx.effect(() => {
685
+ return ctx.locale.register("dsh-context", { zh: DICT_ZH, en: DICT_EN });
686
+ }, "dsh-context: dictionaries");
687
+ const t = ctx.locale.bind("dsh-context");
688
+ ctx.effect(() => {
689
+ const tag = document.createElement("style");
690
+ tag.setAttribute("data-plugin", "dsh-context");
691
+ tag.textContent = STYLES;
692
+ document.head.appendChild(tag);
693
+ return () => {
694
+ if (tag.parentNode !== null) tag.parentNode.removeChild(tag);
695
+ };
696
+ }, "dsh-context: styles");
697
+ const ContextView = makeView(ctx, t);
698
+ ctx.slots.inject("conversation.view", () => {
547
699
  return ctx.slots.register(
548
700
  // order 20 renders right of Chat (0) and Trajectory (10).
549
- { name: 'conversation.view', id: 'context', order: 20, label: function () { return t('tab') } },
550
- function (props) { return h(ContextView, props) },
551
- )
552
- })
701
+ { name: "conversation.view", id: "context", order: 20, label: () => t("tab") },
702
+ (props) => h(ContextView, props)
703
+ );
704
+ });
553
705
  }
554
-
555
706
  module.exports = {
556
- name: 'dsh-context',
557
- inject: ['connection', 'slots', 'locale'],
558
- apply: apply,
559
- }
707
+ name: "dsh-context",
708
+ inject: ["connection", "slots", "locale"],
709
+ apply
710
+ };
560
711
 
561
712
  return module.exports;
562
713
  },