dsh-context 0.21.1 → 0.22.1

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
@@ -1,2140 +1,2334 @@
1
1
  window.__ModuleLoader__.load({
2
- id: "dsh-context",
3
- factory: (require) => {
4
- var module = { exports: {} };
5
- var exports = module.exports;
6
-
7
- "use strict";
8
-
9
- // src/client/i18n.ts
10
- var DICT_ZH = {
11
- "tab": "\u4E0A\u4E0B\u6587",
12
- "cat.system": "\u7CFB\u7EDF\u63D0\u793A\u8BCD",
13
- "cat.tools": "\u5DE5\u5177\u5B9A\u4E49",
14
- "cat.user": "\u7528\u6237\u6D88\u606F",
15
- "cat.inject": "\u6CE8\u5165\u5185\u5BB9",
16
- "cat.assistant": "\u52A9\u624B\u6D88\u606F",
17
- "cat.tool": "\u5DE5\u5177\u7ED3\u679C",
18
- "overview.title": "\u5F53\u524D\u6784\u6210",
19
- "overview.estimate": "tokens\uFF08\u4F30\u7B97\uFF09",
20
- "overview.free": "\u5269\u4F59\u7A97\u53E3",
21
- "overview.used": "\u4E0A\u4E0B\u6587\u5DF2\u7528",
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",
24
- "stats.title": "\u4E0A\u4E0B\u6587\u7EDF\u8BA1",
25
- "stats.hint": "\u7EDF\u8BA1\u5F53\u524D\u4FDD\u7559\u7684\u5386\u53F2\u7A97\u53E3\uFF08\u4E0E\u8D8B\u52BF\u56FE\u4E00\u81F4\uFF09",
26
- "stats.turns": "\u8F6E\u6B21",
27
- "stats.steps": "\u6B65\u6570",
28
- "stats.injects": "\u6CE8\u5165",
29
- "stats.compactions": "\u538B\u7F29",
30
- "stats.prunes": "\u526A\u679D",
31
- "stats.cacheHit": "\u7F13\u5B58\u547D\u4E2D",
32
- "stats.cost": "\u9884\u4F30\u8D39\u7528",
33
- "stats.costTip": "\u6309 DeepSeek \u5B98\u65B9\u520A\u4F8B\u4EF7\u4F30\u7B97\u6574\u4E2A\u4F1A\u8BDD\u7684\u7D2F\u8BA1\u8D39\u7528\uFF1A\u8F93\u5165\u533A\u5206\u7F13\u5B58\u547D\u4E2D/\u672A\u547D\u4E2D\uFF0C\u8F93\u51FA\u542B\u601D\u8003\uFF1B\u6309\u8BF7\u6C42\u65F6\u95F4\u533A\u5206\u9AD8\u5CF0\uFF08UTC 01:00\u201304:00\u300106:00\u201310:00\uFF09\u4E0E\u7A7A\u95F2\u65F6\u6BB5\uFF08\u534A\u4EF7\uFF09\u3002\u9002\u7528\u4E8E deepseek-v4-flash / deepseek-v4-pro\uFF08\u4E0D\u9650 provider\uFF09\uFF0C\u4EF7\u683C\u5199\u6B7B\u5728\u4EE3\u7801\u4E2D\uFF0C\u4EC5\u4F9B\u53C2\u8003\u3002",
34
- "stats.costPriceHead": "\u6BCF\u767E\u4E07 tokens \u4EF7\u683C\uFF08\u9AD8\u5CF0 | \u7A7A\u95F2\u534A\u4EF7\uFF09\uFF1A",
35
- "stats.costHit": "\u547D\u4E2D",
36
- "stats.costMiss": "\u672A\u547D\u4E2D",
37
- "stats.costOut": "\u8F93\u51FA",
38
- "plugin.title": "\u63D2\u4EF6\u4FE1\u606F",
39
- "plugin.hint": "The best DSH context plugin \u2B50",
40
- "plugin.name": "\u63D2\u4EF6",
41
- "plugin.github": "GitHub",
42
- "tools.top": "\u5DE5\u5177\u5B9A\u4E49 Top\uFF1A",
43
- "tools.more": "\u7B49 {n} \u4E2A",
44
- "trend.title": "\u5386\u53F2\u8D8B\u52BF",
45
- "gran.step": "\u6B65\u9AA4",
46
- "gran.turn": "\u8F6E\u6B21",
47
- "trend.hint": "\u60AC\u505C\u67E5\u770B\u8BE6\u60C5\uFF0C\u2702 \u8868\u793A\u538B\u7F29/\u526A\u679D\uFF0CStep/Turn \u5207\u6362\u7C92\u5EA6",
48
- "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",
49
- "detail.step": "\u7B2C {t} \u8F6E \xB7 \u7B2C {s} \u6B65",
50
- "detail.turn": "\u7B2C {t} \u8F6E \xB7 \u5171 {n} \u6B65",
51
- "detail.lastStep": "\u672B\u6B65",
52
- "detail.estTotal": "\u4F30\u7B97\u5408\u8BA1 \u2248 {n}",
53
- "detail.actual": "\u5B9E\u9645 prompt {n}",
54
- "detail.output": "\u8F93\u51FA {n}",
55
- "events.title": "\u4E0A\u4E0B\u6587\u4E8B\u4EF6",
56
- "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",
57
- "events.at": "\u7B2C {t} \u8F6E \xB7 \u7B2C {s} \u6B65",
58
- "events.range": "\u7B2C {t} \u8F6E \xB7 \u7B2C {a}\u2192{b} \u6B65",
59
- "events.rangeTo": "\u7B2C {a} \u8F6E \xB7 \u7B2C {as} \u6B65 \u2192 \u7B2C {b} \u8F6E \xB7 \u7B2C {bs} \u6B65",
60
- "kind.inject": "\u6CE8\u5165",
61
- "kind.compaction": "\u538B\u7F29",
62
- "kind.prune": "\u526A\u679D",
63
- "kind.model": "\u5207\u6362",
64
- "nodes.title": "\u6D88\u606F\u6784\u6210",
65
- "nodes.hint": "\u5F53\u524D\u6A21\u578B\u53EF\u89C1\u7684\u6D88\u606F\uFF0C\u6700\u65B0\u5728\u524D",
66
- "nodes.more": "\u2026 \u66F4\u65E9\u7684 {n} \u6761\u6D88\u606F\u5DF2\u7701\u7565",
67
- "nodes.empty": "\u5F53\u524D\u6CA1\u6709\u6A21\u578B\u53EF\u89C1\u7684\u6D88\u606F",
68
- "loading": "\u6B63\u5728\u8BFB\u53D6\u4F1A\u8BDD\u65E5\u5FD7\u2026",
69
- "error": "\u4E0A\u4E0B\u6587\u6570\u636E\u8BFB\u53D6\u5931\u8D25\uFF1A",
70
- "error.retry": "\u91CD\u8BD5",
71
- "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",
72
- "tip.step": "\u7B2C {t} \u8F6E \xB7 \u7B2C{s}\u6B65",
73
- "tip.turn": "\u7B2C {t} \u8F6E \xB7 \u5171 {n} \u6B65",
74
- "tip.total": "\u5408\u8BA1 \u2248 {n}",
75
- "tip.actual": "\uFF08\u5B9E\u9645 {n}\uFF09",
76
- "ev.compaction": "\u538B\u7F29\u4E0A\u4E0B\u6587\uFF08\u6458\u8981\u66FF\u6362 {n} \u6761\u6D88\u606F\uFF09",
77
- "ev.prune": "\u526A\u679D\u5DE5\u5177\u8F93\u51FA",
78
- "ev.skill": "Skill \u6CE8\u5165\uFF08{name}\uFF09",
79
- "ev.model": "\u6A21\u578B\u5207\u6362\uFF1A{a} \u2192 {b}",
80
- "form.instructions": "\u6307\u4EE4\u6CE8\u5165",
81
- "form.catalog": "\u76EE\u5F55\u66F4\u65B0",
82
- "form.snapshot": "\u72B6\u6001\u5FEB\u7167",
83
- "form.notice": "\u901A\u77E5",
84
- "form.relay": "\u4EE3\u7406\u8F6C\u53D1",
85
- "form.recall": "\u5386\u53F2\u53EC\u56DE",
86
- "form.context": "\u4E0A\u4E0B\u6587\u6CE8\u5165",
87
- "node.toolResult": "\u5DE5\u5177\u7ED3\u679C",
88
- "node.calls": "\u8C03\u7528 ",
89
- "node.empty": "(\u7A7A\u56DE\u590D)",
90
- "node.nonText": "(\u975E\u6587\u672C\u6D88\u606F)",
91
- "node.snapshot": "\u5FEB\u7167: ",
92
- "cmd.desc": "\u67E5\u770B\u5F53\u524D\u4E0A\u4E0B\u6587\u6784\u6210\uFF0C\u6D4F\u89C8\u5404\u6B65\u9AA4\u7EC4\u6210",
93
- "cmd.close": "\u5173\u95ED",
94
- "browser.title": "\u4E0A\u4E0B\u6587\u6D4F\u89C8\u5668",
95
- "browser.live": "\u5F53\u524D\uFF08\u4E0B\u4E00\u6B21\u8BF7\u6C42\uFF09",
96
- "browser.liveNow": "\u5F53\u524D \xB7 \u4E0B\u4E00\u6B21\u8BF7\u6C42",
97
- "browser.items": "{n} \u9879",
98
- "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",
99
- "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",
100
- "browser.deltaHint": "\u5BF9\u6BD4\u4E0A\u8F6E\u672B\u6B65\u7684\u53D8\u52A8",
101
- "browser.noHeader": "\u6B64\u6570\u636E\u6765\u81EA\u65E7\u7248\u63D2\u4EF6\uFF1A\u4EC5\u63D0\u4F9B token \u4F30\u7B97\uFF0C\u65E0\u5B9E\u9645\u5185\u5BB9",
102
- "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",
103
- "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",
104
- "browser.loading": "\u6B63\u5728\u4ECE\u66F4\u65E9\u7684\u4F1A\u8BDD\u5386\u53F2\u52A0\u8F7D\u5B8C\u6574\u5185\u5BB9\u2026",
105
- "browser.preview": "\u9884\u89C8",
106
- // Tool schema browser card: parsed parameter table sits above the (collapsed
107
- // by default) raw JSON so the common case — skimming arguments — stays
108
- // scannable; the JSON is still one click away for the full payload.
109
- "tool.desc": "\u63CF\u8FF0",
110
- "tool.params": "\u53C2\u6570",
111
- "tool.paramsEmpty": "\uFF08\u65E0\u53C2\u6570\uFF09",
112
- "tool.jsonToggle": "\u67E5\u770B\u539F\u59CB JSON",
113
- "tool.jsonHide": "\u6536\u8D77",
114
- // Detail-card view switch (system prompt / tool description / message and
115
- // injection bodies): a segmented pill — segment labels + tooltips.
116
- "rich.raw": "\u539F\u6587",
117
- "rich.md": "Markdown",
118
- "rich.toMd": "\u6309 Markdown \u6E32\u67D3\u67E5\u770B",
119
- "rich.toRaw": "\u67E5\u770B\u539F\u59CB\u6587\u672C"
120
- };
121
- var DICT_EN = {
122
- "tab": "Context",
123
- "cat.system": "System Prompt",
124
- "cat.tools": "Tool Schemas",
125
- "cat.user": "User Messages",
126
- "cat.inject": "Injected Context",
127
- "cat.assistant": "Assistant Messages",
128
- "cat.tool": "Tool Results",
129
- "overview.title": "Current Context",
130
- "overview.estimate": "tokens (estimated)",
131
- "overview.free": "Free Window",
132
- "overview.used": "of context used",
133
- "overview.ofUsed": "of used context",
134
- "overview.compactReserve": "Auto-compaction reserve: auto-compaction triggers at {pct}% of the window \u2014 this area is normally kept as headroom, not actually used",
135
- "stats.title": "Context Stats",
136
- "stats.hint": "Over the retained history window (same as the History chart)",
137
- "stats.turns": "Turns",
138
- "stats.steps": "Steps",
139
- "stats.injects": "Injections",
140
- "stats.compactions": "Compactions",
141
- "stats.prunes": "Prunes",
142
- "stats.cacheHit": "Cache Hits",
143
- "stats.cost": "Est. Cost",
144
- "stats.costTip": "Rough cumulative cost of the whole session at DeepSeek\u2019s list prices: input split by cache hit/miss, output includes reasoning; each request is priced at the peak (01:00\u201304:00, 06:00\u201310:00 UTC) or half-price off-peak rate by its time. Applies to deepseek-v4-flash / deepseek-v4-pro on any provider; prices are hardcoded, for reference only.",
145
- "stats.costPriceHead": "Per-1M-token rates (peak | off-peak at half price):",
146
- "stats.costHit": "hit",
147
- "stats.costMiss": "miss",
148
- "stats.costOut": "output",
149
- "plugin.title": "Plugin Info",
150
- "plugin.hint": "The best DSH context plugin \u2B50",
151
- "plugin.name": "Plugin",
152
- "plugin.github": "GitHub",
153
- "tools.top": "Top Tool Schemas:",
154
- "tools.more": "of {n}",
155
- "trend.title": "Context History",
156
- "gran.step": "Step",
157
- "gran.turn": "Turn",
158
- "trend.hint": "Hover to view details, \u2702 marks compaction/prune, Step/Turn switches granularity",
159
- "trend.empty": "Send a message and each model request\u2019s context makeup shows up here",
160
- "detail.step": "Turn {t} \xB7 Step {s}",
161
- "detail.turn": "Turn {t} \xB7 {n} steps",
162
- "detail.lastStep": "Last Step",
163
- "detail.estTotal": "Estimated \u2248 {n}",
164
- "detail.actual": "Actual Prompt {n}",
165
- "detail.output": "Output {n}",
166
- "events.title": "Context Events",
167
- "events.empty": "No context events yet (compaction, injections, model switches appear here)",
168
- "events.at": "Turn {t} \xB7 Step {s}",
169
- "events.range": "Turn {t} \xB7 Step {a}\u2192{b}",
170
- "events.rangeTo": "Turn {a} \xB7 Step {as} \u2192 Turn {b} \xB7 Step {bs}",
171
- "kind.inject": "Inject",
172
- "kind.compaction": "Compact",
173
- "kind.prune": "Prune",
174
- "kind.model": "Switch",
175
- "nodes.title": "Messages",
176
- "nodes.hint": "currently model-visible, newest first",
177
- "nodes.more": "\u2026 {n} earlier messages omitted",
178
- "nodes.empty": "No model-visible messages right now",
179
- "loading": "Reading the session log\u2026",
180
- "error": "Failed to read context data: ",
181
- "error.retry": "Retry",
182
- "footer": "Estimate: same fixed-density heuristic as dsh\u2019s built-in tokenMeter (~4 chars \u2248 1 token); \u201Cactual\u201D is provider-reported usage.",
183
- "tip.step": "Turn {t} \xB7 Step {s}",
184
- "tip.turn": "Turn {t} \xB7 {n} steps",
185
- "tip.total": "Total \u2248 {n}",
186
- "tip.actual": " (actual {n})",
187
- "ev.compaction": "Context compacted (summary replaced {n} messages)",
188
- "ev.prune": "Tool output pruned",
189
- "ev.skill": "Skill injected ({name})",
190
- "ev.model": "Model switched: {a} \u2192 {b}",
191
- "form.instructions": "Instructions",
192
- "form.catalog": "Catalog Update",
193
- "form.snapshot": "State Snapshot",
194
- "form.notice": "Notice",
195
- "form.relay": "Agent Relay",
196
- "form.recall": "Recall",
197
- "form.context": "Context Injection",
198
- "node.toolResult": "Tool Result",
199
- "node.calls": "Calls ",
200
- "node.empty": "(empty reply)",
201
- "node.nonText": "(non-text message)",
202
- "node.snapshot": "Snapshot: ",
203
- "cmd.desc": "View current context makeup, browse per-step composition",
204
- "cmd.close": "Close",
205
- "browser.title": "Context Browser",
206
- "browser.live": "Live (Next Request)",
207
- "browser.liveNow": "Live \xB7 Next Request",
208
- "browser.items": "{n} Items",
209
- "browser.missingLive": "\u2026 {n} earlier messages are also part of the context (outside the served window)",
210
- "browser.approx": "Some removed messages of this step exceed retention \u2014 the makeup below is approximate",
211
- "browser.deltaHint": "vs previous turn",
212
- "browser.noHeader": "Served by an older plugin build: token estimates only, no content",
213
- "browser.noEpoch": "The header content (System Prompt / Tool Schemas) of this step is outside retention",
214
- "browser.noContent": "Full content is outside the loaded message window (load older history in Chat to view)",
215
- "browser.loading": "Loading full content from older session history\u2026",
216
- "browser.preview": "Preview",
217
- // Tool schema browser card: parsed parameter table sits above the (collapsed
218
- // by default) raw JSON so the common case — skimming arguments — stays
219
- // scannable; the JSON is still one click away for the full payload.
220
- "tool.desc": "Description",
221
- "tool.params": "Parameters",
222
- "tool.paramsEmpty": "(no parameters)",
223
- "tool.jsonToggle": "View Raw JSON",
224
- "tool.jsonHide": "Collapse",
225
- // Detail-card view switch (system prompt / tool description / message and
226
- // injection bodies): a segmented pill — segment labels + tooltips.
227
- "rich.raw": "Raw",
228
- "rich.md": "Markdown",
229
- "rich.toMd": "View as Markdown",
230
- "rich.toRaw": "View Raw Text"
231
- };
232
-
233
- // src/client/modalStore.ts
234
- var stores = /* @__PURE__ */ new Map();
235
- function modalStoreOf(sessionId) {
236
- const existing = stores.get(sessionId);
237
- if (existing !== void 0) return existing;
238
- let open = false;
239
- const listeners = /* @__PURE__ */ new Set();
240
- const store = {
241
- subscribe(listener) {
242
- listeners.add(listener);
243
- return () => {
244
- listeners.delete(listener);
245
- };
246
- },
247
- getSnapshot: () => open,
248
- set(next) {
249
- if (next === open) return;
250
- open = next;
251
- for (const listener of listeners) listener();
252
- }
253
- };
254
- stores.set(sessionId, store);
255
- return store;
256
- }
257
- var pendingConsume = /* @__PURE__ */ new Map();
258
- function setPendingConsume(sessionId, guard) {
259
- pendingConsume.set(sessionId, guard);
260
- }
261
- function takePendingConsume(sessionId) {
262
- const guard = pendingConsume.get(sessionId);
263
- if (guard !== void 0) pendingConsume.delete(sessionId);
264
- return guard;
265
- }
266
-
267
- // src/client/command.ts
268
- var COMMAND = "context";
269
- var LINE = "/" + COMMAND;
270
- function registerContextCommand(ctx, kit) {
271
- ctx.effect(() => {
272
- const inputTriggers = ctx.get("inputTriggers");
273
- if (inputTriggers === void 0) return () => {
274
- };
275
- return inputTriggers.registerSource({
276
- trigger: "/",
277
- name: COMMAND,
278
- order: 1,
279
- candidates: (_session, req) => {
280
- if (req.position !== "leading") return Promise.resolve([]);
281
- const query = req.query.trim().toLowerCase();
282
- if (query !== "" && !COMMAND.startsWith(query)) return Promise.resolve([]);
283
- return Promise.resolve([{ name: COMMAND, description: kit.t("cmd.desc") }]);
284
- },
285
- onPick: (pick) => {
286
- setPendingConsume(pick.session.sessionId, { kind: "span", span: pick.span });
287
- modalStoreOf(pick.session.sessionId).set(true);
288
- return "handled";
289
- },
290
- matchEnter: (session, line) => {
291
- if (line !== LINE) return Promise.resolve(void 0);
292
- setPendingConsume(session.sessionId, { kind: "bare-token", token: LINE });
293
- modalStoreOf(session.sessionId).set(true);
294
- return Promise.resolve("handled");
295
- }
296
- });
297
- }, "dsh-context: /context command");
298
- }
299
-
300
- // src/client/categories.ts
301
- var CATS = [
302
- { key: "system", color: "#6366f1" },
303
- { key: "tools", color: "#f59e0b" },
304
- { key: "user", color: "#22c55e" },
305
- { key: "inject", color: "#a855f7" },
306
- { key: "assistant", color: "#3b82f6" },
307
- { key: "tool", color: "#14b8a6" }
308
- ];
309
- function partsOf(breakdown) {
310
- return CATS.map((c) => {
311
- return { key: c.key, color: c.color, value: breakdown[c.key] || 0 };
312
- });
313
- }
314
- function anchoredParts(parts, target) {
315
- if (target === null || target <= 0) return parts;
316
- let total = 0;
317
- for (const p of parts) total += p.value;
318
- if (total <= 0 || total === target) return parts;
319
- const scale = target / total;
320
- return parts.map((p) => ({ ...p, value: Math.round(p.value * scale) }));
321
- }
322
-
323
- // src/client/headline.ts
324
- function headlineOf(data, pressure = null) {
325
- const current = data.current;
326
- const projected = pressure !== null && typeof pressure.projectedTokens === "number" ? pressure.projectedTokens : void 0;
327
- const requests = data.requests || [];
328
- const lastReq = requests.length > 0 ? requests[requests.length - 1] : null;
329
- const derived = lastReq !== null && typeof lastReq.prompt === "number" ? lastReq.prompt + (current.total - lastReq.total) : void 0;
330
- const occupancyTokens = projected ?? derived ?? null;
331
- const window2 = pressure !== null && typeof pressure.contextWindow === "number" ? pressure.contextWindow : data.contextWindow;
332
- const tokens = occupancyTokens ?? current.total;
333
- const pct = window2 !== void 0 && window2 > 0 ? Math.min(100, Math.round(tokens / window2 * 100)) : null;
334
- const parts = anchoredParts(partsOf(current), occupancyTokens !== null && tokens > 0 ? tokens : null);
335
- return { tokens, window: window2, pct, parts };
336
- }
337
-
338
- // src/client/services.ts
339
- function asRecord(value) {
340
- if (value === null || value === void 0 || typeof value !== "object") return null;
341
- return value;
342
- }
343
- function numOf(value) {
344
- return typeof value === "number" && Number.isFinite(value) ? value : 0;
345
- }
346
- function objectsOf(value) {
347
- if (!Array.isArray(value)) return [];
348
- return value.filter((v) => v !== null && typeof v === "object");
349
- }
350
- function timelineOf(value) {
351
- const data = asRecord(value);
352
- if (data === null) return null;
353
- const current = data.current;
354
- const numericBreakdown = current !== null && typeof current === "object" && ["system", "tools", "user", "inject", "assistant", "tool", "total"].every((k) => typeof current[k] === "number");
355
- if (numericBreakdown && Array.isArray(data.requests) && Array.isArray(data.events) && Array.isArray(data.nodes) && Array.isArray(data.archive) && Array.isArray(data.toolList)) {
356
- return data;
357
- }
358
- const safeCurrent = current !== null && typeof current === "object" ? current : {};
359
- const cost = typeof data.cost === "object" && data.cost !== null && !Array.isArray(data.cost) ? data.cost : void 0;
360
- return {
361
- ok: true,
362
- ...typeof data.model === "string" ? { model: data.model } : {},
363
- ...typeof data.provider === "string" ? { provider: data.provider } : {},
364
- ...typeof data.contextWindow === "number" ? { contextWindow: data.contextWindow } : {},
365
- current: {
366
- system: numOf(safeCurrent.system),
367
- tools: numOf(safeCurrent.tools),
368
- user: numOf(safeCurrent.user),
369
- inject: numOf(safeCurrent.inject),
370
- assistant: numOf(safeCurrent.assistant),
371
- tool: numOf(safeCurrent.tool),
372
- total: numOf(safeCurrent.total)
373
- },
374
- toolList: objectsOf(data.toolList),
375
- requests: objectsOf(data.requests),
376
- events: objectsOf(data.events),
377
- nodes: objectsOf(data.nodes),
378
- droppedNodes: numOf(data.droppedNodes),
379
- archive: objectsOf(data.archive),
380
- ...cost !== void 0 ? { cost } : {},
381
- ...typeof data.surfaceFloor === "number" ? { surfaceFloor: data.surfaceFloor } : {},
382
- ...typeof data.archiveFloor === "number" ? { archiveFloor: data.archiveFloor } : {}
383
- };
384
- }
385
- var contextPressureOf = (value) => asRecord(value);
386
- var tokenUsageOf = (value) => asRecord(value);
387
- function headersOf(value) {
388
- const headers = asRecord(value);
389
- if (headers === null || !Array.isArray(headers.headers)) return null;
390
- for (const h2 of headers.headers) {
391
- if (h2 === null || typeof h2 !== "object") return null;
392
- const entry = h2;
393
- if (!Array.isArray(entry.tools)) return null;
394
- if (entry.system !== void 0 && typeof entry.system !== "string") return null;
395
- }
396
- return headers;
397
- }
398
-
399
- // src/client/assemble.ts
400
- function headerAt(headers, seq) {
401
- if (headers === null || headers.headers.length === 0) return null;
402
- if (seq === null) return headers.headers[headers.headers.length - 1];
403
- for (let i = headers.headers.length - 1; i >= 0; i--) {
404
- if (headers.headers[i].seq < seq) return headers.headers[i];
405
- }
406
- return null;
407
- }
408
- function assemble(data, headers, seq) {
409
- const live = seq === null;
410
- let nodes;
411
- if (live) {
412
- nodes = data.nodes.slice();
413
- } else {
414
- const picked = [];
415
- for (const n of data.nodes) {
416
- if (n.seq < seq) picked.push(n);
417
- }
418
- for (const n of data.archive || []) {
419
- if (n.seq < seq && n.gone !== void 0 && n.gone > seq) picked.push(n);
420
- }
421
- nodes = picked;
422
- }
423
- nodes.sort((a, b) => a.seq - b.seq);
424
- let missingLive = 0;
425
- if (data.droppedNodes > 0) {
426
- if (live || data.surfaceFloor !== void 0 && seq > data.surfaceFloor) {
427
- missingLive = data.droppedNodes;
428
- }
429
- }
430
- const approximate = !live && data.archiveFloor !== void 0 && seq < data.archiveFloor;
431
- return { live, header: headerAt(headers, seq), nodes, missingLive, approximate };
432
- }
433
-
434
- // src/client/react.ts
435
- var React = require("react");
436
- var h = React.createElement;
437
-
438
- // src/client/components/nodes.tsx
439
- function makeNodeText(kit) {
440
- const { t } = kit;
441
- return function nodeText(n) {
442
- if (n.cat === "tool") {
443
- return t("node.toolResult") + (n.tool ? " \u2190 " + n.tool : "") + (n.err ? " \u26A0" : "");
444
- }
445
- if (n.skill) return "Skill: " + n.skill;
446
- if (n.calls) return t("node.calls") + n.calls.join(", ");
447
- if (n.text) return n.form === "snapshot" ? t("node.snapshot") + n.text : n.text;
448
- if (n.cat === "assistant") return t("node.empty");
449
- if (n.cat === "inject") return t("form." + (n.form || "context"));
450
- return t("node.nonText");
451
- };
452
- }
453
- function makeNodeList(kit) {
454
- const { t, fmt: fmt3, fmtTime: fmtTime2 } = kit;
455
- const nodeText = makeNodeText(kit);
456
- return function NodeList(props) {
457
- if (props.nodes.length === 0) {
458
- return /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("nodes.empty"));
459
- }
460
- const catColor = {};
461
- for (const c of CATS) catColor[c.key] = c.color;
462
- const rows = props.nodes.slice().reverse();
463
- 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) => {
464
- const text = nodeText(n);
465
- 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)));
466
- }));
467
- };
468
- }
469
-
470
- // src/client/components/richText.tsx
471
- var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
472
- function makeRichText(kit) {
473
- const { t } = kit;
474
- function useRichMode() {
475
- const [mode, setMode] = React.useState("md");
476
- return [mode, setMode];
477
- }
478
- function RichSwitch(props) {
479
- const seg = (m, label, tip) => /* @__PURE__ */ React.createElement(
480
- "button",
481
- {
482
- type: "button",
483
- className: "lc-rich-seg-btn" + (props.mode === m ? " lc-rich-seg-on" : ""),
484
- title: tip,
485
- onClick: () => {
486
- props.onPick(m);
487
- }
488
- },
489
- label
490
- );
491
- return /* @__PURE__ */ React.createElement("span", { className: "lc-rich-seg" + (props.inHead === true ? " lc-rich-seg-head" : "") }, seg("raw", t("rich.raw"), t("rich.toRaw")), seg("md", t("rich.md"), t("rich.toMd")));
492
- }
493
- function RichText(props) {
494
- if (props.mode === "md") {
495
- return /* @__PURE__ */ React.createElement("div", { className: props.mdClass ?? "lc-br-md" }, /* @__PURE__ */ React.createElement(import_dsh_client_ui_primitives.MarkdownText, { text: props.text }));
496
- }
497
- return /* @__PURE__ */ React.createElement("pre", { className: props.rawClass ?? "lc-br-pre" }, props.text);
498
- }
499
- function RichCard(props) {
500
- const [mode, setMode] = useRichMode();
501
- return /* @__PURE__ */ React.createElement("div", { className: props.className ?? "lc-br-content" }, /* @__PURE__ */ React.createElement(RichSwitch, { mode, onPick: setMode }), props.render(mode));
502
- }
503
- return { RichText, RichCard, RichSwitch, useRichMode };
504
- }
505
-
506
- // src/client/components/browser.tsx
507
- function unionTypesOf(p) {
508
- const branches = [];
509
- if (Array.isArray(p.anyOf)) branches.push(...p.anyOf);
510
- if (Array.isArray(p.oneOf)) branches.push(...p.oneOf);
511
- if (branches.length === 0) return null;
512
- const parts = [];
513
- for (const b of branches) {
514
- if (b !== null && typeof b === "object") parts.push(typeOf(b));
515
- }
516
- return parts.length > 0 ? parts.join(" | ") : null;
517
- }
518
- function typeOf(p) {
519
- const u = unionTypesOf(p);
520
- if (u !== null) return u;
521
- const t = p.type;
522
- if (t === "array") {
523
- const items = p.items;
524
- if (items !== null && typeof items === "object") {
525
- const inner = typeOf(items);
526
- return "array<" + inner + ">";
527
- }
528
- return "array";
529
- }
530
- if (typeof t === "string") {
531
- if (t === "object") {
532
- const props = p.properties;
533
- if (props !== null && typeof props === "object" && Object.keys(props).length > 0) {
534
- return "object{" + Object.keys(props).length + "}";
535
- }
536
- }
537
- if (Array.isArray(p.enum) && p.enum.length > 0) {
538
- return t + " (enum)";
539
- }
540
- return t;
541
- }
542
- if (Array.isArray(p.enum) && p.enum.length > 0) return "(enum)";
543
- return "unknown";
544
- }
545
- function paramsOf(schema) {
546
- if (schema === null || typeof schema !== "object") return null;
547
- const s = schema;
548
- const candidate = (v) => v !== null && typeof v === "object" ? v : null;
549
- const nested = candidate(s.parameters) ?? candidate(s.input_schema) ?? candidate(s.inputSchema);
550
- if (nested !== null) return nested;
551
- if (s.type === "object" && s.properties !== void 0 && typeof s.properties === "object") {
552
- return s;
553
- }
554
- return null;
555
- }
556
- function ParamRow(props) {
557
- const typeLabel = typeOf(props.schema);
558
- const desc = props.schema.description;
559
- 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);
560
- }
561
- function ToolSchema(props) {
562
- const { rich } = props;
563
- const [jsonOpen, setJsonOpen] = React.useState(false);
564
- const [descMode, setDescMode] = rich.useRichMode();
565
- const params = React.useMemo(() => paramsOf(props.schema), [props.schema]);
566
- const rows = React.useMemo(() => {
567
- if (params === null) return [];
568
- const props2 = params.properties;
569
- if (props2 === null || typeof props2 !== "object") return [];
570
- const req = Array.isArray(params.required) ? new Set(params.required.filter((x) => typeof x === "string")) : /* @__PURE__ */ new Set();
571
- const out = [];
572
- for (const k of Object.keys(props2)) {
573
- const v = props2[k];
574
- if (v === null || typeof v !== "object") continue;
575
- out.push({ name: k, schema: v, required: req.has(k) });
576
- }
577
- return out;
578
- }, [params]);
579
- const schemaJson = React.useMemo(
580
- () => props.schema !== void 0 ? JSON.stringify(props.schema, null, 2) : "",
581
- [props.schema]
582
- );
583
- 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(rich.RichSwitch, { mode: descMode, onPick: setDescMode, inHead: true })), /* @__PURE__ */ React.createElement(
584
- rich.RichText,
585
- {
586
- text: props.description,
587
- mode: descMode,
588
- rawClass: "lc-ts-desc-body",
589
- mdClass: "lc-ts-desc-md"
590
- }
591
- )) : 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(
592
- "button",
593
- {
594
- type: "button",
595
- className: "lc-ts-json-toggle",
596
- onClick: () => {
597
- setJsonOpen((o) => !o);
598
- }
599
- },
600
- (jsonOpen ? "\u25BE " : "\u25B8 ") + (jsonOpen ? props.labels.hide : props.labels.show)
601
- ), jsonOpen ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre lc-br-dim" }, schemaJson) : null) : null);
602
- }
603
- function RawBlocks(props) {
604
- const { rich } = props;
605
- return /* @__PURE__ */ React.createElement(React.Fragment, null, props.blocks.map((b, i) => {
606
- const blk = b;
607
- if (blk !== null && typeof blk === "object" && (blk.type === "text" || blk.type === "reasoning") && typeof blk.text === "string") {
608
- if (blk.type === "reasoning") {
609
- return /* @__PURE__ */ React.createElement("pre", { key: i, className: "lc-br-pre lc-br-dim" }, blk.text);
610
- }
611
- return /* @__PURE__ */ React.createElement(rich.RichText, { key: i, text: blk.text, mode: props.mode });
612
- }
613
- if (blk !== null && typeof blk === "object" && blk.type === "tool-result" && Array.isArray(blk.content)) {
614
- return /* @__PURE__ */ React.createElement(RawBlocks, { key: i, blocks: blk.content, mode: props.mode, rich });
615
- }
616
- return /* @__PURE__ */ React.createElement("pre", { key: i, className: "lc-br-pre lc-br-dim" }, JSON.stringify(b, null, 2));
617
- }));
618
- }
619
- function NodeContent(props) {
620
- const { node, conv, rich } = props;
621
- const richable = node.cat === "user" || node.cat === "assistant" || node.cat === "inject";
622
- const wrap = (render) => richable ? /* @__PURE__ */ React.createElement(rich.RichCard, { render }) : /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, render("raw"));
623
- if (conv === void 0) {
624
- if (node.text === void 0 || node.text === "") {
625
- return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, props.hint));
626
- }
627
- return wrap((mode) => /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(rich.RichText, { text: node.text ?? "", mode }), /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, props.hint)));
628
- }
629
- if (conv.kind === "assistant" && Array.isArray(conv.blocks)) {
630
- const blocks = conv.blocks;
631
- return wrap((mode) => /* @__PURE__ */ React.createElement(React.Fragment, null, blocks.map((b, i) => {
632
- const blk = b;
633
- if (blk.kind === "text" && typeof blk.text === "string") {
634
- return /* @__PURE__ */ React.createElement(rich.RichText, { key: i, text: blk.text, mode });
635
- }
636
- if (blk.kind === "reasoning" && typeof blk.text === "string") {
637
- return /* @__PURE__ */ React.createElement("pre", { key: i, className: "lc-br-pre lc-br-dim" }, blk.text);
638
- }
639
- if (blk.kind === "tool-call") {
640
- 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);
641
- }
642
- return null;
643
- })));
644
- }
645
- if (conv.kind === "tool-result") {
646
- 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, mode: "raw", rich }) : null);
647
- }
648
- if (conv.kind === "compaction") {
649
- return wrap((mode) => /* @__PURE__ */ React.createElement(React.Fragment, null, typeof conv.summary === "string" && conv.summary !== "" ? /* @__PURE__ */ React.createElement(rich.RichText, { text: conv.summary, mode }) : null));
650
- }
651
- if (Array.isArray(conv.content)) {
652
- return wrap((mode) => /* @__PURE__ */ React.createElement(RawBlocks, { blocks: conv.content, mode, rich }));
653
- }
654
- return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, props.hint));
655
- }
656
- function byCatOf(asm) {
657
- var _a;
658
- const m = {};
659
- for (const n of asm.nodes) (m[_a = n.cat] ?? (m[_a] = [])).push(n);
660
- return m;
661
- }
662
- function countOf(asm, c) {
663
- if (c === "system") return asm.header !== null && asm.header.system !== void 0 ? 1 : 0;
664
- if (c === "tools") return asm.header !== null ? asm.header.tools.length : 0;
665
- return byCatOf(asm)[c]?.length ?? 0;
666
- }
667
- function lastOfTurn(requests, turn) {
668
- for (let i = requests.length - 1; i >= 0; i--) if ((requests[i].turn ?? 0) === turn) return requests[i];
669
- return null;
670
- }
671
- function makeContextBrowser(kit, StackedBar) {
672
- const { t, fmt: fmt3, fmtTime: fmtTime2, catLabel } = kit;
673
- const nodeText = makeNodeText(kit);
674
- const rich = makeRichText(kit);
675
- const catColor = {};
676
- for (const c of CATS) catColor[c.key] = c.color;
677
- const MAX_AUTO_PAGES = 20;
678
- return function ContextBrowser(props) {
679
- const { data, headers } = props;
680
- const [sel, setSel] = React.useState("live");
681
- const [openCat, setOpenCat] = React.useState(null);
682
- const [openElem, setOpenElem] = React.useState(null);
683
- const convNodes = typeof props.useSession === "function" ? props.useSession((s) => s.nodes) : void 0;
684
- const bySeq = React.useMemo(() => {
685
- const m = /* @__PURE__ */ new Map();
686
- for (const n of convNodes ?? []) m.set(n.seq, n);
687
- return m;
688
- }, [convNodes]);
689
- const hasMore = typeof props.useSession === "function" ? props.useSession((s) => s.hasMore === true) : false;
690
- const loadingOlder = typeof props.useSession === "function" ? props.useSession((s) => s.loadingOlder === true) : false;
691
- const openSeq = openElem !== null && openElem.startsWith("n") ? Number(openElem.slice(1)) : null;
692
- const missingSeq = openSeq !== null && !bySeq.has(openSeq) ? openSeq : null;
693
- const [exhausted, setExhausted] = React.useState(false);
694
- const pagesRef = React.useRef(0);
695
- React.useEffect(() => {
696
- pagesRef.current = 0;
697
- setExhausted(false);
698
- }, [openElem]);
699
- const loadOlderHistory = props.loadOlderHistory;
700
- React.useEffect(() => {
701
- if (missingSeq === null || !hasMore || loadingOlder || exhausted) return;
702
- if (loadOlderHistory === void 0) return;
703
- if (pagesRef.current >= MAX_AUTO_PAGES) {
704
- setExhausted(true);
705
- return;
706
- }
707
- pagesRef.current += 1;
708
- void loadOlderHistory();
709
- }, [missingSeq, hasMore, loadingOlder, exhausted, bySeq, loadOlderHistory]);
710
- React.useEffect(() => {
711
- if (!hasMore && missingSeq !== null && !exhausted) setExhausted(true);
712
- }, [hasMore, missingSeq, exhausted]);
713
- const pinSeq = props.pinSeq;
714
- React.useEffect(() => {
715
- setSel(pinSeq === null || pinSeq === void 0 ? "live" : pinSeq);
716
- setOpenCat(null);
717
- setOpenElem(null);
718
- }, [pinSeq]);
719
- const toolFocus = props.toolFocus;
720
- React.useEffect(() => {
721
- if (toolFocus === null || toolFocus === void 0) return;
722
- setSel("live");
723
- setOpenCat("tools");
724
- setOpenElem(toolFocus.tool !== void 0 ? "tool:" + toolFocus.tool : null);
725
- if (props.onToolFocusHandled !== void 0) props.onToolFocusHandled();
726
- }, [toolFocus, props.onToolFocusHandled]);
727
- const awaiting = missingSeq !== null && !exhausted && loadOlderHistory !== void 0 && hasMore;
728
- const requests = data.requests || [];
729
- const hoverReq = props.previewSeq !== null && props.previewSeq !== void 0 ? requests.find((r) => r.seq === props.previewSeq) ?? null : null;
730
- const req = hoverReq ?? (sel === "live" ? null : requests.find((r) => r.seq === sel) ?? null);
731
- const seq = req !== null ? req.seq : null;
732
- const linked = req === null && props.onHoverKey !== void 0;
733
- const linkKey = linked && props.hoverKey !== null && props.hoverKey !== "free" ? props.hoverKey : null;
734
- const view = assemble(data, headers, seq);
735
- const breakdown = req !== null ? req : data.current;
736
- const parts = partsOf(breakdown);
737
- const total = breakdown.total;
738
- const pick = (v) => {
739
- setSel(v === "live" ? "live" : Number(v));
740
- setOpenCat(null);
741
- setOpenElem(null);
742
- };
743
- const refReq = req === null ? requests.length > 0 ? requests[requests.length - 1] : null : lastOfTurn(requests, (req.turn ?? 0) - 1);
744
- const prevView = refReq !== null ? assemble(data, headers, refReq.seq) : null;
745
- const byCat = byCatOf(view);
746
- const toolCount = (c) => countOf(view, c);
747
- const toggleCat = (c) => {
748
- const openable = toolCount(c) > 0 || (c === "system" || c === "tools") && view.header === null;
749
- if (!openable) return;
750
- setOpenCat(openCat === c ? null : c);
751
- setOpenElem(null);
752
- };
753
- const toggleElem = (key) => setOpenElem(openElem === key ? null : key);
754
- const elemRow = (key, tag, preview, tokens, time, body) => {
755
- const open = openElem === key;
756
- 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: () => {
757
- toggleElem(key);
758
- } }, /* @__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);
759
- };
760
- const catBody = (c) => {
761
- if (c === "system") {
762
- if (view.header === null) return /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, t(headers === null ? "browser.noHeader" : "browser.noEpoch"));
763
- const system = view.header.system;
764
- if (system === void 0) return null;
765
- return elemRow(
766
- "sys",
767
- null,
768
- system.replace(/\s+/g, " ").trim().slice(0, 80),
769
- breakdown.system,
770
- void 0,
771
- /* @__PURE__ */ React.createElement(rich.RichCard, { render: (mode) => /* @__PURE__ */ React.createElement(rich.RichText, { text: system, mode }) })
772
- );
773
- }
774
- if (c === "tools") {
775
- if (view.header === null) return /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, t(headers === null ? "browser.noHeader" : "browser.noEpoch"));
776
- const labels = {
777
- desc: t("tool.desc"),
778
- title: t("tool.params"),
779
- empty: t("tool.paramsEmpty"),
780
- show: t("tool.jsonToggle"),
781
- hide: t("tool.jsonHide")
782
- };
783
- return view.header.tools.slice().sort((a, b) => b.tokens - a.tokens).map((tool) => {
784
- return elemRow(
785
- "tool:" + tool.name,
786
- null,
787
- tool.name,
788
- tool.tokens,
789
- void 0,
790
- /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, /* @__PURE__ */ React.createElement(ToolSchema, { description: tool.description, schema: tool.schema, rich, labels }))
791
- );
792
- });
793
- }
794
- const nodes = (byCat[c] ?? []).slice().reverse();
795
- return nodes.map((n) => {
796
- let tag = null;
797
- let preview = nodeText(n);
798
- if (n.cat === "tool") {
799
- tag = (n.tool ?? "?") + (n.err ? " \u26A0" : "");
800
- preview = t("node.toolResult") + (n.err ? " \u26A0" : "");
801
- } else if (n.cat === "inject" && !n.skill) {
802
- tag = t("form." + (n.form || "context"));
803
- if (n.text !== void 0 && n.text !== "") {
804
- preview = n.form === "snapshot" ? t("node.snapshot") + n.text : n.text;
805
- }
806
- }
807
- return elemRow(
808
- "n" + n.seq,
809
- tag,
810
- preview,
811
- n.tokens,
812
- n.time,
813
- /* @__PURE__ */ React.createElement(
814
- NodeContent,
815
- {
816
- node: n,
817
- conv: bySeq.get(n.seq),
818
- rich,
819
- hint: bySeq.get(n.seq) === void 0 && awaiting ? t("browser.loading") : t("browser.noContent")
820
- }
821
- )
822
- );
823
- });
824
- };
825
- 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(
826
- "select",
827
- {
828
- className: "lc-br-pick",
829
- value: seq === null ? "live" : String(seq),
830
- onChange: (e) => {
831
- pick(e.target.value);
832
- }
833
- },
834
- /* @__PURE__ */ React.createElement("option", { value: "live" }, t("browser.live")),
835
- 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)))
836
- )), /* @__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(
837
- StackedBar,
838
- {
839
- parts,
840
- height: 10,
841
- hoverKey: linked ? linkKey : void 0,
842
- onHoverKey: linked ? props.onHoverKey : void 0,
843
- tip: false
844
- }
845
- )), 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) => {
846
- const count = toolCount(c.key);
847
- const v = breakdown[c.key] || 0;
848
- const prevCount = prevView !== null ? countOf(prevView, c.key) : null;
849
- const countDelta = prevCount !== null ? count - prevCount : null;
850
- const prevTokens = refReq !== null ? refReq[c.key] || 0 : null;
851
- const tokenDelta = prevTokens !== null ? v - prevTokens : null;
852
- const openable = count > 0 || (c.key === "system" || c.key === "tools") && view.header === null;
853
- const open = openCat === c.key && openable;
854
- return /* @__PURE__ */ React.createElement("div", { key: c.key, className: "lc-br-cat" + (openable ? "" : " lc-br-cat-empty") }, /* @__PURE__ */ React.createElement(
855
- "button",
856
- {
857
- type: "button",
858
- className: "lc-br-cat-row" + (linked && props.hoverKey === c.key ? " lc-br-cat-on" : ""),
859
- onMouseEnter: linked ? () => {
860
- if (props.onHoverKey !== void 0) props.onHoverKey(c.key);
861
- } : void 0,
862
- onMouseLeave: linked ? () => {
863
- if (props.onHoverKey !== void 0) props.onHoverKey(null);
864
- } : void 0,
865
- onClick: () => {
866
- toggleCat(c.key);
867
- }
868
- },
869
- /* @__PURE__ */ React.createElement("span", { className: "lc-br-chev" + (open ? " lc-br-chev-on" : "") }, "\u25B8"),
870
- /* @__PURE__ */ React.createElement("i", { style: { background: c.color } }),
871
- /* @__PURE__ */ React.createElement("span", { className: "lc-br-cat-label" }, catLabel(c.key)),
872
- /* @__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),
873
- /* @__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))),
874
- /* @__PURE__ */ React.createElement("span", { className: "lc-br-pct" }, total > 0 ? Math.round(v / total * 100) + "%" : "")
875
- ), open ? /* @__PURE__ */ React.createElement("div", { className: "lc-br-body" }, catBody(c.key)) : null);
876
- })));
877
- };
878
- }
879
-
880
- // src/client/components/stackedBar.tsx
881
- var AUTO_COMPACT_RATIO = 0.8;
882
- function makeStackedBar(kit) {
883
- const { t, fmt: fmt3, catLabel } = kit;
884
- return function StackedBar(props) {
885
- const [reserveOn, setReserveOn] = React.useState(false);
886
- let total = 0;
887
- for (const p of props.parts) total += p.value;
888
- const scale = props.max !== void 0 && props.max > total ? props.max : total;
889
- const free = props.max !== void 0 && props.max > total ? props.max - total : 0;
890
- const usedPct = scale > 0 ? total / scale * 100 : 0;
891
- const hovering = props.hoverKey !== null && props.hoverKey !== void 0;
892
- const showBox = free > 0 && hovering;
893
- const reserve = props.reserve !== void 0 && props.max !== void 0 && props.max > 0 ? props.reserve : null;
894
- const reserveLeft = reserve !== null ? Math.round(props.max * reserve.ratio / scale * 1e3) / 10 : 0;
895
- const reserveWidth = reserve !== null ? Math.round((1 - reserve.ratio) * props.max / scale * 1e3) / 10 : 0;
896
- let tip = null;
897
- if (reserveOn && reserve !== null) {
898
- tip = {
899
- text: reserve.label,
900
- leftPct: Math.max(12, Math.min(reserveLeft + reserveWidth / 2, 88))
901
- };
902
- } else if (props.hoverKey !== null && props.hoverKey !== void 0) {
903
- if (props.hoverKey === "free" && free > 0) {
904
- const pct = scale > 0 ? free / scale * 100 : 0;
905
- tip = {
906
- text: t("overview.free") + " " + fmt3(free) + " (" + Math.round(pct) + "%)",
907
- leftPct: Math.max(12, Math.min(total / scale * 100 + pct / 2, 88))
908
- };
909
- } else {
910
- let acc = 0;
911
- for (const p of props.parts) {
912
- const pct = scale > 0 ? p.value / scale * 100 : 0;
913
- if (p.key === props.hoverKey && p.value > 0) {
914
- tip = {
915
- // "(pct%)" is a share of the OCCUPIED total — the solid box
916
- // that appears on hover frames exactly this reference region.
917
- text: catLabel(p.key) + " \u2248" + fmt3(p.value) + " (" + Math.round(p.value / total * 100) + "%) " + t("overview.ofUsed"),
918
- leftPct: Math.max(12, Math.min(acc + pct / 2, 88))
919
- };
920
- break;
921
- }
922
- acc += pct;
923
- }
924
- }
925
- }
926
- return /* @__PURE__ */ React.createElement("div", { className: "lc-stacked-wrap" }, /* @__PURE__ */ React.createElement(
927
- "div",
928
- {
929
- className: "lc-stacked" + (hovering ? " lc-stacked-dim" : ""),
930
- style: { height: (props.height || 14) + "px" },
931
- onMouseLeave: () => {
932
- if (props.onHoverKey !== void 0) props.onHoverKey(null);
933
- setReserveOn(false);
934
- }
935
- },
936
- total > 0 ? props.parts.map((p) => {
937
- if (!p.value) return null;
938
- const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
939
- return /* @__PURE__ */ React.createElement(
940
- "div",
941
- {
942
- key: p.key,
943
- className: "lc-stacked-seg" + (on ? " lc-stacked-seg-on" : ""),
944
- style: { width: p.value / scale * 100 + "%", background: p.color },
945
- onMouseEnter: () => {
946
- if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
947
- }
948
- }
949
- );
950
- }) : null,
951
- free > 0 ? /* @__PURE__ */ React.createElement(
952
- "div",
953
- {
954
- key: "free",
955
- className: "lc-stacked-free" + (props.hoverKey === "free" ? " lc-stacked-free-on" : ""),
956
- style: { width: free / scale * 100 + "%" },
957
- onMouseEnter: () => {
958
- if (props.onHoverKey !== void 0) props.onHoverKey("free");
959
- }
960
- }
961
- ) : null,
962
- reserve !== null ? /* @__PURE__ */ React.createElement(
963
- "div",
964
- {
965
- className: "lc-reserve",
966
- style: { left: reserveLeft + "%", width: reserveWidth + "%" },
967
- onMouseEnter: () => {
968
- setReserveOn(true);
969
- if (props.onHoverKey !== void 0) props.onHoverKey(null);
970
- },
971
- onMouseLeave: () => setReserveOn(false)
972
- }
973
- ) : null,
974
- /* @__PURE__ */ React.createElement(
975
- "div",
976
- {
977
- className: "lc-occupied-box" + (showBox ? " lc-occupied-box-on" : ""),
978
- style: { width: usedPct + "%" }
979
- }
980
- )
981
- ), props.tip !== false ? /* @__PURE__ */ React.createElement(
982
- "div",
983
- {
984
- className: "lc-bar-tip" + (tip ? " lc-bar-tip-on" : ""),
985
- style: { left: tip ? tip.leftPct + "%" : "50%" }
986
- },
987
- tip ? tip.text : ""
988
- ) : null);
989
- };
990
- }
991
- function makeLegend(kit) {
992
- const { t, fmt: fmt3, catLabel } = kit;
993
- return function Legend(props) {
994
- let total = 0;
995
- for (const p of props.parts) total += p.value;
996
- return /* @__PURE__ */ React.createElement("div", { className: "lc-legend" }, props.parts.map((p) => {
997
- const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
998
- return /* @__PURE__ */ React.createElement(
999
- "span",
1000
- {
1001
- key: p.key,
1002
- className: "lc-chip" + (on ? " lc-chip-on" : ""),
1003
- title: t("overview.ofUsed"),
1004
- onMouseEnter: () => {
1005
- if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
1006
- },
1007
- onMouseLeave: () => {
1008
- if (props.onHoverKey !== void 0) props.onHoverKey(null);
1009
- }
1010
- },
1011
- /* @__PURE__ */ React.createElement("i", { style: { background: p.color } }),
1012
- /* @__PURE__ */ React.createElement("span", { className: "lc-chip-label" }, catLabel(p.key)),
1013
- /* @__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)
1014
- );
1015
- }));
1016
- };
1017
- }
1018
-
1019
- // src/client/components/currentComposition.tsx
1020
- function makeCurrentComposition(kit, StackedBar, Legend) {
1021
- const { t, fmt: fmt3 } = kit;
1022
- return function CurrentComposition(props) {
1023
- const head = props.head;
1024
- 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;
1025
- 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: () => {
1026
- if (props.onToolFocus !== void 0) props.onToolFocus({});
1027
- } }, 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: () => {
1028
- if (props.onToolFocus !== void 0) props.onToolFocus({ tool: tool.name });
1029
- } }, tool.name + " " + fmt3(tool.tokens))), props.tools.length > 5 ? /* @__PURE__ */ React.createElement("button", { type: "button", className: "lc-tools-more", onClick: () => {
1030
- if (props.onToolFocus !== void 0) props.onToolFocus({});
1031
- } }, t("tools.more", { n: props.tools.length })) : null) : null);
1032
- };
1033
- }
1034
-
1035
- // src/client/components/errorBoundary.tsx
1036
- function makeErrorBoundary(t) {
1037
- return class ErrorBoundary extends React.Component {
1038
- constructor(props) {
1039
- super(props);
1040
- this.state = { error: null };
1041
- }
1042
- static getDerivedStateFromError(error) {
1043
- return { error: error instanceof Error ? error : new Error(String(error)) };
1044
- }
1045
- render() {
1046
- const error = this.state.error;
1047
- if (error === null) return this.props.children;
1048
- return /* @__PURE__ */ React.createElement("div", { className: "lc-root" }, /* @__PURE__ */ React.createElement("div", { className: "lc-empty lc-error" }, /* @__PURE__ */ React.createElement("span", null, t("error")), /* @__PURE__ */ React.createElement("code", { className: "lc-error-msg" }, error.message), /* @__PURE__ */ React.createElement(
1049
- "button",
1050
- {
1051
- type: "button",
1052
- className: "lc-error-retry",
1053
- onClick: () => {
1054
- this.setState({ error: null });
1055
- }
1056
- },
1057
- t("error.retry")
1058
- )));
1059
- }
1060
- };
1061
- }
1062
-
1063
- // src/client/components/contextModal.tsx
1064
- function makeContextModal(ctx, kit) {
1065
- const { t } = kit;
1066
- const sessions = ctx.get("sessions");
1067
- const StackedBar = makeStackedBar(kit);
1068
- const Legend = makeLegend(kit);
1069
- const CurrentComposition = makeCurrentComposition(kit, StackedBar, Legend);
1070
- const ContextBrowser = makeContextBrowser(kit, StackedBar);
1071
- const ErrorBoundary = makeErrorBoundary(t);
1072
- function ContextModalBody(props) {
1073
- const sessionId = typeof props.sessionId === "string" ? props.sessionId : "";
1074
- const open = typeof props.useContextModal === "function" ? props.useContextModal((s) => s) : false;
1075
- const data = typeof props.useProjection === "function" ? timelineOf(props.useProjection("contextTimeline")) : null;
1076
- const pressure = typeof props.useProjection === "function" ? contextPressureOf(props.useProjection("contextPressure")) : null;
1077
- const headers = typeof props.useProjection === "function" ? headersOf(props.useProjection("contextHeaders")) : null;
1078
- const [hoverCat, setHoverCat] = React.useState(null);
1079
- const [toolFocus, setToolFocus] = React.useState(null);
1080
- const close = React.useCallback(() => {
1081
- if (sessionId === "") return;
1082
- modalStoreOf(sessionId).set(false);
1083
- const guard = takePendingConsume(sessionId);
1084
- if (guard === void 0 || sessions === void 0) return;
1085
- const scope = sessions.scope(sessionId);
1086
- if (scope !== void 0) scope.bail(scope, "slash/input-consume-token", { guard });
1087
- }, [sessionId]);
1088
- React.useEffect(() => {
1089
- if (!open) return void 0;
1090
- const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
1091
- const onKey = (ev) => {
1092
- if (ev.key !== "Escape") return;
1093
- ev.preventDefault();
1094
- ev.stopPropagation();
1095
- close();
1096
- };
1097
- window.addEventListener("keydown", onKey, true);
1098
- return () => {
1099
- window.removeEventListener("keydown", onKey, true);
1100
- if (previous !== null && document.contains(previous)) previous.focus();
1101
- };
1102
- }, [open, close]);
1103
- if (!open) return null;
1104
- const head = data !== null ? headlineOf(data, pressure) : null;
1105
- const subtitle = data !== null ? (data.model ? data.model : "") + (data.provider ? " \xB7 " + data.provider : "") : "";
1106
- return /* @__PURE__ */ React.createElement("div", { className: "lc-modal-backdrop", onClick: close }, /* @__PURE__ */ React.createElement("div", { className: "lc-modal-card", onClick: (ev) => {
1107
- ev.stopPropagation();
1108
- } }, /* @__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(
1109
- CurrentComposition,
1110
- {
1111
- head,
1112
- subtitle,
1113
- hoverKey: hoverCat,
1114
- onHoverKey: setHoverCat,
1115
- tools: data.toolList,
1116
- onToolFocus: setToolFocus
1117
- }
1118
- ), /* @__PURE__ */ React.createElement(
1119
- ContextBrowser,
1120
- {
1121
- data,
1122
- headers,
1123
- useSession: props.useSession,
1124
- loadOlderHistory: props.loadOlderHistory,
1125
- hoverKey: hoverCat,
1126
- onHoverKey: setHoverCat,
1127
- toolFocus,
1128
- onToolFocusHandled: () => {
1129
- setToolFocus(null);
1130
- }
1131
- }
1132
- ))));
1133
- }
1134
- return function ContextModal(props) {
1135
- return h(ErrorBoundary, null, h(ContextModalBody, props));
1136
- };
1137
- }
1138
-
1139
- // src/client/styles.ts
1140
- var STYLES = [
1141
- ".lc-root { padding: 16px 20px 32px; overflow-y: auto; height: 100%; box-sizing: border-box; color: var(--dsw-alias-label-primary); font-size: 13px; }",
1142
- ".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; }",
1143
- ".lc-card-title { font-weight: 600; margin-bottom: 10px; display: flex; align-items: baseline; gap: 8px; }",
1144
- ".lc-card-title-text { flex: none; white-space: nowrap; }",
1145
- ".lc-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(92px, 1fr)); gap: 8px; }",
1146
- ".lc-stat { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 8px 10px; display: flex; flex-direction: column; gap: 2px; min-width: 0; }",
1147
- ".lc-stat-label { color: var(--dsw-alias-label-secondary); font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
1148
- ".lc-stat-value { color: var(--dsw-alias-label-primary); font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: center; }",
1149
- ".lc-stat-sub { color: var(--dsw-alias-label-secondary); font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
1150
- // Stats cell tooltip (cost cell): the explanation renders as styled DOM
1151
- // because the harness GUI never shows the native `title` tooltip. A "?"
1152
- // marker after the label signals the affordance; hovering ANYWHERE on the
1153
- // cell reveals the bubble BELOW it (the cell is the tip's position frame).
1154
- // The bubble may overlay the card content underneath — nothing below clips
1155
- // it, and z-index 6 matches the chart tips — so it can exceed the card
1156
- // without being cut off. Same chrome as `.lc-bar-tip`: layer-2 fill, border,
1157
- // 6px radius, soft shadow, 120ms opacity ease.
1158
- ".lc-stat-tipped { position: relative; }",
1159
- ".lc-stat-q { display: inline-block; margin-left: 4px; width: 11px; height: 11px; line-height: 11px; border-radius: 50%; text-align: center; font-size: 9px; font-weight: 700; font-style: normal; color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); cursor: help; }",
1160
- ".lc-stat-tipped:hover .lc-stat-q { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-primary); }",
1161
- ".lc-stat-tip { position: absolute; top: calc(100% + 6px); left: 0; z-index: 6; width: max-content; max-width: 280px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 6px 10px; font-size: 12px; line-height: 1.5; font-weight: 400; 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; }",
1162
- ".lc-stat-tipped:hover .lc-stat-tip { opacity: 1; }",
1163
- ".lc-stat-tip-prices { display: block; margin-top: 5px; border-top: 1px dashed var(--dsw-alias-border-l1); padding-top: 5px; }",
1164
- ".lc-stat-tip-head { display: block; font-weight: 600; }",
1165
- ".lc-stat-tip-row { display: block; margin-top: 2px; }",
1166
- ".lc-stat-tip-model { color: var(--dsw-alias-label-primary); }",
1167
- ".lc-card-sub { font-weight: 400; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
1168
- ".lc-gran, .lc-kinds { margin-left: auto; display: flex; gap: 2px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 1px; }",
1169
- ".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; }",
1170
- ".lc-gran-btn:hover { color: var(--dsw-alias-label-primary); }",
1171
- ".lc-gran-on, .lc-gran-on:hover { background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-foreground); }",
1172
- ".lc-overview-num { display: flex; align-items: baseline; gap: 6px; margin-bottom: 8px; }",
1173
- ".lc-overview-num > b { font-size: 20px; }",
1174
- ".lc-overview-num span { color: var(--dsw-alias-label-secondary); }",
1175
- // The used-percentage figure is the emphasis of the line: pushed to the
1176
- // right edge, large and primary-colored, with a small "used" caption.
1177
- ".lc-overview-pct { margin-left: auto; font-size: 11px; }",
1178
- ".lc-overview-pct b { font-size: 20px; color: var(--dsw-alias-label-primary); margin-right: 4px; }",
1179
- ".lc-stacked-wrap { position: relative; width: 100%; }",
1180
- ".lc-stacked { display: flex; width: 100%; border-radius: 5px; overflow: hidden; background: rgba(128,128,128,0.18); position: relative; }",
1181
- // Hover reference frame around the OCCUPIED region of the composition bar:
1182
- // a SOLID box from the left edge to the used/window boundary, so the
1183
- // legend's "share of used" percentages visibly map to the boxed part (the
1184
- // free track sits outside it). pointer-events: none keeps hover on the
1185
- // segments/free. A soft LIGHT-GRAY stroke — a gentle guide rather than a
1186
- // hard black box that still reads through its 2px thickness against the
1187
- // dimmed parts underneath it (`.lc-stacked-dim`). The free track echoes
1188
- // the same weight with a dashed box, so the two read as one system.
1189
- ".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; }",
1190
- ".lc-occupied-box-on { opacity: 1; }",
1191
- ".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; }",
1192
- ".lc-bar-tip-on { opacity: 1; }",
1193
- ".lc-stacked > div { height: 100%; }",
1194
- // Hover eases: the shared composition hover (dim + segment brighten, on the
1195
- // overview or the mirrored browser bar) glides in and out — same 120ms
1196
- // rhythm as the rest of the view.
1197
- ".lc-stacked-seg { transition: filter 120ms ease, opacity 120ms ease; }",
1198
- ".lc-stacked-free { box-sizing: border-box; transition: box-shadow 120ms ease, opacity 120ms ease; }",
1199
- ".lc-stacked-seg-on { filter: brightness(1.18); }",
1200
- // The free track mirrors the occupied frame: a DASHED 2px box (the occupied
1201
- // frame is solid) at the same weight border, not the old inset shadow, so
1202
- // the dash pattern renders; the base box-sizing keeps the width from shifting.
1203
- // Auto-compaction reserve band: the rightmost (1−ratio) of the window,
1204
- // striped diagonally like a warning plate so it reads as "headroom, not
1205
- // real usage" rather than plain free space. Sits above the track/segments
1206
- // (overlay visible even past the threshold) with its own pointer so the
1207
- // hover explains it (help cursor signals "more info").
1208
- ".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); }",
1209
- ".lc-stacked-free-on { border: 2px dashed var(--dsw-alias-label-secondary); border-radius: 3px; }",
1210
- // Hover focus: everything except the hovered part (segment, legend chip, or
1211
- // free track) recedes, so the composition highlight and the occupied-region
1212
- // frame read clearly. The selected segment/free keeps full opacity.
1213
- ".lc-stacked-dim .lc-stacked-seg { opacity: 0.35; }",
1214
- ".lc-stacked-dim .lc-stacked-seg-on { opacity: 1; }",
1215
- ".lc-stacked-dim .lc-stacked-free { opacity: 0.35; }",
1216
- ".lc-stacked-dim .lc-stacked-free-on { opacity: 1; }",
1217
- // Current-composition legend: an auto-flowing grid (as many columns as the
1218
- // card fits, narrowing flows to fewer) whose cells are label-left /
1219
- // numbers-right rows each category a tidy " 系统提示词 ≈2.0k 2%".
1220
- ".lc-legend { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 6px 14px; margin-top: 10px; }",
1221
- ".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; }",
1222
- // The category name is the row's emphasis: bold at rest (the chip is a
1223
- // legend label, not inline prose), ellipsized if a narrow cell squeezes it
1224
- // while the right-aligned figures stay intact.
1225
- ".lc-chip-label { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }",
1226
- // The numeric group (≈tokens + percent) pins to the cell's right edge while
1227
- // the category label stays left; a baseline acts as the shared vertical
1228
- // rhythm for the two figures.
1229
- ".lc-chip-nums { margin-left: auto; flex: none; display: inline-flex; align-items: baseline; gap: 6px; white-space: nowrap; }",
1230
- ".lc-chip i, .lc-detail-row i, .lc-node i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; }",
1231
- ".lc-chip i { transition: box-shadow 120ms ease; }",
1232
- ".lc-chip em { font-style: normal; color: var(--dsw-alias-label-secondary); }",
1233
- // The selected chip glows with the shared hover tint (smoothly, like the
1234
- // browser rows) instead of snapping its weight; the dot ring on top marks
1235
- // the exact segment.
1236
- ".lc-chip-on { font-weight: 600; background: var(--dsw-alias-interactive-bg-hover); }",
1237
- ".lc-chip-on i { box-shadow: 0 0 0 1px var(--dsw-alias-brand-primary); }",
1238
- ".lc-tools { margin-top: 10px; color: var(--dsw-alias-label-secondary); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }",
1239
- // The overview tool list links into the Context browser: the "工具定义 Top"
1240
- // label, its "等 N 个" overflow link, and every tool chip are buttons, and
1241
- // hover underlines them (the chip already reads as a tag, so the underline
1242
- // is the only affordance).
1243
- ".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; }",
1244
- ".lc-tools-label:hover, .lc-tools-more:hover { text-decoration: underline; }",
1245
- ".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; }",
1246
- ".lc-tool-chip:hover { text-decoration: underline; }",
1247
- ".lc-chartrow { display: flex; gap: 6px; align-items: stretch; }",
1248
- ".lc-axis { position: relative; width: 40px; height: 150px; padding-top: 18px; box-sizing: border-box; color: var(--dsw-alias-label-secondary); font-size: 11px; }",
1249
- ".lc-axis span { position: absolute; right: 0; line-height: 1; }",
1250
- ".lc-axis-top { top: 13px; }",
1251
- ".lc-axis-mid { top: 69px; }",
1252
- ".lc-axis-bot { top: 125px; }",
1253
- ".lc-chart-scroll { position: relative; flex: 1; overflow-x: auto; overflow-y: hidden; min-width: 0; scrollbar-width: thin; }",
1254
- ".lc-chart-fade { position: absolute; top: 0; bottom: 0; width: 26px; pointer-events: none; z-index: 2; }",
1255
- ".lc-chart-fade-l { left: 0; background: linear-gradient(to right, var(--dsw-alias-bg-layer-1), transparent); }",
1256
- ".lc-chart-fade-r { right: 0; background: linear-gradient(to left, var(--dsw-alias-bg-layer-1), transparent); }",
1257
- ".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%; }",
1258
- ".lc-grid { position: absolute; left: 0; right: 0; border-top: 1px dashed var(--dsw-alias-border-l1); pointer-events: none; }",
1259
- ".lc-grid-top { top: 18px; }",
1260
- ".lc-grid-mid { top: 74px; }",
1261
- ".lc-bar { position: relative; width: 14px; flex: none; height: 100%; display: flex; align-items: flex-end; cursor: pointer; border-radius: 2px; transition: opacity 120ms ease, background-color 120ms ease; }",
1262
- // Turn-aware dimming: while a turn is focused, bars OUTSIDE the active
1263
- // turn fade to 35% and the whole current turn stays fully opaque.
1264
- ".lc-chart-dim .lc-bar { opacity: 0.35; }",
1265
- ".lc-chart-dim .lc-bar-in-turn { opacity: 1; }",
1266
- ".lc-chart-dim .lc-turn { opacity: 0.35; }",
1267
- ".lc-chart-dim .lc-turn-on { opacity: 1; }",
1268
- ".lc-chart-tip { position: absolute; top: 0; transform: translateX(-50%); z-index: 5; white-space: nowrap; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 3px 8px; font-size: 12px; color: var(--dsw-alias-label-primary); box-shadow: 0 2px 8px rgba(0,0,0,0.18); pointer-events: none; }",
1269
- ".lc-bar:hover { background: var(--dsw-alias-bg-layer-2); }",
1270
- // Hovering previews a bar with a thin dashed frame; a CLICKED (locked)
1271
- // bar must always show the strong solid frame, so the selected rule is
1272
- // declared LAST (equal specificity) — otherwise the dashed hover frame
1273
- // would keep overriding it while the pointer is still on the just-clicked
1274
- // bar and the lock would only become visible after the pointer leaves.
1275
- ".lc-bar-hovered { outline: 1px dashed var(--dsw-alias-brand-primary); outline-offset: 1px; }",
1276
- ".lc-bar-selected { outline: 2px solid var(--dsw-alias-label-primary); outline-offset: 1px; }",
1277
- ".lc-bar-in-turn { background: rgba(128,128,128,0.14); }",
1278
- ".lc-bar-stack { display: flex; flex-direction: column-reverse; width: 100%; }",
1279
- ".lc-bar-stack > div { width: 100%; }",
1280
- ".lc-bar-marker { position: absolute; top: -16px; left: 50%; transform: translateX(-50%); font-size: 11px; color: var(--dsw-alias-state-warn-primary); }",
1281
- // Turn strip: NEUTRAL zebra fills (inline, see TURN_FILLS) with secondary
1282
- // text never the category palette, or the strip reads as a detached
1283
- // bottom segment of the bars above it.
1284
- ".lc-turns { display: flex; gap: 2px; width: max-content; min-width: 100%; margin-top: 4px; }",
1285
- ".lc-turn { flex: none; box-sizing: border-box; text-align: center; font-size: 10px; line-height: 14px; font-weight: 600; color: var(--dsw-alias-label-secondary); border-radius: 3px; height: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: default; transition: filter 120ms, opacity 120ms; }",
1286
- ".lc-turn-on { filter: brightness(1.35); color: var(--dsw-alias-label-primary); }",
1287
- ".lc-detail { margin-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1); padding-top: 12px; }",
1288
- ".lc-detail-head { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 8px; color: var(--dsw-alias-label-secondary); }",
1289
- ".lc-detail-head b { color: var(--dsw-alias-label-primary); }",
1290
- ".lc-detail-marker { color: var(--dsw-alias-state-warn-primary); font-size: 11px; background: var(--dsw-alias-bg-layer-2); border-radius: 6px; padding: 1px 7px; }",
1291
- ".lc-detail-head .lc-actual { color: var(--dsw-alias-state-success-primary); }",
1292
- ".lc-detail-tag { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 4px; padding: 0 6px; font-size: 11px; color: var(--dsw-alias-label-secondary); }",
1293
- ".lc-detail-rows { margin-top: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px 24px; }",
1294
- ".lc-detail-row { display: flex; align-items: center; gap: 8px; }",
1295
- ".lc-detail-label { min-width: 70px; white-space: nowrap; color: var(--dsw-alias-label-secondary); }",
1296
- ".lc-bar-track { flex: 1; height: 5px; border-radius: 3px; background: rgba(128,128,128,0.18); overflow: hidden; display: block; }",
1297
- ".lc-bar-fill { display: block; height: 100%; border-radius: 3px; }",
1298
- ".lc-detail-num { width: 44px; text-align: right; }",
1299
- ".lc-detail-pct { width: 34px; text-align: right; color: var(--dsw-alias-label-secondary); }",
1300
- // Section rows share one 14px vertical rhythm, owned by the ROW (flex gap
1301
- // covers the wrapped-row axis too), never by the cards inside: a card that
1302
- // IS a column (head, events/messages) drops its own bottom margin, and a
1303
- // card stacked inside a wrapper column (overview + trend) keeps its margin
1304
- // as the inner gap but the last one drops it — so every section boundary
1305
- // measures exactly one 14px, whichever way the flex wraps.
1306
- ".lc-cols { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 14px; }",
1307
- ".lc-col { flex: 1; min-width: 280px; }",
1308
- ".lc-cols > .lc-card { margin-bottom: 0; }",
1309
- ".lc-col > .lc-card:last-child { margin-bottom: 0; }",
1310
- // Head row: stats board + plugin info sit side by side under the shared
1311
- // `lc-cols` flex stats takes ~7/10 of the row, plugin info ~3/10, both
1312
- // wrap onto their own line when the available width falls below each card's
1313
- // min-width (so a narrow viewport keeps both readable). Both children are
1314
- // `.lc-card` themselves (rendered by StatsBoard / PluginInfo); the flex
1315
- // sizing lives on the head row's direct children so the existing card
1316
- // chrome doesn't change.
1317
- ".lc-head > .lc-card:first-child { flex: 7 1 0; min-width: 360px; }",
1318
- ".lc-head > .lc-card:last-child { flex: 3 1 0; min-width: 220px; }",
1319
- // Plugin info: two full-width rows (Plugin / GitHub), each one horizontal
1320
- // line with the label on the left and the value on the right — a compact
1321
- // definition list rather than a multi-cell grid.
1322
- ".lc-pi-grid { display: grid; grid-template-columns: 1fr; gap: 8px; }",
1323
- // The row IS the link: an `<a>` with the label + value on one baseline.
1324
- // Hovering a row underlines its value — the only hover affordance.
1325
- ".lc-pi-row { display: flex; flex-direction: row; justify-content: space-between; align-items: baseline; gap: 12px; min-width: 0; text-decoration: none; color: inherit; }",
1326
- ".lc-pi-row:hover .lc-pi-value { text-decoration: underline; }",
1327
- // Upgrade chip appended to the Plugin value when the npm registry has a
1328
- // newer version than the baked-in one.
1329
- ".lc-pi-update { color: var(--dsw-alias-state-warn-primary); font-size: 11px; margin-left: 6px; white-space: nowrap; }",
1330
- ".lc-pi-label { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
1331
- // min-width: 0 lets the value shrink inside the flex row and truncate with
1332
- // an ellipsis; without it a narrow card pushes the value over the label.
1333
- ".lc-pi-value { min-width: 0; color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: right; }",
1334
- ".lc-events, .lc-nodes { display: flex; flex-direction: column; gap: 2px; max-height: 320px; overflow-y: auto; }",
1335
- ".lc-event { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
1336
- ".lc-event-icon { width: 18px; text-align: center; color: var(--dsw-alias-state-warn-primary); }",
1337
- ".lc-event-icon.lc-event-inject { color: #a855f7; }",
1338
- ".lc-event-icon.lc-event-model { color: var(--dsw-alias-brand-primary); }",
1339
- // Kind chip: the event classification at a glance; the tint matches the
1340
- // impact direction (inject = adds context, compaction/prune = frees it,
1341
- // model switch = neutral), mirroring the token sign colors below.
1342
- ".lc-kind { flex: none; font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px; white-space: nowrap; }",
1343
- ".lc-kind-inject { background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent); color: var(--dsw-alias-state-success-primary); }",
1344
- ".lc-kind-compaction { background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 15%, transparent); color: var(--dsw-alias-state-error-primary); }",
1345
- ".lc-kind-prune { background: color-mix(in srgb, #8b5cf6 15%, transparent); color: #8b5cf6; }",
1346
- ".lc-kind-model { background: color-mix(in srgb, var(--dsw-alias-brand-primary) 15%, transparent); color: var(--dsw-alias-brand-primary); }",
1347
- ".lc-event-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
1348
- ".lc-event-at { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; white-space: nowrap; }",
1349
- ".lc-event-tokens { color: var(--dsw-alias-state-success-primary); font-weight: 600; white-space: nowrap; }",
1350
- ".lc-event-tokens.lc-up { color: var(--dsw-alias-state-warn-primary); }",
1351
- ".lc-event-time { color: var(--dsw-alias-label-secondary); font-size: 12px; }",
1352
- ".lc-node { display: flex; align-items: center; gap: 8px; padding: 3px 0; }",
1353
- ".lc-node-preview { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); }",
1354
- ".lc-node-time { color: var(--dsw-alias-label-secondary); font-size: 12px; min-width: 54px; text-align: right; }",
1355
- ".lc-node-tokens { color: var(--dsw-alias-label-secondary); }",
1356
- ".lc-nodes-more { color: var(--dsw-alias-label-secondary); padding: 3px 0; }",
1357
- ".lc-empty { color: var(--dsw-alias-label-secondary); padding: 18px 0; text-align: center; }",
1358
- // Error-boundary fallback card: the tab degrades to this instead of
1359
- // white-screening; the message runs in mono on a padded chip, and Retry
1360
- // re-mounts the subtree once a healthy value arrives.
1361
- ".lc-error { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 40px 16px; }",
1362
- ".lc-error-msg { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--dsw-alias-state-error-primary); background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 4px 8px; max-width: 100%; overflow-wrap: anywhere; }",
1363
- ".lc-error-retry { border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; padding: 4px 14px; border-radius: 6px; cursor: pointer; }",
1364
- ".lc-error-retry:hover { border-color: var(--dsw-alias-label-primary); }",
1365
- ".lc-foot { color: var(--dsw-alias-label-secondary); font-size: 12px; margin-top: 4px; }",
1366
- // ---- /context modal (centered dialog; escapes the composer anchor via fixed positioning) ----
1367
- ".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; }",
1368
- ".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; }",
1369
- ".lc-modal-head { display: flex; align-items: baseline; gap: 8px; margin-bottom: 12px; }",
1370
- ".lc-modal-title { font-weight: 600; font-size: 14px; }",
1371
- ".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; }",
1372
- ".lc-modal-close:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); }",
1373
- // ---- Context browser card (progressive disclosure: category accordion ->
1374
- // element rows -> per-kind content) ----
1375
- // The δ-baseline caption sits at the card title's right edge, just left of
1376
- // the step picker (it takes the row's auto margin the picker used to own).
1377
- ".lc-br-hint { margin-left: auto; color: var(--dsw-alias-label-secondary); font-size: 11px; font-weight: 400; white-space: nowrap; }",
1378
- ".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; }",
1379
- ".lc-br-meta { display: flex; flex-wrap: wrap; gap: 6px 16px; margin-bottom: 8px; color: var(--dsw-alias-label-secondary); }",
1380
- ".lc-br-meta b { color: var(--dsw-alias-label-primary); }",
1381
- ".lc-br-meta .lc-actual { color: var(--dsw-alias-state-success-primary); }",
1382
- ".lc-br-note { margin-top: 8px; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
1383
- ".lc-br-cats { margin-top: 10px; display: flex; flex-direction: column; gap: 4px; }",
1384
- ".lc-br-cat { border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; background: var(--dsw-alias-bg-layer-2); overflow: hidden; }",
1385
- ".lc-br-cat-empty { opacity: 0.55; }",
1386
- ".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; }",
1387
- ".lc-br-cat-row:hover { background: var(--dsw-alias-interactive-bg-hover); }",
1388
- // Current-composition hover echo: the category row lit by the shared hover
1389
- // link (hovering the Current Composition card's bar/legend) looks hovered
1390
- // too — same tint as the physical hover, plus a ring on its color dot.
1391
- ".lc-br-cat-on { background: var(--dsw-alias-interactive-bg-hover); }",
1392
- ".lc-br-cat-on i { box-shadow: 0 0 0 1px var(--dsw-alias-brand-primary); }",
1393
- ".lc-br-cat-row i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; flex: none; transition: box-shadow 120ms ease; }",
1394
- ".lc-br-cat-label { font-weight: 600; }",
1395
- ".lc-br-cat-count { color: var(--dsw-alias-label-secondary); font-size: 12px; white-space: nowrap; }",
1396
- // Count + Δ pill form one attached group: the group absorbs the row's free
1397
- // space (tokens/percent stay right-aligned) with a tight 4px inner gap so
1398
- // the pill hugs the item count instead of standing at the row's 8px rhythm.
1399
- ".lc-br-count-grp { flex: 1; min-width: 0; display: inline-flex; align-items: center; gap: 4px; }",
1400
- ".lc-br-chev { flex: none; width: 12px; color: var(--dsw-alias-label-secondary); transition: transform 120ms ease; }",
1401
- ".lc-br-chev-on { transform: rotate(90deg); }",
1402
- ".lc-br-tokens { flex: none; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
1403
- // Δ pills on a category row: count and token swings vs the previous turn's
1404
- // last step, tinted by direction (added = success, freed = error — the same
1405
- // language as the events card's kind chips).
1406
- ".lc-br-delta, .lc-br-tdelta { flex: none; font-size: 11px; font-weight: 600; padding: 0 5px; border-radius: 4px; white-space: nowrap; }",
1407
- ".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); }",
1408
- ".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); }",
1409
- // The token Δ pill hugs the LEFT of the token figure (tight 4px gap, like
1410
- // the count group) while the figure itself keeps its right-aligned slot.
1411
- ".lc-br-tokens-grp { flex: none; min-width: 0; display: inline-flex; align-items: center; gap: 4px; }",
1412
- ".lc-br-pct { flex: none; width: 36px; text-align: right; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
1413
- ".lc-br-body { border-top: 1px solid var(--dsw-alias-border-l1); padding: 4px 6px; display: flex; flex-direction: column; gap: 2px; }",
1414
- ".lc-br-elem { border-radius: 6px; }",
1415
- ".lc-br-elem-on { background: var(--dsw-alias-interactive-bg-active); }",
1416
- ".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; }",
1417
- ".lc-br-elem-row:hover { background: var(--dsw-alias-interactive-bg-hover); }",
1418
- ".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; }",
1419
- ".lc-br-preview { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
1420
- ".lc-br-time { flex: none; color: var(--dsw-alias-label-secondary); font-size: 11px; }",
1421
- ".lc-br-content { padding: 2px 6px 8px 26px; display: flex; flex-direction: column; gap: 6px; }",
1422
- ".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; }",
1423
- ".lc-br-dim { color: var(--dsw-alias-label-secondary); }",
1424
- ".lc-br-call { display: flex; flex-direction: column; gap: 4px; align-items: flex-start; }",
1425
- // ---- Tool schema body (browser card, tools category). The description
1426
- // and parameter table share one "card with a small title" chrome — a
1427
- // labeled bordered block so a reader can scan three stacked sections
1428
- // without losing track of which is which. The raw JSON sits below as a
1429
- // collapsed toggle (no card chrome — it's a debug view, not a primary
1430
- // section). ----
1431
- ".lc-ts-card { background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; overflow: hidden; }",
1432
- ".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); }",
1433
- ".lc-ts-card-head b { color: var(--dsw-alias-label-primary); }",
1434
- ".lc-ts-card-count { margin-left: auto; }",
1435
- ".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; }",
1436
- // Grid: name + type + required chip + description. Columns wrap on narrow
1437
- // rows; the description cell spans the full second row so a long blurb
1438
- // never breaks the one-line rhythm of name/type/required.
1439
- ".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; }",
1440
- ".lc-ts-param-row:first-of-type { border-top: 0; }",
1441
- ".lc-ts-param-name { font-weight: 600; color: var(--dsw-alias-label-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
1442
- ".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; }",
1443
- ".lc-ts-param-req { color: var(--dsw-alias-state-warn-primary); font-size: 11px; font-weight: 600; }",
1444
- ".lc-ts-param-req-off { color: var(--dsw-alias-label-secondary); font-size: 11px; }",
1445
- ".lc-ts-param-desc { grid-column: 1 / -1; color: var(--dsw-alias-label-secondary); line-height: 1.5; }",
1446
- ".lc-ts-params-empty { padding: 8px 10px; color: var(--dsw-alias-label-secondary); font-size: 12px; }",
1447
- // Raw JSON toggle: a slim button that owns the open/closed state, sitting
1448
- // just under the parameter table. The <pre> itself only mounts when open
1449
- // (avoids stringifying the schema on every render and keeps the default
1450
- // body compact).
1451
- ".lc-ts-json { display: flex; flex-direction: column; gap: 4px; }",
1452
- ".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; }",
1453
- ".lc-ts-json-toggle:hover { color: var(--dsw-alias-label-primary); text-decoration: underline; }",
1454
- // ---- Raw/markdown view switch (detail cards: system prompt, tool
1455
- // description, user/assistant message + injection bodies). A segmented
1456
- // pill mirroring the trend chart's 步骤/轮次 control (`lc-gran`), riding
1457
- // the card's top-right corner as a self-end item of the content column
1458
- // (no absolute overlay, so it never covers text); the `in-head` variant
1459
- // sits at the right end of the tool description card's title row. The
1460
- // markdown box reuses the raw <pre> chrome minus the pre whitespace. ----
1461
- ".lc-rich-seg { align-self: flex-end; display: flex; gap: 2px; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 1px; }",
1462
- ".lc-rich-seg-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; }",
1463
- ".lc-rich-seg-btn:hover { color: var(--dsw-alias-label-primary); }",
1464
- ".lc-rich-seg-on, .lc-rich-seg-on:hover { background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-foreground); }",
1465
- ".lc-rich-seg-head { align-self: auto; margin-left: auto; }",
1466
- ".lc-br-md { padding: 4px 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; word-break: break-word; max-height: 320px; overflow-y: auto; scrollbar-width: thin; }",
1467
- ".lc-ts-desc-md { padding: 4px 10px; color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 1.55; word-break: break-word; max-height: 160px; overflow-y: auto; scrollbar-width: thin; }",
1468
- // The shared markdown renderer sizes its root/headings for chat bubbles;
1469
- // inside a detail card the base size sits one step ABOVE the raw text
1470
- // (13px vs 12px a proportional face at 13px matches the visual weight
1471
- // of the raw view's 12px monospace) and the heading scale collapses to a
1472
- // weight-only bump (specificity beats the renderer's one-class rules).
1473
- // Inline code keeps the FULL size too — the renderer shrinks it to
1474
- // 0.875em (!important) and table code to 13px, both visibly off. Its
1475
- // `font` shorthand also drags in a ~22px line-height, TALLER than the
1476
- // 13px/1.55 paragraph line (~20px), so a code chip wrapping onto the
1477
- // next line sticks to the neighbor line's chip; pin the chip just under
1478
- // the paragraph line (1.3em + 1px padding ≈ 19px) to reopen the gap.
1479
- ".lc-br-md > div, .lc-ts-desc-md > div { font-size: 13px; line-height: 1.55; }",
1480
- ".lc-br-md div :is(h1, h2, h3, h4, h5, h6), .lc-ts-desc-md div :is(h1, h2, h3, h4, h5, h6) { font-size: 13px; font-weight: 600; margin: 10px 0 4px; }",
1481
- ".lc-br-md div p, .lc-ts-desc-md div p { margin: 6px 0; }",
1482
- ".lc-br-md div pre, .lc-ts-desc-md div pre { margin: 6px 0; }",
1483
- ".lc-br-md div :not(pre) > code, .lc-ts-desc-md div :not(pre) > code { font-size: 1em !important; line-height: 1.3; padding: 1px 5px; box-decoration-break: clone; }",
1484
- ".lc-br-md div table code, .lc-ts-desc-md div table code { font-size: inherit; }"
1485
- ].join("\n");
1486
-
1487
- // src/client/components/events.tsx
1488
- var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
1489
-
1490
- // src/client/format.ts
1491
- function fmt(n) {
1492
- if (n === void 0 || n === null || isNaN(n)) return "\u2014";
1493
- if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
1494
- if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
1495
- return String(Math.round(n));
1496
- }
1497
- function fmtTime(t) {
1498
- const d = new Date(t);
1499
- if (isNaN(d.getTime())) return "\u2014";
1500
- return d.toLocaleTimeString("en-GB", { hour12: false });
1501
- }
1502
-
1503
- // src/client/components/events.tsx
1504
- var EVENT_ICONS = { compaction: "\u2702", prune: "\u2702", inject: "\uFF0B", model: "\u21C4" };
1505
- function makeEventText(t) {
1506
- function eventLabel(ev) {
1507
- if (ev.kind === "compaction") return t("ev.compaction", { n: ev.count || 0 });
1508
- if (ev.kind === "prune") return t("ev.prune");
1509
- if (ev.kind === "model") return t("ev.model", { a: ev.from || "?", b: ev.to || "?" });
1510
- if (ev.kind === "inject") {
1511
- if (ev.sub === "skill") return t("ev.skill", { name: ev.name || "?" });
1512
- const base = t("form." + (ev.form || "context"));
1513
- return ev.name ? base + " \xB7 " + ev.name : base;
1514
- }
1515
- return ev.kind;
1516
- }
1517
- function eventAt(ev) {
1518
- if (ev.kind === "compaction" || ev.kind === "prune") {
1519
- if (typeof ev.turn === "number" && typeof ev.step === "number") {
1520
- if (typeof ev.fromTurn === "number" && typeof ev.fromStep === "number") {
1521
- if (ev.fromTurn === ev.turn) return t("events.range", { t: ev.turn, a: ev.fromStep, b: ev.step });
1522
- return t("events.rangeTo", { a: ev.fromTurn, as: ev.fromStep, b: ev.turn, bs: ev.step });
1523
- }
1524
- return t("events.at", { t: ev.turn, s: ev.step });
1525
- }
1526
- return null;
1527
- }
1528
- if (typeof ev.turn === "number" && typeof ev.step === "number") {
1529
- return t("events.at", { t: ev.turn, s: ev.step });
1530
- }
1531
- return null;
1532
- }
1533
- return { eventLabel, eventAt };
1534
- }
1535
- function makeEventList(kit) {
1536
- const { t, fmt: fmt3, fmtTime: fmtTime2, eventLabel, eventAt } = kit;
1537
- return function EventList(props) {
1538
- if (props.events.length === 0) {
1539
- return /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("events.empty"));
1540
- }
1541
- const rootRef = React.useRef(null);
1542
- React.useLayoutEffect(() => {
1543
- const root = rootRef.current;
1544
- if (!root) return;
1545
- const sync = () => {
1546
- for (const el of root.querySelectorAll(".lc-event-label")) {
1547
- el.title = el.scrollWidth > el.clientWidth ? el.textContent || "" : "";
1548
- }
1549
- };
1550
- sync();
1551
- window.addEventListener("resize", sync);
1552
- return () => window.removeEventListener("resize", sync);
1553
- });
1554
- const sorted = props.events.slice().reverse();
1555
- return /* @__PURE__ */ React.createElement("div", { className: "lc-events", ref: rootRef }, sorted.map((ev, i) => {
1556
- const label = eventLabel(ev);
1557
- const at = eventAt(ev);
1558
- const glyph = ev.kind === "inject" ? /* @__PURE__ */ React.createElement(import_dsh_client_ui_primitives2.IconPlusOutline16, null) : ev.kind === "model" ? /* @__PURE__ */ React.createElement(import_dsh_client_ui_primitives2.IconBranchOutline16, null) : EVENT_ICONS[ev.kind] || "\u2022";
1559
- return /* @__PURE__ */ React.createElement("div", { key: ev.seq + "-" + i, className: "lc-event" }, /* @__PURE__ */ React.createElement("span", { className: "lc-event-icon lc-event-" + ev.kind }, glyph), /* @__PURE__ */ React.createElement("span", { className: "lc-kind lc-kind-" + ev.kind }, t("kind." + ev.kind)), /* @__PURE__ */ React.createElement("span", { className: "lc-event-label" }, label), at !== null ? /* @__PURE__ */ React.createElement("span", { className: "lc-event-at" }, at) : null, ev.tokens ? /* @__PURE__ */ React.createElement("span", { className: "lc-event-tokens" + (ev.kind === "inject" ? " lc-up" : " lc-down") }, (ev.kind === "inject" ? "+" : "\u2212") + fmt3(ev.tokens)) : null, /* @__PURE__ */ React.createElement("span", { className: "lc-event-time" }, fmtTime2(ev.time)));
1560
- }));
1561
- };
1562
- }
1563
-
1564
- // src/client/meta.ts
1565
- var PLUGIN_NAME = "dsh-context";
1566
- var PLUGIN_VERSION = true ? "0.21.1" : "0.0.0-dev";
1567
- var PLUGIN_REPO = true ? "https://github.com/bowenliang123/dsh-context" : "https://github.com/bowenliang123/dsh-context";
1568
- var PLUGIN_REPO_SHORT = PLUGIN_REPO.replace(/^https?:\/\/github\.com\//, "");
1569
-
1570
- // src/client/latestVersion.ts
1571
- var REGISTRY_URL = "https://registry.npmjs.org/" + PLUGIN_NAME + "/latest";
1572
- var TTL_MS = 60 * 60 * 1e3;
1573
- var cached = null;
1574
- function fetchLatestVersion() {
1575
- if (!cached || Date.now() - cached.at >= TTL_MS) {
1576
- cached = {
1577
- at: Date.now(),
1578
- promise: fetch(REGISTRY_URL).then((res) => res.ok ? res.json() : null).then((body) => body && typeof body.version === "string" ? body.version : null).catch(() => null)
1579
- };
1580
- }
1581
- return cached.promise;
1582
- }
1583
- function isNewerVersion(latest, current) {
1584
- const parse = (v) => v.replace(/^v/, "").split("-", 1)[0].split(".").map((n) => parseInt(n, 10) || 0);
1585
- const a = parse(latest);
1586
- const b = parse(current);
1587
- for (let i = 0; i < Math.max(a.length, b.length); i++) {
1588
- const x = a[i] || 0;
1589
- const y = b[i] || 0;
1590
- if (x !== y) return x > y;
1591
- }
1592
- return false;
1593
- }
1594
-
1595
- // src/client/components/pluginInfo.tsx
1596
- function makePluginInfo(kit) {
1597
- const { t } = kit;
1598
- const row = (label, value, href) => /* @__PURE__ */ React.createElement("a", { className: "lc-pi-row", href, target: "_blank", rel: "noreferrer" }, /* @__PURE__ */ React.createElement("div", { className: "lc-pi-label" }, label), /* @__PURE__ */ React.createElement("div", { className: "lc-pi-value" }, value));
1599
- return function PluginInfo() {
1600
- const [latest, setLatest] = React.useState(null);
1601
- React.useEffect(() => {
1602
- if (PLUGIN_VERSION.includes("-dev")) return;
1603
- let on = true;
1604
- fetchLatestVersion().then((v) => {
1605
- if (on && v) setLatest(v);
1606
- });
1607
- return () => {
1608
- on = false;
1609
- };
1610
- }, []);
1611
- const update = latest !== null && isNewerVersion(latest, PLUGIN_VERSION) ? latest : null;
1612
- const nameValue = [PLUGIN_NAME + " (v" + PLUGIN_VERSION + ")"];
1613
- if (update) nameValue.push(/* @__PURE__ */ React.createElement("span", { key: "update", className: "lc-pi-update" }, "\u2191 v" + update));
1614
- 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("plugin.title")), /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("plugin.hint"))), /* @__PURE__ */ React.createElement("div", { className: "lc-pi-grid" }, row(t("plugin.name"), nameValue, PLUGIN_REPO + "/releases"), row(t("plugin.github"), PLUGIN_REPO_SHORT, PLUGIN_REPO)));
1615
- };
1616
- }
1617
-
1618
- // src/client/components/requestDetail.tsx
1619
- function makeRequestDetail(kit, StackedBar) {
1620
- const { t, fmt: fmt3, fmtTime: fmtTime2, catLabel, eventLabel, eventAt } = kit;
1621
- return function RequestDetail(props) {
1622
- const req = props.request;
1623
- if (!req) return null;
1624
- const isTurn = req.stepCount !== void 0 && req.stepCount > 1;
1625
- 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 });
1626
- const marker = props.marker ?? null;
1627
- const markerAt = marker !== null ? eventAt(marker) : null;
1628
- 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) => {
1629
- const v = req[c.key] || 0;
1630
- 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) + "%" : ""));
1631
- })));
1632
- };
1633
- }
1634
-
1635
- // src/client/cost.ts
1636
- var PRICES = {
1637
- usd: {
1638
- flash: { peak: { hit: 0.014, miss: 0.44, out: 1.32 }, off: { hit: 7e-3, miss: 0.22, out: 0.66 } },
1639
- pro: { peak: { hit: 0.044, miss: 1.32, out: 3.96 }, off: { hit: 0.022, miss: 0.66, out: 1.98 } }
1640
- },
1641
- cny: {
1642
- flash: { peak: { hit: 0.1, miss: 3, out: 9 }, off: { hit: 0.05, miss: 1.5, out: 4.5 } },
1643
- pro: { peak: { hit: 0.3, miss: 9, out: 27 }, off: { hit: 0.15, miss: 4.5, out: 13.5 } }
1644
- }
1645
- };
1646
- function estimateSessionCost(usage, currency) {
1647
- if (usage === null || usage === void 0) return null;
1648
- let total = 0;
1649
- let any = false;
1650
- for (const family of ["flash", "pro"]) {
1651
- const fam = usage[family];
1652
- if (fam === void 0) continue;
1653
- for (const period of ["peak", "off"]) {
1654
- const b = fam[period];
1655
- if (b === void 0) continue;
1656
- const p = PRICES[currency][family][period];
1657
- total += (numOf(b.cacheRead) * p.hit + (numOf(b.uncached) + numOf(b.cacheWrite)) * p.miss + numOf(b.output) * p.out) / 1e6;
1658
- any = true;
1659
- }
1660
- }
1661
- return any ? total : null;
1662
- }
1663
- function formatCost(amount, currency) {
1664
- const symbol = currency === "cny" ? "\xA5" : "$";
1665
- return symbol + (amount >= 1 ? amount.toFixed(2) : amount.toPrecision(2));
1666
- }
1667
- function sessionPrices(currency) {
1668
- return ["flash", "pro"].map((id) => ({
1669
- family: id === "flash" ? "deepseek-v4-flash" : "deepseek-v4-pro",
1670
- peak: PRICES[currency][id].peak,
1671
- off: PRICES[currency][id].off
1672
- }));
1673
- }
1674
- function formatPriceRate(amount, currency) {
1675
- return formatCost(amount, currency).replace(/0+$/, "").replace(/\.$/, "");
1676
- }
1677
-
1678
- // src/client/components/statsBoard.tsx
1679
- function cacheHitPercent(usage) {
1680
- const uncached = numOf(usage.uncachedInputTokens);
1681
- const reads = numOf(usage.cacheReadTokens);
1682
- const writes = numOf(usage.cacheWriteTokens);
1683
- const billed = uncached + reads + writes;
1684
- if (billed === 0) return null;
1685
- const hundredths = Math.trunc(reads / billed * 1e4 + 1e-9);
1686
- return `${Math.floor(hundredths / 100)}.${String(hundredths % 100).padStart(2, "0")}`;
1687
- }
1688
- function makeStatsBoard(kit) {
1689
- const { t, fmt: fmt3 } = kit;
1690
- return function StatsBoard(props) {
1691
- const turns = /* @__PURE__ */ new Set();
1692
- let steps = 0, compactions = 0, prunes = 0, injects = 0;
1693
- for (const req of props.requests) {
1694
- turns.add(req.turn ?? 0);
1695
- steps++;
1696
- }
1697
- for (const ev of props.events) {
1698
- if (ev.kind === "compaction") compactions++;
1699
- else if (ev.kind === "prune") prunes++;
1700
- else if (ev.kind === "inject") injects++;
1701
- }
1702
- const hit = props.usage !== null ? cacheHitPercent(props.usage) : null;
1703
- const currency = props.locale === "zh" ? "cny" : "usd";
1704
- const cost = estimateSessionCost(props.cost, currency);
1705
- const fmtRate = (n) => formatPriceRate(n, currency);
1706
- const costTip = [
1707
- t("stats.costTip"),
1708
- /* @__PURE__ */ React.createElement("span", { key: "prices", className: "lc-stat-tip-prices" }, /* @__PURE__ */ React.createElement("span", { className: "lc-stat-tip-head" }, t("stats.costPriceHead")), sessionPrices(currency).map((r) => /* @__PURE__ */ React.createElement("span", { key: r.family, className: "lc-stat-tip-row" }, /* @__PURE__ */ React.createElement("b", { className: "lc-stat-tip-model" }, r.family), " ", t("stats.costHit"), " ", fmtRate(r.peak.hit), "/", fmtRate(r.off.hit), " \xB7 ", t("stats.costMiss"), " ", fmtRate(r.peak.miss), "/", fmtRate(r.off.miss), " \xB7 ", t("stats.costOut"), " ", fmtRate(r.peak.out), "/", fmtRate(r.off.out))))
1709
- ];
1710
- const cell = (label, value, tip) => /* @__PURE__ */ React.createElement("div", { className: "lc-stat" + (tip === void 0 ? "" : " lc-stat-tipped") }, /* @__PURE__ */ React.createElement("span", { className: "lc-stat-label" }, label, tip !== void 0 && /* @__PURE__ */ React.createElement("i", { className: "lc-stat-q", "aria-hidden": "true" }, "?")), /* @__PURE__ */ React.createElement("b", { className: "lc-stat-value" }, value), tip !== void 0 && /* @__PURE__ */ React.createElement("span", { className: "lc-stat-tip", role: "tooltip" }, tip));
1711
- 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 + "%"), cell(t("stats.cost"), cost === null ? "\u2014" : formatCost(cost, currency), costTip)));
1712
- };
1713
- }
1714
-
1715
- // src/client/components/trendChart.tsx
1716
- function aggregateByTurn(requests) {
1717
- const out = [];
1718
- let runSteps = 0;
1719
- for (const req of requests) {
1720
- const last = out.length > 0 ? out[out.length - 1] : null;
1721
- if (last !== null && (last.turn ?? 0) === (req.turn ?? 0)) {
1722
- runSteps++;
1723
- out[out.length - 1] = { ...req, stepCount: runSteps };
1724
- } else {
1725
- runSteps = 1;
1726
- out.push({ ...req, stepCount: 1 });
1727
- }
1728
- }
1729
- return out;
1730
- }
1731
- function attachMarkers(requests, events) {
1732
- const markers = new Array(requests.length);
1733
- for (const ev of events) {
1734
- if (ev.kind !== "compaction" && ev.kind !== "prune") continue;
1735
- for (let r = 0; r < requests.length; r++) {
1736
- if (requests[r].seq >= ev.seq) {
1737
- if (markers[r] === void 0) markers[r] = ev;
1738
- break;
1739
- }
1740
- }
1741
- }
1742
- return markers;
1743
- }
1744
- function makeTrendChart(kit) {
1745
- const { t, fmt: fmt3, fmtTime: fmtTime2, catLabel, eventLabel, eventAt } = kit;
1746
- const CHART_H = 112;
1747
- const BAR_W = 14;
1748
- const BAR_GAP = 2;
1749
- const TURN_FILLS = ["rgba(128,128,128,0.12)", "rgba(128,128,128,0.26)"];
1750
- return function TrendChart(props) {
1751
- const requests = props.requests;
1752
- const markers = props.markers;
1753
- const anchorOf = (req) => typeof req.prompt === "number" && req.prompt > 0 && req.total > 0 ? req.prompt / req.total : 1;
1754
- const barTotalOf = (req) => typeof req.prompt === "number" && req.prompt > 0 ? req.prompt : req.total;
1755
- let maxTotal = 1;
1756
- for (const req of requests) {
1757
- const bt = barTotalOf(req);
1758
- if (bt > maxTotal) maxTotal = bt;
1759
- }
1760
- const groups = [];
1761
- for (const req of requests) {
1762
- let grp = groups.length > 0 ? groups[groups.length - 1] : null;
1763
- if (grp === null || grp.turn !== (req.turn ?? 0)) {
1764
- grp = { turn: req.turn ?? 0, count: 0, span: 0, agg: req.stepCount !== void 0 };
1765
- groups.push(grp);
1766
- }
1767
- grp.count++;
1768
- grp.span += req.stepCount ?? 1;
1769
- }
1770
- const scrollRef = React.useRef(null);
1771
- const scrolledOnce = React.useRef(false);
1772
- const lastGranRef = React.useRef(props.granularity);
1773
- const [edges, setEdges] = React.useState({ left: false, right: false });
1774
- const edgesRef = React.useRef(edges);
1775
- const updateEdges = (el) => {
1776
- const left = el.scrollLeft > 4;
1777
- const right = el.scrollLeft + el.clientWidth < el.scrollWidth - 4;
1778
- const prev = edgesRef.current;
1779
- if (prev.left === left && prev.right === right) return;
1780
- edgesRef.current = { left, right };
1781
- setEdges({ left, right });
1782
- };
1783
- React.useLayoutEffect(() => {
1784
- const el = scrollRef.current;
1785
- if (el === null) return;
1786
- if (props.granularity !== lastGranRef.current) {
1787
- lastGranRef.current = props.granularity;
1788
- scrolledOnce.current = false;
1789
- }
1790
- if (!scrolledOnce.current) {
1791
- scrolledOnce.current = true;
1792
- el.scrollLeft = el.scrollWidth;
1793
- } else if (el.scrollLeft + el.clientWidth >= el.scrollWidth - 24) {
1794
- el.scrollLeft = el.scrollWidth;
1795
- }
1796
- updateEdges(el);
1797
- });
1798
- const tipOf = (req) => {
1799
- 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 });
1800
- 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) }) : "");
1801
- };
1802
- const hoveredIdx = props.hoveredSeq !== null ? requests.findIndex((r) => r.seq === props.hoveredSeq) : -1;
1803
- const hoveredReq = hoveredIdx >= 0 ? requests[hoveredIdx] : null;
1804
- 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(
1805
- "div",
1806
- {
1807
- className: "lc-chart-scroll" + (props.activeTurn !== null ? " lc-chart-dim" : ""),
1808
- ref: scrollRef,
1809
- onScroll: (e) => {
1810
- updateEdges(e.currentTarget);
1811
- }
1812
- },
1813
- edges.left ? /* @__PURE__ */ React.createElement("div", { className: "lc-chart-fade lc-chart-fade-l" }) : null,
1814
- /* @__PURE__ */ React.createElement(
1815
- "div",
1816
- {
1817
- className: "lc-chart",
1818
- onMouseLeave: () => {
1819
- props.onHover(null);
1820
- }
1821
- },
1822
- /* @__PURE__ */ React.createElement("div", { className: "lc-grid lc-grid-top" }),
1823
- /* @__PURE__ */ React.createElement("div", { className: "lc-grid lc-grid-mid" }),
1824
- requests.map((req, i) => {
1825
- const marker = markers[i];
1826
- const markerAt = marker !== void 0 ? eventAt(marker) : null;
1827
- const selected = props.selectedSeq === req.seq;
1828
- const hovered = props.hoveredSeq === req.seq;
1829
- const inTurn = props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn;
1830
- return /* @__PURE__ */ React.createElement(
1831
- "div",
1832
- {
1833
- key: req.seq,
1834
- className: "lc-bar" + (selected ? " lc-bar-selected" : "") + (hovered ? " lc-bar-hovered" : "") + (inTurn ? " lc-bar-in-turn" : ""),
1835
- style: { width: BAR_W + "px" },
1836
- onClick: () => {
1837
- props.onSelect(selected ? null : req.seq);
1838
- },
1839
- onMouseEnter: () => {
1840
- props.onHover(req.seq);
1841
- }
1842
- },
1843
- marker !== void 0 ? /* @__PURE__ */ React.createElement(
1844
- "span",
1845
- {
1846
- className: "lc-bar-marker",
1847
- title: "\u2702 " + (markerAt !== null ? markerAt + " \u2014 " : "") + eventLabel(marker)
1848
- },
1849
- "\u2702"
1850
- ) : null,
1851
- /* @__PURE__ */ React.createElement("div", { className: "lc-bar-stack" }, CATS.map((c) => {
1852
- const v = (req[c.key] || 0) * anchorOf(req);
1853
- if (!v) return null;
1854
- return /* @__PURE__ */ React.createElement("div", { key: c.key, style: { height: Math.max(1, Math.round(v / maxTotal * CHART_H)) + "px", background: c.color } });
1855
- }))
1856
- );
1857
- })
1858
- ),
1859
- hoveredReq !== null ? /* @__PURE__ */ React.createElement(
1860
- "div",
1861
- {
1862
- className: "lc-chart-tip",
1863
- style: { left: hoveredIdx * (BAR_W + BAR_GAP) + BAR_W / 2 + "px" }
1864
- },
1865
- tipOf(hoveredReq)
1866
- ) : null,
1867
- /* @__PURE__ */ React.createElement("div", { className: "lc-turns", onMouseLeave: () => {
1868
- props.onHoverTurn(null);
1869
- } }, groups.map((grp, gi) => {
1870
- const on = props.activeTurn === grp.turn;
1871
- const blockW = grp.agg ? BAR_W : grp.span * (BAR_W + BAR_GAP) - BAR_GAP;
1872
- return /* @__PURE__ */ React.createElement(
1873
- "span",
1874
- {
1875
- key: "turn-" + gi,
1876
- className: "lc-turn" + (on ? " lc-turn-on" : ""),
1877
- style: {
1878
- width: blockW + "px",
1879
- background: TURN_FILLS[gi % TURN_FILLS.length]
1880
- },
1881
- title: "T" + grp.turn,
1882
- onMouseEnter: () => {
1883
- props.onHoverTurn(grp.turn);
1884
- }
1885
- },
1886
- "T" + grp.turn
1887
- );
1888
- }))
1889
- ), edges.right ? /* @__PURE__ */ React.createElement("div", { className: "lc-chart-fade lc-chart-fade-r" }) : null);
1890
- };
1891
- }
1892
-
1893
- // src/client/components/contextView.tsx
1894
- var viewScroll = /* @__PURE__ */ new Map();
1895
- var EVENT_KINDS = ["inject", "compaction", "prune", "model"];
1896
- function makeContextView(ctx, kit) {
1897
- const { t } = kit;
1898
- const StackedBar = makeStackedBar(kit);
1899
- const Legend = makeLegend(kit);
1900
- const CurrentComposition = makeCurrentComposition(kit, StackedBar, Legend);
1901
- const TrendChart = makeTrendChart(kit);
1902
- const RequestDetail = makeRequestDetail(kit, StackedBar);
1903
- const EventList = makeEventList(kit);
1904
- const NodeList = makeNodeList(kit);
1905
- const StatsBoard = makeStatsBoard(kit);
1906
- const PluginInfo = makePluginInfo(kit);
1907
- const ContextBrowser = makeContextBrowser(kit, StackedBar);
1908
- const ErrorBoundary = makeErrorBoundary(t);
1909
- function ContextViewBody(props) {
1910
- const sessionId = props.sessionId;
1911
- const data = typeof props.useProjection === "function" ? timelineOf(props.useProjection("contextTimeline")) : null;
1912
- const pressure = typeof props.useProjection === "function" ? contextPressureOf(props.useProjection("contextPressure")) : null;
1913
- const usage = typeof props.useProjection === "function" ? tokenUsageOf(props.useProjection("tokenUsage")) : null;
1914
- const headers = typeof props.useProjection === "function" ? headersOf(props.useProjection("contextHeaders")) : null;
1915
- const [selectedSeq, setSelectedSeq] = React.useState(null);
1916
- const [hoveredSeq, setHoveredSeq] = React.useState(null);
1917
- const [hoverTurn, setHoverTurn] = React.useState(null);
1918
- const [tick, setTick] = React.useState(0);
1919
- const [granularity, setGranularity] = React.useState("step");
1920
- const [hoverCat, setHoverCat] = React.useState(null);
1921
- const [pickedKinds, setPickedKinds] = React.useState([...EVENT_KINDS]);
1922
- const toggleKind = (k) => {
1923
- setPickedKinds((p) => {
1924
- if (p.length === EVENT_KINDS.length) return [k];
1925
- if (!p.includes(k)) return [...p, k];
1926
- return p.length === 1 ? [...EVENT_KINDS] : p.filter((x) => x !== k);
1927
- });
1928
- };
1929
- const [toolFocus, setToolFocus] = React.useState(null);
1930
- const clearToolFocus = React.useCallback(() => {
1931
- setToolFocus(null);
1932
- }, []);
1933
- const rootRef = React.useRef(null);
1934
- const scrollerRef = React.useRef(null);
1935
- const restoredRef = React.useRef(null);
1936
- React.useLayoutEffect(() => {
1937
- if (typeof sessionId !== "string" || sessionId === "" || data === null) return;
1938
- if (restoredRef.current === sessionId) return;
1939
- restoredRef.current = sessionId;
1940
- const scroller = rootRef.current !== null ? rootRef.current.closest("[data-conversation-scroll]") : null;
1941
- if (scroller === null) return;
1942
- scrollerRef.current = scroller;
1943
- scroller.scrollTop = viewScroll.get(sessionId) ?? 0;
1944
- }, [sessionId, data]);
1945
- React.useLayoutEffect(() => {
1946
- return () => {
1947
- if (typeof sessionId !== "string" || sessionId === "") return;
1948
- const scroller = scrollerRef.current;
1949
- if (scroller === null) return;
1950
- viewScroll.set(sessionId, scroller.scrollTop);
1951
- };
1952
- }, [sessionId]);
1953
- React.useEffect(() => {
1954
- const localeSvc2 = ctx.get("locale");
1955
- if (!localeSvc2) return void 0;
1956
- return localeSvc2.subscribe(() => setTick((x) => x + 1));
1957
- }, []);
1958
- void tick;
1959
- if (!data) {
1960
- return /* @__PURE__ */ React.createElement("div", { className: "lc-root", ref: rootRef }, /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("loading")));
1961
- }
1962
- const requests = data.requests || [];
1963
- const events = data.events || [];
1964
- const shownEvents = pickedKinds.length === EVENT_KINDS.length ? events : events.filter((e) => pickedKinds.includes(e.kind));
1965
- const nodes = data.nodes || [];
1966
- const displayRequests = granularity === "turn" ? aggregateByTurn(requests) : requests;
1967
- const markers = attachMarkers(displayRequests, events);
1968
- const markerOf = (req) => {
1969
- const i = displayRequests.indexOf(req);
1970
- return i >= 0 ? markers[i] : void 0;
1971
- };
1972
- let pinnedReq = null;
1973
- for (const req of displayRequests) if (req.seq === selectedSeq) pinnedReq = req;
1974
- let activeReq = null;
1975
- if (hoveredSeq !== null) {
1976
- for (const req of displayRequests) if (req.seq === hoveredSeq) activeReq = req;
1977
- }
1978
- if (activeReq === null) activeReq = pinnedReq;
1979
- if (activeReq === null && displayRequests.length > 0) activeReq = displayRequests[displayRequests.length - 1];
1980
- let activeTurn = hoverTurn;
1981
- if (activeTurn === null && hoveredSeq !== null) {
1982
- for (const req of displayRequests) if (req.seq === hoveredSeq) {
1983
- activeTurn = req.turn ?? null;
1984
- break;
1985
- }
1986
- }
1987
- const head = headlineOf(data, pressure);
1988
- const localeSvc = ctx.get("locale");
1989
- const activeLocale = localeSvc !== void 0 && typeof localeSvc.getLocale === "function" ? localeSvc.getLocale().active : "en";
1990
- 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, cost: data.cost, locale: activeLocale }), /* @__PURE__ */ React.createElement(PluginInfo, null)), /* @__PURE__ */ React.createElement("div", { className: "lc-cols" }, /* @__PURE__ */ React.createElement("div", { className: "lc-col" }, /* @__PURE__ */ React.createElement(
1991
- CurrentComposition,
1992
- {
1993
- head,
1994
- subtitle: (data.model ? data.model : "") + (data.provider ? " \xB7 " + data.provider : ""),
1995
- hoverKey: hoverCat,
1996
- onHoverKey: setHoverCat,
1997
- tools: data.toolList,
1998
- onToolFocus: setToolFocus
1999
- }
2000
- ), /* @__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(
2001
- "button",
2002
- {
2003
- className: "lc-gran-btn" + (granularity === "step" ? " lc-gran-on" : ""),
2004
- onClick: () => {
2005
- setGranularity("step");
2006
- }
2007
- },
2008
- t("gran.step")
2009
- ), /* @__PURE__ */ React.createElement(
2010
- "button",
2011
- {
2012
- className: "lc-gran-btn" + (granularity === "turn" ? " lc-gran-on" : ""),
2013
- onClick: () => {
2014
- setGranularity("turn");
2015
- }
2016
- },
2017
- t("gran.turn")
2018
- ))), displayRequests.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("trend.empty")) : /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(
2019
- TrendChart,
2020
- {
2021
- key: sessionId,
2022
- requests: displayRequests,
2023
- markers,
2024
- selectedSeq: pinnedReq ? pinnedReq.seq : null,
2025
- hoveredSeq,
2026
- activeTurn,
2027
- granularity,
2028
- onSelect: setSelectedSeq,
2029
- onHover: setHoveredSeq,
2030
- onHoverTurn: setHoverTurn
2031
- }
2032
- ), /* @__PURE__ */ React.createElement(RequestDetail, { request: activeReq, marker: activeReq !== null ? markerOf(activeReq) : void 0 })))), /* @__PURE__ */ React.createElement("div", { className: "lc-col" }, /* @__PURE__ */ React.createElement(
2033
- ContextBrowser,
2034
- {
2035
- data,
2036
- headers,
2037
- useSession: props.useSession,
2038
- loadOlderHistory: props.loadOlderHistory,
2039
- previewSeq: hoveredSeq,
2040
- pinSeq: pinnedReq !== null ? pinnedReq.seq : null,
2041
- hoverKey: hoverCat,
2042
- onHoverKey: setHoverCat,
2043
- toolFocus,
2044
- onToolFocusHandled: clearToolFocus
2045
- }
2046
- ))), /* @__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(
2047
- "button",
2048
- {
2049
- key: k,
2050
- className: "lc-gran-btn" + (pickedKinds.includes(k) ? " lc-gran-on lc-kind-" + k : ""),
2051
- onClick: () => {
2052
- toggleKind(k);
2053
- }
2054
- },
2055
- t("kind." + k)
2056
- )))), /* @__PURE__ */ React.createElement(EventList, { events: shownEvents })), /* @__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("nodes.title")), /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("nodes.hint"))), /* @__PURE__ */ React.createElement(NodeList, { nodes, dropped: data.droppedNodes || 0 }))), /* @__PURE__ */ React.createElement("div", { className: "lc-foot" }, t("footer")));
2057
- }
2058
- return function ContextView(props) {
2059
- return h(ErrorBoundary, null, h(ContextViewBody, props));
2060
- };
2061
- }
2062
-
2063
- // src/client/viewkit.ts
2064
- function makeViewKit(t) {
2065
- const { eventLabel, eventAt } = makeEventText(t);
2066
- return {
2067
- t,
2068
- fmt,
2069
- fmtTime,
2070
- catLabel: (key) => t("cat." + key),
2071
- eventLabel,
2072
- eventAt
2073
- };
2074
- }
2075
-
2076
- // src/client/index.ts
2077
- var NS = "dsh-context";
2078
- function apply(ctx) {
2079
- ctx.effect(() => {
2080
- return ctx.locale.register(NS, { zh: DICT_ZH, en: DICT_EN });
2081
- }, "dsh-context: dictionaries");
2082
- const t = ctx.locale.bind(NS);
2083
- ctx.effect(() => {
2084
- const tag = document.createElement("style");
2085
- tag.setAttribute("data-plugin", "dsh-context");
2086
- tag.textContent = STYLES;
2087
- document.head.appendChild(tag);
2088
- return () => {
2089
- if (tag.parentNode !== null) tag.parentNode.removeChild(tag);
2090
- };
2091
- }, "dsh-context: styles");
2092
- const kit = makeViewKit(t);
2093
- const ContextView = makeContextView(ctx, kit);
2094
- ctx.effect(() => {
2095
- const noop = () => {
2096
- };
2097
- const sessions = ctx.get("sessions");
2098
- if (sessions === void 0 || typeof sessions.provide !== "function") return noop;
2099
- try {
2100
- return sessions.provide({
2101
- props: ["loadOlderHistory"],
2102
- resolve: (binding) => ({ props: { loadOlderHistory: () => binding.session.loadOlder() } })
2103
- });
2104
- } catch {
2105
- return noop;
2106
- }
2107
- }, "dsh-context: loadOlderHistory prop");
2108
- ctx.slots.inject("conversation.view", () => {
2109
- return ctx.slots.register(
2110
- // order 20 renders right of Chat (0) and Trajectory (10); the locale
2111
- // namespace put the framework `t` seat on the component's props too.
2112
- { name: "conversation.view", id: "context", order: 20, locale: NS, label: () => t("tab") },
2113
- (props) => h(ContextView, props)
2114
- );
2115
- });
2116
- registerContextCommand(ctx, kit);
2117
- const ContextModal = makeContextModal(ctx, kit);
2118
- ctx.slots.inject("conversation.input.overlay", () => {
2119
- return ctx.slots.register(
2120
- {
2121
- name: "conversation.input.overlay",
2122
- id: "context-modal",
2123
- order: 10,
2124
- locale: NS,
2125
- inject: (sessionId) => ({ hooks: { contextModal: modalStoreOf(sessionId) } })
2126
- },
2127
- (props) => h(ContextModal, props)
2128
- );
2129
- });
2130
- }
2131
- module.exports = {
2132
- name: "dsh-context",
2133
- inject: ["slots", "locale"],
2134
- apply
2135
- };
2136
-
2137
- return module.exports;
2138
- },
2
+ id: "dsh-context",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ module.exports;
6
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
7
+ //#region src/client/i18n.ts
8
+ const DICT_ZH = {
9
+ "tab": "上下文",
10
+ "cat.system": "系统提示词",
11
+ "cat.tools": "工具定义",
12
+ "cat.user": "用户消息",
13
+ "cat.inject": "注入内容",
14
+ "cat.assistant": "助手消息",
15
+ "cat.tool": "工具结果",
16
+ "overview.title": "当前构成",
17
+ "overview.estimate": "tokens(估算)",
18
+ "overview.free": "剩余窗口",
19
+ "overview.used": "上下文已用",
20
+ "overview.ofUsed": "占已用上下文",
21
+ "overview.compactReserve": "自动压缩预留:占用达 {pct}% 窗口时触发压缩,此区域一般不实际占用",
22
+ "stats.title": "上下文统计",
23
+ "stats.hint": "统计当前保留的历史窗口(与趋势图一致)",
24
+ "stats.turns": "轮次",
25
+ "stats.steps": "步数",
26
+ "stats.injects": "注入",
27
+ "stats.compactions": "压缩",
28
+ "stats.prunes": "剪枝",
29
+ "stats.cacheHit": "缓存命中",
30
+ "stats.cost": "预估费用",
31
+ "stats.costTip": "按 DeepSeek 官方刊例价估算整个会话的累计费用:输入区分缓存命中/未命中,输出含思考;按请求时间区分高峰(UTC 01:00–04:00、06:00–10:00)与空闲时段(半价)。适用于 deepseek-v4-flash / deepseek-v4-pro(不限 provider),价格写死在代码中,仅供参考。",
32
+ "stats.costPriceHead": "每百万 tokens 价格(高峰 | 空闲半价):",
33
+ "stats.costHit": "命中",
34
+ "stats.costMiss": "未命中",
35
+ "stats.costOut": "输出",
36
+ "plugin.title": "插件信息",
37
+ "plugin.hint": "The best DSH context plugin ⭐",
38
+ "plugin.name": "插件",
39
+ "plugin.github": "GitHub",
40
+ "tools.top": "工具定义 Top:",
41
+ "tools.more": "等 {n} 个",
42
+ "trend.title": "历史趋势",
43
+ "gran.step": "步骤",
44
+ "gran.turn": "轮次",
45
+ "trend.hint": "悬停查看详情,✂ 表示压缩/剪枝,Step/Turn 切换粒度",
46
+ "trend.empty": "发起一轮对话后,这里会展示每次模型请求的上下文构成",
47
+ "detail.step": " {t} 轮 · 第 {s} 步",
48
+ "detail.turn": "第 {t} 轮 · 共 {n} 步",
49
+ "detail.lastStep": "末步",
50
+ "detail.estTotal": "估算合计 {n}",
51
+ "detail.actual": "实际 prompt {n}",
52
+ "detail.output": "输出 {n}",
53
+ "events.title": "上下文事件",
54
+ "events.empty": "暂无上下文事件(压缩、注入、模型切换会出现在这里)",
55
+ "events.at": "第 {t} 轮 · 第 {s} 步",
56
+ "events.range": "第 {t} 轮 · 第 {a}→{b} 步",
57
+ "events.rangeTo": " {a} · {as} 步 → 第 {b} 轮 · 第 {bs} 步",
58
+ "kind.inject": "注入",
59
+ "kind.compaction": "压缩",
60
+ "kind.prune": "剪枝",
61
+ "kind.model": "切换",
62
+ "nodes.title": "消息构成",
63
+ "nodes.hint": "当前模型可见的消息,最新在前",
64
+ "nodes.more": "… 更早的 {n} 条消息已省略",
65
+ "nodes.empty": "当前没有模型可见的消息",
66
+ "loading": "正在读取会话日志…",
67
+ "error": "上下文数据读取失败:",
68
+ "error.retry": "重试",
69
+ "footer": "估算口径:与 dsh 内置 tokenMeter 相同的固定密度启发式(约 4 字符 ≈ 1 token);「实际」为供应商上报用量。",
70
+ "tip.step": "第 {t} 轮 · 第{s}步",
71
+ "tip.turn": " {t} · {n} ",
72
+ "tip.total": "合计 {n}",
73
+ "tip.actual": "(实际 {n}",
74
+ "ev.compaction": "压缩上下文(摘要替换 {n} 条消息)",
75
+ "ev.prune": "剪枝工具输出",
76
+ "ev.skill": "Skill 注入({name}",
77
+ "ev.model": "模型切换:{a} → {b}",
78
+ "form.instructions": "指令注入",
79
+ "form.catalog": "目录更新",
80
+ "form.snapshot": "状态快照",
81
+ "form.notice": "通知",
82
+ "form.relay": "代理转发",
83
+ "form.recall": "历史召回",
84
+ "form.context": "上下文注入",
85
+ "node.toolResult": "工具结果",
86
+ "node.calls": "调用 ",
87
+ "node.empty": "(空回复)",
88
+ "node.nonText": "(非文本消息)",
89
+ "node.snapshot": "快照: ",
90
+ "cmd.desc": "查看当前上下文构成,浏览各步骤组成",
91
+ "cmd.close": "关闭",
92
+ "browser.title": "上下文浏览器",
93
+ "browser.live": "当前(下一次请求)",
94
+ "browser.liveNow": "当前 · 下一次请求",
95
+ "browser.items": "{n} 项",
96
+ "browser.missingLive": " 另有 {n} 条更早的消息也在上下文中(超出展示窗口)",
97
+ "browser.approx": "该步骤涉及的部分已移除消息超出保留范围,以下为近似构成",
98
+ "browser.deltaHint": "对比上轮末步的变动",
99
+ "browser.noHeader": "此数据来自旧版插件:仅提供 token 估算,无实际内容",
100
+ "browser.noEpoch": "该步骤的头部内容(系统提示词 / 工具定义)不在保留范围内",
101
+ "browser.noContent": "完整内容不在当前加载的消息窗口内(在聊天页加载更早历史后可查看)",
102
+ "browser.loading": "正在从更早的会话历史加载完整内容…",
103
+ "browser.preview": "预览",
104
+ "tool.desc": "描述",
105
+ "tool.params": "参数",
106
+ "tool.paramsEmpty": "(无参数)",
107
+ "tool.jsonToggle": "查看原始 JSON",
108
+ "tool.jsonHide": "收起",
109
+ "rich.raw": "原文",
110
+ "rich.md": "Markdown",
111
+ "rich.toMd": "按 Markdown 渲染查看",
112
+ "rich.toRaw": "查看原始文本",
113
+ "block.thinking": "思考",
114
+ "block.answer": "回答",
115
+ "attach.images": "图片附件",
116
+ "attach.other": "其他内容",
117
+ "attach.image": "图片",
118
+ "attach.open": "查看原图",
119
+ "attach.loading": "",
120
+ "attach.loadFailed": "加载失败 · 点击重试",
121
+ "attach.orig": "原图 {d}"
122
+ };
123
+ const DICT_EN = {
124
+ "tab": "Context",
125
+ "cat.system": "System Prompt",
126
+ "cat.tools": "Tool Schemas",
127
+ "cat.user": "User Messages",
128
+ "cat.inject": "Injected Context",
129
+ "cat.assistant": "Assistant Messages",
130
+ "cat.tool": "Tool Results",
131
+ "overview.title": "Current Context",
132
+ "overview.estimate": "tokens (estimated)",
133
+ "overview.free": "Free Window",
134
+ "overview.used": "of context used",
135
+ "overview.ofUsed": "of used context",
136
+ "overview.compactReserve": "Auto-compaction reserve: auto-compaction triggers at {pct}% of the window this area is normally kept as headroom, not actually used",
137
+ "stats.title": "Context Stats",
138
+ "stats.hint": "Over the retained history window (same as the History chart)",
139
+ "stats.turns": "Turns",
140
+ "stats.steps": "Steps",
141
+ "stats.injects": "Injections",
142
+ "stats.compactions": "Compactions",
143
+ "stats.prunes": "Prunes",
144
+ "stats.cacheHit": "Cache Hits",
145
+ "stats.cost": "Est. Cost",
146
+ "stats.costTip": "Rough cumulative cost of the whole session at DeepSeek’s list prices: input split by cache hit/miss, output includes reasoning; each request is priced at the peak (01:00–04:00, 06:00–10:00 UTC) or half-price off-peak rate by its time. Applies to deepseek-v4-flash / deepseek-v4-pro on any provider; prices are hardcoded, for reference only.",
147
+ "stats.costPriceHead": "Per-1M-token rates (peak | off-peak at half price):",
148
+ "stats.costHit": "hit",
149
+ "stats.costMiss": "miss",
150
+ "stats.costOut": "output",
151
+ "plugin.title": "Plugin Info",
152
+ "plugin.hint": "The best DSH context plugin ⭐",
153
+ "plugin.name": "Plugin",
154
+ "plugin.github": "GitHub",
155
+ "tools.top": "Top Tool Schemas:",
156
+ "tools.more": "of {n}",
157
+ "trend.title": "Context History",
158
+ "gran.step": "Step",
159
+ "gran.turn": "Turn",
160
+ "trend.hint": "Hover to view details, ✂ marks compaction/prune, Step/Turn switches granularity",
161
+ "trend.empty": "Send a message and each model request’s context makeup shows up here",
162
+ "detail.step": "Turn {t} · Step {s}",
163
+ "detail.turn": "Turn {t} · {n} steps",
164
+ "detail.lastStep": "Last Step",
165
+ "detail.estTotal": "Estimated {n}",
166
+ "detail.actual": "Actual Prompt {n}",
167
+ "detail.output": "Output {n}",
168
+ "events.title": "Context Events",
169
+ "events.empty": "No context events yet (compaction, injections, model switches appear here)",
170
+ "events.at": "Turn {t} · Step {s}",
171
+ "events.range": "Turn {t} · Step {a}→{b}",
172
+ "events.rangeTo": "Turn {a} · Step {as} → Turn {b} · Step {bs}",
173
+ "kind.inject": "Inject",
174
+ "kind.compaction": "Compact",
175
+ "kind.prune": "Prune",
176
+ "kind.model": "Switch",
177
+ "nodes.title": "Messages",
178
+ "nodes.hint": "currently model-visible, newest first",
179
+ "nodes.more": " {n} earlier messages omitted",
180
+ "nodes.empty": "No model-visible messages right now",
181
+ "loading": "Reading the session log…",
182
+ "error": "Failed to read context data: ",
183
+ "error.retry": "Retry",
184
+ "footer": "Estimate: same fixed-density heuristic as dsh’s built-in tokenMeter (~4 chars ≈ 1 token); “actual” is provider-reported usage.",
185
+ "tip.step": "Turn {t} · Step {s}",
186
+ "tip.turn": "Turn {t} · {n} steps",
187
+ "tip.total": "Total {n}",
188
+ "tip.actual": " (actual {n})",
189
+ "ev.compaction": "Context compacted (summary replaced {n} messages)",
190
+ "ev.prune": "Tool output pruned",
191
+ "ev.skill": "Skill injected ({name})",
192
+ "ev.model": "Model switched: {a} → {b}",
193
+ "form.instructions": "Instructions",
194
+ "form.catalog": "Catalog Update",
195
+ "form.snapshot": "State Snapshot",
196
+ "form.notice": "Notice",
197
+ "form.relay": "Agent Relay",
198
+ "form.recall": "Recall",
199
+ "form.context": "Context Injection",
200
+ "node.toolResult": "Tool Result",
201
+ "node.calls": "Calls ",
202
+ "node.empty": "(empty reply)",
203
+ "node.nonText": "(non-text message)",
204
+ "node.snapshot": "Snapshot: ",
205
+ "cmd.desc": "View current context makeup, browse per-step composition",
206
+ "cmd.close": "Close",
207
+ "browser.title": "Context Browser",
208
+ "browser.live": "Live (Next Request)",
209
+ "browser.liveNow": "Live · Next Request",
210
+ "browser.items": "{n} Items",
211
+ "browser.missingLive": " {n} earlier messages are also part of the context (outside the served window)",
212
+ "browser.approx": "Some removed messages of this step exceed retention the makeup below is approximate",
213
+ "browser.deltaHint": "vs previous turn",
214
+ "browser.noHeader": "Served by an older plugin build: token estimates only, no content",
215
+ "browser.noEpoch": "The header content (System Prompt / Tool Schemas) of this step is outside retention",
216
+ "browser.noContent": "Full content is outside the loaded message window (load older history in Chat to view)",
217
+ "browser.loading": "Loading full content from older session history…",
218
+ "browser.preview": "Preview",
219
+ "tool.desc": "Description",
220
+ "tool.params": "Parameters",
221
+ "tool.paramsEmpty": "(no parameters)",
222
+ "tool.jsonToggle": "View Raw JSON",
223
+ "tool.jsonHide": "Collapse",
224
+ "rich.raw": "Raw",
225
+ "rich.md": "Markdown",
226
+ "rich.toMd": "View as Markdown",
227
+ "rich.toRaw": "View Raw Text",
228
+ "block.thinking": "Reasoning",
229
+ "block.answer": "Response",
230
+ "attach.images": "Images",
231
+ "attach.other": "Other content",
232
+ "attach.image": "Image",
233
+ "attach.open": "Open full image",
234
+ "attach.loading": "…",
235
+ "attach.loadFailed": "Load failed · click to retry",
236
+ "attach.orig": "original {d}"
237
+ };
238
+ //#endregion
239
+ //#region src/client/modalStore.ts
240
+ const stores = /* @__PURE__ */ new Map();
241
+ function modalStoreOf(sessionId) {
242
+ const existing = stores.get(sessionId);
243
+ if (existing !== void 0) return existing;
244
+ let open = false;
245
+ const listeners = /* @__PURE__ */ new Set();
246
+ const store = {
247
+ subscribe(listener) {
248
+ listeners.add(listener);
249
+ return () => {
250
+ listeners.delete(listener);
251
+ };
252
+ },
253
+ getSnapshot: () => open,
254
+ set(next) {
255
+ if (next === open) return;
256
+ open = next;
257
+ for (const listener of listeners) listener();
258
+ }
259
+ };
260
+ stores.set(sessionId, store);
261
+ return store;
262
+ }
263
+ const pendingConsume = /* @__PURE__ */ new Map();
264
+ function setPendingConsume(sessionId, guard) {
265
+ pendingConsume.set(sessionId, guard);
266
+ }
267
+ /** Take (and clear) the guard recorded by the open path, if any. */
268
+ function takePendingConsume(sessionId) {
269
+ const guard = pendingConsume.get(sessionId);
270
+ if (guard !== void 0) pendingConsume.delete(sessionId);
271
+ return guard;
272
+ }
273
+ //#endregion
274
+ //#region src/client/command.ts
275
+ /**
276
+ * `/context` — a client-owned slash command that opens the context modal
277
+ * (current composition + recent trend) in the center of the page.
278
+ *
279
+ * Implemented as the plugin's own '/' trigger source instead of a host
280
+ * command: nothing is dispatched to the host, no session log records are
281
+ * written, and nothing becomes model-visible — the invocation never enters
282
+ * the message history. Both paths answer `'handled'` and open the modal,
283
+ * leaving the `/context` token in the composer while it is open; the modal's
284
+ * close path consumes the token then (see modalStore.ts).
285
+ */
286
+ const COMMAND = "context";
287
+ const LINE = "/context";
288
+ function registerContextCommand(ctx, kit) {
289
+ ctx.effect(() => {
290
+ const inputTriggers = ctx.get("inputTriggers");
291
+ if (inputTriggers === void 0) return () => {};
292
+ return inputTriggers.registerSource({
293
+ trigger: "/",
294
+ name: COMMAND,
295
+ order: 1,
296
+ candidates: (_session, req) => {
297
+ if (req.position !== "leading") return Promise.resolve([]);
298
+ const query = req.query.trim().toLowerCase();
299
+ if (query !== "" && !COMMAND.startsWith(query)) return Promise.resolve([]);
300
+ return Promise.resolve([{
301
+ name: COMMAND,
302
+ description: kit.t("cmd.desc")
303
+ }]);
304
+ },
305
+ onPick: (pick) => {
306
+ setPendingConsume(pick.session.sessionId, {
307
+ kind: "span",
308
+ span: pick.span
309
+ });
310
+ modalStoreOf(pick.session.sessionId).set(true);
311
+ return "handled";
312
+ },
313
+ matchEnter: (session, line) => {
314
+ if (line !== LINE) return Promise.resolve(void 0);
315
+ setPendingConsume(session.sessionId, {
316
+ kind: "bare-token",
317
+ token: LINE
318
+ });
319
+ modalStoreOf(session.sessionId).set(true);
320
+ return Promise.resolve("handled");
321
+ }
322
+ });
323
+ }, "dsh-context: /context command");
324
+ }
325
+ //#endregion
326
+ //#region src/client/categories.ts
327
+ const CATS = [
328
+ {
329
+ key: "system",
330
+ color: "#6366f1"
331
+ },
332
+ {
333
+ key: "tools",
334
+ color: "#f59e0b"
335
+ },
336
+ {
337
+ key: "user",
338
+ color: "#22c55e"
339
+ },
340
+ {
341
+ key: "inject",
342
+ color: "#a855f7"
343
+ },
344
+ {
345
+ key: "assistant",
346
+ color: "#3b82f6"
347
+ },
348
+ {
349
+ key: "tool",
350
+ color: "#14b8a6"
351
+ }
352
+ ];
353
+ function partsOf(breakdown) {
354
+ return CATS.map((c) => {
355
+ return {
356
+ key: c.key,
357
+ color: c.color,
358
+ value: breakdown[c.key] || 0
359
+ };
360
+ });
361
+ }
362
+ /**
363
+ * Reproportion heuristic parts so they sum to a provider-anchored target
364
+ * the same trick the official ContextMeter uses: the heuristic breakdown
365
+ * supplies the composition RATIOS, the provider sample the total. Returns
366
+ * the parts unchanged when no anchor applies.
367
+ */
368
+ function anchoredParts(parts, target) {
369
+ if (target === null || target <= 0) return parts;
370
+ let total = 0;
371
+ for (const p of parts) total += p.value;
372
+ if (total <= 0 || total === target) return parts;
373
+ const scale = target / total;
374
+ return parts.map((p) => ({
375
+ ...p,
376
+ value: Math.round(p.value * scale)
377
+ }));
378
+ }
379
+ //#endregion
380
+ //#region src/client/headline.ts
381
+ function headlineOf(data, pressure = null) {
382
+ const current = data.current;
383
+ const projected = pressure !== null && typeof pressure.projectedTokens === "number" ? pressure.projectedTokens : void 0;
384
+ const requests = data.requests;
385
+ const lastReq = requests.length > 0 ? requests[requests.length - 1] : null;
386
+ const derived = lastReq !== null && typeof lastReq.prompt === "number" ? lastReq.prompt + (current.total - lastReq.total) : void 0;
387
+ const occupancyTokens = projected ?? derived ?? null;
388
+ const window = pressure !== null && typeof pressure.contextWindow === "number" ? pressure.contextWindow : data.contextWindow;
389
+ const tokens = occupancyTokens ?? current.total;
390
+ return {
391
+ tokens,
392
+ window,
393
+ pct: window !== void 0 && window > 0 ? Math.min(100, Math.round(tokens / window * 100)) : null,
394
+ parts: anchoredParts(partsOf(current), occupancyTokens !== null && tokens > 0 ? tokens : null)
395
+ };
396
+ }
397
+ //#endregion
398
+ //#region src/client/services.ts
399
+ /**
400
+ * Narrow an unknown projection value to a string-keyed record, or null when
401
+ * it is not one. The boundary type is Record<string, unknown> on purpose:
402
+ * every field read below must re-prove itself (the no-white-screen
403
+ * guarantee), so no field may borrow the wire type before its check.
404
+ */
405
+ function asRecord(value) {
406
+ if (value === null || value === void 0 || typeof value !== "object") return null;
407
+ return value;
408
+ }
409
+ /**
410
+ * Safe finite-number read: a missing/non-numeric/NaN field degrades to 0
411
+ * instead of leaking into the UI as NaN percentages or broken arithmetic.
412
+ */
413
+ function numOf(value) {
414
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
415
+ }
416
+ /** The usable (non-null object) entries of a delivered list; [] when the field is not a list. */
417
+ function objectsOf(value) {
418
+ if (!Array.isArray(value)) return [];
419
+ return value.filter((v) => v !== null && typeof v === "object");
420
+ }
421
+ /**
422
+ * Narrow a delivered projection value to a RENDER-SAFE context timeline —
423
+ * the client's no-white-screen guarantee against backend/parse failures.
424
+ *
425
+ * A value that is not a record at all (capability absent, nothing delivered
426
+ * yet) stays `null` and callers show the loading screen. A record that fails
427
+ * the wire shape (corrupt checkpoint restore, a failed/older host payload,
428
+ * plugin drift) is SANITIZED instead of rejected: every collection becomes
429
+ * an array, non-object entries are dropped, `current` becomes a numeric
430
+ * breakdown, and wrong-typed scalars are dropped or zeroed so the whole
431
+ * tab still renders with every usable piece of data instead of throwing
432
+ * during render and unmounting the conversation view.
433
+ */
434
+ function timelineOf(value) {
435
+ const data = asRecord(value);
436
+ if (data === null) return null;
437
+ const current = data.current;
438
+ if (current !== null && typeof current === "object" && [
439
+ "system",
440
+ "tools",
441
+ "user",
442
+ "inject",
443
+ "assistant",
444
+ "tool",
445
+ "total"
446
+ ].every((k) => typeof current[k] === "number") && Array.isArray(data.requests) && Array.isArray(data.events) && Array.isArray(data.nodes) && Array.isArray(data.archive) && Array.isArray(data.toolList)) return data;
447
+ const safeCurrent = current !== null && typeof current === "object" ? current : {};
448
+ const cost = typeof data.cost === "object" && data.cost !== null && !Array.isArray(data.cost) ? data.cost : void 0;
449
+ return {
450
+ ok: true,
451
+ ...typeof data.model === "string" ? { model: data.model } : {},
452
+ ...typeof data.provider === "string" ? { provider: data.provider } : {},
453
+ ...typeof data.contextWindow === "number" ? { contextWindow: data.contextWindow } : {},
454
+ current: {
455
+ system: numOf(safeCurrent.system),
456
+ tools: numOf(safeCurrent.tools),
457
+ user: numOf(safeCurrent.user),
458
+ inject: numOf(safeCurrent.inject),
459
+ assistant: numOf(safeCurrent.assistant),
460
+ tool: numOf(safeCurrent.tool),
461
+ total: numOf(safeCurrent.total)
462
+ },
463
+ toolList: objectsOf(data.toolList),
464
+ requests: objectsOf(data.requests),
465
+ events: objectsOf(data.events),
466
+ nodes: objectsOf(data.nodes),
467
+ droppedNodes: numOf(data.droppedNodes),
468
+ archive: objectsOf(data.archive),
469
+ ...cost !== void 0 ? { cost } : {},
470
+ ...typeof data.surfaceFloor === "number" ? { surfaceFloor: data.surfaceFloor } : {},
471
+ ...typeof data.archiveFloor === "number" ? { archiveFloor: data.archiveFloor } : {}
472
+ };
473
+ }
474
+ /**
475
+ * Narrow a delivered projection value to the official token-meter
476
+ * `contextPressure` projection (provider-anchored occupancy of the next
477
+ * request). Absent key or value = the meter's projection is not composed
478
+ * (e.g. a harness without the session-projection registry) — callers fall
479
+ * back to their derived anchor, so the UI degrades gracefully.
480
+ */
481
+ function contextPressureOf(value) {
482
+ return asRecord(value);
483
+ }
484
+ /**
485
+ * Narrow a delivered projection value to the official token-meter
486
+ * `tokenUsage` projection (durable cumulative provider usage). Absent key or
487
+ * value = the meter's projection is not composed (or no request has reported
488
+ * usage yet) — callers drop the cache-hit cell to a dash.
489
+ */
490
+ function tokenUsageOf(value) {
491
+ return asRecord(value);
492
+ }
493
+ /**
494
+ * Narrow a delivered projection value to the plugin's `contextHeaders`
495
+ * (request-header content epochs). Absent key = an older Host half without
496
+ * the companion unit — the Context browser degrades its system/tools
497
+ * sections to tokens-only with a note.
498
+ *
499
+ * Entry-level shape is checked too: a malformed epoch (corrupt payload with
500
+ * a missing tools list or wrong-typed system prompt) would crash the
501
+ * browser's tools/sections reads, so the WHOLE projection degrades to null
502
+ * and the card falls back to its tokens-only note.
503
+ */
504
+ function headersOf(value) {
505
+ const headers = asRecord(value);
506
+ if (headers === null || !Array.isArray(headers.headers)) return null;
507
+ for (const h of headers.headers) {
508
+ if (h === null || typeof h !== "object") return null;
509
+ const entry = h;
510
+ if (!Array.isArray(entry.tools)) return null;
511
+ if (entry.system !== void 0 && typeof entry.system !== "string") return null;
512
+ }
513
+ return headers;
514
+ }
515
+ //#endregion
516
+ //#region src/client/assemble.ts
517
+ /** The header epoch in force at `seq` (last logged before it), or the newest. */
518
+ function headerAt(headers, seq) {
519
+ if (headers === null || headers.headers.length === 0) return null;
520
+ if (seq === null) return headers.headers[headers.headers.length - 1];
521
+ for (let i = headers.headers.length - 1; i >= 0; i--) if (headers.headers[i].seq < seq) return headers.headers[i];
522
+ return null;
523
+ }
524
+ /**
525
+ * Reconstruct the assembled surface for one step (`seq` = the request
526
+ * record's seq) or the live surface (`seq` = null).
527
+ */
528
+ function assemble(data, headers, seq) {
529
+ const live = seq === null;
530
+ let nodes;
531
+ if (live) nodes = data.nodes.slice();
532
+ else {
533
+ const picked = [];
534
+ for (const n of data.nodes) if (n.seq < seq) picked.push(n);
535
+ for (const n of data.archive) if (n.seq < seq && n.gone !== void 0 && n.gone > seq) picked.push(n);
536
+ nodes = picked;
537
+ }
538
+ nodes.sort((a, b) => a.seq - b.seq);
539
+ let missingLive = 0;
540
+ if (data.droppedNodes > 0) {
541
+ if (live || data.surfaceFloor !== void 0 && seq > data.surfaceFloor) missingLive = data.droppedNodes;
542
+ }
543
+ const approximate = !live && data.archiveFloor !== void 0 && seq < data.archiveFloor;
544
+ return {
545
+ live,
546
+ header: headerAt(headers, seq),
547
+ nodes,
548
+ missingLive,
549
+ approximate
550
+ };
551
+ }
552
+ //#endregion
553
+ //#region src/client/react.ts
554
+ const React = require("react");
555
+ const h = React.createElement;
556
+ //#endregion
557
+ //#region src/client/components/nodes.tsx
558
+ /** One-line preview of a surface node (shared by the node list and the Context browser's element rows). */
559
+ function makeNodeText(kit) {
560
+ const { t } = kit;
561
+ return function nodeText(n) {
562
+ if (n.cat === "tool") return t("node.toolResult") + (n.tool ? " ← " + n.tool : "") + (n.err ? " ⚠" : "");
563
+ if (n.skill) return "Skill: " + n.skill;
564
+ if (n.calls) return t("node.calls") + n.calls.join(", ");
565
+ if (n.text) return n.form === "snapshot" ? t("node.snapshot") + n.text : n.text;
566
+ if (n.cat === "assistant") return t("node.empty");
567
+ if (n.cat === "inject") return t("form." + (n.form || "context"));
568
+ return t("node.nonText");
569
+ };
570
+ }
571
+ function makeNodeList(kit) {
572
+ const { t, fmt, fmtTime } = kit;
573
+ const nodeText = makeNodeText(kit);
574
+ return function NodeList(props) {
575
+ if (props.nodes.length === 0) return /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("nodes.empty"));
576
+ const catColor = {};
577
+ for (const c of CATS) catColor[c.key] = c.color;
578
+ const rows = props.nodes.slice().reverse();
579
+ 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) => {
580
+ const text = nodeText(n);
581
+ return /* @__PURE__ */ React.createElement("div", {
582
+ key: n.seq,
583
+ className: "lc-node"
584
+ }, /* @__PURE__ */ React.createElement("i", { style: { background: catColor[n.cat] || "#999" } }), /* @__PURE__ */ React.createElement("span", {
585
+ className: "lc-node-preview",
586
+ title: text
587
+ }, text), typeof n.time === "number" ? /* @__PURE__ */ React.createElement("span", { className: "lc-node-time" }, fmtTime(n.time)) : null, /* @__PURE__ */ React.createElement("span", { className: "lc-node-tokens" }, fmt(n.tokens)));
588
+ }));
589
+ };
590
+ }
591
+ //#endregion
592
+ //#region src/client/format.ts
593
+ /**
594
+ * Number/time formatting for the UI. `fmt` uses the same k/M suffix style
595
+ * everywhere (bars, details, stats); `fmtTime` renders a local HH:MM:SS.
596
+ */
597
+ function fmt(n) {
598
+ if (n === void 0 || n === null || isNaN(n)) return "—";
599
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
600
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
601
+ return String(Math.round(n));
602
+ }
603
+ /** Byte sizes for attachment metadata (1 kB = 1000 B, matching the k/M style of `fmt`). */
604
+ function fmtBytes(n) {
605
+ if (n === void 0 || n === null || isNaN(n) || n < 0) return "—";
606
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + " MB";
607
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + " kB";
608
+ return String(Math.round(n)) + " B";
609
+ }
610
+ function fmtTime(t) {
611
+ const d = new Date(t);
612
+ if (isNaN(d.getTime())) return "—";
613
+ return d.toLocaleTimeString("en-GB", { hour12: false });
614
+ }
615
+ //#endregion
616
+ //#region src/client/components/images.tsx
617
+ /**
618
+ * Narrow an unknown content block to a durable image reference. Accepts both
619
+ * raw message blocks (`{ type: 'image', attachment }`) and the conversation
620
+ * snapshot's assistant blocks (`{ kind: 'image', attachment }`); everything
621
+ * else returns null. Lenient on the ref's optional facts (name/bytes/dims)
622
+ * `resolveImage` reads only `attachmentId`.
623
+ */
624
+ function imageRefOf(block) {
625
+ if (block === null || typeof block !== "object") return null;
626
+ const b = block;
627
+ if (b.type !== "image" && b.kind !== "image") return null;
628
+ const a = b.attachment;
629
+ if (a === null || typeof a !== "object") return null;
630
+ const r = a;
631
+ if (typeof r.attachmentId !== "string" || r.attachmentId === "") return null;
632
+ const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : void 0;
633
+ const orig = r.originalDimensions !== null && typeof r.originalDimensions === "object" ? r.originalDimensions : void 0;
634
+ const origDims = orig !== void 0 && num(orig.width) !== void 0 && num(orig.height) !== void 0 ? {
635
+ width: num(orig.width),
636
+ height: num(orig.height)
637
+ } : void 0;
638
+ return {
639
+ attachmentId: r.attachmentId,
640
+ ...typeof r.name === "string" && r.name !== "" ? { name: r.name } : {},
641
+ ...num(r.bytes) !== void 0 ? { bytes: num(r.bytes) } : {},
642
+ ...num(r.width) !== void 0 ? { width: num(r.width) } : {},
643
+ ...num(r.height) !== void 0 ? { height: num(r.height) } : {},
644
+ ...origDims !== void 0 ? { originalDimensions: origDims } : {}
645
+ };
646
+ }
647
+ /**
648
+ * One attachment card: a 64px cover tile (click opens the full image in a
649
+ * new tab; load failures retry on click) beside a metadata column display
650
+ * name, normalized dimensions (plus the pre-normalization size dsh 0.1.1
651
+ * records), and the stored byte size.
652
+ */
653
+ function makeImageCard(kit) {
654
+ const { t } = kit;
655
+ return function ImageCard(props) {
656
+ const { attachment, load } = props;
657
+ const [src, setSrc] = React.useState(null);
658
+ const [error, setError] = React.useState(false);
659
+ const [attempt, setAttempt] = React.useState(0);
660
+ React.useEffect(() => {
661
+ if (load === void 0) return;
662
+ let live = true;
663
+ setError(false);
664
+ setSrc(null);
665
+ load(attachment).then((url) => {
666
+ if (live) setSrc(url);
667
+ }).catch(() => {
668
+ if (live) setError(true);
669
+ });
670
+ return () => {
671
+ live = false;
672
+ };
673
+ }, [
674
+ attachment,
675
+ load,
676
+ attempt
677
+ ]);
678
+ const name = attachment.name ?? t("attach.image");
679
+ const dims = attachment.width !== void 0 && attachment.height !== void 0 ? `${attachment.width}×${attachment.height}` : null;
680
+ const orig = attachment.originalDimensions;
681
+ const facts = [];
682
+ if (dims !== null) facts.push(dims);
683
+ if (orig !== void 0) facts.push(t("attach.orig", { d: `${orig.width}×${orig.height}` }));
684
+ if (attachment.bytes !== void 0) facts.push(fmtBytes(attachment.bytes));
685
+ const open = () => {
686
+ if (src === null) return;
687
+ try {
688
+ window.open(src, "_blank", "noopener");
689
+ } catch {}
690
+ };
691
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-att-item" }, /* @__PURE__ */ React.createElement("button", {
692
+ type: "button",
693
+ className: "lc-att-thumb",
694
+ title: error ? t("attach.loadFailed") : t("attach.open"),
695
+ onClick: error ? () => {
696
+ setAttempt((a) => a + 1);
697
+ } : open
698
+ }, src !== null ? /* @__PURE__ */ React.createElement("img", {
699
+ src,
700
+ alt: name
701
+ }) : /* @__PURE__ */ React.createElement("span", { className: error ? "lc-att-err" : "lc-att-ph" }, error ? "⚠" : load === void 0 ? "🖼" : t("attach.loading"))), /* @__PURE__ */ React.createElement("div", { className: "lc-att-meta" }, /* @__PURE__ */ React.createElement("span", {
702
+ className: "lc-att-name",
703
+ title: name
704
+ }, name), facts.length > 0 ? /* @__PURE__ */ React.createElement("span", { className: "lc-att-dims" }, facts.join(" · ")) : null));
705
+ };
706
+ }
707
+ //#endregion
708
+ //#region src/client/components/richText.tsx
709
+ function makeRichText(kit) {
710
+ const { t } = kit;
711
+ function useRichMode() {
712
+ const [mode, setMode] = React.useState("md");
713
+ return [mode, setMode];
714
+ }
715
+ function RichSwitch(props) {
716
+ const seg = (m, label, tip) => /* @__PURE__ */ React.createElement("button", {
717
+ type: "button",
718
+ className: "lc-rich-seg-btn" + (props.mode === m ? " lc-rich-seg-on" : ""),
719
+ title: tip,
720
+ onClick: () => {
721
+ props.onPick(m);
722
+ }
723
+ }, label);
724
+ return /* @__PURE__ */ React.createElement("span", { className: "lc-rich-seg" + (props.inHead === true ? " lc-rich-seg-head" : "") }, seg("raw", t("rich.raw"), t("rich.toRaw")), seg("md", t("rich.md"), t("rich.toMd")));
725
+ }
726
+ function RichText(props) {
727
+ if (props.mode === "md") return /* @__PURE__ */ React.createElement("div", { className: props.mdClass ?? "lc-br-md" }, /* @__PURE__ */ React.createElement(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, { text: props.text }));
728
+ return /* @__PURE__ */ React.createElement("pre", { className: props.rawClass ?? "lc-br-pre" }, props.text);
729
+ }
730
+ function RichCard(props) {
731
+ const [mode, setMode] = useRichMode();
732
+ return /* @__PURE__ */ React.createElement("div", { className: props.className ?? "lc-br-content" }, /* @__PURE__ */ React.createElement(RichSwitch, {
733
+ mode,
734
+ onPick: setMode
735
+ }), props.render(mode));
736
+ }
737
+ return {
738
+ RichText,
739
+ RichCard,
740
+ RichSwitch,
741
+ useRichMode
742
+ };
743
+ }
744
+ //#endregion
745
+ //#region src/client/components/browser.tsx
746
+ /** Flatten the `anyOf` / `oneOf` alternation into one displayable string. */
747
+ function unionTypesOf(p) {
748
+ const branches = [];
749
+ if (Array.isArray(p.anyOf)) branches.push(...p.anyOf);
750
+ if (Array.isArray(p.oneOf)) branches.push(...p.oneOf);
751
+ if (branches.length === 0) return null;
752
+ const parts = [];
753
+ for (const b of branches) if (b !== null && typeof b === "object") parts.push(typeOf(b));
754
+ return parts.length > 0 ? parts.join(" | ") : null;
755
+ }
756
+ /**
757
+ * Derive a short, human-readable type label from a JSON Schema fragment.
758
+ * Arrays show their element type (`array<number>`); unions fold into
759
+ * `a | b`; enums collapse into `(enum)`. Returns `unknown` when the schema
760
+ * carries no usable signal — the row falls back to its description then.
761
+ */
762
+ function typeOf(p) {
763
+ const u = unionTypesOf(p);
764
+ if (u !== null) return u;
765
+ const t = p.type;
766
+ if (t === "array") {
767
+ const items = p.items;
768
+ if (items !== null && typeof items === "object") return "array<" + typeOf(items) + ">";
769
+ return "array";
770
+ }
771
+ if (typeof t === "string") {
772
+ if (t === "object") {
773
+ const props = p.properties;
774
+ if (props !== null && typeof props === "object" && Object.keys(props).length > 0) return `object{${Object.keys(props).length}}`;
775
+ }
776
+ if (Array.isArray(p.enum) && p.enum.length > 0) return t + " (enum)";
777
+ return t;
778
+ }
779
+ if (Array.isArray(p.enum) && p.enum.length > 0) return "(enum)";
780
+ return "unknown";
781
+ }
782
+ /**
783
+ * Locate the parameter-bearing object inside a raw tool schema. Producers
784
+ * may nest it under `parameters`, `input_schema`, `inputSchema`, or hand
785
+ * the schema as the bare JSON Schema (when `type === 'object'`).
786
+ */
787
+ function paramsOf(schema) {
788
+ if (schema === null || typeof schema !== "object") return null;
789
+ const s = schema;
790
+ const candidate = (v) => v !== null && typeof v === "object" ? v : null;
791
+ const nested = candidate(s.parameters) ?? candidate(s.input_schema) ?? candidate(s.inputSchema);
792
+ if (nested !== null) return nested;
793
+ if (s.type === "object" && s.properties !== void 0 && typeof s.properties === "object") return s;
794
+ return null;
795
+ }
796
+ /**
797
+ * One row of the parsed parameter table — name (mono-styled, with a
798
+ * required chip), short type label, and the description on its own line
799
+ * (so a long blurb never breaks the name/type rhythm).
800
+ */
801
+ function ParamRow(props) {
802
+ const typeLabel = typeOf(props.schema);
803
+ const desc = props.schema.description;
804
+ 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 ? "✓" : "·"), typeof desc === "string" && desc !== "" ? /* @__PURE__ */ React.createElement("span", { className: "lc-ts-param-desc" }, desc) : null);
805
+ }
806
+ /**
807
+ * The full body of one expanded tool row: description, a parsed parameter
808
+ * table (when the schema carries one), and the raw JSON behind a toggle.
809
+ * Owns its own open/closed state for the JSON so two expanded tools stay
810
+ * independent. The description card carries the shared raw/markdown view
811
+ * toggle in its head (per-card state via `rich.useRichMode`).
812
+ */
813
+ function ToolSchema(props) {
814
+ const { rich } = props;
815
+ const [jsonOpen, setJsonOpen] = React.useState(false);
816
+ const [descMode, setDescMode] = rich.useRichMode();
817
+ const params = React.useMemo(() => paramsOf(props.schema), [props.schema]);
818
+ const rows = React.useMemo(() => {
819
+ if (params === null) return [];
820
+ const props = params.properties;
821
+ if (props === null || typeof props !== "object") return [];
822
+ const req = Array.isArray(params.required) ? new Set(params.required.filter((x) => typeof x === "string")) : /* @__PURE__ */ new Set();
823
+ const out = [];
824
+ for (const k of Object.keys(props)) {
825
+ const v = props[k];
826
+ if (v === null || typeof v !== "object") continue;
827
+ out.push({
828
+ name: k,
829
+ schema: v,
830
+ required: req.has(k)
831
+ });
832
+ }
833
+ return out;
834
+ }, [params]);
835
+ const schemaJson = React.useMemo(() => props.schema !== void 0 ? JSON.stringify(props.schema, null, 2) : "", [props.schema]);
836
+ 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(rich.RichSwitch, {
837
+ mode: descMode,
838
+ onPick: setDescMode,
839
+ inHead: true
840
+ })), /* @__PURE__ */ React.createElement(rich.RichText, {
841
+ text: props.description,
842
+ mode: descMode,
843
+ rawClass: "lc-ts-desc-body",
844
+ mdClass: "lc-ts-desc-md"
845
+ })) : 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, {
846
+ key: r.name,
847
+ name: r.name,
848
+ schema: r.schema,
849
+ required: r.required
850
+ }))) : 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("button", {
851
+ type: "button",
852
+ className: "lc-ts-json-toggle",
853
+ onClick: () => {
854
+ setJsonOpen((o) => !o);
855
+ }
856
+ }, (jsonOpen ? "▾ " : "▸ ") + (jsonOpen ? props.labels.hide : props.labels.show)), jsonOpen ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre lc-br-dim" }, schemaJson) : null) : null);
857
+ }
858
+ /** One raw content block (text/reasoning/tool-result/image/…), rendered defensively. */
859
+ function RawBlocks(props) {
860
+ const { rich, img } = props;
861
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, props.blocks.map((b, i) => {
862
+ const blk = b !== null && typeof b === "object" ? b : null;
863
+ if (blk !== null && (blk.type === "text" || blk.type === "reasoning") && typeof blk.text === "string") {
864
+ if (blk.type === "reasoning") return /* @__PURE__ */ React.createElement("pre", {
865
+ key: i,
866
+ className: "lc-br-pre lc-br-dim"
867
+ }, blk.text);
868
+ return /* @__PURE__ */ React.createElement(rich.RichText, {
869
+ key: i,
870
+ text: blk.text,
871
+ mode: props.mode
872
+ });
873
+ }
874
+ const image = imageRefOf(b);
875
+ if (image !== null) return /* @__PURE__ */ React.createElement(img.Card, {
876
+ key: i,
877
+ attachment: image,
878
+ load: img.load
879
+ });
880
+ if (blk !== null && blk.type === "tool-result" && Array.isArray(blk.content)) return /* @__PURE__ */ React.createElement(RawBlocks, {
881
+ key: i,
882
+ blocks: blk.content,
883
+ mode: props.mode,
884
+ rich,
885
+ img
886
+ });
887
+ return /* @__PURE__ */ React.createElement("pre", {
888
+ key: i,
889
+ className: "lc-br-pre lc-br-dim"
890
+ }, JSON.stringify(b, null, 2));
891
+ }));
892
+ }
893
+ /**
894
+ * One assistant-message block (thinking / answer) as its own card: the
895
+ * title row carries the block label plus the block's OWN raw/markdown
896
+ * switch (per-card state via `rich.useRichMode`, mirroring the tool
897
+ * description card), so a mixed reply's parts toggle independently.
898
+ */
899
+ function BlockCard(props) {
900
+ const { rich } = props;
901
+ const [mode, setMode] = rich.useRichMode();
902
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-ts-card" }, /* @__PURE__ */ React.createElement("div", { className: "lc-ts-card-head" }, /* @__PURE__ */ React.createElement("b", null, props.label), /* @__PURE__ */ React.createElement(rich.RichSwitch, {
903
+ mode,
904
+ onPick: setMode,
905
+ inHead: true
906
+ })), /* @__PURE__ */ React.createElement(rich.RichText, {
907
+ text: props.text,
908
+ mode,
909
+ rawClass: "lc-ts-desc-body",
910
+ mdClass: "lc-ts-desc-md"
911
+ }));
912
+ }
913
+ /** The actual content of one surface element, joined from the conversation snapshot. */
914
+ function NodeContent(props) {
915
+ const { node, conv, rich, img } = props;
916
+ const richable = node.cat === "user" || node.cat === "assistant" || node.cat === "inject";
917
+ const wrap = (render) => richable ? /* @__PURE__ */ React.createElement(rich.RichCard, { render }) : /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, render("raw"));
918
+ if (conv === void 0) {
919
+ if (node.text === void 0 || node.text === "") return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, props.hint));
920
+ return wrap((mode) => /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(rich.RichText, {
921
+ text: node.text ?? "",
922
+ mode
923
+ }), /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, props.hint)));
924
+ }
925
+ if (conv.kind === "assistant" && Array.isArray(conv.blocks)) return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, conv.blocks.map((b, i) => {
926
+ const blk = b;
927
+ if (blk.kind === "text" && typeof blk.text === "string") return /* @__PURE__ */ React.createElement(BlockCard, {
928
+ key: i,
929
+ label: props.labels.answer,
930
+ text: blk.text,
931
+ rich
932
+ });
933
+ if (blk.kind === "reasoning" && typeof blk.text === "string") return /* @__PURE__ */ React.createElement(BlockCard, {
934
+ key: i,
935
+ label: props.labels.thinking,
936
+ text: blk.text,
937
+ rich
938
+ });
939
+ if (blk.kind === "tool-call") return /* @__PURE__ */ React.createElement("div", {
940
+ key: i,
941
+ className: "lc-br-call"
942
+ }, /* @__PURE__ */ React.createElement("span", { className: "lc-br-tag" }, "→ " + (blk.name ?? "?")), typeof blk.argsRaw === "string" && blk.argsRaw !== "" ? /* @__PURE__ */ React.createElement("pre", { className: "lc-br-pre lc-br-dim" }, blk.argsRaw) : null);
943
+ const image = imageRefOf(b);
944
+ if (image !== null) return /* @__PURE__ */ React.createElement(img.Card, {
945
+ key: i,
946
+ attachment: image,
947
+ load: img.load
948
+ });
949
+ return null;
950
+ }));
951
+ if (conv.kind === "tool-result") 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" }, "← " + 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, {
952
+ blocks: conv.content,
953
+ mode: "raw",
954
+ rich,
955
+ img
956
+ }) : null);
957
+ if (conv.kind === "compaction") return wrap((mode) => /* @__PURE__ */ React.createElement(React.Fragment, null, typeof conv.summary === "string" && conv.summary !== "" ? /* @__PURE__ */ React.createElement(rich.RichText, {
958
+ text: conv.summary,
959
+ mode
960
+ }) : null));
961
+ if (Array.isArray(conv.content)) {
962
+ const blocks = conv.content;
963
+ if (node.cat !== "user") return wrap((mode) => /* @__PURE__ */ React.createElement(RawBlocks, {
964
+ blocks,
965
+ mode,
966
+ rich,
967
+ img
968
+ }));
969
+ const texts = [];
970
+ const images = [];
971
+ const others = [];
972
+ for (const b of blocks) {
973
+ const blk = b;
974
+ if (blk !== null && typeof blk === "object" && blk.type === "text" && typeof blk.text === "string") {
975
+ texts.push(blk.text);
976
+ continue;
977
+ }
978
+ const image = imageRefOf(b);
979
+ if (image !== null) {
980
+ images.push(image);
981
+ continue;
982
+ }
983
+ others.push(b);
984
+ }
985
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, texts.length > 0 ? /* @__PURE__ */ React.createElement(rich.RichCard, {
986
+ className: "lc-br-rich-inline",
987
+ render: (mode) => /* @__PURE__ */ React.createElement(React.Fragment, null, texts.map((text, i) => /* @__PURE__ */ React.createElement(rich.RichText, {
988
+ key: i,
989
+ text,
990
+ mode
991
+ })))
992
+ }) : null, images.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.images), /* @__PURE__ */ React.createElement("span", { className: "lc-ts-card-count" }, images.length)), /* @__PURE__ */ React.createElement("div", { className: "lc-att-grid" }, images.map((a, i) => /* @__PURE__ */ React.createElement(img.Card, {
993
+ key: `${a.attachmentId}:${i}`,
994
+ attachment: a,
995
+ load: img.load
996
+ })))) : null, others.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.other), /* @__PURE__ */ React.createElement("span", { className: "lc-ts-card-count" }, others.length)), others.map((b, i) => /* @__PURE__ */ React.createElement("pre", {
997
+ key: i,
998
+ className: "lc-br-pre lc-br-dim"
999
+ }, JSON.stringify(b, null, 2)))) : null);
1000
+ }
1001
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, props.hint));
1002
+ }
1003
+ /** Group an assembled surface's nodes by category (for per-category counts). */
1004
+ function byCatOf(asm) {
1005
+ const m = {};
1006
+ for (const n of asm.nodes) (m[n.cat] ??= []).push(n);
1007
+ return m;
1008
+ }
1009
+ /** Count one category's elements in an assembled surface (header cats included). */
1010
+ function countOf(asm, c) {
1011
+ if (c === "system") return asm.header !== null && asm.header.system !== void 0 ? 1 : 0;
1012
+ if (c === "tools") return asm.header !== null ? asm.header.tools.length : 0;
1013
+ return byCatOf(asm)[c]?.length ?? 0;
1014
+ }
1015
+ /** The last request of `turn` in a seq-ordered timeline, or null. */
1016
+ function lastOfTurn(requests, turn) {
1017
+ for (let i = requests.length - 1; i >= 0; i--) if ((requests[i].turn ?? 0) === turn) return requests[i];
1018
+ return null;
1019
+ }
1020
+ function makeContextBrowser(kit, StackedBar) {
1021
+ const { t, fmt, fmtTime, catLabel } = kit;
1022
+ const nodeText = makeNodeText(kit);
1023
+ const rich = makeRichText(kit);
1024
+ const ImageCard = makeImageCard(kit);
1025
+ const catColor = {};
1026
+ for (const c of CATS) catColor[c.key] = c.color;
1027
+ const MAX_AUTO_PAGES = 20;
1028
+ return function ContextBrowser(props) {
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
+ loadOlderHistory();
1059
+ }, [
1060
+ missingSeq,
1061
+ hasMore,
1062
+ loadingOlder,
1063
+ exhausted,
1064
+ bySeq,
1065
+ loadOlderHistory
1066
+ ]);
1067
+ React.useEffect(() => {
1068
+ if (!hasMore && missingSeq !== null && !exhausted) setExhausted(true);
1069
+ }, [
1070
+ hasMore,
1071
+ missingSeq,
1072
+ exhausted
1073
+ ]);
1074
+ const pinSeq = props.pinSeq;
1075
+ React.useEffect(() => {
1076
+ setSel(pinSeq === null || pinSeq === void 0 ? "live" : pinSeq);
1077
+ setOpenCat(null);
1078
+ setOpenElem(null);
1079
+ }, [pinSeq]);
1080
+ const toolFocus = props.toolFocus;
1081
+ React.useEffect(() => {
1082
+ if (toolFocus === null || toolFocus === void 0) return;
1083
+ setSel("live");
1084
+ setOpenCat("tools");
1085
+ setOpenElem(toolFocus.tool !== void 0 ? "tool:" + toolFocus.tool : null);
1086
+ if (props.onToolFocusHandled !== void 0) props.onToolFocusHandled();
1087
+ }, [toolFocus, props.onToolFocusHandled]);
1088
+ const awaiting = missingSeq !== null && !exhausted && loadOlderHistory !== void 0 && hasMore;
1089
+ const requests = data.requests;
1090
+ const hoverReq = props.previewSeq !== null && props.previewSeq !== void 0 ? requests.find((r) => r.seq === props.previewSeq) ?? null : null;
1091
+ const req = hoverReq ?? (sel === "live" ? null : requests.find((r) => r.seq === sel) ?? null);
1092
+ const seq = req !== null ? req.seq : null;
1093
+ const linked = req === null && props.onHoverKey !== void 0;
1094
+ const linkKey = linked && props.hoverKey !== null && props.hoverKey !== "free" ? props.hoverKey : null;
1095
+ const view = assemble(data, headers, seq);
1096
+ const breakdown = req !== null ? req : data.current;
1097
+ const parts = partsOf(breakdown);
1098
+ const total = breakdown.total;
1099
+ const pick = (v) => {
1100
+ setSel(v === "live" ? "live" : Number(v));
1101
+ setOpenCat(null);
1102
+ setOpenElem(null);
1103
+ };
1104
+ const refReq = req === null ? requests.length > 0 ? requests[requests.length - 1] : null : lastOfTurn(requests, (req.turn ?? 0) - 1);
1105
+ const prevView = refReq !== null ? assemble(data, headers, refReq.seq) : null;
1106
+ const byCat = byCatOf(view);
1107
+ const toolCount = (c) => countOf(view, c);
1108
+ const toggleCat = (c) => {
1109
+ if (!(toolCount(c) > 0 || (c === "system" || c === "tools") && view.header === null)) return;
1110
+ setOpenCat(openCat === c ? null : c);
1111
+ setOpenElem(null);
1112
+ };
1113
+ const toggleElem = (key) => {
1114
+ setOpenElem(openElem === key ? null : key);
1115
+ };
1116
+ /** One expandable element row (preview line; content when open). */
1117
+ const elemRow = (key, tag, preview, tokens, time, body) => {
1118
+ const open = openElem === key;
1119
+ return /* @__PURE__ */ React.createElement("div", {
1120
+ key,
1121
+ className: "lc-br-elem" + (open ? " lc-br-elem-on" : "")
1122
+ }, /* @__PURE__ */ React.createElement("button", {
1123
+ type: "button",
1124
+ className: "lc-br-elem-row",
1125
+ onClick: () => {
1126
+ toggleElem(key);
1127
+ }
1128
+ }, /* @__PURE__ */ React.createElement("span", { className: "lc-br-chev" + (open ? " lc-br-chev-on" : "") }, "▸"), 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" }, fmtTime(time)) : null, /* @__PURE__ */ React.createElement("span", { className: "lc-br-tokens" }, "≈" + fmt(tokens))), open ? body : null);
1129
+ };
1130
+ const catBody = (c) => {
1131
+ if (c === "system") {
1132
+ if (view.header === null) return /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, t(headers === null ? "browser.noHeader" : "browser.noEpoch"));
1133
+ const system = view.header.system;
1134
+ if (system === void 0) return null;
1135
+ return elemRow("sys", null, system.replace(/\s+/g, " ").trim().slice(0, 80), breakdown.system, void 0, /* @__PURE__ */ React.createElement(rich.RichCard, { render: (mode) => /* @__PURE__ */ React.createElement(rich.RichText, {
1136
+ text: system,
1137
+ mode
1138
+ }) }));
1139
+ }
1140
+ if (c === "tools") {
1141
+ if (view.header === null) return /* @__PURE__ */ React.createElement("div", { className: "lc-br-note" }, t(headers === null ? "browser.noHeader" : "browser.noEpoch"));
1142
+ const labels = {
1143
+ desc: t("tool.desc"),
1144
+ title: t("tool.params"),
1145
+ empty: t("tool.paramsEmpty"),
1146
+ show: t("tool.jsonToggle"),
1147
+ hide: t("tool.jsonHide")
1148
+ };
1149
+ return view.header.tools.slice().sort((a, b) => b.tokens - a.tokens).map((tool) => {
1150
+ return elemRow("tool:" + tool.name, null, tool.name, tool.tokens, void 0, /* @__PURE__ */ React.createElement("div", { className: "lc-br-content" }, /* @__PURE__ */ React.createElement(ToolSchema, {
1151
+ description: tool.description,
1152
+ schema: tool.schema,
1153
+ rich,
1154
+ labels
1155
+ })));
1156
+ });
1157
+ }
1158
+ return (byCat[c] ?? []).slice().reverse().map((n) => {
1159
+ let tag = null;
1160
+ let preview = nodeText(n);
1161
+ if (n.cat === "tool") {
1162
+ tag = (n.tool ?? "?") + (n.err ? " ⚠" : "");
1163
+ preview = t("node.toolResult") + (n.err ? " ⚠" : "");
1164
+ } else if (n.cat === "inject" && !n.skill) {
1165
+ tag = t("form." + (n.form || "context"));
1166
+ if (n.text !== void 0 && n.text !== "") preview = n.form === "snapshot" ? t("node.snapshot") + n.text : n.text;
1167
+ }
1168
+ return elemRow(`n${n.seq}`, tag, preview, n.tokens, n.time, /* @__PURE__ */ React.createElement(NodeContent, {
1169
+ node: n,
1170
+ conv: bySeq.get(n.seq),
1171
+ rich,
1172
+ img: {
1173
+ Card: ImageCard,
1174
+ load: props.loadImage
1175
+ },
1176
+ labels: {
1177
+ thinking: t("block.thinking"),
1178
+ answer: t("block.answer"),
1179
+ images: t("attach.images"),
1180
+ other: t("attach.other")
1181
+ },
1182
+ hint: bySeq.get(n.seq) === void 0 && awaiting ? t("browser.loading") : t("browser.noContent")
1183
+ }));
1184
+ });
1185
+ };
1186
+ 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("select", {
1187
+ className: "lc-br-pick",
1188
+ value: seq === null ? "live" : String(seq),
1189
+ onChange: (e) => {
1190
+ pick(e.target.value);
1191
+ }
1192
+ }, /* @__PURE__ */ React.createElement("option", { value: "live" }, t("browser.live")), requests.slice().reverse().map((r) => /* @__PURE__ */ React.createElement("option", {
1193
+ key: r.seq,
1194
+ value: String(r.seq)
1195
+ }, t("detail.step", {
1196
+ t: r.turn ?? 0,
1197
+ s: r.step ?? 0
1198
+ }) + " · " + fmtTime(r.time))))), /* @__PURE__ */ React.createElement("div", { className: "lc-br-meta" }, /* @__PURE__ */ React.createElement("b", null, req !== null ? t("detail.step", {
1199
+ t: req.turn ?? 0,
1200
+ s: req.step ?? 0
1201
+ }) : t("browser.liveNow")), req !== null ? /* @__PURE__ */ React.createElement("span", null, fmtTime(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: fmt(total) })), req !== null && req.prompt !== void 0 ? /* @__PURE__ */ React.createElement("span", { className: "lc-actual" }, t("detail.actual", { n: fmt(req.prompt) })) : null), /* @__PURE__ */ React.createElement("div", { className: "lc-br-bar" }, /* @__PURE__ */ React.createElement(StackedBar, {
1202
+ parts,
1203
+ height: 10,
1204
+ hoverKey: linked ? linkKey : void 0,
1205
+ onHoverKey: linked ? props.onHoverKey : void 0,
1206
+ tip: false
1207
+ })), 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) => {
1208
+ const count = toolCount(c.key);
1209
+ const v = breakdown[c.key] || 0;
1210
+ const prevCount = prevView !== null ? countOf(prevView, c.key) : null;
1211
+ const countDelta = prevCount !== null ? count - prevCount : null;
1212
+ const prevTokens = refReq !== null ? refReq[c.key] || 0 : null;
1213
+ const tokenDelta = prevTokens !== null ? v - prevTokens : null;
1214
+ const openable = count > 0 || (c.key === "system" || c.key === "tools") && view.header === null;
1215
+ const open = openCat === c.key && openable;
1216
+ return /* @__PURE__ */ React.createElement("div", {
1217
+ key: c.key,
1218
+ className: "lc-br-cat" + (openable ? "" : " lc-br-cat-empty")
1219
+ }, /* @__PURE__ */ React.createElement("button", {
1220
+ type: "button",
1221
+ className: "lc-br-cat-row" + (linked && props.hoverKey === c.key ? " lc-br-cat-on" : ""),
1222
+ onMouseEnter: linked ? () => {
1223
+ if (props.onHoverKey !== void 0) props.onHoverKey(c.key);
1224
+ } : void 0,
1225
+ onMouseLeave: linked ? () => {
1226
+ if (props.onHoverKey !== void 0) props.onHoverKey(null);
1227
+ } : void 0,
1228
+ onClick: () => {
1229
+ toggleCat(c.key);
1230
+ }
1231
+ }, /* @__PURE__ */ React.createElement("span", { className: "lc-br-chev" + (open ? " lc-br-chev-on" : "") }, "▸"), /* @__PURE__ */ React.createElement("i", { style: { background: c.color } }), /* @__PURE__ */ React.createElement("span", { className: "lc-br-cat-label" }, catLabel(c.key)), /* @__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), /* @__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 ? "+" : "") + fmt(tokenDelta)) : null, /* @__PURE__ */ React.createElement("span", { className: "lc-br-tokens" }, "≈" + fmt(v))), /* @__PURE__ */ React.createElement("span", { className: "lc-br-pct" }, total > 0 ? `${Math.round(v / total * 100)}%` : "")), open ? /* @__PURE__ */ React.createElement("div", { className: "lc-br-body" }, catBody(c.key)) : null);
1232
+ })));
1233
+ };
1234
+ }
1235
+ //#endregion
1236
+ //#region src/client/components/stackedBar.tsx
1237
+ /**
1238
+ * DSH's automatic compaction trigger as a fraction of the routed context
1239
+ * window the default `thresholdRatio` of `@deepseek-ai/dsh-compaction-basic`
1240
+ * (it compacts at step boundaries once `floor(contextWindow × ratio)` is
1241
+ * reached). DSH does not publish the configured ratio to plugins or clients,
1242
+ * so the UI mirrors the default here; deployments that tune `thresholdRatio`
1243
+ * / `modelPolicies` can adjust it if they want the reserve band to match.
1244
+ */
1245
+ const AUTO_COMPACT_RATIO = .8;
1246
+ function makeStackedBar(kit) {
1247
+ const { t, fmt, catLabel } = kit;
1248
+ return function StackedBar(props) {
1249
+ const [reserveOn, setReserveOn] = React.useState(false);
1250
+ let total = 0;
1251
+ for (const p of props.parts) total += p.value;
1252
+ const scale = props.max !== void 0 && props.max > total ? props.max : total;
1253
+ const free = props.max !== void 0 && props.max > total ? props.max - total : 0;
1254
+ const usedPct = scale > 0 ? total / scale * 100 : 0;
1255
+ const hovering = props.hoverKey !== null && props.hoverKey !== void 0;
1256
+ const showBox = free > 0 && hovering;
1257
+ const reserve = props.reserve !== void 0 && props.max !== void 0 && props.max > 0 ? {
1258
+ ...props.reserve,
1259
+ max: props.max
1260
+ } : null;
1261
+ const reserveLeft = reserve !== null ? Math.round(reserve.max * reserve.ratio / scale * 1e3) / 10 : 0;
1262
+ const reserveWidth = reserve !== null ? Math.round((1 - reserve.ratio) * reserve.max / scale * 1e3) / 10 : 0;
1263
+ let tip = null;
1264
+ if (reserveOn && reserve !== null) tip = {
1265
+ text: reserve.label,
1266
+ leftPct: Math.max(12, Math.min(reserveLeft + reserveWidth / 2, 88))
1267
+ };
1268
+ else if (props.hoverKey !== null && props.hoverKey !== void 0) {
1269
+ if (props.hoverKey === "free" && free > 0) {
1270
+ const pct = scale > 0 ? free / scale * 100 : 0;
1271
+ tip = {
1272
+ text: `${t("overview.free")} ${fmt(free)} (${Math.round(pct)}%)`,
1273
+ leftPct: Math.max(12, Math.min(total / scale * 100 + pct / 2, 88))
1274
+ };
1275
+ } else {
1276
+ let acc = 0;
1277
+ for (const p of props.parts) {
1278
+ const pct = scale > 0 ? p.value / scale * 100 : 0;
1279
+ if (p.key === props.hoverKey && p.value > 0) {
1280
+ tip = {
1281
+ text: `${catLabel(p.key)} ≈${fmt(p.value)} (${Math.round(p.value / total * 100)}%) ` + t("overview.ofUsed"),
1282
+ leftPct: Math.max(12, Math.min(acc + pct / 2, 88))
1283
+ };
1284
+ break;
1285
+ }
1286
+ acc += pct;
1287
+ }
1288
+ }
1289
+ }
1290
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-stacked-wrap" }, /* @__PURE__ */ React.createElement("div", {
1291
+ className: "lc-stacked" + (hovering ? " lc-stacked-dim" : ""),
1292
+ style: { height: `${props.height || 14}px` },
1293
+ onMouseLeave: () => {
1294
+ if (props.onHoverKey !== void 0) props.onHoverKey(null);
1295
+ setReserveOn(false);
1296
+ }
1297
+ }, total > 0 ? props.parts.map((p) => {
1298
+ if (!p.value) return null;
1299
+ const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
1300
+ return /* @__PURE__ */ React.createElement("div", {
1301
+ key: p.key,
1302
+ className: "lc-stacked-seg" + (on ? " lc-stacked-seg-on" : ""),
1303
+ style: {
1304
+ width: `${p.value / scale * 100}%`,
1305
+ background: p.color
1306
+ },
1307
+ onMouseEnter: () => {
1308
+ if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
1309
+ }
1310
+ });
1311
+ }) : null, free > 0 ? /* @__PURE__ */ React.createElement("div", {
1312
+ key: "free",
1313
+ className: "lc-stacked-free" + (props.hoverKey === "free" ? " lc-stacked-free-on" : ""),
1314
+ style: { width: `${free / scale * 100}%` },
1315
+ onMouseEnter: () => {
1316
+ if (props.onHoverKey !== void 0) props.onHoverKey("free");
1317
+ }
1318
+ }) : null, reserve !== null ? /* @__PURE__ */ React.createElement("div", {
1319
+ className: "lc-reserve",
1320
+ style: {
1321
+ left: `${reserveLeft}%`,
1322
+ width: `${reserveWidth}%`
1323
+ },
1324
+ onMouseEnter: () => {
1325
+ setReserveOn(true);
1326
+ if (props.onHoverKey !== void 0) props.onHoverKey(null);
1327
+ },
1328
+ onMouseLeave: () => {
1329
+ setReserveOn(false);
1330
+ }
1331
+ }) : null, /* @__PURE__ */ React.createElement("div", {
1332
+ className: "lc-occupied-box" + (showBox ? " lc-occupied-box-on" : ""),
1333
+ style: { width: `${usedPct}%` }
1334
+ })), props.tip !== false ? /* @__PURE__ */ React.createElement("div", {
1335
+ className: "lc-bar-tip" + (tip ? " lc-bar-tip-on" : ""),
1336
+ style: { left: tip ? `${tip.leftPct}%` : "50%" }
1337
+ }, tip ? tip.text : "") : null);
1338
+ };
1339
+ }
1340
+ function makeLegend(kit) {
1341
+ const { t, fmt, catLabel } = kit;
1342
+ return function Legend(props) {
1343
+ let total = 0;
1344
+ for (const p of props.parts) total += p.value;
1345
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-legend" }, props.parts.map((p) => {
1346
+ const on = props.hoverKey !== void 0 && props.hoverKey === p.key;
1347
+ return /* @__PURE__ */ React.createElement("span", {
1348
+ key: p.key,
1349
+ className: "lc-chip" + (on ? " lc-chip-on" : ""),
1350
+ title: t("overview.ofUsed"),
1351
+ onMouseEnter: () => {
1352
+ if (props.onHoverKey !== void 0) props.onHoverKey(p.key);
1353
+ },
1354
+ onMouseLeave: () => {
1355
+ if (props.onHoverKey !== void 0) props.onHoverKey(null);
1356
+ }
1357
+ }, /* @__PURE__ */ React.createElement("i", { style: { background: p.color } }), /* @__PURE__ */ React.createElement("span", { className: "lc-chip-label" }, catLabel(p.key)), /* @__PURE__ */ React.createElement("span", { className: "lc-chip-nums" }, "≈" + fmt(p.value), total > 0 ? /* @__PURE__ */ React.createElement("em", null, `${Math.round(p.value / total * 100)}%`) : null));
1358
+ }));
1359
+ };
1360
+ }
1361
+ //#endregion
1362
+ //#region src/client/components/currentComposition.tsx
1363
+ function makeCurrentComposition(kit, StackedBar, Legend) {
1364
+ const { t, fmt } = kit;
1365
+ return function CurrentComposition(props) {
1366
+ const head = props.head;
1367
+ const reserve = head.window != null && head.window > 0 ? {
1368
+ ratio: AUTO_COMPACT_RATIO,
1369
+ label: t("overview.compactReserve", { pct: Math.round(AUTO_COMPACT_RATIO * 100) })
1370
+ } : void 0;
1371
+ 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, fmt(head.tokens)), /* @__PURE__ */ React.createElement("span", null, head.window ? " / " + fmt(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, {
1372
+ parts: head.parts,
1373
+ height: 16,
1374
+ max: head.window,
1375
+ hoverKey: props.hoverKey,
1376
+ onHoverKey: props.onHoverKey,
1377
+ reserve
1378
+ }), /* @__PURE__ */ React.createElement(Legend, {
1379
+ parts: head.parts,
1380
+ hoverKey: props.hoverKey,
1381
+ onHoverKey: props.onHoverKey
1382
+ }), props.tools !== void 0 && props.tools.length > 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-tools" }, /* @__PURE__ */ React.createElement("button", {
1383
+ type: "button",
1384
+ className: "lc-tools-label",
1385
+ onClick: () => {
1386
+ if (props.onToolFocus !== void 0) props.onToolFocus({});
1387
+ }
1388
+ }, t("tools.top")), props.tools.slice().sort((a, b) => b.tokens - a.tokens).slice(0, 5).map((tool) => /* @__PURE__ */ React.createElement("button", {
1389
+ key: tool.name,
1390
+ type: "button",
1391
+ className: "lc-tool-chip",
1392
+ onClick: () => {
1393
+ if (props.onToolFocus !== void 0) props.onToolFocus({ tool: tool.name });
1394
+ }
1395
+ }, tool.name + " " + fmt(tool.tokens))), props.tools.length > 5 ? /* @__PURE__ */ React.createElement("button", {
1396
+ type: "button",
1397
+ className: "lc-tools-more",
1398
+ onClick: () => {
1399
+ if (props.onToolFocus !== void 0) props.onToolFocus({});
1400
+ }
1401
+ }, t("tools.more", { n: props.tools.length })) : null) : null);
1402
+ };
1403
+ }
1404
+ //#endregion
1405
+ //#region src/client/components/errorBoundary.tsx
1406
+ function makeErrorBoundary(t) {
1407
+ return class ErrorBoundary extends React.Component {
1408
+ constructor(props) {
1409
+ super(props);
1410
+ this.state = { error: null };
1411
+ }
1412
+ static getDerivedStateFromError(error) {
1413
+ return { error: error instanceof Error ? error : new Error(String(error)) };
1414
+ }
1415
+ render() {
1416
+ const error = this.state.error;
1417
+ if (error === null) return this.props.children;
1418
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-root" }, /* @__PURE__ */ React.createElement("div", { className: "lc-empty lc-error" }, /* @__PURE__ */ React.createElement("span", null, t("error")), /* @__PURE__ */ React.createElement("code", { className: "lc-error-msg" }, error.message), /* @__PURE__ */ React.createElement("button", {
1419
+ type: "button",
1420
+ className: "lc-error-retry",
1421
+ onClick: () => {
1422
+ this.setState({ error: null });
1423
+ }
1424
+ }, t("error.retry"))));
1425
+ }
1426
+ };
1427
+ }
1428
+ //#endregion
1429
+ //#region src/client/components/contextModal.tsx
1430
+ function makeContextModal(ctx, kit) {
1431
+ const { t } = kit;
1432
+ const sessions = ctx.get("sessions");
1433
+ const StackedBar = makeStackedBar(kit);
1434
+ const CurrentComposition = makeCurrentComposition(kit, StackedBar, makeLegend(kit));
1435
+ const ContextBrowser = makeContextBrowser(kit, StackedBar);
1436
+ const ErrorBoundary = makeErrorBoundary(t);
1437
+ function ContextModalBody(props) {
1438
+ const sessionId = typeof props.sessionId === "string" ? props.sessionId : "";
1439
+ const open = typeof props.useContextModal === "function" ? props.useContextModal((s) => s) : false;
1440
+ const data = typeof props.useProjection === "function" ? timelineOf(props.useProjection("contextTimeline")) : null;
1441
+ const pressure = typeof props.useProjection === "function" ? contextPressureOf(props.useProjection("contextPressure")) : null;
1442
+ const headers = typeof props.useProjection === "function" ? headersOf(props.useProjection("contextHeaders")) : null;
1443
+ const [hoverCat, setHoverCat] = React.useState(null);
1444
+ const [toolFocus, setToolFocus] = React.useState(null);
1445
+ const close = React.useCallback(() => {
1446
+ if (sessionId === "") return;
1447
+ modalStoreOf(sessionId).set(false);
1448
+ const guard = takePendingConsume(sessionId);
1449
+ if (guard === void 0 || sessions === void 0) return;
1450
+ const scope = sessions.scope(sessionId);
1451
+ if (scope !== void 0) scope.bail(scope, "slash/input-consume-token", { guard });
1452
+ }, [sessionId]);
1453
+ React.useEffect(() => {
1454
+ if (!open) return void 0;
1455
+ const previous = document.activeElement instanceof HTMLElement ? document.activeElement : null;
1456
+ const onKey = (ev) => {
1457
+ if (ev.key !== "Escape") return;
1458
+ ev.preventDefault();
1459
+ ev.stopPropagation();
1460
+ close();
1461
+ };
1462
+ window.addEventListener("keydown", onKey, true);
1463
+ return () => {
1464
+ window.removeEventListener("keydown", onKey, true);
1465
+ if (previous !== null && document.contains(previous)) previous.focus();
1466
+ };
1467
+ }, [open, close]);
1468
+ if (!open) return null;
1469
+ const head = data !== null ? headlineOf(data, pressure) : null;
1470
+ const subtitle = data !== null ? (data.model ? data.model : "") + (data.provider ? " · " + data.provider : "") : "";
1471
+ return /* @__PURE__ */ React.createElement("div", {
1472
+ className: "lc-modal-backdrop",
1473
+ onClick: close
1474
+ }, /* @__PURE__ */ React.createElement("div", {
1475
+ className: "lc-modal-card",
1476
+ onClick: (ev) => {
1477
+ ev.stopPropagation();
1478
+ }
1479
+ }, /* @__PURE__ */ React.createElement("div", { className: "lc-modal-head" }, /* @__PURE__ */ React.createElement("span", { className: "lc-modal-title" }, t("tab")), /* @__PURE__ */ React.createElement("button", {
1480
+ className: "lc-modal-close",
1481
+ "aria-label": t("cmd.close"),
1482
+ onClick: close
1483
+ }, "×")), data === null || head === null ? /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("loading")) : /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(CurrentComposition, {
1484
+ head,
1485
+ subtitle,
1486
+ hoverKey: hoverCat,
1487
+ onHoverKey: setHoverCat,
1488
+ tools: data.toolList,
1489
+ onToolFocus: setToolFocus
1490
+ }), /* @__PURE__ */ React.createElement(ContextBrowser, {
1491
+ data,
1492
+ headers,
1493
+ useSession: props.useSession,
1494
+ loadOlderHistory: props.loadOlderHistory,
1495
+ hoverKey: hoverCat,
1496
+ onHoverKey: setHoverCat,
1497
+ toolFocus,
1498
+ onToolFocusHandled: () => {
1499
+ setToolFocus(null);
1500
+ }
1501
+ }))));
1502
+ }
1503
+ return function ContextModal(props) {
1504
+ return h(ErrorBoundary, null, h(ContextModalBody, props));
1505
+ };
1506
+ }
1507
+ //#endregion
1508
+ //#region src/client/components/events.tsx
1509
+ const EVENT_ICONS = {
1510
+ compaction: "",
1511
+ prune: "",
1512
+ inject: "",
1513
+ model: ""
1514
+ };
1515
+ /** Build the event text helpers bound to the translate function. */
1516
+ function makeEventText(t) {
1517
+ function eventLabel(ev) {
1518
+ if (ev.kind === "compaction") return t("ev.compaction", { n: ev.count || 0 });
1519
+ if (ev.kind === "prune") return t("ev.prune");
1520
+ if (ev.kind === "model") return t("ev.model", {
1521
+ a: ev.from || "?",
1522
+ b: ev.to || "?"
1523
+ });
1524
+ if (ev.sub === "skill") return t("ev.skill", { name: ev.name || "?" });
1525
+ const base = t("form." + (ev.form || "context"));
1526
+ return ev.name ? base + " · " + ev.name : base;
1527
+ }
1528
+ /**
1529
+ * Where this event sits in the request timeline, as a label or null.
1530
+ * Boundary events (compaction/prune) show the GAP they sit in: same-turn
1531
+ * "Step 2→3", cross-turn "Turn 50 · Step 8 → Turn 51 · Step 1". Injections
1532
+ * and model switches belong to one request and keep the single point.
1533
+ * Events with no following request (in flight) stay unlabeled.
1534
+ */
1535
+ function eventAt(ev) {
1536
+ if (ev.kind === "compaction" || ev.kind === "prune") {
1537
+ if (typeof ev.turn === "number" && typeof ev.step === "number") {
1538
+ if (typeof ev.fromTurn === "number" && typeof ev.fromStep === "number") {
1539
+ if (ev.fromTurn === ev.turn) return t("events.range", {
1540
+ t: ev.turn,
1541
+ a: ev.fromStep,
1542
+ b: ev.step
1543
+ });
1544
+ return t("events.rangeTo", {
1545
+ a: ev.fromTurn,
1546
+ as: ev.fromStep,
1547
+ b: ev.turn,
1548
+ bs: ev.step
1549
+ });
1550
+ }
1551
+ return t("events.at", {
1552
+ t: ev.turn,
1553
+ s: ev.step
1554
+ });
1555
+ }
1556
+ return null;
1557
+ }
1558
+ if (typeof ev.turn === "number" && typeof ev.step === "number") return t("events.at", {
1559
+ t: ev.turn,
1560
+ s: ev.step
1561
+ });
1562
+ return null;
1563
+ }
1564
+ return {
1565
+ eventLabel,
1566
+ eventAt
1567
+ };
1568
+ }
1569
+ function makeEventList(kit) {
1570
+ const { t, fmt, fmtTime, eventLabel, eventAt } = kit;
1571
+ return function EventList(props) {
1572
+ if (props.events.length === 0) return /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("events.empty"));
1573
+ const rootRef = React.useRef(null);
1574
+ React.useLayoutEffect(() => {
1575
+ const root = rootRef.current;
1576
+ if (!root) return;
1577
+ const sync = () => {
1578
+ for (const el of root.querySelectorAll(".lc-event-label")) el.title = el.scrollWidth > el.clientWidth ? el.textContent || "" : "";
1579
+ };
1580
+ sync();
1581
+ window.addEventListener("resize", sync);
1582
+ return () => {
1583
+ window.removeEventListener("resize", sync);
1584
+ };
1585
+ });
1586
+ const sorted = props.events.slice().reverse();
1587
+ return /* @__PURE__ */ React.createElement("div", {
1588
+ className: "lc-events",
1589
+ ref: rootRef
1590
+ }, sorted.map((ev, i) => {
1591
+ const label = eventLabel(ev);
1592
+ const at = eventAt(ev);
1593
+ const glyph = ev.kind === "inject" ? /* @__PURE__ */ React.createElement(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, null) : ev.kind === "model" ? /* @__PURE__ */ React.createElement(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutline16, null) : EVENT_ICONS[ev.kind] || "•";
1594
+ return /* @__PURE__ */ React.createElement("div", {
1595
+ key: `${ev.seq}-${i}`,
1596
+ className: "lc-event"
1597
+ }, /* @__PURE__ */ React.createElement("span", { className: "lc-event-icon lc-event-" + ev.kind }, glyph), /* @__PURE__ */ React.createElement("span", { className: "lc-kind lc-kind-" + ev.kind }, t("kind." + ev.kind)), /* @__PURE__ */ React.createElement("span", { className: "lc-event-label" }, label), at !== null ? /* @__PURE__ */ React.createElement("span", { className: "lc-event-at" }, at) : null, ev.tokens ? /* @__PURE__ */ React.createElement("span", { className: "lc-event-tokens" + (ev.kind === "inject" ? " lc-up" : " lc-down") }, (ev.kind === "inject" ? "+" : "−") + fmt(ev.tokens)) : null, /* @__PURE__ */ React.createElement("span", { className: "lc-event-time" }, fmtTime(ev.time)));
1598
+ }));
1599
+ };
1600
+ }
1601
+ const PLUGIN_REPO = "https://github.com/bowenliang123/dsh-context";
1602
+ /** Short `owner/repo` form of the GitHub URL, for display only. */
1603
+ const PLUGIN_REPO_SHORT = PLUGIN_REPO.replace(/^https?:\/\/github\.com\//, "");
1604
+ //#endregion
1605
+ //#region src/client/latestVersion.ts
1606
+ /**
1607
+ * Latest published version of this plugin on the npm registry — the one
1608
+ * live check the Plugin info card performs. Fetched lazily on the first
1609
+ * card mount and cached with a 1-hour TTL, so the registry is hit at most
1610
+ * once per hour per page. Every failure mode (offline, CSP-blocked,
1611
+ * non-200, malformed body) narrows to null = no upgrade hint, so the card
1612
+ * silently keeps its static version.
1613
+ */
1614
+ const REGISTRY_URL = "https://registry.npmjs.org/dsh-context/latest";
1615
+ /** How long a registry result stays fresh before the next check refetches. */
1616
+ const TTL_MS = 36e5;
1617
+ let cached = null;
1618
+ function fetchLatestVersion() {
1619
+ if (!cached || Date.now() - cached.at >= TTL_MS) cached = {
1620
+ at: Date.now(),
1621
+ promise: fetch(REGISTRY_URL).then((res) => res.ok ? res.json() : null).then((body) => body !== null && typeof body.version === "string" ? body.version : null).catch(() => null)
1622
+ };
1623
+ return cached.promise;
1624
+ }
1625
+ /** Numeric semver compare (pre-release suffix ignored): is `latest` strictly newer than `current`? */
1626
+ function isNewerVersion(latest, current) {
1627
+ const parse = (v) => v.replace(/^v/, "").split("-", 1)[0].split(".").map((n) => parseInt(n, 10) || 0);
1628
+ const a = parse(latest);
1629
+ const b = parse(current);
1630
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
1631
+ const x = a[i] || 0;
1632
+ const y = b[i] || 0;
1633
+ if (x !== y) return x > y;
1634
+ }
1635
+ return false;
1636
+ }
1637
+ //#endregion
1638
+ //#region src/client/components/pluginInfo.tsx
1639
+ function makePluginInfo(kit) {
1640
+ const { t } = kit;
1641
+ const row = (label, value, href) => /* @__PURE__ */ React.createElement("a", {
1642
+ className: "lc-pi-row",
1643
+ href,
1644
+ target: "_blank",
1645
+ rel: "noreferrer"
1646
+ }, /* @__PURE__ */ React.createElement("div", { className: "lc-pi-label" }, label), /* @__PURE__ */ React.createElement("div", { className: "lc-pi-value" }, value));
1647
+ return function PluginInfo() {
1648
+ const [latest, setLatest] = React.useState(null);
1649
+ React.useEffect(() => {
1650
+ if ("0.22.1".includes("-dev")) return;
1651
+ let on = true;
1652
+ fetchLatestVersion().then((v) => {
1653
+ if (on && v) setLatest(v);
1654
+ });
1655
+ return () => {
1656
+ on = false;
1657
+ };
1658
+ }, []);
1659
+ const update = latest !== null && isNewerVersion(latest, "0.22.1") ? latest : null;
1660
+ const nameValue = ["dsh-context (v0.22.1)"];
1661
+ if (update) nameValue.push(/* @__PURE__ */ React.createElement("span", {
1662
+ key: "update",
1663
+ className: "lc-pi-update"
1664
+ }, " v" + update));
1665
+ 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("plugin.title")), /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("plugin.hint"))), /* @__PURE__ */ React.createElement("div", { className: "lc-pi-grid" }, row(t("plugin.name"), nameValue, PLUGIN_REPO + "/releases"), row(t("plugin.github"), PLUGIN_REPO_SHORT, PLUGIN_REPO)));
1666
+ };
1667
+ }
1668
+ //#endregion
1669
+ //#region src/client/components/requestDetail.tsx
1670
+ function makeRequestDetail(kit, StackedBar) {
1671
+ const { t, fmt, fmtTime, catLabel, eventLabel, eventAt } = kit;
1672
+ return function RequestDetail(props) {
1673
+ const req = props.request;
1674
+ if (!req) return null;
1675
+ const isTurn = req.stepCount !== void 0 && req.stepCount > 1;
1676
+ const head = isTurn ? t("detail.turn", {
1677
+ t: req.turn ?? 0,
1678
+ n: req.stepCount ?? 0
1679
+ }) : t("detail.step", {
1680
+ t: req.turn ?? 0,
1681
+ s: req.step ?? 0
1682
+ });
1683
+ const marker = props.marker ?? null;
1684
+ const markerAt = marker !== null ? eventAt(marker) : null;
1685
+ 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", {
1686
+ className: "lc-detail-marker",
1687
+ title: eventLabel(marker)
1688
+ }, "✂ " + markerAt) : null, isTurn ? /* @__PURE__ */ React.createElement("span", { className: "lc-detail-tag" }, t("detail.lastStep")) : null, /* @__PURE__ */ React.createElement("span", null, fmtTime(req.time)), /* @__PURE__ */ React.createElement("span", null, t("detail.estTotal", { n: fmt(req.total) })), req.prompt !== void 0 ? /* @__PURE__ */ React.createElement("span", { className: "lc-actual" }, t("detail.actual", { n: fmt(req.prompt) })) : null, req.output !== void 0 ? /* @__PURE__ */ React.createElement("span", null, t("detail.output", { n: fmt(req.output) })) : null), /* @__PURE__ */ React.createElement(StackedBar, {
1689
+ parts: partsOf(req),
1690
+ height: 10
1691
+ }), /* @__PURE__ */ React.createElement("div", { className: "lc-detail-rows" }, CATS.map((c) => {
1692
+ const v = req[c.key] || 0;
1693
+ return /* @__PURE__ */ React.createElement("div", {
1694
+ key: c.key,
1695
+ className: "lc-detail-row"
1696
+ }, /* @__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", {
1697
+ className: "lc-bar-fill",
1698
+ style: {
1699
+ width: `${req.total > 0 ? v / req.total * 100 : 0}%`,
1700
+ background: c.color
1701
+ }
1702
+ })), /* @__PURE__ */ React.createElement("span", { className: "lc-detail-num" }, "≈" + fmt(v)), /* @__PURE__ */ React.createElement("span", { className: "lc-detail-pct" }, req.total > 0 ? `${Math.round(v / req.total * 100)}%` : ""));
1703
+ })));
1704
+ };
1705
+ }
1706
+ //#endregion
1707
+ //#region src/client/cost.ts
1708
+ const PRICES = {
1709
+ usd: {
1710
+ flash: {
1711
+ peak: {
1712
+ hit: .014,
1713
+ miss: .44,
1714
+ out: 1.32
1715
+ },
1716
+ off: {
1717
+ hit: .007,
1718
+ miss: .22,
1719
+ out: .66
1720
+ }
1721
+ },
1722
+ pro: {
1723
+ peak: {
1724
+ hit: .044,
1725
+ miss: 1.32,
1726
+ out: 3.96
1727
+ },
1728
+ off: {
1729
+ hit: .022,
1730
+ miss: .66,
1731
+ out: 1.98
1732
+ }
1733
+ }
1734
+ },
1735
+ cny: {
1736
+ flash: {
1737
+ peak: {
1738
+ hit: .1,
1739
+ miss: 3,
1740
+ out: 9
1741
+ },
1742
+ off: {
1743
+ hit: .05,
1744
+ miss: 1.5,
1745
+ out: 4.5
1746
+ }
1747
+ },
1748
+ pro: {
1749
+ peak: {
1750
+ hit: .3,
1751
+ miss: 9,
1752
+ out: 27
1753
+ },
1754
+ off: {
1755
+ hit: .15,
1756
+ miss: 4.5,
1757
+ out: 13.5
1758
+ }
1759
+ }
1760
+ }
1761
+ };
1762
+ /**
1763
+ * Price the session's cumulative billed-token totals. Cache reads bill at
1764
+ * the hit rate; uncached input AND cache writes bill at the miss rate;
1765
+ * output (reasoning included) bills at the out rate. Null when nothing was
1766
+ * priced (no DeepSeek V4 usage folded yet), so the cell can show a dash.
1767
+ */
1768
+ function estimateSessionCost(usage, currency) {
1769
+ if (usage === null || usage === void 0) return null;
1770
+ let total = 0;
1771
+ let any = false;
1772
+ for (const family of ["flash", "pro"]) {
1773
+ const fam = usage[family];
1774
+ if (fam === void 0) continue;
1775
+ for (const period of ["peak", "off"]) {
1776
+ const b = fam[period];
1777
+ if (b === void 0) continue;
1778
+ const p = PRICES[currency][family][period];
1779
+ total += (numOf(b.cacheRead) * p.hit + (numOf(b.uncached) + numOf(b.cacheWrite)) * p.miss + numOf(b.output) * p.out) / 1e6;
1780
+ any = true;
1781
+ }
1782
+ }
1783
+ return any ? total : null;
1784
+ }
1785
+ /** Short money format: two decimals at and above one unit, two significant digits below. */
1786
+ function formatCost(amount, currency) {
1787
+ return (currency === "cny" ? "¥" : "$") + (amount >= 1 ? amount.toFixed(2) : amount.toPrecision(2));
1788
+ }
1789
+ /**
1790
+ * All priced model families for one currency, in display order, with their
1791
+ * peak/off-peak rate triples — the raw material of the stats-board tooltip's
1792
+ * price list. The numbers stay hardcoded in PRICES above; this function only
1793
+ * reshapes the table for display, so what the tooltip prints can never drift
1794
+ * from the math that prices the session.
1795
+ */
1796
+ function sessionPrices(currency) {
1797
+ return ["flash", "pro"].map((id) => ({
1798
+ family: id === "flash" ? "deepseek-v4-flash" : "deepseek-v4-pro",
1799
+ peak: PRICES[currency][id].peak,
1800
+ off: PRICES[currency][id].off
1801
+ }));
1802
+ }
1803
+ /** Price-list figure: the same money format as formatCost, trailing zeros trimmed (¥3.00 → ¥3, $0.0070 $0.007). */
1804
+ function formatPriceRate(amount, currency) {
1805
+ return formatCost(amount, currency).replace(/0+$/, "").replace(/\.$/, "");
1806
+ }
1807
+ //#endregion
1808
+ //#region src/client/components/statsBoard.tsx
1809
+ /**
1810
+ * Cache-hit share of billed prompt-side input — same buckets as the harness's
1811
+ * chat stats line below the input box (`cacheReadTokens` over the three
1812
+ * disjoint billed buckets: uncached + reads + writes). Unlike that line's
1813
+ * whole-percent rounding, this one TRUNCATES to two decimal places (cut, not
1814
+ * round). Null when no input was billed. A tiny epsilon keeps a double stored
1815
+ * a hair below a cent boundary (e.g. 80.00 as 79.9999999999…) from losing its
1816
+ * last digit — with integer token counts no genuine value ever sits that close
1817
+ * to a boundary, so the epsilon can only absorb float noise. Bucket reads go
1818
+ * through numOf so a malformed payload degrades to a dash instead of NaN text.
1819
+ */
1820
+ function cacheHitPercent(usage) {
1821
+ const uncached = numOf(usage.uncachedInputTokens);
1822
+ const reads = numOf(usage.cacheReadTokens);
1823
+ const writes = numOf(usage.cacheWriteTokens);
1824
+ const billed = uncached + reads + writes;
1825
+ if (billed === 0) return null;
1826
+ const hundredths = Math.trunc(reads / billed * 1e4 + 1e-9);
1827
+ return `${Math.floor(hundredths / 100)}.${String(hundredths % 100).padStart(2, "0")}`;
1828
+ }
1829
+ function makeStatsBoard(kit) {
1830
+ const { t, fmt } = kit;
1831
+ return function StatsBoard(props) {
1832
+ const turns = /* @__PURE__ */ new Set();
1833
+ let steps = 0, compactions = 0, prunes = 0, injects = 0;
1834
+ for (const req of props.requests) {
1835
+ turns.add(req.turn ?? 0);
1836
+ steps++;
1837
+ }
1838
+ for (const ev of props.events) if (ev.kind === "compaction") compactions++;
1839
+ else if (ev.kind === "prune") prunes++;
1840
+ else if (ev.kind === "inject") injects++;
1841
+ const hit = props.usage !== null ? cacheHitPercent(props.usage) : null;
1842
+ const currency = props.locale === "zh" ? "cny" : "usd";
1843
+ const cost = estimateSessionCost(props.cost, currency);
1844
+ const fmtRate = (n) => formatPriceRate(n, currency);
1845
+ const costTip = [t("stats.costTip"), /* @__PURE__ */ React.createElement("span", {
1846
+ key: "prices",
1847
+ className: "lc-stat-tip-prices"
1848
+ }, /* @__PURE__ */ React.createElement("span", { className: "lc-stat-tip-head" }, t("stats.costPriceHead")), sessionPrices(currency).map((r) => /* @__PURE__ */ React.createElement("span", {
1849
+ key: r.family,
1850
+ className: "lc-stat-tip-row"
1851
+ }, /* @__PURE__ */ React.createElement("b", { className: "lc-stat-tip-model" }, r.family), " ", t("stats.costHit"), " ", fmtRate(r.peak.hit), "/", fmtRate(r.off.hit), " · ", t("stats.costMiss"), " ", fmtRate(r.peak.miss), "/", fmtRate(r.off.miss), " · ", t("stats.costOut"), " ", fmtRate(r.peak.out), "/", fmtRate(r.off.out))))];
1852
+ const cell = (label, value, tip) => /* @__PURE__ */ React.createElement("div", { className: "lc-stat" + (tip === void 0 ? "" : " lc-stat-tipped") }, /* @__PURE__ */ React.createElement("span", { className: "lc-stat-label" }, label, tip !== void 0 && /* @__PURE__ */ React.createElement("i", {
1853
+ className: "lc-stat-q",
1854
+ "aria-hidden": "true"
1855
+ }, "?")), /* @__PURE__ */ React.createElement("b", { className: "lc-stat-value" }, value), tip !== void 0 && /* @__PURE__ */ React.createElement("span", {
1856
+ className: "lc-stat-tip",
1857
+ role: "tooltip"
1858
+ }, tip));
1859
+ 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"), fmt(turns.size)), cell(t("stats.steps"), fmt(steps)), cell(t("stats.injects"), fmt(injects)), cell(t("stats.compactions"), fmt(compactions)), cell(t("stats.prunes"), fmt(prunes)), cell(t("stats.cacheHit"), hit === null ? "—" : hit + "%"), cell(t("stats.cost"), cost === null ? "—" : formatCost(cost, currency), costTip)));
1860
+ };
1861
+ }
1862
+ //#endregion
1863
+ //#region src/client/components/trendChart.tsx
1864
+ /**
1865
+ * Collapse a per-step request timeline into one bar per turn: each turn is
1866
+ * represented by its LAST step's record (the context state when the turn
1867
+ * finished), tagged with the number of steps the turn spans so the bar can
1868
+ * keep the turn's column width and the detail can label it. Requests of one
1869
+ * turn are consecutive in the log, so a run of equal turns is replaced by
1870
+ * its final record.
1871
+ */
1872
+ function aggregateByTurn(requests) {
1873
+ const out = [];
1874
+ let runSteps = 0;
1875
+ for (const req of requests) {
1876
+ const last = out.length > 0 ? out[out.length - 1] : null;
1877
+ if (last !== null && (last.turn ?? 0) === (req.turn ?? 0)) {
1878
+ runSteps++;
1879
+ out[out.length - 1] = {
1880
+ ...req,
1881
+ stepCount: runSteps
1882
+ };
1883
+ } else {
1884
+ runSteps = 1;
1885
+ out.push({
1886
+ ...req,
1887
+ stepCount: 1
1888
+ });
1889
+ }
1890
+ }
1891
+ return out;
1892
+ }
1893
+ /**
1894
+ * Attach each boundary event (compaction/prune) to the first request
1895
+ * logged after it one entry per request index, for the ✂ marker above
1896
+ * the bar and the range chip in the detail header. Shared by TrendChart
1897
+ * and the detail panel so both show the SAME event for a request.
1898
+ */
1899
+ function attachMarkers(requests, events) {
1900
+ const markers = new Array(requests.length);
1901
+ for (const ev of events) {
1902
+ if (ev.kind !== "compaction" && ev.kind !== "prune") continue;
1903
+ for (let r = 0; r < requests.length; r++) if (requests[r].seq >= ev.seq) {
1904
+ if (markers[r] === void 0) markers[r] = ev;
1905
+ break;
1906
+ }
1907
+ }
1908
+ return markers;
1909
+ }
1910
+ function makeTrendChart(kit) {
1911
+ const { t, fmt, fmtTime, eventLabel, eventAt } = kit;
1912
+ const CHART_H = 112;
1913
+ const BAR_W = 14;
1914
+ const BAR_GAP = 2;
1915
+ const TURN_FILLS = ["rgba(128,128,128,0.12)", "rgba(128,128,128,0.26)"];
1916
+ return function TrendChart(props) {
1917
+ const requests = props.requests;
1918
+ const markers = props.markers;
1919
+ const anchorOf = (req) => typeof req.prompt === "number" && req.prompt > 0 && req.total > 0 ? req.prompt / req.total : 1;
1920
+ const barTotalOf = (req) => typeof req.prompt === "number" && req.prompt > 0 ? req.prompt : req.total;
1921
+ let maxTotal = 1;
1922
+ for (const req of requests) {
1923
+ const bt = barTotalOf(req);
1924
+ if (bt > maxTotal) maxTotal = bt;
1925
+ }
1926
+ const groups = [];
1927
+ for (const req of requests) {
1928
+ let grp = groups.length > 0 ? groups[groups.length - 1] : null;
1929
+ if (grp === null || grp.turn !== (req.turn ?? 0)) {
1930
+ grp = {
1931
+ turn: req.turn ?? 0,
1932
+ count: 0,
1933
+ span: 0,
1934
+ agg: req.stepCount !== void 0
1935
+ };
1936
+ groups.push(grp);
1937
+ }
1938
+ grp.count++;
1939
+ grp.span += req.stepCount ?? 1;
1940
+ }
1941
+ const scrollRef = React.useRef(null);
1942
+ const scrolledOnce = React.useRef(false);
1943
+ const lastGranRef = React.useRef(props.granularity);
1944
+ const [edges, setEdges] = React.useState({
1945
+ left: false,
1946
+ right: false
1947
+ });
1948
+ const edgesRef = React.useRef(edges);
1949
+ const updateEdges = (el) => {
1950
+ const left = el.scrollLeft > 4;
1951
+ const right = el.scrollLeft + el.clientWidth < el.scrollWidth - 4;
1952
+ const prev = edgesRef.current;
1953
+ if (prev.left === left && prev.right === right) return;
1954
+ edgesRef.current = {
1955
+ left,
1956
+ right
1957
+ };
1958
+ setEdges({
1959
+ left,
1960
+ right
1961
+ });
1962
+ };
1963
+ React.useLayoutEffect(() => {
1964
+ const el = scrollRef.current;
1965
+ if (el === null) return;
1966
+ if (props.granularity !== lastGranRef.current) {
1967
+ lastGranRef.current = props.granularity;
1968
+ scrolledOnce.current = false;
1969
+ }
1970
+ if (!scrolledOnce.current) {
1971
+ scrolledOnce.current = true;
1972
+ el.scrollLeft = el.scrollWidth;
1973
+ } else if (el.scrollLeft + el.clientWidth >= el.scrollWidth - 24) el.scrollLeft = el.scrollWidth;
1974
+ updateEdges(el);
1975
+ });
1976
+ const tipOf = (req) => {
1977
+ return (req.stepCount !== void 0 && req.stepCount > 1 ? t("tip.turn", {
1978
+ t: req.turn ?? 0,
1979
+ n: req.stepCount
1980
+ }) : t("tip.step", {
1981
+ t: req.turn ?? 0,
1982
+ s: req.step ?? 0
1983
+ })) + " · " + fmtTime(req.time) + " · " + t("tip.total", { n: fmt(req.total) }) + (req.prompt !== void 0 ? " · " + t("tip.actual", { n: fmt(req.prompt) }) : "");
1984
+ };
1985
+ const hoveredIdx = props.hoveredSeq !== null ? requests.findIndex((r) => r.seq === props.hoveredSeq) : -1;
1986
+ const hoveredReq = hoveredIdx >= 0 ? requests[hoveredIdx] : null;
1987
+ return /* @__PURE__ */ React.createElement("div", { className: "lc-chartrow" }, /* @__PURE__ */ React.createElement("div", { className: "lc-axis" }, /* @__PURE__ */ React.createElement("span", { className: "lc-axis-top" }, fmt(maxTotal)), /* @__PURE__ */ React.createElement("span", { className: "lc-axis-mid" }, fmt(Math.round(maxTotal / 2))), /* @__PURE__ */ React.createElement("span", { className: "lc-axis-bot" }, "0")), /* @__PURE__ */ React.createElement("div", {
1988
+ className: "lc-chart-scroll" + (props.activeTurn !== null ? " lc-chart-dim" : ""),
1989
+ ref: scrollRef,
1990
+ onScroll: (e) => {
1991
+ updateEdges(e.currentTarget);
1992
+ }
1993
+ }, edges.left ? /* @__PURE__ */ React.createElement("div", { className: "lc-chart-fade lc-chart-fade-l" }) : null, /* @__PURE__ */ React.createElement("div", {
1994
+ className: "lc-chart",
1995
+ onMouseLeave: () => {
1996
+ props.onHover(null);
1997
+ }
1998
+ }, /* @__PURE__ */ React.createElement("div", { className: "lc-grid lc-grid-top" }), /* @__PURE__ */ React.createElement("div", { className: "lc-grid lc-grid-mid" }), requests.map((req, i) => {
1999
+ const marker = markers[i];
2000
+ const markerAt = marker !== void 0 ? eventAt(marker) : null;
2001
+ const selected = props.selectedSeq === req.seq;
2002
+ const hovered = props.hoveredSeq === req.seq;
2003
+ const inTurn = props.activeTurn !== null && (req.turn ?? 0) === props.activeTurn;
2004
+ return /* @__PURE__ */ React.createElement("div", {
2005
+ key: req.seq,
2006
+ className: "lc-bar" + (selected ? " lc-bar-selected" : "") + (hovered ? " lc-bar-hovered" : "") + (inTurn ? " lc-bar-in-turn" : ""),
2007
+ style: { width: `${BAR_W}px` },
2008
+ onClick: () => {
2009
+ props.onSelect(selected ? null : req.seq);
2010
+ },
2011
+ onMouseEnter: () => {
2012
+ props.onHover(req.seq);
2013
+ }
2014
+ }, marker !== void 0 ? /* @__PURE__ */ React.createElement("span", {
2015
+ className: "lc-bar-marker",
2016
+ title: "✂ " + (markerAt !== null ? markerAt + " — " : "") + eventLabel(marker)
2017
+ }, "✂") : null, /* @__PURE__ */ React.createElement("div", { className: "lc-bar-stack" }, CATS.map((c) => {
2018
+ const v = (req[c.key] || 0) * anchorOf(req);
2019
+ if (!v) return null;
2020
+ return /* @__PURE__ */ React.createElement("div", {
2021
+ key: c.key,
2022
+ style: {
2023
+ height: `${Math.max(1, Math.round(v / maxTotal * CHART_H))}px`,
2024
+ background: c.color
2025
+ }
2026
+ });
2027
+ })));
2028
+ })), hoveredReq !== null ? /* @__PURE__ */ React.createElement("div", {
2029
+ className: "lc-chart-tip",
2030
+ style: { left: `${hoveredIdx * 16 + BAR_W / 2}px` }
2031
+ }, tipOf(hoveredReq)) : null, /* @__PURE__ */ React.createElement("div", {
2032
+ className: "lc-turns",
2033
+ onMouseLeave: () => {
2034
+ props.onHoverTurn(null);
2035
+ }
2036
+ }, groups.map((grp, gi) => {
2037
+ const on = props.activeTurn === grp.turn;
2038
+ const blockW = grp.agg ? BAR_W : grp.span * 16 - BAR_GAP;
2039
+ return /* @__PURE__ */ React.createElement("span", {
2040
+ key: `turn-${gi}`,
2041
+ className: "lc-turn" + (on ? " lc-turn-on" : ""),
2042
+ style: {
2043
+ width: `${blockW}px`,
2044
+ background: TURN_FILLS[gi % TURN_FILLS.length]
2045
+ },
2046
+ title: `T${grp.turn}`,
2047
+ onMouseEnter: () => {
2048
+ props.onHoverTurn(grp.turn);
2049
+ }
2050
+ }, `T${grp.turn}`);
2051
+ }))), edges.right ? /* @__PURE__ */ React.createElement("div", { className: "lc-chart-fade lc-chart-fade-r" }) : null);
2052
+ };
2053
+ }
2054
+ //#endregion
2055
+ //#region src/client/components/contextView.tsx
2056
+ const viewScroll = /* @__PURE__ */ new Map();
2057
+ const EVENT_KINDS = [
2058
+ "inject",
2059
+ "compaction",
2060
+ "prune",
2061
+ "model"
2062
+ ];
2063
+ function makeContextView(ctx, kit) {
2064
+ const { t } = kit;
2065
+ const StackedBar = makeStackedBar(kit);
2066
+ const CurrentComposition = makeCurrentComposition(kit, StackedBar, makeLegend(kit));
2067
+ const TrendChart = makeTrendChart(kit);
2068
+ const RequestDetail = makeRequestDetail(kit, StackedBar);
2069
+ const EventList = makeEventList(kit);
2070
+ const NodeList = makeNodeList(kit);
2071
+ const StatsBoard = makeStatsBoard(kit);
2072
+ const PluginInfo = makePluginInfo(kit);
2073
+ const ContextBrowser = makeContextBrowser(kit, StackedBar);
2074
+ const ErrorBoundary = makeErrorBoundary(t);
2075
+ function ContextViewBody(props) {
2076
+ const sessionId = props.sessionId;
2077
+ const data = typeof props.useProjection === "function" ? timelineOf(props.useProjection("contextTimeline")) : null;
2078
+ const pressure = typeof props.useProjection === "function" ? contextPressureOf(props.useProjection("contextPressure")) : null;
2079
+ const usage = typeof props.useProjection === "function" ? tokenUsageOf(props.useProjection("tokenUsage")) : null;
2080
+ const headers = typeof props.useProjection === "function" ? headersOf(props.useProjection("contextHeaders")) : null;
2081
+ const [selectedSeq, setSelectedSeq] = React.useState(null);
2082
+ const [hoveredSeq, setHoveredSeq] = React.useState(null);
2083
+ const [hoverTurn, setHoverTurn] = React.useState(null);
2084
+ const [tick, setTick] = React.useState(0);
2085
+ const [granularity, setGranularity] = React.useState("step");
2086
+ const [hoverCat, setHoverCat] = React.useState(null);
2087
+ const [pickedKinds, setPickedKinds] = React.useState([...EVENT_KINDS]);
2088
+ const toggleKind = (k) => {
2089
+ setPickedKinds((p) => {
2090
+ if (p.length === EVENT_KINDS.length) return [k];
2091
+ if (!p.includes(k)) return [...p, k];
2092
+ return p.length === 1 ? [...EVENT_KINDS] : p.filter((x) => x !== k);
2093
+ });
2094
+ };
2095
+ const [toolFocus, setToolFocus] = React.useState(null);
2096
+ const clearToolFocus = React.useCallback(() => {
2097
+ setToolFocus(null);
2098
+ }, []);
2099
+ const loadImage = React.useMemo(() => {
2100
+ if (typeof sessionId !== "string" || sessionId === "") return void 0;
2101
+ const conversation = ctx.get("conversation");
2102
+ if (conversation === void 0 || typeof conversation.resolveImage !== "function") return void 0;
2103
+ return (attachment) => conversation.resolveImage(sessionId, attachment);
2104
+ }, [sessionId]);
2105
+ const rootRef = React.useRef(null);
2106
+ const scrollerRef = React.useRef(null);
2107
+ const restoredRef = React.useRef(null);
2108
+ React.useLayoutEffect(() => {
2109
+ if (typeof sessionId !== "string" || sessionId === "" || data === null) return;
2110
+ if (restoredRef.current === sessionId) return;
2111
+ restoredRef.current = sessionId;
2112
+ const scroller = rootRef.current !== null ? rootRef.current.closest("[data-conversation-scroll]") : null;
2113
+ if (scroller === null) return;
2114
+ scrollerRef.current = scroller;
2115
+ scroller.scrollTop = viewScroll.get(sessionId) ?? 0;
2116
+ }, [sessionId, data]);
2117
+ React.useLayoutEffect(() => {
2118
+ return () => {
2119
+ if (typeof sessionId !== "string" || sessionId === "") return;
2120
+ const scroller = scrollerRef.current;
2121
+ if (scroller === null) return;
2122
+ viewScroll.set(sessionId, scroller.scrollTop);
2123
+ };
2124
+ }, [sessionId]);
2125
+ React.useEffect(() => {
2126
+ const localeSvc = ctx.get("locale");
2127
+ if (!localeSvc) return void 0;
2128
+ return localeSvc.subscribe(() => {
2129
+ setTick((x) => x + 1);
2130
+ });
2131
+ }, []);
2132
+ if (!data) return /* @__PURE__ */ React.createElement("div", {
2133
+ className: "lc-root",
2134
+ ref: rootRef
2135
+ }, /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("loading")));
2136
+ const requests = data.requests;
2137
+ const events = data.events;
2138
+ const shownEvents = pickedKinds.length === EVENT_KINDS.length ? events : events.filter((e) => pickedKinds.includes(e.kind));
2139
+ const nodes = data.nodes;
2140
+ const displayRequests = granularity === "turn" ? aggregateByTurn(requests) : requests;
2141
+ const markers = attachMarkers(displayRequests, events);
2142
+ const markerOf = (req) => {
2143
+ const i = displayRequests.indexOf(req);
2144
+ return i >= 0 ? markers[i] : void 0;
2145
+ };
2146
+ let pinnedReq = null;
2147
+ for (const req of displayRequests) if (req.seq === selectedSeq) pinnedReq = req;
2148
+ let activeReq = null;
2149
+ if (hoveredSeq !== null) {
2150
+ for (const req of displayRequests) if (req.seq === hoveredSeq) activeReq = req;
2151
+ }
2152
+ if (activeReq === null) activeReq = pinnedReq;
2153
+ if (activeReq === null && displayRequests.length > 0) activeReq = displayRequests[displayRequests.length - 1];
2154
+ let activeTurn = hoverTurn;
2155
+ if (activeTurn === null && hoveredSeq !== null) {
2156
+ for (const req of displayRequests) if (req.seq === hoveredSeq) {
2157
+ activeTurn = req.turn ?? null;
2158
+ break;
2159
+ }
2160
+ }
2161
+ const head = headlineOf(data, pressure);
2162
+ const localeSvc = ctx.get("locale");
2163
+ const activeLocale = localeSvc !== void 0 && typeof localeSvc.getLocale === "function" ? localeSvc.getLocale().active : "en";
2164
+ return /* @__PURE__ */ React.createElement("div", {
2165
+ className: "lc-root",
2166
+ ref: rootRef
2167
+ }, /* @__PURE__ */ React.createElement("div", { className: "lc-cols lc-head" }, /* @__PURE__ */ React.createElement(StatsBoard, {
2168
+ requests,
2169
+ events,
2170
+ usage,
2171
+ cost: data.cost,
2172
+ locale: activeLocale
2173
+ }), /* @__PURE__ */ React.createElement(PluginInfo, null)), /* @__PURE__ */ React.createElement("div", { className: "lc-cols" }, /* @__PURE__ */ React.createElement("div", { className: "lc-col" }, /* @__PURE__ */ React.createElement(CurrentComposition, {
2174
+ head,
2175
+ subtitle: (data.model ? data.model : "") + (data.provider ? " · " + data.provider : ""),
2176
+ hoverKey: hoverCat,
2177
+ onHoverKey: setHoverCat,
2178
+ tools: data.toolList,
2179
+ onToolFocus: setToolFocus
2180
+ }), /* @__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("button", {
2181
+ className: "lc-gran-btn" + (granularity === "step" ? " lc-gran-on" : ""),
2182
+ onClick: () => {
2183
+ setGranularity("step");
2184
+ }
2185
+ }, t("gran.step")), /* @__PURE__ */ React.createElement("button", {
2186
+ className: "lc-gran-btn" + (granularity === "turn" ? " lc-gran-on" : ""),
2187
+ onClick: () => {
2188
+ setGranularity("turn");
2189
+ }
2190
+ }, t("gran.turn")))), displayRequests.length === 0 ? /* @__PURE__ */ React.createElement("div", { className: "lc-empty" }, t("trend.empty")) : /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement(TrendChart, {
2191
+ key: sessionId,
2192
+ requests: displayRequests,
2193
+ markers,
2194
+ selectedSeq: pinnedReq ? pinnedReq.seq : null,
2195
+ hoveredSeq,
2196
+ activeTurn,
2197
+ granularity,
2198
+ onSelect: setSelectedSeq,
2199
+ onHover: setHoveredSeq,
2200
+ onHoverTurn: setHoverTurn
2201
+ }), /* @__PURE__ */ React.createElement(RequestDetail, {
2202
+ request: activeReq,
2203
+ marker: activeReq !== null ? markerOf(activeReq) : void 0
2204
+ })))), /* @__PURE__ */ React.createElement("div", { className: "lc-col" }, /* @__PURE__ */ React.createElement(ContextBrowser, {
2205
+ data,
2206
+ headers,
2207
+ useSession: props.useSession,
2208
+ loadOlderHistory: props.loadOlderHistory,
2209
+ previewSeq: hoveredSeq,
2210
+ pinSeq: pinnedReq !== null ? pinnedReq.seq : null,
2211
+ hoverKey: hoverCat,
2212
+ onHoverKey: setHoverCat,
2213
+ toolFocus,
2214
+ onToolFocusHandled: clearToolFocus,
2215
+ loadImage
2216
+ }))), /* @__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("button", {
2217
+ key: k,
2218
+ className: "lc-gran-btn" + (pickedKinds.includes(k) ? " lc-gran-on lc-kind-" + k : ""),
2219
+ onClick: () => {
2220
+ toggleKind(k);
2221
+ }
2222
+ }, t("kind." + k))))), /* @__PURE__ */ React.createElement(EventList, { events: shownEvents })), /* @__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("nodes.title")), /* @__PURE__ */ React.createElement("span", { className: "lc-card-sub" }, t("nodes.hint"))), /* @__PURE__ */ React.createElement(NodeList, {
2223
+ nodes,
2224
+ dropped: data.droppedNodes || 0
2225
+ }))), /* @__PURE__ */ React.createElement("div", { className: "lc-foot" }, t("footer")));
2226
+ }
2227
+ return function ContextView(props) {
2228
+ return h(ErrorBoundary, null, h(ContextViewBody, props));
2229
+ };
2230
+ }
2231
+ //#endregion
2232
+ //#region src/client/viewkit.ts
2233
+ function makeViewKit(t) {
2234
+ const { eventLabel, eventAt } = makeEventText(t);
2235
+ return {
2236
+ t,
2237
+ fmt,
2238
+ fmtTime,
2239
+ catLabel: (key) => t("cat." + key),
2240
+ eventLabel,
2241
+ eventAt
2242
+ };
2243
+ }
2244
+ //#endregion
2245
+ //#region \0dsh-global-css:/home/runner/work/dsh-context/dsh-context/src/client/styles.css.mjs
2246
+ const css = ".lc-root{box-sizing:border-box;height:100%;color:var(--dsw-alias-label-primary);padding:16px 20px 32px;font-size:13px;overflow-y:auto}.lc-card{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;margin-bottom:14px;padding:14px 16px}.lc-card-title{align-items:baseline;gap:8px;margin-bottom:10px;font-weight:600;display:flex}.lc-card-title-text{white-space:nowrap;flex:none}.lc-stats{grid-template-columns:repeat(auto-fit,minmax(92px,1fr));gap:8px;display:grid}.lc-stat{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;flex-direction:column;gap:2px;min-width:0;padding:8px 10px;display:flex}.lc-stat-label{color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;font-size:11px;font-weight:600;overflow:hidden}.lc-stat-value{color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;text-align:center;font-size:14px;font-weight:600;overflow:hidden}.lc-stat-sub{color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;font-size:11px;overflow:hidden}.lc-stat-tipped{position:relative}.lc-stat-q{text-align:center;width:11px;height:11px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);cursor:help;border-radius:50%;margin-left:4px;font-size:9px;font-style:normal;font-weight:700;line-height:11px;display:inline-block}.lc-stat-tipped:hover .lc-stat-q{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary)}.lc-stat-tip{z-index:6;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);width:max-content;max-width:280px;color:var(--dsw-alias-label-primary);pointer-events:none;opacity:0;border-radius:6px;padding:6px 10px;font-size:12px;font-weight:400;line-height:1.5;transition:opacity .12s;position:absolute;top:calc(100% + 6px);left:0;box-shadow:0 2px 8px #0000002e}.lc-stat-tipped:hover .lc-stat-tip{opacity:1}.lc-stat-tip-prices{border-top:1px dashed var(--dsw-alias-border-l1);margin-top:5px;padding-top:5px;display:block}.lc-stat-tip-head{font-weight:600;display:block}.lc-stat-tip-row{margin-top:2px;display:block}.lc-stat-tip-model{color:var(--dsw-alias-label-primary)}.lc-card-sub{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:400}.lc-gran,.lc-kinds{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:6px;gap:2px;margin-left:auto;padding:1px;display:flex}.lc-gran-btn{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:0;border-radius:5px;padding:3px 8px;font-family:inherit;font-size:11px;line-height:1}.lc-gran-btn:hover{color:var(--dsw-alias-label-primary)}.lc-gran-on,.lc-gran-on:hover{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.lc-overview-num{align-items:baseline;gap:6px;margin-bottom:8px;display:flex}.lc-overview-num>b{font-size:20px}.lc-overview-num span{color:var(--dsw-alias-label-secondary)}.lc-overview-pct{margin-left:auto;font-size:11px}.lc-overview-pct b{color:var(--dsw-alias-label-primary);margin-right:4px;font-size:20px}.lc-stacked-wrap{width:100%;position:relative}.lc-stacked{background:#8080802e;border-radius:5px;width:100%;display:flex;position:relative;overflow:hidden}.lc-occupied-box{border:2px solid var(--dsw-alias-label-tertiary);box-sizing:border-box;pointer-events:none;opacity:0;box-shadow:0 0 0 1px var(--dsw-alias-bg-layer-2);border-radius:5px;transition:opacity .12s;position:absolute;top:0;bottom:0;left:0}.lc-occupied-box-on{opacity:1}.lc-bar-tip{z-index:5;white-space:nowrap;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);pointer-events:none;opacity:0;border-radius:6px;padding:3px 8px;font-size:12px;transition:opacity .12s;position:absolute;bottom:calc(100% + 6px);transform:translate(-50%);box-shadow:0 2px 8px #0000002e}.lc-bar-tip-on{opacity:1}.lc-stacked>div{height:100%}.lc-stacked-seg{transition:filter .12s,opacity .12s}.lc-stacked-free{box-sizing:border-box;transition:box-shadow .12s,opacity .12s}.lc-stacked-seg-on{filter:brightness(1.18)}.lc-reserve{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);position:absolute;top:0;bottom:0}.lc-stacked-free-on{border:2px dashed var(--dsw-alias-label-secondary);border-radius:3px}.lc-stacked-dim .lc-stacked-seg{opacity:.35}.lc-stacked-dim .lc-stacked-seg-on{opacity:1}.lc-stacked-dim .lc-stacked-free{opacity:.35}.lc-stacked-dim .lc-stacked-free-on{opacity:1}.lc-legend{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:6px 14px;margin-top:10px;display:grid}.lc-chip{min-width:0;color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:6px;align-items:center;gap:5px;padding:1px 6px;transition:background-color .12s;display:flex}.lc-chip-label{white-space:nowrap;text-overflow:ellipsis;min-width:0;font-weight:600;overflow:hidden}.lc-chip-nums{white-space:nowrap;flex:none;align-items:baseline;gap:6px;margin-left:auto;display:inline-flex}.lc-chip i,.lc-detail-row i,.lc-node i{border-radius:2px;width:8px;height:8px;display:inline-block}.lc-chip i{transition:box-shadow .12s}.lc-chip em{color:var(--dsw-alias-label-secondary);font-style:normal}.lc-chip-on{background:var(--dsw-alias-interactive-bg-hover);font-weight:600}.lc-chip-on i{box-shadow:0 0 0 1px var(--dsw-alias-brand-primary)}.lc-tools{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;align-items:center;gap:6px;margin-top:10px;display:flex}.lc-tools-label,.lc-tools-more{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:0;padding:0;font-family:inherit;font-size:12px}.lc-tools-label:hover,.lc-tools-more:hover{text-decoration:underline}.lc-tool-chip{background:var(--dsw-alias-bg-layer-2);font:inherit;color:var(--dsw-alias-label-primary);cursor:pointer;border:0;border-radius:4px;padding:1px 7px;font-size:12px}.lc-tool-chip:hover{text-decoration:underline}.lc-chartrow{align-items:stretch;gap:6px;display:flex}.lc-axis{box-sizing:border-box;width:40px;height:150px;color:var(--dsw-alias-label-secondary);padding-top:18px;font-size:11px;position:relative}.lc-axis span{line-height:1;position:absolute;right:0}.lc-axis-top{top:13px}.lc-axis-mid{top:69px}.lc-axis-bot{top:125px}.lc-chart-scroll{scrollbar-width:thin;flex:1;min-width:0;position:relative;overflow:auto hidden}.lc-chart-fade{pointer-events:none;z-index:2;width:26px;position:absolute;top:0;bottom:0}.lc-chart-fade-l{background:linear-gradient(to right, var(--dsw-alias-bg-layer-1), transparent);left:0}.lc-chart-fade-r{background:linear-gradient(to left, var(--dsw-alias-bg-layer-1), transparent);right:0}.lc-chart{box-sizing:border-box;align-items:flex-end;gap:2px;width:max-content;min-width:100%;height:130px;padding-top:18px;display:flex;position:relative}.lc-grid{border-top:1px dashed var(--dsw-alias-border-l1);pointer-events:none;position:absolute;left:0;right:0}.lc-grid-top{top:18px}.lc-grid-mid{top:74px}.lc-bar{cursor:pointer;border-radius:2px;flex:none;align-items:flex-end;width:14px;height:100%;transition:opacity .12s,background-color .12s;display:flex;position:relative}.lc-chart-dim .lc-bar{opacity:.35}.lc-chart-dim .lc-bar-in-turn{opacity:1}.lc-chart-dim .lc-turn{opacity:.35}.lc-chart-dim .lc-turn-on{opacity:1}.lc-chart-tip{z-index:5;white-space:nowrap;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);pointer-events:none;border-radius:6px;padding:3px 8px;font-size:12px;position:absolute;top:0;transform:translate(-50%);box-shadow:0 2px 8px #0000002e}.lc-bar:hover{background:var(--dsw-alias-bg-layer-2)}.lc-bar-hovered{outline:1px dashed var(--dsw-alias-brand-primary);outline-offset:1px}.lc-bar-selected{outline:2px solid var(--dsw-alias-label-primary);outline-offset:1px}.lc-bar-in-turn{background:#80808024}.lc-bar-stack{flex-direction:column-reverse;width:100%;display:flex}.lc-bar-stack>div{width:100%}.lc-bar-marker{color:var(--dsw-alias-state-warn-primary);font-size:11px;position:absolute;top:-16px;left:50%;transform:translate(-50%)}.lc-turns{gap:2px;width:max-content;min-width:100%;margin-top:4px;display:flex}.lc-turn{box-sizing:border-box;text-align:center;color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;cursor:default;border-radius:3px;flex:none;height:14px;font-size:10px;font-weight:600;line-height:14px;transition:filter .12s,opacity .12s;overflow:hidden}.lc-turn-on{filter:brightness(1.35);color:var(--dsw-alias-label-primary)}.lc-detail{border-top:1px solid var(--dsw-alias-border-l1);margin-top:12px;padding-top:12px}.lc-detail-head{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:6px 16px;margin-bottom:8px;display:flex}.lc-detail-head b{color:var(--dsw-alias-label-primary)}.lc-detail-marker{color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-bg-layer-2);border-radius:6px;padding:1px 7px;font-size:11px}.lc-detail-head .lc-actual{color:var(--dsw-alias-state-success-primary)}.lc-detail-tag{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary);border-radius:4px;padding:0 6px;font-size:11px}.lc-detail-rows{grid-template-columns:1fr 1fr;gap:4px 24px;margin-top:10px;display:grid}.lc-detail-row{align-items:center;gap:8px;display:flex}.lc-detail-label{white-space:nowrap;min-width:70px;color:var(--dsw-alias-label-secondary)}.lc-bar-track{background:#8080802e;border-radius:3px;flex:1;height:5px;display:block;overflow:hidden}.lc-bar-fill{border-radius:3px;height:100%;display:block}.lc-detail-num{text-align:right;width:44px}.lc-detail-pct{text-align:right;width:34px;color:var(--dsw-alias-label-secondary)}.lc-cols{flex-wrap:wrap;gap:14px;margin-bottom:14px;display:flex}.lc-col{flex:1;min-width:280px}.lc-cols>.lc-card,.lc-col>.lc-card:last-child{margin-bottom:0}.lc-head>.lc-card:first-child{flex:7 1 0;min-width:360px}.lc-head>.lc-card:last-child{flex:3 1 0;min-width:220px}.lc-pi-grid{grid-template-columns:1fr;gap:8px;display:grid}.lc-pi-row{min-width:0;color:inherit;flex-direction:row;justify-content:space-between;align-items:baseline;gap:12px;text-decoration:none;display:flex}.lc-pi-row:hover .lc-pi-value{text-decoration:underline}.lc-pi-update{color:var(--dsw-alias-state-warn-primary);white-space:nowrap;margin-left:6px;font-size:11px}.lc-pi-label{color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;flex:none;font-size:11px;font-weight:600;overflow:hidden}.lc-pi-value{min-width:0;color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;text-align:right;font-size:13px;font-weight:600;overflow:hidden}.lc-events,.lc-nodes{flex-direction:column;gap:2px;max-height:320px;display:flex;overflow-y:auto}.lc-event{align-items:center;gap:8px;padding:3px 0;display:flex}.lc-event-icon{text-align:center;width:18px;color:var(--dsw-alias-state-warn-primary)}.lc-event-icon.lc-event-inject{color:#a855f7}.lc-event-icon.lc-event-model{color:var(--dsw-alias-brand-primary)}.lc-kind{white-space:nowrap;border-radius:4px;flex:none;padding:1px 6px;font-size:10px;font-weight:600}.lc-kind-inject{background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 15%, transparent);color:var(--dsw-alias-state-success-primary)}.lc-kind-compaction{background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 15%, transparent);color:var(--dsw-alias-state-error-primary)}.lc-kind-prune{color:#8b5cf6;background:#8b5cf626}.lc-kind-model{background:color-mix(in srgb, var(--dsw-alias-brand-primary) 15%, transparent);color:var(--dsw-alias-brand-primary)}.lc-event-label{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.lc-event-at{color:var(--dsw-alias-label-secondary);white-space:nowrap;flex:none;font-size:11px}.lc-event-tokens{color:var(--dsw-alias-state-success-primary);white-space:nowrap;font-weight:600}.lc-event-tokens.lc-up{color:var(--dsw-alias-state-warn-primary)}.lc-event-time{color:var(--dsw-alias-label-secondary);font-size:12px}.lc-node{align-items:center;gap:8px;padding:3px 0;display:flex}.lc-node-preview{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary);flex:1;overflow:hidden}.lc-node-time{color:var(--dsw-alias-label-secondary);text-align:right;min-width:54px;font-size:12px}.lc-node-tokens{color:var(--dsw-alias-label-secondary)}.lc-nodes-more{color:var(--dsw-alias-label-secondary);padding:3px 0}.lc-empty{color:var(--dsw-alias-label-secondary);text-align:center;padding:18px 0}.lc-error{flex-direction:column;align-items:center;gap:8px;padding:40px 16px;display:flex}.lc-error-msg{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);overflow-wrap:anywhere;border-radius:6px;max-width:100%;padding:4px 8px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}.lc-error-retry{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;border-radius:6px;padding:4px 14px;font-size:12px}.lc-error-retry:hover{border-color:var(--dsw-alias-label-primary)}.lc-foot{color:var(--dsw-alias-label-secondary);margin-top:4px;font-size:12px}.lc-modal-backdrop{z-index:200;background:#00000073;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.lc-modal-card{box-sizing:border-box;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);width:min(720px,100vw - 48px);max-height:min(82vh,760px);box-shadow:var(--dsw-shadow-lv3,0 12px 32px #0006);color:var(--dsw-alias-label-primary);border-radius:12px;padding:16px 18px 18px;font-size:13px;overflow-y:auto}.lc-modal-head{align-items:baseline;gap:8px;margin-bottom:12px;display:flex}.lc-modal-title{font-size:14px;font-weight:600}.lc-modal-close{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:0;border-radius:6px;margin-left:auto;padding:2px 6px;font-family:inherit;font-size:18px;line-height:1}.lc-modal-close:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}.lc-br-hint{color:var(--dsw-alias-label-secondary);white-space:nowrap;margin-left:auto;font-size:11px;font-weight:400}.lc-br-pick{font:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:6px;max-width:240px;padding:3px 6px;font-size:12px}.lc-br-meta{color:var(--dsw-alias-label-secondary);flex-wrap:wrap;gap:6px 16px;margin-bottom:8px;display:flex}.lc-br-meta b{color:var(--dsw-alias-label-primary)}.lc-br-meta .lc-actual{color:var(--dsw-alias-state-success-primary)}.lc-br-note{color:var(--dsw-alias-label-secondary);margin-top:8px;font-size:12px}.lc-br-cats{flex-direction:column;gap:4px;margin-top:10px;display:flex}.lc-br-cat{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-2);border-radius:8px;overflow:hidden}.lc-br-cat-empty{opacity:.55}.lc-br-cat-row{width:100%;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;text-align:left;background:0 0;border:0;align-items:center;gap:8px;padding:7px 10px;transition:background-color .12s;display:flex}.lc-br-cat-row:hover,.lc-br-cat-on{background:var(--dsw-alias-interactive-bg-hover)}.lc-br-cat-on i{box-shadow:0 0 0 1px var(--dsw-alias-brand-primary)}.lc-br-cat-row i{border-radius:2px;flex:none;width:8px;height:8px;transition:box-shadow .12s;display:inline-block}.lc-br-cat-label{font-weight:600}.lc-br-cat-count{color:var(--dsw-alias-label-secondary);white-space:nowrap;font-size:12px}.lc-br-count-grp{flex:1;align-items:center;gap:4px;min-width:0;display:inline-flex}.lc-br-chev{width:12px;color:var(--dsw-alias-label-secondary);flex:none;transition:transform .12s}.lc-br-chev-on{transform:rotate(90deg)}.lc-br-tokens{color:var(--dsw-alias-label-secondary);flex:none;font-size:12px}.lc-br-delta,.lc-br-tdelta{white-space:nowrap;border-radius:4px;flex:none;padding:0 5px;font-size:11px;font-weight:600}.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)}.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)}.lc-br-tokens-grp{flex:none;align-items:center;gap:4px;min-width:0;display:inline-flex}.lc-br-pct{text-align:right;width:36px;color:var(--dsw-alias-label-secondary);flex:none;font-size:12px}.lc-br-body{border-top:1px solid var(--dsw-alias-border-l1);flex-direction:column;gap:2px;padding:4px 6px;display:flex}.lc-br-elem{border-radius:6px}.lc-br-elem-on{background:var(--dsw-alias-interactive-bg-active)}.lc-br-elem-row{width:100%;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;text-align:left;background:0 0;border:0;border-radius:6px;align-items:center;gap:8px;padding:5px 6px;font-size:12px;transition:background-color .12s;display:flex}.lc-br-elem-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.lc-br-tag{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);text-overflow:ellipsis;white-space:nowrap;border-radius:4px;flex:none;max-width:220px;padding:0 6px;font-size:11px;overflow:hidden}.lc-br-preview{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.lc-br-time{color:var(--dsw-alias-label-secondary);flex:none;font-size:11px}.lc-br-content{flex-direction:column;gap:6px;padding:2px 6px 8px 26px;display:flex}.lc-br-pre{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word;scrollbar-width:thin;border-radius:6px;max-height:320px;margin:0;padding:8px 10px;font-size:12px;line-height:1.55;overflow-y:auto}.lc-br-dim{color:var(--dsw-alias-label-secondary)}.lc-br-call{flex-direction:column;align-items:flex-start;gap:4px;display:flex}.lc-br-blk{flex-direction:column;gap:4px;display:flex}.lc-br-blk-label{color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:600}.lc-ts-card{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:6px;overflow:hidden}.lc-ts-card-head{color:var(--dsw-alias-label-secondary);border-bottom:1px solid var(--dsw-alias-border-l1);align-items:center;gap:8px;padding:6px 10px;font-size:11px;font-weight:600;display:flex}.lc-ts-card-head b{color:var(--dsw-alias-label-primary)}.lc-ts-card-count{margin-left:auto}.lc-ts-desc-body{color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word;scrollbar-width:thin;max-height:160px;margin:0;padding:8px 10px;font-size:12px;line-height:1.55;overflow-y:auto}.lc-ts-param-row{border-top:1px solid var(--dsw-alias-border-l1);grid-template-columns:140px 90px 56px 1fr;align-items:baseline;gap:2px 10px;padding:6px 10px;font-size:12px;display:grid}.lc-ts-param-row:first-of-type{border-top:0}.lc-ts-param-name{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;font-weight:600;overflow:hidden}.lc-ts-param-type{color:var(--dsw-alias-label-secondary);text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;overflow:hidden}.lc-ts-param-req{color:var(--dsw-alias-state-warn-primary);font-size:11px;font-weight:600}.lc-ts-param-req-off{color:var(--dsw-alias-label-secondary);font-size:11px}.lc-ts-param-desc{color:var(--dsw-alias-label-secondary);grid-column:1/-1;line-height:1.5}.lc-ts-params-empty{color:var(--dsw-alias-label-secondary);padding:8px 10px;font-size:12px}.lc-ts-json{flex-direction:column;gap:4px;display:flex}.lc-ts-json-toggle{color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;background:0 0;border:0;align-self:flex-start;padding:0;font-size:11px}.lc-ts-json-toggle:hover{color:var(--dsw-alias-label-primary);text-decoration:underline}.lc-rich-seg{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);border-radius:6px;align-self:flex-end;gap:2px;padding:1px;display:flex}.lc-rich-seg-btn{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:0;border-radius:5px;padding:3px 8px;font-family:inherit;font-size:11px;line-height:1}.lc-rich-seg-btn:hover{color:var(--dsw-alias-label-primary)}.lc-rich-seg-on,.lc-rich-seg-on:hover{background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.lc-rich-seg-head{align-self:auto;margin-left:auto}.lc-br-md{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);word-break:break-word;scrollbar-width:thin;border-radius:6px;max-height:320px;padding:4px 10px;font-size:12px;line-height:1.55;overflow-y:auto}.lc-ts-desc-md{color:var(--dsw-alias-label-primary);word-break:break-word;scrollbar-width:thin;max-height:160px;padding:4px 10px;font-size:12px;line-height:1.55;overflow-y:auto}.lc-br-md>div,.lc-ts-desc-md>div{font-size:13px;line-height:1.55}.lc-br-md div :is(h1,h2,h3,h4,h5,h6),.lc-ts-desc-md div :is(h1,h2,h3,h4,h5,h6){margin:10px 0 4px;font-size:13px;font-weight:600}.lc-br-md div p,.lc-ts-desc-md div p,.lc-br-md div pre,.lc-ts-desc-md div pre{margin:6px 0}.lc-br-md div :not(pre)>code,.lc-ts-desc-md div :not(pre)>code{box-decoration-break:clone;padding:1px 5px;line-height:1.3;font-size:1em!important}.lc-br-md div table code,.lc-ts-desc-md div table code{font-size:inherit}.lc-br-rich-inline{flex-direction:column;gap:6px;display:flex}.lc-ts-card .lc-br-pre{margin:8px 10px}.lc-att-grid{flex-wrap:wrap;gap:10px;padding:8px 10px;display:flex}.lc-att-item{align-items:center;gap:8px;min-width:0;display:flex}.lc-att-thumb{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);cursor:pointer;width:64px;height:64px;font:inherit;border-radius:6px;flex:none;justify-content:center;align-items:center;padding:0;display:flex;overflow:hidden}.lc-att-thumb img{object-fit:cover;width:100%;height:100%;display:block}.lc-att-ph,.lc-att-err{color:var(--dsw-alias-label-secondary);text-align:center;padding:2px;font-size:10px}.lc-att-meta{flex-direction:column;gap:2px;min-width:0;display:flex}.lc-att-name{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;max-width:200px;font-size:12px;overflow:hidden}.lc-att-dims{color:var(--dsw-alias-label-secondary);font-size:11px}";
2247
+ const tagId = "dsh-context/styles.css";
2248
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
2249
+ const tag = document.createElement("style");
2250
+ tag.dataset.plugin = "dsh-context";
2251
+ tag.dataset.pluginCss = tagId;
2252
+ tag.textContent = css;
2253
+ document.head.appendChild(tag);
2254
+ }
2255
+ //#endregion
2256
+ //#region src/client/index.ts
2257
+ /**
2258
+ * dsh-context — Client half (installed package bundle entry).
2259
+ *
2260
+ * Registers a "上下文/Context" tab in the conversation view ring
2261
+ * (`conversation.view` slot, beside Chat/Trajectory) and renders the
2262
+ * context-composition timeline: current makeup, per-request stacked-bar
2263
+ * history, context events, and the live message list.
2264
+ *
2265
+ * Since v0.9 the tab needs no custom data plane: the Host half pushes its
2266
+ * fold through the harness's session-projection pipeline
2267
+ * (`contextTimeline` projection key), and this half reads the finished value
2268
+ * from the framework standard kit (`useProjection('contextTimeline')`, a
2269
+ * standard prop on every session-scope slot component). No polling, no RPC,
2270
+ * no client-side cache.
2271
+ *
2272
+ * This module is the body of the package's `./client` bundle: tsdown
2273
+ * (tsdown.config.ts) bundles it (external `react` — the browser module table
2274
+ * supplies it via the injected `require`) into the web boot handoff
2275
+ * (`window.__ModuleLoader__.load({id, factory})`). All imports from other
2276
+ * client modules are inlined by the bundler; everything here is zero-runtime
2277
+ * beyond the bundled source.
2278
+ */
2279
+ const NS = "dsh-context";
2280
+ function apply(ctx) {
2281
+ ctx.effect(() => {
2282
+ return ctx.locale.register(NS, {
2283
+ zh: DICT_ZH,
2284
+ en: DICT_EN
2285
+ });
2286
+ }, "dsh-context: dictionaries");
2287
+ const t = ctx.locale.bind(NS);
2288
+ const kit = makeViewKit(t);
2289
+ const ContextView = makeContextView(ctx, kit);
2290
+ ctx.effect(() => {
2291
+ const noop = () => {};
2292
+ const sessions = ctx.get("sessions");
2293
+ if (sessions === void 0 || typeof sessions.provide !== "function") return noop;
2294
+ try {
2295
+ return sessions.provide({
2296
+ props: ["loadOlderHistory"],
2297
+ resolve: (binding) => ({ props: { loadOlderHistory: () => binding.session.loadOlder() } })
2298
+ });
2299
+ } catch {
2300
+ return noop;
2301
+ }
2302
+ }, "dsh-context: loadOlderHistory prop");
2303
+ ctx.slots.inject("conversation.view", () => {
2304
+ return ctx.slots.register({
2305
+ name: "conversation.view",
2306
+ id: "context",
2307
+ order: 20,
2308
+ locale: NS,
2309
+ label: () => t("tab")
2310
+ }, (props) => h(ContextView, props));
2311
+ });
2312
+ registerContextCommand(ctx, kit);
2313
+ const ContextModal = makeContextModal(ctx, kit);
2314
+ ctx.slots.inject("conversation.input.overlay", () => {
2315
+ return ctx.slots.register({
2316
+ name: "conversation.input.overlay",
2317
+ id: "context-modal",
2318
+ order: 10,
2319
+ locale: NS,
2320
+ inject: (sessionId) => ({ hooks: { contextModal: modalStoreOf(sessionId) } })
2321
+ }, (props) => h(ContextModal, props));
2322
+ });
2323
+ }
2324
+ module.exports = {
2325
+ name: "dsh-context",
2326
+ inject: ["slots", "locale"],
2327
+ apply
2328
+ };
2329
+ //#endregion
2330
+ return module.exports;
2331
+ }
2139
2332
  });
2140
2333
 
2334
+ //# sourceMappingURL=client.js.map