dsh-token-chart 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -2
  3. package/cordis.patch.yml +7 -0
  4. package/icon.svg +7 -0
  5. package/lib/buckets.js +115 -0
  6. package/lib/chunk/echarts.entry.js +25 -0
  7. package/lib/client.echarts.js +58 -0
  8. package/lib/client.js +1743 -0
  9. package/lib/constants.js +22 -0
  10. package/lib/index.js +640 -0
  11. package/lib/paths.js +24 -0
  12. package/lib/prefs.js +54 -0
  13. package/lib/store.js +273 -0
  14. package/lib/token-chart.css +568 -0
  15. package/lib/types/buckets.d.ts +24 -0
  16. package/lib/types/chunk/echarts.entry.d.ts +11 -0
  17. package/lib/types/constants.d.ts +22 -0
  18. package/lib/types/index.d.ts +8 -0
  19. package/lib/types/paths.d.ts +3 -0
  20. package/lib/types/prefs.d.ts +8 -0
  21. package/lib/types/store.d.ts +72 -0
  22. package/lib/types/types.d.ts +230 -0
  23. package/lib/types/update.d.ts +24 -0
  24. package/lib/types.js +10 -0
  25. package/lib/update.js +110 -0
  26. package/locale/en.json +4 -0
  27. package/locale/zh.json +4 -0
  28. package/package.json +110 -4
  29. package/src/buckets.ts +120 -0
  30. package/src/chunk/echarts.entry.ts +27 -0
  31. package/src/client/api.ts +126 -0
  32. package/src/client/charts.tsx +255 -0
  33. package/src/client/chrome.tsx +127 -0
  34. package/src/client/contract.ts +62 -0
  35. package/src/client/drawer.tsx +113 -0
  36. package/src/client/format.ts +36 -0
  37. package/src/client/globals.d.ts +21 -0
  38. package/src/client/index.tsx +227 -0
  39. package/src/client/locales.ts +164 -0
  40. package/src/client/models.ts +76 -0
  41. package/src/client/mount.tsx +312 -0
  42. package/src/client/panel.tsx +152 -0
  43. package/src/client/pill.ts +21 -0
  44. package/src/client/react.ts +12 -0
  45. package/src/client/series.tsx +140 -0
  46. package/src/client/settings.tsx +347 -0
  47. package/src/client/styles/components.less +191 -0
  48. package/src/client/styles/index.less +7 -0
  49. package/src/client/styles/tailwind.css +21 -0
  50. package/src/client/styles/tokens.less +25 -0
  51. package/src/client/table.tsx +153 -0
  52. package/src/client/ui.tsx +87 -0
  53. package/src/constants.ts +26 -0
  54. package/src/index.ts +673 -0
  55. package/src/paths.ts +28 -0
  56. package/src/prefs.ts +61 -0
  57. package/src/store.ts +295 -0
  58. package/src/types.ts +216 -0
  59. package/src/update.ts +106 -0
