dsh-token-use 0.2.0 → 0.2.2

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/README.en.md CHANGED
@@ -84,6 +84,14 @@ pnpm run build # regenerate client/client.js (= tree-shaken ECharts + cli
84
84
 
85
85
  `client/client.js` is a committed build artifact, so users install nothing and build nothing.
86
86
 
87
+ **Development loop** (with this repo linked into a profile: `dsh plugin --profile web add /path/to/dsh-token-use`):
88
+
89
+ - Edit `client/src.js` → `pnpm run build` → **the panel in the browser hot-reloads by itself** (the host stat-polls every plugin row's client bundle, pushes `rebuilt` over the SSE channel `/plugins/events`, and the browser swaps it in) — no `dsh web` restart and no page refresh; measured swap latency is under a second.
90
+ - Edit `lib/*.js` (the host half, e.g. `pricing.js`) → still needs a `dsh web` restart.
91
+ - Hot reload requires an **idempotent `apply()`**: a reload brings a new fiber in, and re-registering the same locale namespace or slot on top of the previous one throws, which takes the whole settings section down until the page is refreshed. The dictionaries, the rail stylesheet and the `settings.section` registration here all take over from the previous fiber, so reloads are safe.
92
+
93
+ The UI is built with DSH's own component library `@deepseek-ai/dsh-client-ui-primitives` (`Button` / `Pill` / `Input` / `Menu` / `Tooltip` / `StateDot` and its icon set): a packaged plugin just `require`s it, and each control degrades to a native one on a shell that predates it. When previewing a dynamic cordis plugin in creator mode — where external imports are unavailable — use `window.__DSH_MODULES__.import("@deepseek-ai/dsh-client-ui-primitives")` instead (the module is one of the shell's static modules; a normal Web GUI has no `window.__DSH_MODULES__`).
94
+
87
95
  Published to npm as `dsh-token-use`; its `repository` field points back here, which is how the official market links the package and shows download counts. To cut a release: bump `version`, run `npm publish`, and users pick it up with `dsh plugin update` or the market's update button.
88
96
 
89
97
  ## Field definitions
package/README.md CHANGED
@@ -84,6 +84,14 @@ pnpm run build # 重新生成 client/client.js(= 精简 ECharts + clien
84
84
 
85
85
  `client/client.js` 是已提交的构建产物,使用者无需安装依赖或构建。
86
86
 
87
+ **开发环路**(把本仓库以 `link:` 装进 profile 后:`dsh plugin --profile web add /path/to/dsh-token-use`):
88
+
89
+ - 改 `client/src.js` → `pnpm run build` → **浏览器里的面板自动热更新**(宿主侧会轮询每个插件行的 client bundle,变化时通过 SSE `/plugins/events` 推 `rebuilt`,浏览器侧热替换),**不用重启 `dsh web`、也不用刷新页面**;实测换装延迟 < 1s。
90
+ - 改 `lib/*.js`(宿主侧,如 `pricing.js`)→ 仍需重启 `dsh web`。
91
+ - **热重载要求 `apply()` 幂等**:重载会把新 fiber 装进来,若它直接重复注册同一个 locale 命名空间 / 槽位就会抛错,整个设置分区会消失直到刷新页面。本插件的字典、导航样式与 `settings.section` 注册都是「接管式」的(先退掉旧注册再登记),因此可以安全热重载。
92
+
93
+ 界面用 DSH 自带的组件库 `@deepseek-ai/dsh-client-ui-primitives`(`Button` / `Pill` / `Input` / `Menu` / `Tooltip` / `StateDot` 与图标集):打包插件里直接 `require("@deepseek-ai/dsh-client-ui-primitives")` 即可;老版本 shell 上组件缺失时逐项降级为原生控件。**创造模式预览动态 cordis 插件**时无法引入外部依赖,可改用 `window.__DSH_MODULES__.import("@deepseek-ai/dsh-client-ui-primitives")`(该模块属于 shell 的静态模块,普通 Web GUI 里没有 `window.__DSH_MODULES__`)。
94
+
87
95
  已发布到 npm(包名 `dsh-token-use`,`repository` 指回本仓库,因此官方市场会自动关联并显示下载量)。升级流程:改 `version` → `npm publish` → 用户 `dsh plugin update` 或市场一键更新。
88
96
 
89
97
  ## 字段口径
package/client/client.js CHANGED
@@ -27,18 +27,48 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
27
27
  var h = react.createElement;
28
28
 
29
29
  /**
30
- * The app's own floating tooltip, when this build exposes it: it positions in
31
- * viewport coordinates, clamps to the window and flips sides, so it is never
32
- * clipped by the settings panel. Older builds fall back to the title
33
- * attribute, which the browser draws itself.
30
+ * The DSH UI primitives: controls, icons and the floating layers every other
31
+ * plugin builds with, so this panel uses the shell's own components instead of
32
+ * re-inventing them. A packaged plugin resolves the module like any dependency
33
+ * (it is one of the shell's static modules); when a shell predates a component
34
+ * the small fallbacks below keep the panel usable rather than blank.
34
35
  */
35
- var tooltip = null;
36
+ var ui = {};
36
37
  try {
37
- var primitives = require("@deepseek-ai/dsh-client-ui-primitives");
38
- if (primitives !== null && typeof primitives === "object" && typeof primitives.Tooltip === "function") tooltip = primitives.Tooltip;
38
+ ui = require("@deepseek-ai/dsh-client-ui-primitives") || {};
39
39
  } catch (error) {
40
- tooltip = null;
40
+ ui = {};
41
41
  }
42
+ var component = function (name, fallback) {
43
+ return typeof ui[name] === "function" ? ui[name] : fallback;
44
+ };
45
+ /**
46
+ * Tooltip is the right layer for the ⓘ: it positions in viewport coordinates,
47
+ * clamps to the window and flips sides, so an anchor at the panel's right edge
48
+ * still shows the whole note. (HoverCard always opens to the *right* of its
49
+ * anchor and never clamps, so there it gets cut off by the window.)
50
+ */
51
+ var tooltip = component("Tooltip", null);
52
+ var stateDot = component("StateDot", null);
53
+ var menu = component("Menu", null);
54
+ var icon = function (name) {
55
+ return typeof ui[name] === "function" ? ui[name] : null;
56
+ };
57
+ var iconQuestion = icon("IconQuestionOutline14");
58
+ var iconChevron = icon("IconChevronDownOutline14");
59
+ var iconRefresh = icon("IconRefreshOutline14");
60
+ var iconGauge = icon("IconGaugeOutline16");
61
+ var iconTrend = icon("IconDataOutline16");
62
+ /** Chip and Button degrade into styled native controls on an older shell. */
63
+ var chip = component("Pill", function (props) {
64
+ return h("button", { type: "button", className: "dshtu_seg", "data-on": props.active === true, onClick: props.onClick, disabled: props.disabled === true }, props.children);
65
+ });
66
+ var button = component("Button", function (props) {
67
+ return h("button", { type: "button", className: "dshtu_btn", onClick: props.onClick, disabled: props.disabled === true }, props.icon, props.children);
68
+ });
69
+ var input = component("Input", function (props) {
70
+ return h("input", { type: props.type, value: props.value, onChange: props.onChange, className: "dshtu_input" });
71
+ });
42
72
 
43
73
  var NS = "settings.tokenUsage";
44
74
  var inject = ["slots", "locale"];
@@ -86,6 +116,8 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
86
116
  auxiliaryNote: "另有 {count} 次辅助调用({detail})只计次数、不计金额:token 数在服务端,本地取不到。",
87
117
  auxSearch: "联网搜索",
88
118
  auxTitle: "会话标题",
119
+ copy: "复制说明",
120
+ copied: "已复制",
89
121
  reasonExcluded: "非 DeepSeek 模型",
90
122
  reasonUnmatched: "未匹配到官方定价",
91
123
  unitOffPeak: "空闲",
@@ -142,6 +174,8 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
142
174
  auxiliaryNote: "A further {count} auxiliary calls ({detail}) are counted but not priced: their tokens exist only server-side.",
143
175
  auxSearch: "web search",
144
176
  auxTitle: "session titles",
177
+ copy: "Copy note",
178
+ copied: "Copied",
145
179
  reasonExcluded: "not a DeepSeek model",
146
180
  reasonUnmatched: "no matching official price",
147
181
  unitOffPeak: "off-peak",
@@ -156,7 +190,7 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
156
190
  retry: "Retry"
157
191
  };
158
192
 