package/lib/client.js ADDED
@@ -0,0 +1,1743 @@
1
+ window.__ModuleLoader__.load({ id: "dsh-token-chart", factory: (require) => {
2
+ var module = { exports: {} };
3
+ var exports = module.exports;
4
+
5
+ "use strict";
6
+ var __create = Object.create;
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __getProtoOf = Object.getPrototypeOf;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
32
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
+
34
+ // src/client/index.tsx
35
+ var index_exports = {};
36
+ __export(index_exports, {
37
+ apply: () => apply,
38
+ inject: () => inject
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+
42
+ // src/constants.ts
43
+ var DAY_MS = 24 * 60 * 60 * 1e3;
44
+ var UPDATE_CHECK_TTL_MS = 30 * 60 * 1e3;
45
+ var METRIC_IDS = ["input", "output", "cache", "total"];
46
+ var CHART_IDS = ["bar", "line", "pie", "table", "multi-bar", "multi-line", "bar-line"];
47
+
48
+ // src/client/contract.ts
49
+ var API = "/token-chart/api";
50
+ var NS = "token-chart";
51
+ var KIND = "dsh-token-chart";
52
+ var PANEL_ID = "token-chart";
53
+ var COLOR = {
54
+ total: "var(--dsw-alias-brand-primary)",
55
+ input: "var(--dsw-alias-state-warn-primary)",
56
+ output: "var(--dsw-alias-state-success-primary)",
57
+ cache: "var(--dsw-alias-state-idle-primary)",
58
+ cacheHitRate: "var(--dsw-alias-state-error-primary)"
59
+ };
60
+ var METRIC_COLOR = {
61
+ total: COLOR.total,
62
+ input: COLOR.input,
63
+ output: COLOR.output,
64
+ cache: COLOR.cache,
65
+ cacheHitRate: COLOR.cacheHitRate
66
+ };
67
+ var CHART_KINDS = [...CHART_IDS];
68
+ var METRIC_IDS2 = [...METRIC_IDS];
69
+ var DEFAULT_PREFS = {
70
+ sessionChart: "multi-bar",
71
+ dailyChart: "multi-bar",
72
+ sessionMetrics: ["input", "output", "cache", "total"],
73
+ dailyMetrics: ["input", "output", "cache", "total"],
74
+ showTable: true,
75
+ showCacheHitRate: true,
76
+ interceptUsageButton: true
77
+ };
78
+
79
+ // src/client/react.ts
80
+ var React = __toESM(require("react"), 1);
81
+
82
+ // src/client/api.ts
83
+ var useSyncExternalStore = typeof React.useSyncExternalStore === "function" ? React.useSyncExternalStore : (subscribe, getSnapshot) => {
84
+ const [value, setValue] = React.useState(getSnapshot);
85
+ React.useEffect(() => subscribe(() => setValue(getSnapshot())), [subscribe, getSnapshot]);
86
+ return value;
87
+ };
88
+ var prefsStore = (() => {
89
+ let value = { ...DEFAULT_PREFS };
90
+ const listeners = /* @__PURE__ */ new Set();
91
+ return {
92
+ get: () => value,
93
+ set: (next) => {
94
+ value = next;
95
+ for (const listener of [...listeners]) listener();
96
+ },
97
+ subscribe: (listener) => {
98
+ listeners.add(listener);
99
+ return () => listeners.delete(listener);
100
+ }
101
+ };
102
+ })();
103
+ function usePrefs() {
104
+ return useSyncExternalStore(prefsStore.subscribe, prefsStore.get);
105
+ }
106
+ async function apiGet(path) {
107
+ const response = await fetch(`${API}${path}`, { headers: { accept: "application/json" } });
108
+ const payload = await response.json().catch(() => null);
109
+ if (payload === null || payload.ok !== true) {
110
+ throw new Error(payload?.error?.message ?? `HTTP ${response.status}`);
111
+ }
112
+ return payload;
113
+ }
114
+ async function apiPost(path, body) {
115
+ const response = await fetch(`${API}${path}`, {
116
+ method: "POST",
117
+ headers: { "content-type": "application/json" },
118
+ body: JSON.stringify(body ?? {})
119
+ });
120
+ const payload = await response.json().catch(() => null);
121
+ if (payload === null || payload.ok !== true) {
122
+ throw new Error(payload?.error?.message ?? `HTTP ${response.status}`);
123
+ }
124
+ return payload;
125
+ }
126
+ function useEndpoint(path, tick) {
127
+ const [state, setState] = React.useState({ loading: true, value: void 0 });
128
+ React.useEffect(() => {
129
+ let live = true;
130
+ setState((current) => ({ ...current, loading: true }));
131
+ apiGet(path).then((payload) => {
132
+ if (!live) return;
133
+ setState({ loading: false, value: payload.value, error: void 0 });
134
+ }).catch((error) => {
135
+ if (!live) return;
136
+ setState({
137
+ loading: false,
138
+ value: void 0,
139
+ error: String(error?.message ?? error)
140
+ });
141
+ });
142
+ return () => {
143
+ live = false;
144
+ };
145
+ }, [path, tick]);
146
+ return state;
147
+ }
148
+ function useElementWidth() {
149
+ const ref = React.useRef(null);
150
+ const [width, setWidth] = React.useState(0);
151
+ React.useEffect(() => {
152
+ const node = ref.current;
153
+ if (node === null) return void 0;
154
+ const update = () => setWidth(node.clientWidth);
155
+ update();
156
+ if (typeof ResizeObserver === "undefined") {
157
+ window.addEventListener("resize", update);
158
+ return () => window.removeEventListener("resize", update);
159
+ }
160
+ const observer = new ResizeObserver(update);
161
+ observer.observe(node);
162
+ return () => observer.disconnect();
163
+ }, []);
164
+ return [ref, width];
165
+ }
166
+
167
+ // src/client/chrome.tsx
168
+ var import_jsx_runtime = require("react/jsx-runtime");
169
+ function PanelIcon({ size = 18, active = false }) {
170
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
171
+ "svg",
172
+ {
173
+ width: size,
174
+ height: size,
175
+ viewBox: "0 0 24 24",
176
+ fill: "none",
177
+ "aria-hidden": true,
178
+ style: { opacity: active ? 1 : 0.9, display: "block" },
179
+ children: [
180
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: 3, y: 12, width: 4, height: 9, rx: 1, fill: "currentColor" }),
181
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: 9, y: 8, width: 4, height: 13, rx: 1, fill: "currentColor", opacity: 0.75 }),
182
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: 15, y: 4, width: 4, height: 17, rx: 1, fill: "currentColor", opacity: 0.5 })
183
+ ]
184
+ }
185
+ );
186
+ }
187
+ function HeaderUsageButton({
188
+ sessionId,
189
+ open,
190
+ t
191
+ }) {
192
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
193
+ "button",
194
+ {
195
+ type: "button",
196
+ className: "dtc-header-button",
197
+ "aria-label": t("usage.openDrawer"),
198
+ title: t("usage.title"),
199
+ onClick: () => open(sessionId),
200
+ children: [
201
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", "aria-hidden": true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M4 19V9M10 19V5M16 19v-7M22 19H2", stroke: "currentColor", strokeWidth: 1.6, strokeLinecap: "round" }) }),
202
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dtc-header-label", children: t("panel.title") })
203
+ ]
204
+ }
205
+ );
206
+ }
207
+ var STYLE_PATH = "/token-chart/assets/token-chart.css";
208
+ var STYLE_LINK_MARKER = "data-dtc-styles";
209
+ var SETTINGS_NAV_MARKER = "data-dtc-settings-nav";
210
+ function registerSettingsNavIcon(label) {
211
+ let disposed = false;
212
+ const sync = () => {
213
+ if (disposed) return;
214
+ const current = label().trim();
215
+ for (const button of document.querySelectorAll('[role="dialog"] nav button')) {
216
+ if (current !== "" && (button.textContent ?? "").trim() === current) button.setAttribute(SETTINGS_NAV_MARKER, "");
217
+ else button.removeAttribute(SETTINGS_NAV_MARKER);
218
+ }
219
+ };
220
+ sync();
221
+ const observer = new MutationObserver(sync);
222
+ observer.observe(document.body, { childList: true, subtree: true, characterData: true });
223
+ return () => {
224
+ disposed = true;
225
+ observer.disconnect();
226
+ for (const marked of document.querySelectorAll(`[${SETTINGS_NAV_MARKER}]`))
227
+ marked.removeAttribute(SETTINGS_NAV_MARKER);
228
+ };
229
+ }
230
+ function installStyles() {
231
+ const existing = document.querySelector(`link[${STYLE_LINK_MARKER}]`);
232
+ if (existing !== null) return () => {
233
+ };
234
+ const link = document.createElement("link");
235
+ link.rel = "stylesheet";
236
+ link.href = STYLE_PATH;
237
+ link.setAttribute(STYLE_LINK_MARKER, "");
238
+ document.head.appendChild(link);
239
+ return () => {
240
+ link.remove();
241
+ };
242
+ }
243
+
244
+ // src/client/pill.ts
245
+ function isUsagePill(target) {
246
+ let node = target;
247
+ for (let depth = 0; depth < 6 && node !== null && node !== void 0; depth += 1) {
248
+ if (node.nodeType === 1 && node.tagName === "BUTTON") {
249
+ const text = node.textContent ?? "";
250
+ if (node.getAttribute("aria-haspopup") === "dialog" && /\btok\b/i.test(text)) return true;
251
+ return false;
252
+ }
253
+ node = node.parentNode;
254
+ }
255
+ return false;
256
+ }
257
+
258
+ // src/client/locales.ts
259
+ var zh = {
260
+ "panel.title": "Token 用量",
261
+ "panel.subtitle": "统计今天、近 3 天、近 7 天的 Token 用量",
262
+ "panel.range.today": "今天",
263
+ "panel.range.3d": "近 3 天",
264
+ "panel.range.7d": "近 7 天",
265
+ "panel.refresh": "刷新",
266
+ "panel.loading": "正在统计…",
267
+ "panel.empty": "所选区间内还没有 Token 用量记录",
268
+ "table.total": "共 {count} 条",
269
+ "table.pagination": "分页",
270
+ "table.prev": "上一页",
271
+ "table.next": "下一页",
272
+ "table.pageSizeLabel": "每页条数",
273
+ "table.page": "第 {page} 页",
274
+ "table.pageSize": "{size} 条/页",
275
+ "panel.updated": "更新于 {time}",
276
+ "panel.retention": "仅保留近 {days} 天的数据",
277
+ "settings.nav": "Token 用量",
278
+ "settings.title": "Token 用量可视化",
279
+ "settings.description": "选择各场景使用的图表形式与展示指标,保存后立即生效。",
280
+ "settings.sessionChart": "会话用量抽屉",
281
+ "settings.sessionChartHint": "点击会话中的「用量」按钮后,在右侧抽屉里展示的图形。",
282
+ "settings.dailyChart": "每日用量面板",
283
+ "settings.dailyChartHint": "侧边栏「Token 用量」菜单中「今天 / 近 3 天 / 近 7 天」的图形。",
284
+ "settings.metrics": "展示指标",
285
+ "settings.showTable": "图表下方附带数据表格",
286
+ "settings.showCacheHitRate": "叠加缓存命中率折线",
287
+ "settings.showCacheHitRateHint": "在柱状图、折线图与组合图中叠加缓存命中率折线(右轴)",
288
+ "settings.interceptUsageButton": "点击「用量」按钮打开侧边抽屉",
289
+ "settings.interceptUsageButtonHint": "关闭后恢复 DSH 默认的用量弹窗。",
290
+ "settings.saved": "已保存",
291
+ "settings.saving": "保存中…",
292
+ "settings.saveFailed": "保存失败:{error}",
293
+ "settings.preview": "预览",
294
+ "settings.reset": "恢复默认",
295
+ "chart.bar": "柱状图",
296
+ "chart.line": "折线图",
297
+ "chart.pie": "饼图",
298
+ "chart.table": "表格",
299
+ "chart.multi-bar": "多柱状图",
300
+ "chart.multi-line": "多折线图",
301
+ "chart.bar-line": "柱状折线组合图",
302
+ "chart.loading": "正在加载图表…",
303
+ "settings.update": "更新版本",
304
+ "settings.update.checking": "正在检查更新…",
305
+ "settings.update.upToDate": "已是最新版本",
306
+ "settings.update.unpublished": "未发布到 registry,无法自动更新",
307
+ "settings.update.running": "正在更新…",
308
+ "settings.update.done": "更新完成:{target}",
309
+ "settings.update.failed": "更新失败:{error}",
310
+ "settings.update.local": "本地安装,请在源码目录重建并重新加载",
311
+ "settings.update.check": "检查更新",
312
+ "metric.total": "总 Token",
313
+ "metric.input": "输入 Token",
314
+ "metric.output": "输出 Token",
315
+ "metric.cache": "缓存 Token",
316
+ "metric.cacheHitRate": "缓存命中率",
317
+ "usage.title": "Token 用量",
318
+ "usage.session": "会话 Token 用量",
319
+ "usage.turns": "对话轮次",
320
+ "usage.sessions": "活跃会话",
321
+ "usage.cacheRead": "缓存读取",
322
+ "usage.cacheWrite": "缓存写入",
323
+ "usage.route": "模型",
324
+ "usage.openDrawer": "打开 Token 用量抽屉",
325
+ "usage.refresh": "刷新用量",
326
+ "table.day": "日期",
327
+ "table.turn": "轮次",
328
+ "table.time": "时间",
329
+ "table.metric": "指标",
330
+ "table.value": "数值",
331
+ "table.share": "占比",
332
+ "table.session": "会话",
333
+ "usage.emptySession": "该会话还没有可统计的 Token 用量",
334
+ "usage.accounting": "总量按计费口径累计:每次请求的上下文(含缓存重读)都计入,与 DSH 产品内「用量」一致",
335
+ "usage.crossCheckFailed": "与产品内「用量」投影不一致(产品 {product})——请反馈此差异"
336
+ };
337
+ var en = {
338
+ "panel.title": "Token usage",
339
+ "panel.subtitle": "Today, last 3 days, and last 7 days of token usage",
340
+ "panel.range.today": "Today",
341
+ "panel.range.3d": "Last 3 days",
342
+ "panel.range.7d": "Last 7 days",
343
+ "panel.refresh": "Refresh",
344
+ "panel.loading": "Collecting…",
345
+ "panel.empty": "No token usage recorded in this window",
346
+ "table.total": "{count} rows",
347
+ "table.pagination": "Pagination",
348
+ "table.prev": "Previous page",
349
+ "table.next": "Next page",
350
+ "table.page": "Page {page}",
351
+ "table.pageSize": "{size} / page",
352
+ "table.pageSizeLabel": "Rows per page",
353
+ "panel.updated": "Updated {time}",
354
+ "panel.retention": "Only the last {days} days are kept",
355
+ "settings.nav": "Token usage",
356
+ "settings.title": "Token usage charts",
357
+ "settings.description": "Choose the chart form and the metrics for each surface; changes apply immediately.",
358
+ "settings.sessionChart": "Session usage drawer",
359
+ "settings.sessionChartHint": "The chart shown in the right drawer after clicking the usage button.",
360
+ "settings.dailyChart": "Daily usage panel",
361
+ "settings.dailyChartHint": "The chart for Today / last 3 days / last 7 days in the sidebar panel.",
362
+ "settings.metrics": "Metrics",
363
+ "settings.showTable": "Show the data table under the chart",
364
+ "settings.showCacheHitRate": "Overlay the cache-hit-rate line",
365
+ "settings.showCacheHitRateHint": "Overlay the cache-hit-rate line (right axis) on bar, line and combo charts",
366
+ "settings.interceptUsageButton": "Open the drawer from the usage button",
367
+ "settings.interceptUsageButtonHint": "Turn off to restore the shipped usage dialog.",
368
+ "settings.saved": "Saved",
369
+ "settings.saving": "Saving…",
370
+ "settings.saveFailed": "Save failed: {error}",
371
+ "settings.preview": "Preview",
372
+ "settings.reset": "Restore defaults",
373
+ "chart.bar": "Bar",
374
+ "chart.line": "Line",
375
+ "chart.pie": "Pie",
376
+ "chart.table": "Table",
377
+ "chart.multi-bar": "Grouped bars",
378
+ "chart.multi-line": "Multi line",
379
+ "chart.bar-line": "Bar + line",
380
+ "chart.loading": "Loading chart…",
381
+ "settings.update": "Update",
382
+ "settings.update.checking": "Checking for updates…",
383
+ "settings.update.upToDate": "Up to date",
384
+ "settings.update.unpublished": "Not published to a registry",
385
+ "settings.update.running": "Updating…",
386
+ "settings.update.done": "Updated: {target}",
387
+ "settings.update.failed": "Update failed: {error}",
388
+ "settings.update.local": "Local install — rebuild and reload from the source directory",
389
+ "settings.update.check": "Check for updates",
390
+ "metric.total": "Total tokens",
391
+ "metric.input": "Input tokens",
392
+ "metric.output": "Output tokens",
393
+ "metric.cache": "Cache tokens",
394
+ "metric.cacheHitRate": "Cache hit rate",
395
+ "usage.title": "Token usage",
396
+ "usage.session": "Session token usage",
397
+ "usage.turns": "Turns",
398
+ "usage.sessions": "Active sessions",
399
+ "usage.cacheRead": "Cache read",
400
+ "usage.cacheWrite": "Cache write",
401
+ "usage.route": "Model",
402
+ "usage.openDrawer": "Open the token usage drawer",
403
+ "usage.refresh": "Refresh usage",
404
+ "table.day": "Day",
405
+ "table.turn": "Turn",
406
+ "table.time": "Time",
407
+ "table.metric": "Metric",
408
+ "table.value": "Value",
409
+ "table.share": "Share",
410
+ "table.session": "Session",
411
+ "usage.emptySession": "This session has no token usage yet",
412
+ "usage.accounting": "Totals follow the billing rule: every request re-reads the context (cache included) and counts — same as the product usage pill",
413
+ "usage.crossCheckFailed": "Differs from the product usage projection ({product}) — please report this"
414
+ };
415
+
416
+ // src/client/format.ts
417
+ function compactTokens(value) {
418
+ const number = Number(value) || 0;
419
+ if (Math.abs(number) >= 1e9) return `${(number / 1e9).toFixed(2)}B`;
420
+ if (Math.abs(number) >= 1e6) return `${(number / 1e6).toFixed(2)}M`;
421
+ if (Math.abs(number) >= 1e4) return `${(number / 1e3).toFixed(1)}K`;
422
+ return String(Math.round(number));
423
+ }
424
+ function exactTokens(value) {
425
+ return (Number(value) || 0).toLocaleString("en-US");
426
+ }
427
+ function percentText(rate) {
428
+ return `${((Number(rate) || 0) * 100).toFixed(1)}%`;
429
+ }
430
+ function clockText(time, withSeconds = false) {
431
+ const date = new Date(Number(time) || Date.now());
432
+ const pad = (input) => String(input).padStart(2, "0");
433
+ const base = `${pad(date.getHours())}:${pad(date.getMinutes())}`;
434
+ return withSeconds ? `${base}:${pad(date.getSeconds())}` : base;
435
+ }
436
+ function fill(template, values) {
437
+ return String(template).replace(
438
+ /\{(\w+)\}/g,
439
+ (_match, key) => values[key] === void 0 ? `{${key}}` : String(values[key])
440
+ );
441
+ }
442
+
443
+ // src/client/ui.tsx
444
+ var import_jsx_runtime2 = require("react/jsx-runtime");
445
+ function KpiRow({ totals, t }) {
446
+ const items = [
447
+ { key: "total", label: t("metric.total") },
448
+ { key: "input", label: t("metric.input") },
449
+ { key: "output", label: t("metric.output") },
450
+ { key: "cache", label: t("metric.cache") }
451
+ ];
452
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dtc-kpis", children: [
453
+ items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dtc-kpi", children: [
454
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dtc-kpi-label", children: item.label }),
455
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dtc-kpi-value", children: compactTokens(totals?.[item.key]) }),
456
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dtc-kpi-exact", children: exactTokens(totals?.[item.key]) })
457
+ ] }, item.key)),
458
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dtc-kpi", children: [
459
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dtc-kpi-label", children: t("metric.cacheHitRate") }),
460
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dtc-kpi-value", children: percentText(totals?.cacheHitRate) }),
461
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dtc-kpi-exact", children: `${exactTokens(totals?.cacheRead)} / ${exactTokens((totals?.cacheRead ?? 0) + (totals?.cacheWrite ?? 0))}` })
462
+ ] })
463
+ ] });
464
+ }
465
+ function UsageFacts({ totals, t, extra }) {
466
+ const rows = [
467
+ [t("usage.turns"), exactTokens(totals?.turns)],
468
+ [t("usage.cacheRead"), exactTokens(totals?.cacheRead)],
469
+ [t("usage.cacheWrite"), exactTokens(totals?.cacheWrite)],
470
+ [t("metric.input"), exactTokens(totals?.input)],
471
+ [t("metric.output"), exactTokens(totals?.output)],
472
+ [t("metric.cacheHitRate"), percentText(totals?.cacheHitRate)],
473
+ ...extra ?? []
474
+ ];
475
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("dl", { className: "dtc-facts", children: rows.flatMap(([label, value]) => [/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("dt", { children: label }, `${label}-dt`), /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("dd", { children: value }, `${label}-dd`)]) });
476
+ }
477
+ function Toolbar({ children }) {
478
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dtc-toolbar", children });
479
+ }
480
+ function Segmented({
481
+ value,
482
+ options,
483
+ onChange
484
+ }) {
485
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dtc-segmented", role: "tablist", children: options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
486
+ "button",
487
+ {
488
+ type: "button",
489
+ role: "tab",
490
+ "aria-selected": value === option.value,
491
+ className: option.value === value ? "dtc-segment dtc-segment-active" : "dtc-segment",
492
+ onClick: () => onChange(option.value),
493
+ children: option.label
494
+ },
495
+ option.value
496
+ )) });
497
+ }
498
+
499
+ // src/client/charts.tsx
500
+ var import_jsx_runtime3 = require("react/jsx-runtime");
501
+ function CartesianChart({
502
+ points,
503
+ series,
504
+ height = 210
505
+ }) {
506
+ const [ref, width] = useElementWidth();
507
+ const box = Math.max(220, width || 320);
508
+ const hasRight = series.some((entry) => entry.axis === "right");
509
+ const padding = { top: 12, right: hasRight ? 46 : 14, bottom: 30, left: 48 };
510
+ const innerWidth = Math.max(40, box - padding.left - padding.right);
511
+ const innerHeight = Math.max(40, height - padding.top - padding.bottom);
512
+ const leftMax = Math.max(
513
+ 1,
514
+ ...series.filter((entry) => entry.axis !== "right").flatMap((entry) => points.map((point) => Number(point.values[entry.key]) || 0))
515
+ );
516
+ const rightMax = Math.max(
517
+ 1e-4,
518
+ ...series.filter((entry) => entry.axis === "right").flatMap((entry) => points.map((point) => Number(point.values[entry.key]) || 0))
519
+ );
520
+ const scaleY = (value, axis) => {
521
+ const max = axis === "right" ? rightMax : leftMax;
522
+ return padding.top + innerHeight - Math.max(0, value) / max * innerHeight;
523
+ };
524
+ const step = innerWidth / Math.max(1, points.length);
525
+ const barSeries = series.filter((entry) => entry.type === "bar");
526
+ const barWidth = Math.max(2, Math.min(26, step * 0.72 / Math.max(1, barSeries.length)));
527
+ const gridLines = [0, 0.25, 0.5, 0.75, 1];
528
+ const labelEvery = Math.max(1, Math.ceil(points.length / Math.max(2, Math.floor(innerWidth / 56))));
529
+ const children = [];
530
+ for (const ratio of gridLines) {
531
+ const y = padding.top + innerHeight * ratio;
532
+ children.push(
533
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
534
+ "line",
535
+ {
536
+ x1: padding.left,
537
+ x2: padding.left + innerWidth,
538
+ y1: y,
539
+ y2: y,
540
+ stroke: "var(--dsw-alias-border-l1)",
541
+ strokeWidth: 1,
542
+ strokeDasharray: ratio === 1 ? void 0 : "3 4"
543
+ },
544
+ `grid-${ratio}`
545
+ )
546
+ );
547
+ }
548
+ children.push(
549
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
550
+ "text",
551
+ {
552
+ x: padding.left - 8,
553
+ y: padding.top + 4,
554
+ textAnchor: "end",
555
+ fontSize: 10,
556
+ fill: "var(--dsw-alias-label-secondary)",
557
+ children: compactTokens(leftMax)
558
+ },
559
+ "axis-left-max"
560
+ )
561
+ );
562
+ children.push(
563
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
564
+ "text",
565
+ {
566
+ x: padding.left - 8,
567
+ y: padding.top + innerHeight,
568
+ textAnchor: "end",
569
+ fontSize: 10,
570
+ fill: "var(--dsw-alias-label-secondary)",
571
+ children: "0"
572
+ },
573
+ "axis-left-zero"
574
+ )
575
+ );
576
+ if (hasRight) {
577
+ children.push(
578
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
579
+ "text",
580
+ {
581
+ x: padding.left + innerWidth + 8,
582
+ y: padding.top + 4,
583
+ fontSize: 10,
584
+ fill: "var(--dsw-alias-label-secondary)",
585
+ children: percentText(rightMax)
586
+ },
587
+ "axis-right-max"
588
+ )
589
+ );
590
+ }
591
+ points.forEach((point, index) => {
592
+ if (index % labelEvery !== 0) return;
593
+ children.push(
594
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
595
+ "text",
596
+ {
597
+ x: padding.left + step * index + step / 2,
598
+ y: padding.top + innerHeight + 16,
599
+ textAnchor: "middle",
600
+ fontSize: 10,
601
+ fill: "var(--dsw-alias-label-secondary)",
602
+ children: String(point.label ?? "")
603
+ },
604
+ `label-${index}`
605
+ )
606
+ );
607
+ });
608
+ barSeries.forEach((entry, seriesIndex) => {
609
+ points.forEach((point, index) => {
610
+ const value = Math.max(0, Number(point.values[entry.key]) || 0);
611
+ if (value <= 0) return;
612
+ const x = padding.left + step * index + step / 2 - barWidth * barSeries.length / 2 + barWidth * seriesIndex;
613
+ const y = scaleY(value, entry.axis);
614
+ children.push(
615
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
616
+ "rect",
617
+ {
618
+ x,
619
+ y,
620
+ width: barWidth,
621
+ height: Math.max(1, padding.top + innerHeight - y),
622
+ rx: Math.min(3, barWidth / 2),
623
+ fill: entry.color,
624
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("title", { children: `${entry.label}: ${exactTokens(value)}` })
625
+ },
626
+ `bar-${entry.key}-${index}`
627
+ )
628
+ );
629
+ });
630
+ });
631
+ for (const entry of series.filter((item) => item.type === "line")) {
632
+ const coordinates = points.map((point, index) => {
633
+ const value = Number(point.values[entry.key]) || 0;
634
+ return [padding.left + step * index + step / 2, scaleY(value, entry.axis)];
635
+ });
636
+ children.push(
637
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
638
+ "polyline",
639
+ {
640
+ points: coordinates.map((pair) => pair.join(",")).join(" "),
641
+ fill: "none",
642
+ stroke: entry.color,
643
+ strokeWidth: 2,
644
+ strokeLinejoin: "round",
645
+ strokeLinecap: "round"
646
+ },
647
+ `line-${entry.key}`
648
+ )
649
+ );
650
+ coordinates.forEach((pair, index) => {
651
+ children.push(
652
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("circle", { cx: pair[0], cy: pair[1], r: 2.6, fill: entry.color, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("title", { children: `${entry.label}: ${exactTokens(points[index].values[entry.key])}` }) }, `dot-${entry.key}-${index}`)
653
+ );
654
+ });
655
+ }
656
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dtc-chart", ref, children: [
657
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: box, height, role: "img", "aria-label": series.map((entry) => entry.label).join(", "), children }),
658
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Legend, { series })
659
+ ] });
660
+ }
661
+ function Legend({ series }) {
662
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dtc-legend", children: series.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "dtc-legend-item", children: [
663
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dtc-swatch", style: { background: entry.color } }),
664
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: entry.label })
665
+ ] }, entry.key)) });
666
+ }
667
+ function PieChart({ slices, t }) {
668
+ const [ref, width] = useElementWidth();
669
+ const box = Math.max(180, Math.min(width || 240, 240));
670
+ const size = box;
671
+ const radius = size / 2 - 6;
672
+ const inner = radius * 0.58;
673
+ const center = size / 2;
674
+ const total = slices.reduce((sum, slice) => sum + (Number(slice.value) || 0), 0);
675
+ if (total <= 0) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "dtc-empty", children: t("panel.empty") });
676
+ let angle = -Math.PI / 2;
677
+ const paths = slices.map((slice, index) => {
678
+ const fraction = (Number(slice.value) || 0) / total;
679
+ const sweep = fraction * Math.PI * 2;
680
+ const start = angle;
681
+ const end = angle + sweep;
682
+ angle = end;
683
+ const large = sweep > Math.PI ? 1 : 0;
684
+ const point = (r, a) => [center + r * Math.cos(a), center + r * Math.sin(a)];
685
+ const [x1, y1] = point(radius, start);
686
+ const [x2, y2] = point(radius, end);
687
+ const [x3, y3] = point(inner, end);
688
+ const [x4, y4] = point(inner, start);
689
+ const d = slice.value <= 0 ? "" : `M ${x1} ${y1} A ${radius} ${radius} 0 ${large} 1 ${x2} ${y2} L ${x3} ${y3} A ${inner} ${inner} 0 ${large} 0 ${x4} ${y4} Z`;
690
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
691
+ "path",
692
+ {
693
+ d,
694
+ fill: slice.color,
695
+ fillOpacity: slice.opacity ?? 1,
696
+ stroke: "var(--dsw-alias-bg-base)",
697
+ strokeWidth: 1.5,
698
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("title", { children: `${slice.label}: ${exactTokens(slice.value)} (${percentText(fraction)})` })
699
+ },
700
+ `slice-${slice.key}-${index}`
701
+ );
702
+ });
703
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dtc-chart dtc-chart-pie", ref, children: [
704
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("svg", { width: size, height: size, role: "img", "aria-label": t("chart.pie"), children: paths }),
705
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dtc-legend dtc-legend-column", children: slices.map((slice, index) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "dtc-legend-item", children: [
706
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "dtc-swatch", style: { background: slice.color, opacity: slice.opacity ?? 1 } }),
707
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: `${slice.label} · ${compactTokens(slice.value)} · ${percentText((Number(slice.value) || 0) / total)}` })
708
+ ] }, `legend-${slice.key}-${index}`)) })
709
+ ] });
710
+ }
711
+
712
+ // src/client/mount.tsx
713
+ var import_jsx_runtime4 = require("react/jsx-runtime");
714
+ var CHART_CHUNK = "./client.echarts.js";
715
+ var chartLibrary = null;
716
+ function loadChartLibrary() {
717
+ if (chartLibrary === null) {
718
+ chartLibrary = Promise.resolve().then(() => require.async(CHART_CHUNK)).then((chunk) => {
719
+ const library = chunk !== null && typeof chunk === "object" && chunk.default !== void 0 ? chunk.default : chunk;
720
+ if (library === null || typeof library !== "object" || typeof library.init !== "function") {
721
+ throw new Error("chart chunk did not export ECharts");
722
+ }
723
+ return library;
724
+ }).catch((error) => {
725
+ chartLibrary = null;
726
+ throw error;
727
+ });
728
+ }
729
+ return chartLibrary;
730
+ }
731
+ function themeTokens() {
732
+ const computed = typeof getComputedStyle === "function" ? getComputedStyle(document.body) : null;
733
+ const read = (name, fallback) => {
734
+ const value = computed === null ? "" : computed.getPropertyValue(name).trim();
735
+ return value === "" ? fallback : value;
736
+ };
737
+ return {
738
+ total: read("--dsw-alias-brand-primary", "#4C6EF5"),
739
+ input: read("--dsw-alias-state-warn-primary", "#F59F00"),
740
+ output: read("--dsw-alias-state-success-primary", "#37B24D"),
741
+ cache: read("--dsw-alias-state-idle-primary", "#868E96"),
742
+ hitRate: read("--dsw-alias-state-error-primary", "#F03E3E"),
743
+ secondary: read("--dsw-alias-label-secondary", "#646A73"),
744
+ border: read("--dsw-alias-border-l1", "#DEE0E3")
745
+ };
746
+ }
747
+ var TOKEN_OF_METRIC = {
748
+ total: "total",
749
+ input: "input",
750
+ output: "output",
751
+ cache: "cache",
752
+ cacheHitRate: "hitRate"
753
+ };
754
+ function withAlpha(color, alpha) {
755
+ const match = /^#([\da-f]{3}|[\da-f]{6})$/i.exec(String(color).trim());
756
+ if (match === null) return color;
757
+ const hex = match[1].length === 3 ? match[1].split("").map((char) => char + char).join("") : match[1];
758
+ const [r, g, b] = [0, 2, 4].map((offset) => Number.parseInt(hex.slice(offset, offset + 2), 16));
759
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
760
+ }
761
+ function useThemeRevision() {
762
+ const [revision, setRevision] = React.useState(0);
763
+ React.useEffect(() => {
764
+ if (typeof MutationObserver === "undefined") return void 0;
765
+ const observer = new MutationObserver(() => setRevision((value) => value + 1));
766
+ const options = { attributes: true, attributeFilter: ["data-ds-dark-theme", "class", "style"] };
767
+ observer.observe(document.documentElement, options);
768
+ observer.observe(document.body, options);
769
+ return () => observer.disconnect();
770
+ }, []);
771
+ return revision;
772
+ }
773
+ function echartsOption({ kind, points, series, slices }, tokens) {
774
+ const axisLabel = { color: tokens.secondary, fontSize: 10 };
775
+ const axisLine = { lineStyle: { color: tokens.border } };
776
+ const categories = points.map((point, index) => String(point.label ?? index + 1));
777
+ const hasRightAxis = series.some((entry) => entry.axis === "right");
778
+ const colorOf = (key) => tokens[TOKEN_OF_METRIC[key] ?? "total"];
779
+ if (kind === "pie") {
780
+ return {
781
+ animationDuration: 200,
782
+ textStyle: { color: tokens.secondary, fontSize: 11 },
783
+ tooltip: {
784
+ trigger: "item",
785
+ formatter: (params) => `${params.name}: ${exactTokens(params.value)} (${Number(params.percent).toFixed(1)}%)`
786
+ },
787
+ legend: {
788
+ bottom: 0,
789
+ itemWidth: 8,
790
+ itemHeight: 8,
791
+ textStyle: { color: tokens.secondary, fontSize: 11 }
792
+ },
793
+ series: [
794
+ {
795
+ type: "pie",
796
+ radius: ["46%", "70%"],
797
+ center: ["50%", "44%"],
798
+ avoidLabelOverlap: true,
799
+ label: { color: tokens.secondary, fontSize: 10 },
800
+ labelLine: { lineStyle: { color: tokens.border } },
801
+ data: slices.map((slice) => ({
802
+ name: slice.label,
803
+ value: slice.value,
804
+ itemStyle: { color: colorOf(slice.metric ?? "total"), opacity: slice.opacity ?? 1 }
805
+ }))
806
+ }
807
+ ]
808
+ };
809
+ }
810
+ const yAxis = [
811
+ {
812
+ type: "value",
813
+ splitLine: { lineStyle: { color: tokens.border, type: "dashed" } },
814
+ axisLabel: { ...axisLabel, formatter: (value) => compactTokens(value) }
815
+ }
816
+ ];
817
+ if (hasRightAxis) {
818
+ yAxis.push({
819
+ type: "value",
820
+ min: 0,
821
+ max: 1,
822
+ position: "right",
823
+ splitLine: { show: false },
824
+ axisLabel: { ...axisLabel, formatter: (value) => percentText(value) }
825
+ });
826
+ }
827
+ const zoomable = categories.length >= 2;
828
+ const legendShown = series.length > 1;
829
+ const dataZoom = zoomable ? [
830
+ { type: "inside", start: 0, end: 100 },
831
+ {
832
+ type: "slider",
833
+ start: 0,
834
+ end: 100,
835
+ height: 18,
836
+ bottom: 6,
837
+ borderColor: tokens.border,
838
+ backgroundColor: "transparent",
839
+ fillerColor: withAlpha(tokens.total, 0.12),
840
+ handleStyle: { color: tokens.total, borderColor: tokens.total },
841
+ moveHandleStyle: { color: tokens.total },
842
+ dataBackground: { lineStyle: { color: tokens.border }, areaStyle: { color: tokens.border, opacity: 0.35 } },
843
+ selectedDataBackground: {
844
+ lineStyle: { color: tokens.secondary },
845
+ areaStyle: { color: tokens.secondary, opacity: 0.3 }
846
+ },
847
+ textStyle: { color: tokens.secondary, fontSize: 10 },
848
+ brushSelect: false
849
+ }
850
+ ] : void 0;
851
+ return {
852
+ animationDuration: 200,
853
+ textStyle: { color: tokens.secondary, fontSize: 11 },
854
+ tooltip: { trigger: "axis", axisPointer: { type: "shadow" }, valueFormatter: (value) => exactTokens(value) },
855
+ legend: legendShown ? { bottom: zoomable ? 30 : 0, itemWidth: 8, itemHeight: 8, textStyle: { color: tokens.secondary, fontSize: 11 } } : { show: false },
856
+ grid: {
857
+ left: 4,
858
+ right: 4,
859
+ top: 14,
860
+ bottom: zoomable ? legendShown ? 74 : 48 : legendShown ? 26 : 6,
861
+ containLabel: true
862
+ },
863
+ ...dataZoom === void 0 ? {} : { dataZoom },
864
+ xAxis: { type: "category", data: categories, axisLabel, axisLine, axisTick: { show: false } },
865
+ yAxis,
866
+ series: series.map((entry) => ({
867
+ name: entry.label,
868
+ type: entry.type,
869
+ yAxisIndex: entry.axis === "right" ? 1 : 0,
870
+ barMaxWidth: 26,
871
+ itemStyle: { color: colorOf(entry.key) },
872
+ lineStyle: { color: colorOf(entry.key), width: 2 },
873
+ symbolSize: 5,
874
+ data: points.map((point) => Number(point.values[entry.key]) || 0),
875
+ ...entry.axis === "right" ? { tooltip: { valueFormatter: (value) => percentText(value) } } : {}
876
+ }))
877
+ };
878
+ }
879
+ function EchartsMount({
880
+ spec,
881
+ height,
882
+ fallback,
883
+ t
884
+ }) {
885
+ const [containerRef, width] = useElementWidth();
886
+ const holderRef = React.useRef(null);
887
+ const chartRef = React.useRef(null);
888
+ const [status, setStatus] = React.useState("pending");
889
+ const themeRevision = useThemeRevision();
890
+ const option = React.useMemo(
891
+ () => status === "failed" ? null : echartsOption(spec, themeTokens()),
892
+ // themeRevision re-reads the tokens after a light/dark switch
893
+ [spec, status, themeRevision]
894
+ );
895
+ React.useEffect(() => {
896
+ let live = true;
897
+ if (status === "failed" || width <= 0) return void 0;
898
+ loadChartLibrary().then((echarts) => {
899
+ if (!live) return;
900
+ const holder = holderRef.current;
901
+ if (holder === null) return;
902
+ let chart = chartRef.current;
903
+ if (chart === null) {
904
+ chart = echarts.init(holder, null, { renderer: "svg", width, height });
905
+ chartRef.current = chart;
906
+ }
907
+ chart.resize({ width, height });
908
+ chart.setOption(option, true);
909
+ setStatus("ready");
910
+ }).catch((error) => {
911
+ if (!live) return;
912
+ console.warn("[dsh-token-chart] chart chunk unavailable, using the built-in renderer:", error);
913
+ setStatus("failed");
914
+ });
915
+ return () => {
916
+ live = false;
917
+ };
918
+ }, [option, width, height, status]);
919
+ React.useEffect(
920
+ () => () => {
921
+ if (chartRef.current !== null) {
922
+ chartRef.current.dispose();
923
+ chartRef.current = null;
924
+ }
925
+ },
926
+ []
927
+ );
928
+ if (status === "failed") return fallback;
929
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
930
+ "div",
931
+ {
932
+ className: "dtc-chart",
933
+ ref: containerRef,
934
+ "data-dtc-renderer": status === "ready" ? "echarts" : "pending",
935
+ "data-dtc-chart": spec.kind,
936
+ "data-dtc-zoom": spec.zoom === true ? "slider" : "none",
937
+ "data-dtc-series": spec.series.length > 0 ? spec.series.map((entry) => entry.type).join(",") : spec.kind === "pie" ? "pie" : "",
938
+ children: [
939
+ status === "ready" ? null : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dtc-empty", children: t("chart.loading") }),
940
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
941
+ "div",
942
+ {
943
+ ref: holderRef,
944
+ className: "dtc-echarts",
945
+ style: { width: "100%", height: `${height}px`, display: status === "ready" ? "block" : "none" }
946
+ }
947
+ )
948
+ ]
949
+ }
950
+ );
951
+ }
952
+
953
+ // src/client/table.tsx
954
+ var import_jsx_runtime5 = require("react/jsx-runtime");
955
+ var PAGE_SIZES = [10, 20, 50];
956
+ var DEFAULT_PAGE_SIZE = 20;
957
+ function pageItems(current, pages) {
958
+ if (pages <= 7) return Array.from({ length: pages }, (_unused, index) => index + 1);
959
+ const items = [1];
960
+ const start = Math.max(2, current - 2);
961
+ const end = Math.min(pages - 1, current + 2);
962
+ if (start > 2) items.push("gap");
963
+ for (let page = start; page <= end; page += 1) items.push(page);
964
+ if (end < pages - 1) items.push("gap");
965
+ items.push(pages);
966
+ return items;
967
+ }
968
+ function DataTable({ columns, rows, t }) {
969
+ const [pageSize, setPageSize] = React.useState(DEFAULT_PAGE_SIZE);
970
+ const [page, setPage] = React.useState(1);
971
+ const pages = Math.max(1, Math.ceil(rows.length / pageSize));
972
+ const current = Math.min(Math.max(page, 1), pages);
973
+ const start = (current - 1) * pageSize;
974
+ const visible = rows.slice(start, start + pageSize);
975
+ React.useEffect(() => {
976
+ setPage(1);
977
+ }, [rows.length, pageSize]);
978
+ if (rows.length === 0) return null;
979
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "dtc-table-block", children: [
980
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "dtc-table-wrap", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("table", { className: "dtc-table", children: [
981
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("thead", { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("tr", { children: columns.map((column) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("th", { className: column.align === "right" ? "dtc-right" : void 0, children: column.label }, column.key)) }) }),
982
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("tbody", { children: visible.map((row, index) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("tr", { children: columns.map((column) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("td", { className: column.align === "right" ? "dtc-right" : void 0, children: column.render ? column.render(row) : String(row[column.key] ?? "") }, column.key)) }, row.key ?? index)) })
983
+ ] }) }),
984
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
985
+ "nav",
986
+ {
987
+ className: "dtc-pager tw:flex tw:items-center tw:justify-end tw:gap-1",
988
+ "data-dtc-pager": `${current}/${pages}`,
989
+ "aria-label": t("table.pagination"),
990
+ children: [
991
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dtc-pager-total", children: fill(t("table.total"), { count: exactTokens(rows.length) }) }),
992
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
993
+ "button",
994
+ {
995
+ type: "button",
996
+ className: "dtc-page dtc-page-nav",
997
+ "data-dtc-page-step": "prev",
998
+ disabled: current <= 1,
999
+ "aria-label": t("table.prev"),
1000
+ onClick: () => setPage((value) => Math.max(1, Math.min(value, pages) - 1)),
1001
+ children: "‹"
1002
+ }
1003
+ ),
1004
+ pageItems(current, pages).map(
1005
+ (item, index) => item === "gap" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dtc-page-gap", "aria-hidden": "true", children: "…" }, `gap-${index}`) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1006
+ "button",
1007
+ {
1008
+ type: "button",
1009
+ className: item === current ? "dtc-page dtc-page-current" : "dtc-page",
1010
+ "data-dtc-page-number": item,
1011
+ "aria-current": item === current ? "page" : void 0,
1012
+ "aria-label": fill(t("table.page"), { page: String(item) }),
1013
+ onClick: () => setPage(item),
1014
+ children: String(item)
1015
+ },
1016
+ item
1017
+ )
1018
+ ),
1019
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1020
+ "button",
1021
+ {
1022
+ type: "button",
1023
+ className: "dtc-page dtc-page-nav",
1024
+ "data-dtc-page-step": "next",
1025
+ disabled: current >= pages,
1026
+ "aria-label": t("table.next"),
1027
+ onClick: () => setPage((value) => Math.min(pages, Math.min(Math.max(value, 1), pages) + 1)),
1028
+ children: "›"
1029
+ }
1030
+ ),
1031
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "dtc-page-size", children: [
1032
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1033
+ "select",
1034
+ {
1035
+ className: "dtc-select",
1036
+ "data-dtc-page-size": pageSize,
1037
+ "aria-label": t("table.pageSizeLabel"),
1038
+ value: String(pageSize),
1039
+ onChange: (event) => setPageSize(Number(event.target.value)),
1040
+ children: PAGE_SIZES.map((size) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("option", { value: String(size), children: fill(t("table.pageSize"), { size: String(size) }) }, size))
1041
+ }
1042
+ ),
1043
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("svg", { className: "dtc-select-chevron", viewBox: "0 0 12 12", width: "12", height: "12", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M3 4.5 6 7.5 9 4.5", fill: "none", stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round" }) })
1044
+ ] })
1045
+ ]
1046
+ }
1047
+ )
1048
+ ] });
1049
+ }
1050
+
1051
+ // src/client/series.tsx
1052
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1053
+ function seriesFor(metrics, kind, showHitRate, t) {
1054
+ const wanted = METRIC_IDS2.filter((id) => metrics.includes(id));
1055
+ const list = wanted.length > 0 ? wanted : ["total"];
1056
+ const overlay = showHitRate ? [{ key: "cacheHitRate", label: t("metric.cacheHitRate"), color: COLOR.cacheHitRate, type: "line", axis: "right" }] : [];
1057
+ if (kind === "multi-bar" || kind === "multi-line") {
1058
+ const type = kind === "multi-bar" ? "bar" : "line";
1059
+ return [
1060
+ ...list.map((id) => ({
1061
+ key: id,
1062
+ label: t(`metric.${id}`),
1063
+ color: METRIC_COLOR[id],
1064
+ type
1065
+ })),
1066
+ ...overlay
1067
+ ];
1068
+ }
1069
+ const primary = list.includes("total") ? "total" : list[0];
1070
+ const bar = kind !== "line";
1071
+ return [
1072
+ { key: primary, label: t(`metric.${primary}`), color: METRIC_COLOR[primary], type: bar ? "bar" : "line" },
1073
+ ...overlay
1074
+ ];
1075
+ }
1076
+ function slicesFor(_kind, points, totals, metrics, t) {
1077
+ const wanted = METRIC_IDS2.filter((id) => metrics.includes(id));
1078
+ if (wanted.length > 1) {
1079
+ return wanted.map((id) => ({
1080
+ key: id,
1081
+ metric: id,
1082
+ label: t(`metric.${id}`),
1083
+ value: Number(totals?.[id]) || 0,
1084
+ color: METRIC_COLOR[id]
1085
+ })).filter((slice) => slice.value > 0);
1086
+ }
1087
+ const key = wanted[0] ?? "total";
1088
+ const sorted = [...points].sort((left, right) => (right.values[key] || 0) - (left.values[key] || 0));
1089
+ const head = sorted.slice(0, 8);
1090
+ const tail = sorted.slice(8);
1091
+ const slices = head.map((point, index) => ({
1092
+ key: `${key}-${index}`,
1093
+ metric: key,
1094
+ label: String(point.label ?? index),
1095
+ value: Number(point.values[key]) || 0,
1096
+ color: COLOR.total,
1097
+ opacity: Math.max(0.22, 1 - index * 0.11)
1098
+ }));
1099
+ if (tail.length > 0) {
1100
+ slices.push({
1101
+ key: `${key}-other`,
1102
+ metric: "cache",
1103
+ label: "…",
1104
+ value: tail.reduce((sum, point) => sum + (Number(point.values[key]) || 0), 0),
1105
+ color: COLOR.cache,
1106
+ opacity: 1
1107
+ });
1108
+ }
1109
+ return slices.filter((slice) => slice.value > 0);
1110
+ }
1111
+ function ChartBlock({
1112
+ kind,
1113
+ points,
1114
+ totals,
1115
+ metrics,
1116
+ columns,
1117
+ rows,
1118
+ showTable,
1119
+ showHitRate,
1120
+ t
1121
+ }) {
1122
+ if (points.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "dtc-empty", children: t("panel.empty") });
1123
+ const table = showTable ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(DataTable, { columns, rows, t }) : null;
1124
+ if (kind === "table") return table ?? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "dtc-empty", children: t("panel.empty") });
1125
+ const slices = kind === "pie" ? slicesFor(kind, points, totals ?? {}, metrics, t) : [];
1126
+ const series = kind === "pie" ? [] : seriesFor(metrics, kind, showHitRate, t);
1127
+ const zoom = kind !== "pie" && points.length >= 2;
1128
+ const fallback = kind === "pie" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PieChart, { slices, t }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CartesianChart, { points, series, t });
1129
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { children: [
1130
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(EchartsMount, { spec: { kind, points, series, slices, zoom }, height: zoom ? 248 : 210, fallback, t }),
1131
+ table
1132
+ ] });
1133
+ }
1134
+
1135
+ // src/client/panel.tsx
1136
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1137
+ function DailyUsagePanel({ t }) {
1138
+ const prefs = usePrefs();
1139
+ const [range, setRange] = React.useState("today");
1140
+ const [tick, setTick] = React.useState(0);
1141
+ const endpoint = useEndpoint(`/overview?range=${range}`, tick);
1142
+ const report = endpoint.value;
1143
+ const totals = report?.totals;
1144
+ const buckets = Array.isArray(report?.buckets) ? report.buckets : [];
1145
+ const points = Array.isArray(report?.points) ? report.points : [];
1146
+ const todayLabels = points.map((point) => clockText(point.time));
1147
+ const todayHasDuplicateMinutes = new Set(todayLabels).size < todayLabels.length;
1148
+ const chartPoints = range === "today" ? points.map((point, index) => ({
1149
+ label: todayHasDuplicateMinutes ? clockText(point.time, true) : todayLabels[index],
1150
+ values: {
1151
+ input: point.input,
1152
+ output: point.output,
1153
+ cache: point.cache,
1154
+ total: point.total,
1155
+ cacheHitRate: point.cacheHitRate
1156
+ }
1157
+ })) : buckets.map((bucket) => ({
1158
+ label: bucket.label,
1159
+ values: {
1160
+ input: bucket.input,
1161
+ output: bucket.output,
1162
+ cache: bucket.cache,
1163
+ total: bucket.total,
1164
+ cacheHitRate: bucket.cacheHitRate
1165
+ }
1166
+ }));
1167
+ const columns = range === "today" ? [
1168
+ { key: "time", label: t("table.time"), render: (row) => clockText(row.time) },
1169
+ { key: "session", label: t("table.session"), render: (row) => String(row.sessionId ?? "").slice(0, 8) },
1170
+ { key: "turn", label: t("table.turn") },
1171
+ { key: "total", label: t("metric.total"), align: "right", render: (row) => exactTokens(row.total) },
1172
+ { key: "input", label: t("metric.input"), align: "right", render: (row) => exactTokens(row.input) },
1173
+ { key: "output", label: t("metric.output"), align: "right", render: (row) => exactTokens(row.output) },
1174
+ { key: "cache", label: t("metric.cache"), align: "right", render: (row) => exactTokens(row.cache) },
1175
+ {
1176
+ key: "cacheHitRate",
1177
+ label: t("metric.cacheHitRate"),
1178
+ align: "right",
1179
+ render: (row) => percentText(row.cacheHitRate)
1180
+ }
1181
+ ] : [
1182
+ { key: "label", label: t("table.day") },
1183
+ { key: "turns", label: t("usage.turns"), align: "right" },
1184
+ { key: "sessions", label: t("usage.sessions"), align: "right" },
1185
+ { key: "total", label: t("metric.total"), align: "right", render: (row) => exactTokens(row.total) },
1186
+ { key: "input", label: t("metric.input"), align: "right", render: (row) => exactTokens(row.input) },
1187
+ { key: "output", label: t("metric.output"), align: "right", render: (row) => exactTokens(row.output) },
1188
+ { key: "cache", label: t("metric.cache"), align: "right", render: (row) => exactTokens(row.cache) },
1189
+ {
1190
+ key: "cacheHitRate",
1191
+ label: t("metric.cacheHitRate"),
1192
+ align: "right",
1193
+ render: (row) => percentText(row.cacheHitRate)
1194
+ }
1195
+ ];
1196
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "dtc-root dtc-panel", children: [
1197
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "dtc-head", children: [
1198
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
1199
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("h2", { className: "dtc-title", children: t("panel.title") }),
1200
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "dtc-subtitle", children: t("panel.subtitle") })
1201
+ ] }),
1202
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(Toolbar, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "dtc-button", onClick: () => setTick((value) => value + 1), children: t("panel.refresh") }) })
1203
+ ] }),
1204
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "dtc-filters", children: [
1205
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1206
+ Segmented,
1207
+ {
1208
+ value: range,
1209
+ onChange: setRange,
1210
+ options: [
1211
+ { value: "today", label: t("panel.range.today") },
1212
+ { value: "3d", label: t("panel.range.3d") },
1213
+ { value: "7d", label: t("panel.range.7d") }
1214
+ ]
1215
+ }
1216
+ ),
1217
+ report !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "dtc-meta", children: `${t("panel.updated", { time: clockText(report.generatedAt) })} · ${t("panel.retention", { days: report.retentionDays ?? 7 })}` }) : null
1218
+ ] }),
1219
+ endpoint.loading && report === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "dtc-empty", children: t("panel.loading") }) : endpoint.error !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("p", { className: "dtc-error", children: endpoint.error }) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(React.Fragment, { children: [
1220
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "dtc-banner", children: [
1221
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "dtc-banner-label", children: t("metric.total") }),
1222
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "dtc-banner-value", children: exactTokens(totals?.total) }),
1223
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "dtc-banner-extra", children: `${exactTokens(totals?.turns)} ${t("usage.turns")} · ${exactTokens(totals?.sessions)} ${t("usage.sessions")}` })
1224
+ ] }),
1225
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(KpiRow, { totals, t }),
1226
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "dtc-section-title", children: t("settings.dailyChart") }),
1227
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1228
+ ChartBlock,
1229
+ {
1230
+ kind: prefs.dailyChart,
1231
+ points: chartPoints,
1232
+ totals,
1233
+ metrics: prefs.dailyMetrics,
1234
+ columns,
1235
+ rows: range === "today" ? points : buckets,
1236
+ showTable: prefs.showTable,
1237
+ showHitRate: prefs.showCacheHitRate,
1238
+ t
1239
+ }
1240
+ )
1241
+ ] })
1242
+ ] });
1243
+ }
1244
+
1245
+ // src/client/drawer.tsx
1246
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1247
+ function SessionUsageBody({ sessionId, t }) {
1248
+ const prefs = usePrefs();
1249
+ const [tick, setTick] = React.useState(0);
1250
+ const endpoint = useEndpoint(`/session?sessionId=${encodeURIComponent(sessionId ?? "")}`, tick);
1251
+ const report = endpoint.value;
1252
+ const totals = report?.totals;
1253
+ const turns = Array.isArray(report?.turns) ? report.turns : [];
1254
+ const points = turns.map((turn) => ({
1255
+ label: `#${turn.turn}`,
1256
+ values: {
1257
+ input: turn.input,
1258
+ output: turn.output,
1259
+ cache: turn.cache,
1260
+ total: turn.total,
1261
+ cacheHitRate: turn.cacheHitRate
1262
+ }
1263
+ }));
1264
+ const columns = [
1265
+ { key: "turn", label: t("table.turn") },
1266
+ { key: "time", label: t("table.time"), render: (row) => clockText(row.time) },
1267
+ { key: "total", label: t("metric.total"), align: "right", render: (row) => exactTokens(row.total) },
1268
+ { key: "input", label: t("metric.input"), align: "right", render: (row) => exactTokens(row.input) },
1269
+ { key: "output", label: t("metric.output"), align: "right", render: (row) => exactTokens(row.output) },
1270
+ { key: "cache", label: t("metric.cache"), align: "right", render: (row) => exactTokens(row.cache) },
1271
+ {
1272
+ key: "cacheHitRate",
1273
+ label: t("metric.cacheHitRate"),
1274
+ align: "right",
1275
+ render: (row) => percentText(row.cacheHitRate)
1276
+ }
1277
+ ];
1278
+ const routes = Array.isArray(totals?.routes) && totals.routes.length > 0 ? [[t("usage.route"), totals.routes.join(", ")]] : [];
1279
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "dtc-root dtc-drawer", children: [
1280
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "dtc-head", children: [
1281
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { children: [
1282
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h3", { className: "dtc-title", children: t("usage.title") }),
1283
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "dtc-subtitle", children: `${t("usage.session")} · ${turns.length} ${t("usage.turns")}` })
1284
+ ] }),
1285
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1286
+ "button",
1287
+ {
1288
+ type: "button",
1289
+ className: "dtc-button",
1290
+ onClick: () => setTick((value) => value + 1),
1291
+ title: t("usage.refresh"),
1292
+ children: t("panel.refresh")
1293
+ }
1294
+ )
1295
+ ] }),
1296
+ endpoint.loading && report === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "dtc-empty", children: t("panel.loading") }) : endpoint.error !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "dtc-error", children: endpoint.error }) : turns.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "dtc-empty", children: t("usage.emptySession") }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(React.Fragment, { children: [
1297
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "dtc-banner", children: [
1298
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "dtc-banner-label", children: t("metric.total") }),
1299
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "dtc-banner-value", children: exactTokens(totals?.total) })
1300
+ ] }),
1301
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(UsageFacts, { totals, t, extra: routes }),
1302
+ report?.crossCheck?.matches === false ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "dtc-warn", "data-dtc-crosscheck": "mismatch", children: fill(t("usage.crossCheckFailed"), { product: exactTokens(report.crossCheck.total) }) }) : null,
1303
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "dtc-footnote", children: t("usage.accounting") }),
1304
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(KpiRow, { totals, t }),
1305
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "dtc-section-title", children: t("settings.sessionChart") }),
1306
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1307
+ ChartBlock,
1308
+ {
1309
+ kind: prefs.sessionChart,
1310
+ points,
1311
+ totals,
1312
+ metrics: prefs.sessionMetrics,
1313
+ columns,
1314
+ rows: turns,
1315
+ showTable: prefs.showTable,
1316
+ showHitRate: prefs.showCacheHitRate,
1317
+ t
1318
+ }
1319
+ )
1320
+ ] })
1321
+ ] });
1322
+ }
1323
+
1324
+ // src/client/settings.tsx
1325
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1326
+ function ChartPicker({
1327
+ value,
1328
+ onChange,
1329
+ t
1330
+ }) {
1331
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "dtc-picker", children: CHART_KINDS.map((kind) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1332
+ "button",
1333
+ {
1334
+ type: "button",
1335
+ className: kind === value ? "dtc-chip dtc-chip-active" : "dtc-chip",
1336
+ "aria-pressed": kind === value,
1337
+ onClick: () => onChange(kind),
1338
+ children: t(`chart.${kind}`)
1339
+ },
1340
+ kind
1341
+ )) });
1342
+ }
1343
+ function MetricPicker({
1344
+ value,
1345
+ onChange,
1346
+ t
1347
+ }) {
1348
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "dtc-picker", children: METRIC_IDS2.map((id) => {
1349
+ const active = value.includes(id);
1350
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1351
+ "button",
1352
+ {
1353
+ type: "button",
1354
+ className: active ? "dtc-chip dtc-chip-active" : "dtc-chip",
1355
+ "aria-pressed": active,
1356
+ onClick: () => onChange(active ? value.filter((item) => item !== id) : [...value, id]),
1357
+ children: t(`metric.${id}`)
1358
+ },
1359
+ id
1360
+ );
1361
+ }) });
1362
+ }
1363
+ function SwitchRow({
1364
+ label,
1365
+ hint,
1366
+ checked,
1367
+ onChange
1368
+ }) {
1369
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dtc-switch-row", children: [
1370
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dtc-switch-text", children: [
1371
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dtc-switch-label", children: label }),
1372
+ hint !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dtc-switch-hint", children: hint }) : null
1373
+ ] }),
1374
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1375
+ "button",
1376
+ {
1377
+ type: "button",
1378
+ role: "switch",
1379
+ "aria-checked": checked,
1380
+ className: checked ? "dtc-switch dtc-switch-on" : "dtc-switch",
1381
+ onClick: () => onChange(!checked),
1382
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dtc-switch-knob" })
1383
+ }
1384
+ )
1385
+ ] });
1386
+ }
1387
+ function PluginHeader({ t }) {
1388
+ const [state, setState] = React.useState({
1389
+ loading: true,
1390
+ value: void 0,
1391
+ error: ""
1392
+ });
1393
+ const [run, setRun] = React.useState({
1394
+ phase: "idle",
1395
+ startedAt: 0,
1396
+ finishedAt: 0,
1397
+ message: "",
1398
+ error: ""
1399
+ });
1400
+ const [tick, setTick] = React.useState(0);
1401
+ React.useEffect(() => {
1402
+ let live = true;
1403
+ apiGet("/version").then((payload) => {
1404
+ if (!live) return;
1405
+ setState({ loading: false, value: payload.value, error: "" });
1406
+ if (payload.value?.run !== void 0) setRun(payload.value.run);
1407
+ }).catch((failure) => {
1408
+ if (!live) return;
1409
+ setState({ loading: false, value: void 0, error: String(failure?.message ?? failure) });
1410
+ });
1411
+ return () => {
1412
+ live = false;
1413
+ };
1414
+ }, [tick]);
1415
+ React.useEffect(() => {
1416
+ if (run.phase !== "running") return void 0;
1417
+ const timer = setTimeout(() => setTick((value) => value + 1), 2500);
1418
+ return () => clearTimeout(timer);
1419
+ }, [run.phase, tick]);
1420
+ const version = state.value?.version;
1421
+ const updateAvailable = state.value?.updateAvailable === true;
1422
+ const status = state.value?.status;
1423
+ const note = state.error !== "" ? state.error : run.phase === "running" ? t("settings.update.running") : run.phase === "done" ? fill(t("settings.update.done"), { target: state.value?.latest ?? "" }) : run.phase === "failed" ? fill(t("settings.update.failed"), { error: run.error }) : run.phase === "manual" ? t("settings.update.local") : state.loading ? t("settings.update.checking") : updateAvailable ? "" : status === "unpublished" ? t("settings.update.unpublished") : status === "ok" ? t("settings.update.upToDate") : "";
1424
+ const trigger = async () => {
1425
+ try {
1426
+ await apiPost("/update", {});
1427
+ setRun({ phase: "running", startedAt: Date.now(), finishedAt: 0, message: "", error: "" });
1428
+ } catch (failure) {
1429
+ setRun({
1430
+ phase: "failed",
1431
+ startedAt: Date.now(),
1432
+ finishedAt: Date.now(),
1433
+ message: "",
1434
+ error: String(failure?.message ?? failure)
1435
+ });
1436
+ } finally {
1437
+ setTick((value) => value + 1);
1438
+ }
1439
+ };
1440
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dtc-plugin-head", children: [
1441
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dtc-plugin-id", children: [
1442
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dtc-plugin-icon", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PanelIcon, { size: 18 }) }),
1443
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h3", { className: "dtc-title", children: t("settings.title") }),
1444
+ version !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dtc-plugin-name", children: state.value?.name ?? "" }) : null,
1445
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dtc-plugin-version", "data-dtc-version": version ?? "", children: version === void 0 ? t("settings.update.checking") : `v${version}` }),
1446
+ updateAvailable ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1447
+ "button",
1448
+ {
1449
+ type: "button",
1450
+ className: "dtc-update-button",
1451
+ "data-dtc-update": "available",
1452
+ disabled: run.phase === "running",
1453
+ onClick: () => {
1454
+ void trigger();
1455
+ },
1456
+ children: run.phase === "running" ? t("settings.update.running") : t("settings.update")
1457
+ }
1458
+ ) : null
1459
+ ] }),
1460
+ note === "" ? null : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dtc-plugin-note", "data-dtc-update-note": run.phase, children: note })
1461
+ ] });
1462
+ }
1463
+ function TokenChartSettings({ t }) {
1464
+ const prefs = usePrefs();
1465
+ const [status, setStatus] = React.useState("idle");
1466
+ const [error, setError] = React.useState("");
1467
+ const timer = React.useRef(null);
1468
+ const save = React.useCallback((patch) => {
1469
+ const next = { ...prefsStore.get(), ...patch };
1470
+ prefsStore.set(next);
1471
+ setStatus("saving");
1472
+ if (timer.current !== null) clearTimeout(timer.current);
1473
+ timer.current = setTimeout(() => {
1474
+ apiPost("/prefs", { patch }).then((payload) => {
1475
+ prefsStore.set({ ...DEFAULT_PREFS, ...payload.value });
1476
+ setStatus("saved");
1477
+ setError("");
1478
+ }).catch((failure) => {
1479
+ setStatus("failed");
1480
+ setError(String(failure?.message ?? failure));
1481
+ });
1482
+ }, 260);
1483
+ }, []);
1484
+ React.useEffect(
1485
+ () => () => {
1486
+ if (timer.current !== null) clearTimeout(timer.current);
1487
+ },
1488
+ []
1489
+ );
1490
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dtc-root dtc-settings", children: [
1491
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PluginHeader, { t }),
1492
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "dtc-subtitle", children: t("settings.description") }),
1493
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { className: "dtc-card", children: [
1494
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h4", { className: "dtc-card-title", children: t("settings.sessionChart") }),
1495
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "dtc-card-hint", children: t("settings.sessionChartHint") }),
1496
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ChartPicker, { value: prefs.sessionChart, onChange: (value) => save({ sessionChart: value }), t }),
1497
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "dtc-field-label", children: t("settings.metrics") }),
1498
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(MetricPicker, { value: prefs.sessionMetrics, onChange: (value) => save({ sessionMetrics: value }), t })
1499
+ ] }),
1500
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { className: "dtc-card", children: [
1501
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h4", { className: "dtc-card-title", children: t("settings.dailyChart") }),
1502
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("p", { className: "dtc-card-hint", children: t("settings.dailyChartHint") }),
1503
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ChartPicker, { value: prefs.dailyChart, onChange: (value) => save({ dailyChart: value }), t }),
1504
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "dtc-field-label", children: t("settings.metrics") }),
1505
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(MetricPicker, { value: prefs.dailyMetrics, onChange: (value) => save({ dailyMetrics: value }), t })
1506
+ ] }),
1507
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("section", { className: "dtc-card", children: [
1508
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1509
+ SwitchRow,
1510
+ {
1511
+ label: t("settings.showTable"),
1512
+ checked: prefs.showTable,
1513
+ onChange: (value) => save({ showTable: value })
1514
+ }
1515
+ ),
1516
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1517
+ SwitchRow,
1518
+ {
1519
+ label: t("settings.showCacheHitRate"),
1520
+ hint: t("settings.showCacheHitRateHint"),
1521
+ checked: prefs.showCacheHitRate,
1522
+ onChange: (value) => save({ showCacheHitRate: value })
1523
+ }
1524
+ ),
1525
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1526
+ SwitchRow,
1527
+ {
1528
+ label: t("settings.interceptUsageButton"),
1529
+ hint: t("settings.interceptUsageButtonHint"),
1530
+ checked: prefs.interceptUsageButton,
1531
+ onChange: (value) => save({ interceptUsageButton: value })
1532
+ }
1533
+ )
1534
+ ] }),
1535
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dtc-settings-footer", children: [
1536
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: status === "failed" ? "dtc-status dtc-status-error" : "dtc-status", children: status === "saving" ? t("settings.saving") : status === "failed" ? fill(t("settings.saveFailed"), { error }) : status === "saved" ? t("settings.saved") : "" }),
1537
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1538
+ "button",
1539
+ {
1540
+ type: "button",
1541
+ className: "dtc-button",
1542
+ onClick: () => {
1543
+ const defaults = { ...DEFAULT_PREFS };
1544
+ prefsStore.set(defaults);
1545
+ apiPost("/prefs", { patch: defaults }).then((payload) => {
1546
+ prefsStore.set({ ...DEFAULT_PREFS, ...payload.value });
1547
+ setStatus("saved");
1548
+ }).catch((failure) => {
1549
+ setStatus("failed");
1550
+ setError(String(failure?.message ?? failure));
1551
+ });
1552
+ },
1553
+ children: t("settings.reset")
1554
+ }
1555
+ )
1556
+ ] })
1557
+ ] });
1558
+ }
1559
+
1560
+ // src/client/index.tsx
1561
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1562
+ var inject = ["slots", "locale"];
1563
+ function apply(ctx) {
1564
+ const t = ctx.locale.bind(NS);
1565
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-token-chart: dictionaries");
1566
+ ctx.effect(() => installStyles(), "dsh-token-chart: styles");
1567
+ ctx.effect(() => registerSettingsNavIcon(() => t("settings.nav")), "dsh-token-chart: settings nav icon");
1568
+ const services = {
1569
+ sidebarRight: void 0,
1570
+ sidebarRightTabs: void 0
1571
+ };
1572
+ const openDrawer = (sessionId) => {
1573
+ const right = services.sidebarRight;
1574
+ if (right === void 0 || typeof sessionId !== "string" || sessionId === "") return false;
1575
+ try {
1576
+ right.openTab(KIND, { params: { sessionId } });
1577
+ return true;
1578
+ } catch (error) {
1579
+ console.warn("[dsh-token-chart] could not open the usage drawer:", error);
1580
+ return false;
1581
+ }
1582
+ };
1583
+ const mountedSessionId = () => {
1584
+ const right = services.sidebarRight;
1585
+ if (right === void 0 || right.mounted === void 0) return void 0;
1586
+ try {
1587
+ return right.mounted.getSnapshot();
1588
+ } catch {
1589
+ return void 0;
1590
+ }
1591
+ };
1592
+ ctx.effect(() => {
1593
+ let live = true;
1594
+ apiGet("/prefs").then((payload) => {
1595
+ if (live) prefsStore.set({ ...DEFAULT_PREFS, ...payload.value });
1596
+ }).catch(() => {
1597
+ });
1598
+ return () => {
1599
+ live = false;
1600
+ };
1601
+ }, "dsh-token-chart: preferences");
1602
+ ctx.slots.inject(
1603
+ "settings.section",
1604
+ () => ctx.slots.register(
1605
+ {
1606
+ name: "settings.section",
1607
+ id: "token-chart",
1608
+ order: 30,
1609
+ label: () => t("settings.nav"),
1610
+ locale: NS
1611
+ },
1612
+ TokenChartSettings
1613
+ )
1614
+ );
1615
+ ctx.slots.inject(
1616
+ "sidebar.panellist",
1617
+ () => ctx.slots.register(
1618
+ {
1619
+ name: "sidebar.panellist",
1620
+ id: PANEL_ID,
1621
+ order: 12,
1622
+ label: () => t("panel.title"),
1623
+ locale: NS
1624
+ },
1625
+ PanelIcon
1626
+ )
1627
+ );
1628
+ ctx.slots.inject(
1629
+ "main",
1630
+ () => ctx.slots.register(
1631
+ {
1632
+ name: "main",
1633
+ key: PANEL_ID,
1634
+ locale: NS
1635
+ },
1636
+ DailyUsagePanel
1637
+ )
1638
+ );
1639
+ ctx.slots.inject(
1640
+ "conversation.session.header.utilities",
1641
+ () => ctx.slots.register(
1642
+ {
1643
+ name: "conversation.session.header.utilities",
1644
+ id: "token-chart",
1645
+ order: 6,
1646
+ locale: NS,
1647
+ inject: (sessionId) => ({ sessionId, open: openDrawer })
1648
+ },
1649
+ HeaderUsageButton
1650
+ )
1651
+ );
1652
+ ctx.inject(["sidebarRightTabs"], (injected) => {
1653
+ const tabs = injected.get("sidebarRightTabs");
1654
+ if (tabs === void 0) return void 0;
1655
+ services.sidebarRightTabs = tabs;
1656
+ const disposers = [];
1657
+ try {
1658
+ disposers.push(
1659
+ tabs.register({
1660
+ id: KIND,
1661
+ kind: KIND,
1662
+ priority: "extension",
1663
+ title: () => t("usage.title"),
1664
+ guide: [
1665
+ {
1666
+ id: KIND,
1667
+ order: 40,
1668
+ title: () => t("usage.title"),
1669
+ description: () => t("usage.session")
1670
+ }
1671
+ ]
1672
+ })
1673
+ );
1674
+ } catch (error) {
1675
+ console.warn("[dsh-token-chart] tab type registration failed:", error);
1676
+ return void 0;
1677
+ }
1678
+ disposers.push(
1679
+ ctx.slots.inject(
1680
+ "sidebar.right.pane.tab",
1681
+ () => ctx.slots.register(
1682
+ {
1683
+ name: "sidebar.right.pane.tab",
1684
+ key: KIND,
1685
+ locale: NS,
1686
+ inject: (sessionId) => ({ sessionId })
1687
+ },
1688
+ SessionUsageBody
1689
+ )
1690
+ )
1691
+ );
1692
+ disposers.push(
1693
+ ctx.slots.inject(
1694
+ "sidebar.right.pane.tab.title",
1695
+ () => ctx.slots.register(
1696
+ {
1697
+ name: "sidebar.right.pane.tab.title",
1698
+ key: KIND,
1699
+ locale: NS
1700
+ },
1701
+ ({ t: translate }) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "dtc-tab-title", children: translate("usage.title") })
1702
+ )
1703
+ )
1704
+ );
1705
+ return () => {
1706
+ services.sidebarRightTabs = void 0;
1707
+ for (const dispose of disposers) {
1708
+ try {
1709
+ dispose();
1710
+ } catch {
1711
+ }
1712
+ }
1713
+ };
1714
+ });
1715
+ ctx.inject(["sidebarRight"], (injected) => {
1716
+ services.sidebarRight = injected.get("sidebarRight");
1717
+ return () => {
1718
+ services.sidebarRight = void 0;
1719
+ };
1720
+ });
1721
+ ctx.effect(() => {
1722
+ const onClick = (event) => {
1723
+ if (prefsStore.get().interceptUsageButton !== true) return;
1724
+ if (isUsagePill(event.target) === false) return;
1725
+ const sessionId = mountedSessionId();
1726
+ if (sessionId === void 0) return;
1727
+ if (openDrawer(sessionId) === false) return;
1728
+ event.preventDefault();
1729
+ event.stopPropagation();
1730
+ };
1731
+ document.addEventListener("click", onClick, true);
1732
+ return () => document.removeEventListener("click", onClick, true);
1733
+ }, "dsh-token-chart: usage pill interception");
1734
+ ctx.effect(
1735
+ () => () => {
1736
+ },
1737
+ "dsh-token-chart: teardown"
1738
+ );
1739
+ }
1740
+
1741
+ return module.exports;
1742
+ } });
1743
+