159
- var css = ".dshtu_wrap{max-width:860px;display:flex;flex-direction:column;gap:14px;padding:2px 2px 4px;color:var(--dsw-alias-label-primary);font-size:13px}.dshtu_card{background:var(--dsw-alias-bg-layer-3);border-radius:14px;box-shadow:var(--dsw-elevation-stroke);padding:14px 16px}.dshtu_bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.dshtu_bar label{color:var(--dsw-alias-label-tertiary);font-size:12px}.dshtu_bar select,.dshtu_bar input{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;padding:4px 8px;font-size:12.5px}.dshtu_bar select{width:168px;text-overflow:ellipsis;white-space:nowrap}.dshtu_field{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}.dshtu_seg{display:inline-flex;border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;overflow:hidden}.dshtu_seg button{border:0;background:0 0;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;padding:4px 10px;font-size:12.5px}.dshtu_seg button[data-on=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dshtu_totalsMeta{margin-top:12px}.dshtu_grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-top:8px}.dshtu_stat{background:var(--dsw-alias-bg-module-platform);border-radius:10px;padding:10px 12px}.dshtu_stat b{display:block;font-size:16px;line-height:24px;font-variant-numeric:tabular-nums}.dshtu_stat span{color:var(--dsw-alias-label-tertiary);font-size:12px}.dshtu_stat[data-main=true]{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,var(--dsw-alias-bg-module-platform))}.dshtu_stat[data-main=true] b{color:var(--dsw-alias-state-business-primary);font-size:20px}.dshtu_table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums}.dshtu_table th,.dshtu_table td{text-align:right;padding:6px 8px;border-bottom:.5px solid var(--dsw-alias-border-l2);font-weight:400}.dshtu_table th:first-child,.dshtu_table td:first-child{text-align:left}.dshtu_table th{color:var(--dsw-alias-label-tertiary);font-size:12px;white-space:nowrap}.dshtu_table td:first-child{overflow-wrap:anywhere;max-width:300px;color:var(--dsw-alias-label-secondary)}.dshtu_meta{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.dshtu_err{color:var(--dsw-alias-state-error-primary);display:flex;align-items:center;gap:10px}.dshtu_err button{border:.5px solid var(--dsw-alias-border-l3);color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;background:0 0;border-radius:6px;padding:4px 10px}.dshtu_chart{width:100%;height:260px}.dshtu_info{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:15px;height:15px;margin-left:5px;border:.5px solid var(--dsw-alias-border-l3);border-radius:50%;color:var(--dsw-alias-label-tertiary);font-size:9px;font-style:normal;line-height:1;cursor:help}.dshtu_info:hover{color:var(--dsw-alias-state-business-primary);border-color:var(--dsw-alias-state-business-primary)}.dshtu_pop{display:block;max-width:420px;font-size:12px;font-weight:400;line-height:18px;text-align:left;white-space:normal}.dshtu_pop .dshtu_sep{margin:0 5px;opacity:.55}.dshtu_nd{color:var(--dsw-alias-label-tertiary)}";
193
+ var css = ".dshtu_wrap{max-width:860px;display:flex;flex-direction:column;gap:14px;padding:2px 2px 4px;color:var(--dsw-alias-label-primary);font-size:13px}.dshtu_card{background:var(--dsw-alias-bg-layer-3);border-radius:14px;box-shadow:var(--dsw-elevation-stroke);padding:14px 16px}.dshtu_bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.dshtu_bar label{color:var(--dsw-alias-label-tertiary);font-size:12px}.dshtu_bar .dshtu_select,.dshtu_bar .dshtu_input{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;padding:4px 8px;font-size:12.5px}.dshtu_bar .dshtu_select{width:168px;text-overflow:ellipsis;white-space:nowrap}.dshtu_chips{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap}.dshtu_subBar{margin-top:12px}.dshtu_pick{display:inline-flex;align-items:center;gap:4px;max-width:230px}.dshtu_pickLabel{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dshtu_metaIcon{display:inline-flex;align-items:center;margin-right:5px;vertical-align:-2px;color:var(--dsw-alias-label-tertiary)}.dshtu_cardTitle{display:inline-flex;align-items:center}.dshtu_foot{display:flex;align-items:center;gap:6px}.dshtu_statLabel{display:inline-flex;align-items:center;gap:3px;white-space:nowrap}.dshtu_field{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}.dshtu_seg{display:inline-flex;border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;overflow:hidden}.dshtu_seg button{border:0;background:0 0;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer;padding:4px 10px;font-size:12.5px}.dshtu_seg button[data-on=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.dshtu_totalsMeta{margin-top:12px}.dshtu_grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-top:8px}.dshtu_stat{background:var(--dsw-alias-bg-module-platform);border-radius:10px;padding:10px 12px}.dshtu_stat b{display:block;font-size:16px;line-height:24px;font-variant-numeric:tabular-nums}.dshtu_stat span{color:var(--dsw-alias-label-tertiary);font-size:12px}.dshtu_stat[data-main=true]{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,var(--dsw-alias-bg-module-platform))}.dshtu_stat[data-main=true] b{color:var(--dsw-alias-state-business-primary);font-size:20px}.dshtu_table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums}.dshtu_table th,.dshtu_table td{text-align:right;padding:6px 8px;border-bottom:.5px solid var(--dsw-alias-border-l2);font-weight:400}.dshtu_table th:first-child,.dshtu_table td:first-child{text-align:left}.dshtu_table th{color:var(--dsw-alias-label-tertiary);font-size:12px;white-space:nowrap}.dshtu_table td:first-child{overflow-wrap:anywhere;max-width:300px;color:var(--dsw-alias-label-secondary)}.dshtu_meta{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.dshtu_err{color:var(--dsw-alias-state-error-primary);display:flex;align-items:center;gap:10px}.dshtu_chart{width:100%;height:260px}.dshtu_info{display:inline-flex;align-items:center;justify-content:center;margin-left:6px;color:var(--dsw-alias-label-tertiary);cursor:help}.dshtu_info:hover{color:var(--dsw-alias-state-business-primary)}.dshtu_pop{display:block;max-width:420px;font-size:12px;font-weight:400;line-height:18px;text-align:left;white-space:normal}.dshtu_pop .dshtu_sep{margin:0 5px;opacity:.55}[role=tooltip]:has(.dshtu_pop){background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);padding:10px 12px;border:0;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.16)}.dshtu_nd{color:var(--dsw-alias-label-tertiary)}";
160
194
 
161
195
  function trimNum(v, decimals) {
162
196
  var f = v.toFixed(decimals);
@@ -212,6 +246,68 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
212
246
  return idx >= 0 ? (text.slice(idx + 1) || text) : text;
213
247
  }
214
248
 
249
+ /** A row of the shell's pills for one small enum (range, unit, chart window). */
250
+ function chips(options, active, onPick) {
251
+ return options.map(function (option) {
252
+ return h(chip, {
253
+ key: option.value,
254
+ active: option.value === active,
255
+ onClick: function () { onPick(option.value); }
256
+ }, option.label);
257
+ });
258
+ }
259
+
260
+ /**
261
+ * Model filter. The shell's Menu (the same popover the composer picks models
262
+ * with) anchored on a pill; a styled native select on a shell without it.
263
+ */
264
+ function ModelPicker(props) {
265
+ var t = props.t;
266
+ var _a = useState(false), open = _a[0], setOpen = _a[1];
267
+ if (menu === null) {
268
+ return h("select", {
269
+ className: "dshtu_select",
270
+ value: props.model,
271
+ onChange: function (event) { props.onChange(event.target.value); }
272
+ }, [h("option", { key: "", value: "" }, t("modelAll"))].concat(props.models.map(function (name) {
273
+ return h("option", { key: name, value: name }, name);
274
+ })));
275
+ }
276
+ var items = [{ id: "", label: t("modelAll") }].concat(props.models.map(function (name) {
277
+ return { id: name, label: name };
278
+ }));
279
+ return h(menu, {
280
+ open: open,
281
+ align: "start",
282
+ side: "bottom",
283
+ portal: true,
284
+ compact: true,
285
+ items: items,
286
+ selectedId: props.model,
287
+ onSelect: function (id) { props.onChange(id); setOpen(false); },
288
+ onClose: function () { setOpen(false); },
289
+ anchor: h(chip, { active: props.model !== "", onClick: function () { setOpen(!open); } },
290
+ h("span", { className: "dshtu_pick" },
291
+ h("span", { className: "dshtu_pickLabel" }, props.model === "" ? t("modelAll") : props.model),
292
+ iconChevron === null ? null : h(iconChevron, {})))
293
+ });
294
+ }
295
+
296
+ /**
297
+ * The money card's ⓘ: the shell's own icon inside the shell's Tooltip, with a
298
+ * plain title attribute as the only fallback on a shell without it.
299
+ */
300
+ function noteTrigger(t, notes) {
301
+ var glyph = h("i", { className: "dshtu_info" }, iconQuestion === null ? "i" : h(iconQuestion, {}));
302
+ if (tooltip === null) return h("i", { className: "dshtu_info", title: notes.text }, "i");
303
+ return h(tooltip, {
304
+ label: h("div", { className: "dshtu_pop" }, notes.node),
305
+ side: "top",
306
+ delayMs: 120,
307
+ maxWidth: 420
308
+ }, glyph);
309
+ }
310
+
215
311
  var BUCKET_KEYS = ["calls", "input", "output", "cacheRead", "cacheWrite", "reasoning", "total", "cost"];
216
312
 
217
313
  /** Project tables read as directory names; same-named projects merge, full path rides the title. */
@@ -245,7 +341,9 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
245
341
  h("thead", null,
246
342
  h("tr", null,
247
343
  h("th", null, keyLabel),
248
- h("th", { title: t("costTipLong") }, t("cost") + "*"),
344
+ h("th", null, tooltip === null
345
+ ? h("span", { title: t("costTipLong") }, t("cost") + "*")
346
+ : h(tooltip, { label: h("div", { className: "dshtu_pop" }, t("costTipLong")), side: "top", delayMs: 250 }, h("span", null, t("cost") + "*"))),
249
347
  h("th", null, t("total")),
250
348
  h("th", null, t("input")),
251
349
  h("th", null, t("output")),
@@ -256,9 +354,14 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
256
354
  var row = rows[key];
257
355
  var note = notes === undefined ? undefined : notes[key];
258
356
  var blank = note !== undefined && note.blank === true;
357
+ var tip = note === undefined ? t("costTip") : note.tip;
358
+ var money = blank ? "—" : fmtMoney(row.cost);
259
359
  return h("tr", { key: key },
260
360
  h("td", { title: titles !== undefined && titles[key] !== undefined ? titles[key] : undefined }, key),
261
- h("td", { className: blank ? "dshtu_nd" : undefined, title: note === undefined ? t("costTip") : note.tip }, blank ? "—" : fmtMoney(row.cost)),
361
+ h("td", { className: blank ? "dshtu_nd" : undefined },
362
+ tooltip === null
363
+ ? h("span", { title: tip }, money)
364
+ : h(tooltip, { label: h("div", { className: "dshtu_pop" }, tip), side: "top", delayMs: 250 }, h("span", { className: "dshtu_moneyCell" }, money))),
262
365
  h("td", null, fmtValue(row.total, unit)),
263
366
  h("td", null, fmtValue(row.input, unit)),
264
367
  h("td", null, fmtValue(row.output, unit)),
@@ -441,29 +544,27 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
441
544
  return h("div", { className: "dshtu_card dshtu_meta" }, t("trendEmpty"));
442
545
  }
443
546
  var rangeButton = function (value) {
444
- return h("button", {
547
+ return h(chip, {
445
548
  key: value,
446
- "data-on": range === value,
549
+ active: range === value,
447
550
  onClick: function () { if (props.onRange !== undefined) props.onRange(value); }
448
551
  }, value + t("daysSuffix"));
449
552
  };
450
553
  return h("div", { className: "dshtu_card" },
451
554
  h("div", { className: "dshtu_bar" },
452
- h("span", { className: "dshtu_meta" }, t("trend") + " · " + shown.from + " ~ " + shown.to + " · " + t("cost") + " " + fmtMoney(cost)),
555
+ h("span", { className: "dshtu_meta dshtu_cardTitle" },
556
+ iconTrend === null ? null : h("span", { className: "dshtu_metaIcon" }, h(iconTrend, {})),
557
+ t("trend") + " · " + shown.from + " ~ " + shown.to + " · " + t("cost") + " " + fmtMoney(cost)),
453
558
  h("span", { style: { flex: "1" } }),
454
- h("span", { className: "dshtu_seg" }, rangeButton(7), rangeButton(30), rangeButton(90))),
559
+ h("span", { className: "dshtu_chips" }, rangeButton(7), rangeButton(30), rangeButton(90))),
455
560
  h("div", { className: "dshtu_chart", ref: holder }));
456
561
  }
457
562
 
458
- /** Money stat card: the headline amount plus the info icon that reveals the notes. */
563
+ /** Money stat card: the headline amount plus the ⓘ that reveals the notes. */
459
564
  function moneyBox(t, label, value, notes) {
460
- var icon = h("i", { className: "dshtu_info", "aria-label": t("costTip") }, "i");
461
- var trigger = tooltip === null
462
- ? h("i", { className: "dshtu_info", title: t("costTipLong") }, "i")
463
- : h(tooltip, { label: h("div", { className: "dshtu_pop" }, notes), side: "top", delayMs: 120, maxWidth: 420 }, icon);
464
565
  return h("div", { className: "dshtu_stat", "data-main": true },
465
566
  h("b", null, fmtMoney(value)),
466
- h("span", null, label, trigger));
567
+ h("span", { className: "dshtu_statLabel" }, label, noteTrigger(t, notes)));
467
568
  }
468
569
 
469
570
  /** Rate line behind one model's amount: cache-hit / cache-miss / output. */
@@ -488,45 +589,68 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
488
589
  return notes;
489
590
  }
490
591
 
491
- /** Where the amounts come from, and what they leave out — the money card's hover notes. */
592
+ /**
593
+ * Where the amounts come from, and what they leave out. Returns both the
594
+ * rendered lines (for the hover layer) and the same text flattened, which is
595
+ * what the layer's copy button puts on the clipboard.
596
+ */
492
597
  function pricingNotes(t, data) {
493
598
  var pricing = data.pricing || {};
494
599
  var notes = data.modelPricing || {};
600
+ var nodes = [];
601
+ var texts = [];
602
+ var push = function (key, node, text, spaced) {
603
+ nodes.push(h("div", { key: key, style: spaced === true ? { marginTop: "5px" } : undefined }, node));
604
+ texts.push(text);
605
+ };
495
606
  var bits = [];
496
607
  if (pricing.builtIn === true) {
497
- bits.push(h("span", { key: "src" }, pricing.error ? t("priceBuiltIn").replace("{error}", pricing.error) : t("priceOffline")));
608
+ var offline = pricing.error ? t("priceBuiltIn").replace("{error}", pricing.error) : t("priceOffline");
609
+ bits.push({ node: h("span", { key: "src" }, offline), text: offline });
498
610
  } else if (pricing.source) {
499
- bits.push(h("span", { key: "src" }, t("priceSource") + " " + String(pricing.source).replace(/^https?:\/\//, "")));
611
+ var shown = String(pricing.source).replace(/^https?:\/\//, "");
612
+ bits.push({ node: h("span", { key: "src" }, t("priceSource") + " " + shown), text: t("priceSource") + " " + pricing.source });
613
+ }
614
+ if (pricing.fetchedAt > 0) {
615
+ var fetched = t("priceFetched").replace("{time}", fmtTime(pricing.fetchedAt));
616
+ bits.push({ node: h("span", { key: "at" }, fetched), text: fetched });
617
+ }
618
+ if (pricing.nextRefreshAt > 0) {
619
+ var next = t("priceNext").replace("{time}", fmtTime(pricing.nextRefreshAt));
620
+ bits.push({ node: h("span", { key: "next" }, next), text: next });
500
621
  }
501
- if (pricing.fetchedAt > 0) bits.push(h("span", { key: "at" }, t("priceFetched").replace("{time}", fmtTime(pricing.fetchedAt))));
502
- if (pricing.nextRefreshAt > 0) bits.push(h("span", { key: "next" }, t("priceNext").replace("{time}", fmtTime(pricing.nextRefreshAt))));
503
622
  if (pricing.peak) {
504
- bits.push(h("span", { key: "peak" }, t("pricePeak").replace("{ranges}", pricing.peak.hoursUtc.map(function (range) { return range[0] + "-" + range[1]; }).join(" / "))));
623
+ var peak = t("pricePeak").replace("{ranges}", pricing.peak.hoursUtc.map(function (range) { return range[0] + "-" + range[1]; }).join(" / "));
624
+ bits.push({ node: h("span", { key: "peak" }, peak), text: peak });
625
+ }
626
+ if (pricing.peakParsed === false) {
627
+ bits.push({ node: h("span", { key: "peakAssumed" }, t("pricePeakAssumed")), text: t("pricePeakAssumed") });
505
628
  }
506
- if (pricing.peakParsed === false) bits.push(h("span", { key: "peakAssumed" }, t("pricePeakAssumed")));
507
629
  var meta = [];
508
630
  bits.forEach(function (bit, index) {
509
631
  if (index > 0) meta.push(h("span", { className: "dshtu_sep", key: "sep" + index }, "·"));
510
- meta.push(bit);
632
+ meta.push(bit.node);
511
633
  });
634
+ push("text", t("costTipLong"), t("costTipLong"));
635
+ push("meta", meta, bits.map(function (bit) { return bit.text; }).join(" · "), true);
512
636
  var skipped = Object.keys(notes).filter(function (name) { return notes[name].status !== "priced"; });
513
- var lines = [
514
- h("div", { key: "text" }, t("costTipLong")),
515
- h("div", { key: "meta", style: { marginTop: "5px" } }, meta),
516
- ];
517
637
  if (skipped.length > 0) {
518
- lines.push(h("div", { key: "skipped", style: { marginTop: "5px" } }, t("notPriced") + ":" + skipped.map(function (name) {
519
- return name + "(" + (notes[name].status === "excluded" ? t("reasonExcluded") : t("reasonUnmatched")) + ")";
520
- }).join("、")));
638
+ var reason = function (name) { return notes[name].status === "excluded" ? t("reasonExcluded") : t("reasonUnmatched"); };
639
+ push("skipped", t("notPriced") + ":" + skipped.map(function (name) {
640
+ return name + "(" + reason(name) + ")";
641
+ }).join("、"), t("notPriced") + ": " + skipped.map(function (name) {
642
+ return name + " (" + reason(name) + ")";
643
+ }).join(", "), true);
521
644
  }
522
645
  var auxiliary = data.auxiliary;
523
646
  if (auxiliary && auxiliary.total > 0) {
524
647
  var detail = [];
525
648
  if (auxiliary.search > 0) detail.push(t("auxSearch") + " " + auxiliary.search);
526
649
  if (auxiliary.title > 0) detail.push(t("auxTitle") + " " + auxiliary.title);
527
- lines.push(h("div", { key: "auxiliary", style: { marginTop: "5px" } }, t("auxiliaryNote").replace("{count}", auxiliary.total).replace("{detail}", detail.join(" / "))));
650
+ var auxiliaryText = t("auxiliaryNote").replace("{count}", auxiliary.total).replace("{detail}", detail.join(" / "));
651
+ push("auxiliary", auxiliaryText, auxiliaryText, true);
528
652
  }
529
- return lines;
653
+ return { node: nodes, text: texts.join("\n") };
530
654
  }
531
655
 
532
656
  function Tab(props) {
@@ -594,13 +718,14 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
594
718
  }, [query, tick]);
595
719
 
596
720
  useEffect(function () {
597
- var el = document.getElementById("dshtu-style");
598
- if (!el) {
599
- el = document.createElement("style");
600
- el.id = "dshtu-style";
601
- el.textContent = css;
602
- document.head.appendChild(el);
603
- }
721
+ // Publish this fiber's stylesheet every mount: a hot reload replaces the
722
+ // component, and the previous fiber's <style> would otherwise keep
723
+ // winning because it is found by id and skipped.
724
+ var el = document.createElement("style");
725
+ el.id = "dshtu-style";
726
+ el.textContent = css;
727
+ document.head.appendChild(el);
728
+ return function () { el.remove(); };
604
729
  }, []);
605
730
 
606
731
  var switchUnit = function (next) {
@@ -608,19 +733,16 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
608
733
  try { localStorage.setItem("dshtu.unit", next); } catch (err) {}
609
734
  };
610
735
 
611
- var seg = function (value, label) {
612
- return h("button", { key: value, "data-on": unit === value, onClick: function () { switchUnit(value); } }, label);
613
- };
614
-
615
- var modeTab = function (value, label) {
616
- return h("button", { key: value, "data-on": mode === value, onClick: function () { setMode(value); setTick(tick + 1); } }, label);
617
- };
618
-
619
736
  if (error) {
620
737
  return h("div", { className: "dshtu_wrap" },
621
738
  h("div", { className: "dshtu_card dshtu_err" },
622
739
  h("span", null, t("error") + ": " + error),
623
- h("button", { onClick: function () { setError(null); setTick(tick + 1); } }, t("retry"))));
740
+ h(button, {
741
+ variant: "outline",
742
+ size: "sm",
743
+ icon: iconRefresh === null ? null : h(iconRefresh, {}),
744
+ onClick: function () { setError(null); setTick(tick + 1); }
745
+ }, t("retry"))));
624
746
  }
625
747
  if (!data) {
626
748
  return h("div", { className: "dshtu_wrap" },
@@ -639,25 +761,36 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
639
761
  metaParts.push(t("scanPending"));
640
762
  }
641
763
  metaParts.push(t("updated").replace("{time}", fmtTime(data.updatedAt)));
764
+ var scanState = data.scan && data.scan.error ? "error" : (data.scan && data.scan.done ? "done" : "ongoing");
642
765
  return h("div", { className: "dshtu_wrap" },
643
766
  h("div", { className: "dshtu_card" },
644
767
  h("div", { className: "dshtu_bar" },
645
768
  h("label", null, t("filterLabel")),
646
- h("span", { className: "dshtu_seg" },
647
- modeTab("day", t("filterDay")),
648
- modeTab("month", t("filterMonth")),
649
- modeTab("all", t("filterAll"))),
650
- mode === "day" ? h("input", { type: "date", value: day, onChange: function (e) { setDay(e.target.value); setTick(tick + 1); } }) : null,
651
- mode === "month" ? h("input", { type: "month", value: month, onChange: function (e) { setMonth(e.target.value); setTick(tick + 1); } }) : null,
769
+ h("span", { className: "dshtu_chips" }, chips([
770
+ { value: "day", label: t("filterDay") },
771
+ { value: "month", label: t("filterMonth") },
772
+ { value: "all", label: t("filterAll") }
773
+ ], mode, function (value) { setMode(value); setTick(tick + 1); })),
774
+ mode === "day" ? h(input, { type: "date", value: day, onChange: function (event) { setDay(event.target.value); setTick(tick + 1); } }) : null,
775
+ mode === "month" ? h(input, { type: "month", value: month, onChange: function (event) { setMonth(event.target.value); setTick(tick + 1); } }) : null,
652
776
  h("span", { className: "dshtu_field" },
653
777
  h("label", null, t("modelLabel")),
654
- h("select", { value: model, onChange: function (e) { setModel(e.target.value); setTick(tick + 1); } },
655
- h("option", { value: "" }, t("modelAll")),
656
- models.map(function (name) { return h("option", { key: name, value: name }, name); }))),
778
+ h(ModelPicker, {
779
+ t: t,
780
+ model: model,
781
+ models: models,
782
+ onChange: function (value) { setModel(value); setTick(tick + 1); }
783
+ }))),
784
+ h("div", { className: "dshtu_bar dshtu_subBar" },
785
+ h("span", { className: "dshtu_meta" },
786
+ iconGauge === null ? null : h("span", { className: "dshtu_metaIcon" }, h(iconGauge, {})),
787
+ t("totals")),
657
788
  h("span", { style: { flex: "1" } }),
658
789
  h("label", null, t("unitLabel")),
659
- h("span", { className: "dshtu_seg" }, seg("zh", t("unitZh")), seg("en", t("unitEn")))),
660
- h("div", { className: "dshtu_meta dshtu_totalsMeta" }, t("totals")),
790
+ h("span", { className: "dshtu_chips" }, chips([
791
+ { value: "zh", label: t("unitZh") },
792
+ { value: "en", label: t("unitEn") }
793
+ ], unit, switchUnit))),
661
794
  h("div", { className: "dshtu_grid" },
662
795
  statBox(t, t("total"), totals.total, unit, true),
663
796
  statBox(t, t("input"), totals.input, unit),
@@ -674,7 +807,9 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
674
807
  var projects = shortenProjects(data.byProject);
675
808
  return table(t, t("byProject"), projects.rows, unit, projects.titles);
676
809
  })(),
677
- h("div", { className: "dshtu_meta" }, metaParts.join(" · ")));
810
+ h("div", { className: "dshtu_foot" },
811
+ stateDot === null ? null : h(stateDot, { state: scanState, size: 8 }),
812
+ h("span", { className: "dshtu_meta" }, metaParts.join(" · "))));
678
813
  }
679
814
 
680
815
  /**
@@ -694,35 +829,83 @@ window.__ModuleLoader__.load({ id: "dsh-token-use", factory: (require) => {
694
829
  + ";mask:url(\"" + NAV_GLYPH + "\") center/16px 16px no-repeat;"
695
830
  + "-webkit-mask:url(\"" + NAV_GLYPH + "\") center/16px 16px no-repeat}";
696
831
 
832
+ /**
833
+ * Handles the newest fiber owns. A client-plugin hot reload re-applies this
834
+ * module, and the replacement fiber must take the rail style and the settings
835
+ * slot over from the fiber it replaces — otherwise the section would vanish
836
+ * after every rebuild until the page is reloaded.
837
+ */
838
+ var railStyle = null;
839
+ var sectionOwner = null;
840
+ var localeOwner = null;
841
+
697
842
  function apply(ctx) {
698
843
  ctx.effect(function () {
699
- ctx.locale.register(NS, { zh: zh, en: en });
844
+ // A reloaded fiber replaces the dictionary it finds: re-registering the
845
+ // same namespace on top of a still-registered one is what makes a hot
846
+ // reload fail, and a failure here would take the whole panel down with it.
847
+ if (localeOwner !== null) {
848
+ try { localeOwner(); } catch (ignored) {}
849
+ localeOwner = null;
850
+ }
851
+ var dispose = null;
852
+ try {
853
+ dispose = ctx.locale.register(NS, { zh: zh, en: en });
854
+ } catch (error) {
855
+ console.error("[dsh-token-use] dictionary registration failed", error);
856
+ }
857
+ localeOwner = typeof dispose === "function" ? dispose : null;
858
+ return function () {
859
+ if (localeOwner === dispose) localeOwner = null;
860
+ if (typeof dispose === "function") dispose();
861
+ };
700
862
  }, "dsh-token-use: dictionaries");
701
863
  ctx.effect(function () {
702
- var existing = document.getElementById("dshtu-nav-style");
703
- if (existing !== null) return function () {};
704
864
  var style = document.createElement("style");
705
865
  style.id = "dshtu-nav-style";
706
866
  style.textContent = navGlyphCss;
707
867
  document.head.appendChild(style);
708
- return function () { style.remove(); };
868
+ railStyle = style;
869
+ return function () {
870
+ style.remove();
871
+ if (railStyle === style) railStyle = null;
872
+ };
709
873
  }, "dsh-token-use: rail glyph");
710
874
  var t = ctx.locale.bind(NS);
711
875
  ctx.slots.inject("settings.section", function () {
712
- return ctx.slots.register({
713
- name: "settings.section",
714
- id: "token-usage",
715
- order: 90,
716
- // The settings rail picks its glyph by section id (`navIcon(id)`) and
717
- // falls back to the settings gear for every id it does not know, so
718
- // this label carries a marker the stylesheet hangs our own chart glyph
719
- // on: `navGlyphCss` hides the gear and masks a bar chart into its place.
720
- // If a future shell changes that markup the selector stops matching and
721
- // the gear simply stays — never two glyphs at once.
722
- label: function () { return h("span", { className: "dshtu_navlabel" }, t("tab")); },
723
- locale: NS,
724
- inject: function () { return { t: t }; }
725
- }, function (props) { return h(Tab, props); });
876
+ var register = function () {
877
+ return ctx.slots.register({
878
+ name: "settings.section",
879
+ id: "token-usage",
880
+ order: 90,
881
+ // The settings rail picks its glyph by section id (`navIcon(id)`) and
882
+ // falls back to the settings gear for every id it does not know, so
883
+ // this label carries a marker the stylesheet hangs our own chart glyph
884
+ // on: `navGlyphCss` hides the gear and masks a bar chart into its place.
885
+ // If a future shell changes that markup the selector stops matching and
886
+ // the gear simply stays — never two glyphs at once.
887
+ label: function () { return h("span", { className: "dshtu_navlabel" }, t("tab")); },
888
+ locale: NS,
889
+ inject: function () { return { t: t }; }
890
+ }, function (props) { return h(Tab, props); });
891
+ };
892
+ var dispose;
893
+ try {
894
+ dispose = register();
895
+ } catch (error) {
896
+ // The reloaded fiber can land while the previous registration is still
897
+ // on the ledger: retire it and claim the slot for this fiber.
898
+ if (sectionOwner !== null) {
899
+ try { sectionOwner(); } catch (ignored) {}
900
+ sectionOwner = null;
901
+ }
902
+ dispose = register();
903
+ }
904
+ sectionOwner = dispose;
905
+ return function () {
906
+ if (sectionOwner === dispose) sectionOwner = null;
907
+ dispose();
908
+ };
726
909
  });
727
910
  }
728
911
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-token-use",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Real-time token usage and cost dashboard for DeepSeek Harness — live totals (input/output/cache/reasoning), estimated spend per model/day/project from DeepSeek's official prices (refreshed daily, peak/off-peak aware), model/day/month/project filters and a smooth trend chart. · 实时 Token 用量与消费金额仪表盘:按模型/按天/按月/按项目查看用量与估算金额(取 DeepSeek 官网定价,每日自动更新,区分高峰/空闲),附趋势曲线。",
5
5
  "keywords": [
6
6
  "dsh",