dsh-provider-usage 0.3.8 → 0.3.9

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.md CHANGED
@@ -33,10 +33,11 @@
33
33
  | `vercel-ai-gateway` | `vercel-ai-gateway` | `GET {baseURL}/v1/credits` | team credit balance |
34
34
  | `xai` | `xai` | `GET {baseURL}/billing/credits` | prepaid balance (USD) |
35
35
  - **Credentials stay safe** — API keys are resolved per request through the harness credentials service (environment variables / `~/.dsh/.credentials.yaml`); never cached, never written to disk. For OAuth providers (OpenAI Codex) the plugin reads the grant record the sign-in flow stored, and refreshes it transparently when it is about to expire.
36
- - **Floating ball widget** — a draggable floating ball opens the usage panel. Drop it anywhere in the viewport (position persisted); it docks by default at the bottom-left of the chat area with equal margins, and the panel-header home button sends it back. The halo around the ball encodes the health of the provider **in use** (the composer's current model selection): green all good, amber some quota below 30% left, red on query failure / missing key / usage ≥90%. Idle providers with low quota do not color the ball — switch to another provider with enough quota and the ball turns green again; the panel marks the in-use provider and still lists every provider's numbers.
36
+ - **Floating ball widget** — a draggable floating ball opens the usage panel. Drop it anywhere in the viewport (position persisted); it docks by default at the bottom-left of the chat area with equal margins, and the panel-header home button sends it back. The halo around the ball encodes the health of the provider **in use** (the composer's current model selection): green all good, amber some quota below 30% left or a balance below the yellow threshold, red on query failure / missing key / usage ≥90% / balance below the red threshold. Idle providers with low quota do not color the ball — switch to another provider with enough quota and the ball turns green again; the panel marks the in-use provider and still lists every provider's numbers.
37
37
  - **Version badge** — the panel header shows the running plugin version next to the title, so it is obvious which release is loaded.
38
38
  - **Bilingual panel** — built-in Chinese/English UI; follows the harness language by default, with a one-click toggle in the panel header (persisted in localStorage).
39
39
  - **Configurable refresh** — adjustable in the panel (15s–30min, persisted in localStorage); the default comes from the plugin config.
40
+ - **Configurable balance thresholds** — balance-type providers (DeepSeek, Moonshot, Vercel AI Gateway, xAI) and the `credits` rows of usage-type providers (OpenRouter, OpenAI Codex) turn red below the red threshold and yellow below the yellow threshold, compared in the balance's own currency (defaults: red < 10, yellow < 30, whether CNY or USD). Both values are editable directly in the panel footer (persisted in localStorage); the defaults come from the plugin config.
40
41
  - **Manual providers** — add arbitrary gateways (e.g. a self-hosted DeepSeek-compatible endpoint) via config.
41
42
 
42
43
  ## Screenshots
@@ -129,14 +130,18 @@ Defaults work out of the box: the plugin auto-detects every provider route of th
129
130
  - id: provider-usage
130
131
  name: dsh-provider-usage
131
132
  config:
132
- refreshSeconds: 60 # suggested panel refresh interval (5–86400)
133
- autoDetect: true # enumerate provider routes from the llm registry
134
- providers: [] # manual specs; an id matching a detected route overrides it
133
+ refreshSeconds: 60 # suggested panel refresh interval (5–86400)
134
+ balanceRedThreshold: 10 # balance below this turns red (per the balance's own currency)
135
+ balanceYellowThreshold: 30 # balance below this turns yellow (per the balance's own currency)
136
+ autoDetect: true # enumerate provider routes from the llm registry
137
+ providers: [] # manual specs; an id matching a detected route overrides it
135
138
  ```
136
139
 
137
140
  | Field | Type | Default | Description |
138
141
  | --- | --- | --- | --- |
139
142
  | `refreshSeconds` | number | `60` | Suggested widget refresh interval in seconds (5–86400) |
143
+ | `balanceRedThreshold` | number | `10` | Balance below this amount turns red, compared in the balance's own currency |
144
+ | `balanceYellowThreshold` | number | `30` | Balance below this amount turns yellow, compared in the balance's own currency |
140
145
  | `autoDetect` | boolean | `true` | Enumerate live provider routes from the llm registry |
141
146
  | `providers` | array | `[]` | Manual provider specs: `{id, kind, baseURL, apiKeyEnv, displayName?, enabled?}`; `kind` is one of the adapter table above |
142
147
 
package/README_ZH.md CHANGED
@@ -33,10 +33,11 @@
33
33
  | `vercel-ai-gateway` | `vercel-ai-gateway` | `GET {baseURL}/v1/credits` | 团队 credit 余额 |
34
34
  | `xai` | `xai` | `GET {baseURL}/billing/credits` | 预付余额(USD) |
35
35
  - **密钥安全** —— 通过 harness 凭据服务按次解析(环境变量 / `~/.dsh/.credentials.yaml`),不缓存、不落地。对 OAuth 类 Provider(OpenAI Codex)则直接读取登录流程存入的授权记录,并在令牌临近过期时自动刷新。
36
- - **悬浮球入口** —— 可任意拖动的悬浮球点击弹出用量面板,位置持久化;默认停靠在主对话区域左下角(左边距 = 底边距),面板头部的归位按钮一键回到默认位置;球体光晕表达**当前正在使用**的 Provider(composer 中选中的模型)的健康度:绿色正常、黄色配额剩余不足 30%、红色查询失败/缺密钥/用量 ≥90%。闲置的 Provider 余量不足不再影响悬浮球颜色——切换到余量充足的另一个 Provider 后球体会恢复绿色;面板会标注"使用中"的 Provider,并照常列出所有 Provider 的用量明细。
36
+ - **悬浮球入口** —— 可任意拖动的悬浮球点击弹出用量面板,位置持久化;默认停靠在主对话区域左下角(左边距 = 底边距),面板头部的归位按钮一键回到默认位置;球体光晕表达**当前正在使用**的 Provider(composer 中选中的模型)的健康度:绿色正常、黄色配额剩余不足 30% 或余额低于黄阈值、红色查询失败/缺密钥/用量 ≥90% 或余额低于红阈值。闲置的 Provider 余量不足不再影响悬浮球颜色——切换到余量充足的另一个 Provider 后球体会恢复绿色;面板会标注"使用中"的 Provider,并照常列出所有 Provider 的用量明细。
37
37
  - **版本徽章** —— 面板标题旁显示当前运行的插件版本,一眼确认加载的是哪个发布版。
38
38
  - **中英双语** —— 面板内置中英文界面,默认跟随 harness 系统语言,标题栏按钮一键切换(localStorage 持久化)。
39
39
  - **刷新周期可调** —— 面板内调整(15s–30min,localStorage 持久化),默认值由插件配置提供。
40
+ - **余额阈值可调** —— 余额型 Provider(DeepSeek、Moonshot、Vercel AI Gateway、xAI)以及 usage 型 Provider 中的 `credits` 行(OpenRouter、OpenAI Codex)会按余额数值变色:低于红阈值变红、低于黄阈值变黄(按查询到的币种本身比较,默认红 < 10、黄 < 30,人民币/美元一致)。两个阈值可直接在面板底部修改(localStorage 持久化),默认值由插件配置提供。
40
41
  - **手动 provider** —— 可通过配置添加任意网关(如自建 DeepSeek 兼容端点)。
41
42
 
42
43
  ## 截图
@@ -129,14 +130,18 @@ dsh plugin --profile web add dsh-provider-usage@latest
129
130
  - id: provider-usage
130
131
  name: dsh-provider-usage
131
132
  config:
132
- refreshSeconds: 60 # 面板默认刷新周期(秒)
133
- autoDetect: true # 自动枚举 llm 注册表中的 provider
134
- providers: [] # 手动补充/覆盖 provider(id 相同则覆盖自动探测结果)
133
+ refreshSeconds: 60 # 面板默认刷新周期(秒)
134
+ balanceRedThreshold: 10 # 余额低于该值变红(按余额自身币种比较)
135
+ balanceYellowThreshold: 30 # 余额低于该值变黄(按余额自身币种比较)
136
+ autoDetect: true # 自动枚举 llm 注册表中的 provider
137
+ providers: [] # 手动补充/覆盖 provider(id 相同则覆盖自动探测结果)
135
138
  ```
136
139
 
137
140
  | 字段 | 类型 | 默认 | 说明 |
138
141
  | --- | --- | --- | --- |
139
142
  | `refreshSeconds` | number | `60` | 面板建议刷新周期(秒),5–86400 |
143
+ | `balanceRedThreshold` | number | `10` | 余额低于该值变红,按余额自身币种比较 |
144
+ | `balanceYellowThreshold` | number | `30` | 余额低于该值变黄,按余额自身币种比较 |
140
145
  | `autoDetect` | boolean | `true` | 从 llm 注册表自动枚举 provider |
141
146
  | `providers` | array | `[]` | 手动 provider 规格:`{id, kind, baseURL, apiKeyEnv, displayName?, enabled?}`,`kind` 取上表中的任一适配器 |
142
147
 
package/lib/client.js CHANGED
@@ -18,6 +18,9 @@ window.__ModuleLoader__.load({
18
18
  "lang.switch": "Switch to English",
19
19
  "panel.resetPos": "悬浮球归位",
20
20
  "panel.interval": "刷新周期",
21
+ "panel.thresholds": "余额阈值",
22
+ "panel.thresholdRed": "红",
23
+ "panel.thresholdYellow": "黄",
21
24
  "panel.updated": "更新于 {time}",
22
25
  "panel.never": "尚未获取",
23
26
  "panel.empty": "未检测到已配置的 Provider",
@@ -42,8 +45,8 @@ window.__ModuleLoader__.load({
42
45
  "status.error": "查询失败",
43
46
  "status.inUse": "使用中",
44
47
  "status.tone.ok": "当前 Provider 正常",
45
- "status.tone.warn": "当前 Provider 用量接近上限",
46
- "status.tone.danger": "当前 Provider 异常或用量将尽",
48
+ "status.tone.warn": "当前 Provider 用量接近上限或余额偏低",
49
+ "status.tone.danger": "当前 Provider 异常、用量将尽或余额过低",
47
50
  "countdown.days": "{days}天{hours}小时",
48
51
  "countdown.hours": "{hours}小时{minutes}分",
49
52
  "countdown.minutes": "{minutes}分钟"
@@ -58,6 +61,9 @@ window.__ModuleLoader__.load({
58
61
  "lang.switch": "切换到中文",
59
62
  "panel.resetPos": "Reset ball position",
60
63
  "panel.interval": "Refresh interval",
64
+ "panel.thresholds": "Balance thresholds",
65
+ "panel.thresholdRed": "Red",
66
+ "panel.thresholdYellow": "Yellow",
61
67
  "panel.updated": "Updated {time}",
62
68
  "panel.never": "Not fetched yet",
63
69
  "panel.empty": "No configured provider detected",
@@ -82,8 +88,8 @@ window.__ModuleLoader__.load({
82
88
  "status.error": "Query failed",
83
89
  "status.inUse": "In use",
84
90
  "status.tone.ok": "Current provider healthy",
85
- "status.tone.warn": "Current provider usage nearing its limit",
86
- "status.tone.danger": "Current provider error or usage almost exhausted",
91
+ "status.tone.warn": "Current provider usage nearing its limit or balance running low",
92
+ "status.tone.danger": "Current provider error, usage almost exhausted, or balance too low",
87
93
  "countdown.days": "{days}d {hours}h",
88
94
  "countdown.hours": "{hours}h {minutes}m",
89
95
  "countdown.minutes": "{minutes}m"
@@ -109,7 +115,13 @@ window.__ModuleLoader__.load({
109
115
  const STORAGE_KEY = "dsh.provider-usage.refreshSeconds";
110
116
  const LANG_KEY = "dsh.provider-usage.lang";
111
117
  const POS_KEY = "dsh.provider-usage.floatPos";
118
+ const THRESHOLD_RED_KEY = "dsh.provider-usage.balanceRedThreshold";
119
+ const THRESHOLD_YELLOW_KEY = "dsh.provider-usage.balanceYellowThreshold";
112
120
  const DEFAULT_INTERVAL = 60;
121
+ /** Balance thresholds fall back to these hardcoded defaults when neither the
122
+ host config nor a stored override provides them. */
123
+ const DEFAULT_BALANCE_RED = 10;
124
+ const DEFAULT_BALANCE_YELLOW = 30;
113
125
  /** Floating ball diameter in px; drag math derives from it. */
114
126
  const BALL_SIZE = 32;
115
127
  /** Panel-level override stored in localStorage; null means follow the harness language. */
@@ -149,6 +161,28 @@ window.__ModuleLoader__.load({
149
161
  localStorage.setItem(STORAGE_KEY, String(value));
150
162
  } catch {}
151
163
  }
164
+ /** Parse a threshold input: a finite number ≥ 0, or null while the field is
165
+ empty/transiently invalid (the string is kept in state so typing never
166
+ fights the controlled input). */
167
+ function parseThreshold(raw) {
168
+ const trimmed = raw.trim();
169
+ if (trimmed === "") return null;
170
+ const value = Number(trimmed);
171
+ return Number.isFinite(value) && value >= 0 ? value : null;
172
+ }
173
+ function readStoredThreshold(key) {
174
+ try {
175
+ const raw = localStorage.getItem(key);
176
+ return raw !== null && parseThreshold(raw) !== null ? raw : null;
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+ function storeThreshold(key, value) {
182
+ try {
183
+ localStorage.setItem(key, value);
184
+ } catch {}
185
+ }
152
186
  /** Equal margin from the chat area's left edge and the window's bottom edge. */
153
187
  const DOCK_MARGIN = 24;
154
188
  /**
@@ -220,11 +254,52 @@ window.__ModuleLoader__.load({
220
254
  if (percent >= 70) return "warn";
221
255
  return "ok";
222
256
  }
257
+ /** Parse a balance amount string ("12.34", "0.00") into a number. */
258
+ function toAmount(value) {
259
+ const parsed = Number(value);
260
+ return Number.isFinite(parsed) ? parsed : null;
261
+ }
262
+ /**
263
+ * Tone for a monetary balance, compared in the balance's own currency:
264
+ * below the red threshold → danger, below the yellow threshold → warn,
265
+ * otherwise ok. The effective red threshold never exceeds the yellow one,
266
+ * so the zones stay ordered no matter how the panel is configured.
267
+ */
268
+ function balanceTone(value, red, yellow) {
269
+ if (value === null) return "ok";
270
+ if (value < Math.min(red, yellow)) return "danger";
271
+ if (value < yellow) return "warn";
272
+ return "ok";
273
+ }
274
+ /**
275
+ * Tone for one usage row. Windows with a percent (5h/weekly limits) keep the
276
+ * percent-based tone; rows labeled 'credits' carry a remaining balance and
277
+ * are judged by the balance thresholds instead.
278
+ */
279
+ function usageRowTone(row, red, yellow) {
280
+ if (row.label === "credits") return balanceTone(row.remaining, red, yellow);
281
+ return barTone(row.percent);
282
+ }
283
+ /** Worst balance/usage tone across one provider's balances and usage rows. */
284
+ function providerTone(provider, red, yellow) {
285
+ let tone = "ok";
286
+ if (provider.kind === "balance") for (const row of provider.balances ?? []) {
287
+ const rowTone = balanceTone(toAmount(row.total), red, yellow);
288
+ if (rowTone === "danger") return "danger";
289
+ if (rowTone === "warn") tone = "warn";
290
+ }
291
+ for (const row of provider.usages ?? []) {
292
+ const rowTone = usageRowTone(row, red, yellow);
293
+ if (rowTone === "danger") return "danger";
294
+ if (rowTone === "warn") tone = "warn";
295
+ }
296
+ return tone;
297
+ }
223
298
  /** Worst health across the fetch + the provider IN USE: drives the trigger's
224
299
  status dot. Only the active provider (the composer's current selection)
225
300
  colors the ball, so an idle low-quota provider does not alarm; when the
226
301
  host reports no active provider, fall back to every provider. */
227
- function healthTone(data, error) {
302
+ function healthTone(data, error, red, yellow) {
228
303
  if (error !== null) return "danger";
229
304
  const providers = data?.providers ?? [];
230
305
  const active = providers.filter((provider) => provider.active);
@@ -232,11 +307,9 @@ window.__ModuleLoader__.load({
232
307
  let tone = "ok";
233
308
  for (const provider of relevant) {
234
309
  if (provider.status === "error" || provider.status === "missing-credential" || provider.status === "missing-authorization") return "danger";
235
- for (const row of provider.usages ?? []) {
236
- const rowTone = barTone(row.percent);
237
- if (rowTone === "danger") return "danger";
238
- if (rowTone === "warn") tone = "warn";
239
- }
310
+ const providerToneValue = providerTone(provider, red, yellow);
311
+ if (providerToneValue === "danger") return "danger";
312
+ if (providerToneValue === "warn") tone = "warn";
240
313
  }
241
314
  return tone;
242
315
  }
@@ -244,11 +317,13 @@ window.__ModuleLoader__.load({
244
317
  if (value === null) return "—";
245
318
  return Number.isInteger(value) ? String(value) : value.toFixed(2);
246
319
  }
247
- function UsageRows({ provider, now, t }) {
320
+ function UsageRows({ provider, now, red, yellow, t }) {
248
321
  const rows = provider.usages ?? [];
249
322
  if (rows.length === 0) return null;
250
323
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: rows.map((row, index) => {
251
324
  const percent = row.percent;
325
+ const isCredits = row.label === "credits";
326
+ const tone = isCredits ? balanceTone(row.remaining, red, yellow) : barTone(row.percent);
252
327
  const reset = row.resetAt !== null ? formatCountdown(row.resetAt, now, t) : null;
253
328
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
254
329
  className: "dsh-usage-row",
@@ -259,14 +334,14 @@ window.__ModuleLoader__.load({
259
334
  className: "dsh-usage-rowLabel",
260
335
  children: row.label === "weekly" ? t("usage.weekly") : row.label
261
336
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
262
- className: "dsh-usage-rowValue",
263
- children: row.limit === null ? row.used !== null ? t("usage.used", { used: formatAmount(row.used) }) : row.remaining !== null ? formatAmount(row.remaining) : t("usage.unlimited") : percent !== null ? `${Math.round(percent)}%` : t("usage.used", { used: formatAmount(row.used) })
337
+ className: `dsh-usage-rowValue${tone === "ok" ? "" : ` dsh-usage-rowValue--${tone}`}`,
338
+ children: isCredits && row.remaining !== null ? formatAmount(row.remaining) : row.limit === null ? row.used !== null ? t("usage.used", { used: formatAmount(row.used) }) : row.remaining !== null ? formatAmount(row.remaining) : t("usage.unlimited") : percent !== null ? `${Math.round(percent)}%` : t("usage.used", { used: formatAmount(row.used) })
264
339
  })]
265
340
  }),
266
341
  percent !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
267
342
  className: "dsh-usage-barTrack",
268
343
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
269
- className: `dsh-usage-barFill dsh-usage-barFill--${barTone(percent)}`,
344
+ className: `dsh-usage-barFill dsh-usage-barFill--${tone}`,
270
345
  style: { width: `${Math.min(100, Math.max(0, percent))}%` }
271
346
  })
272
347
  }) : null,
@@ -278,7 +353,7 @@ window.__ModuleLoader__.load({
278
353
  }, `${row.label}-${index}`);
279
354
  }) });
280
355
  }
281
- function ProviderCard({ provider, now, t }) {
356
+ function ProviderCard({ provider, now, red, yellow, t }) {
282
357
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
283
358
  className: `dsh-usage-card${provider.active ? " dsh-usage-card--active" : ""}`,
284
359
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -306,21 +381,26 @@ window.__ModuleLoader__.load({
306
381
  ": ",
307
382
  provider.message
308
383
  ]
309
- }) : provider.kind === "balance" ? (provider.balances ?? []).map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
310
- className: "dsh-usage-balanceRow",
311
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
312
- className: "dsh-usage-balanceTotal",
313
- children: row.total
384
+ }) : provider.kind === "balance" ? (provider.balances ?? []).map((row, index) => {
385
+ const tone = balanceTone(toAmount(row.total), red, yellow);
386
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
387
+ className: "dsh-usage-balanceRow",
388
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
389
+ className: `dsh-usage-balanceTotal${tone === "ok" ? "" : ` dsh-usage-balanceTotal--${tone}`}`,
390
+ children: row.total
391
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
392
+ className: "dsh-usage-balanceCurrency",
393
+ children: row.currency
394
+ })]
314
395
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
315
- className: "dsh-usage-balanceCurrency",
316
- children: row.currency
317
- })]
318
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
319
- className: "dsh-usage-balanceParts",
320
- children: [row.granted !== "0" && row.granted !== "0.00" ? t("balance.granted", { amount: row.granted }) : null, row.toppedUp !== "0" && row.toppedUp !== "0.00" ? t("balance.toppedUp", { amount: row.toppedUp }) : null].filter(Boolean).join(" · ")
321
- })] }, `${row.currency}-${index}`)) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageRows, {
396
+ className: "dsh-usage-balanceParts",
397
+ children: [row.granted !== "0" && row.granted !== "0.00" ? t("balance.granted", { amount: row.granted }) : null, row.toppedUp !== "0" && row.toppedUp !== "0.00" ? t("balance.toppedUp", { amount: row.toppedUp }) : null].filter(Boolean).join(" · ")
398
+ })] }, `${row.currency}-${index}`);
399
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageRows, {
322
400
  provider,
323
401
  now,
402
+ red,
403
+ yellow,
324
404
  t
325
405
  })]
326
406
  });
@@ -424,6 +504,11 @@ window.__ModuleLoader__.load({
424
504
  const [loading, setLoading] = (0, react.useState)(false);
425
505
  const [intervalSec, setIntervalSec] = (0, react.useState)(() => readStoredInterval() ?? DEFAULT_INTERVAL);
426
506
  const [langOverride, setLangOverride] = (0, react.useState)(() => readStoredLang());
507
+ /** Balance thresholds as raw input strings (kept as text so typing never
508
+ fights the controlled input); parsed on use, overridden locally only
509
+ after the user edits them. */
510
+ const [redThreshold, setRedThreshold] = (0, react.useState)(() => readStoredThreshold(THRESHOLD_RED_KEY) ?? String(DEFAULT_BALANCE_RED));
511
+ const [yellowThreshold, setYellowThreshold] = (0, react.useState)(() => readStoredThreshold(THRESHOLD_YELLOW_KEY) ?? String(DEFAULT_BALANCE_YELLOW));
427
512
  const [now, setNow] = (0, react.useState)(() => Date.now());
428
513
  /** Pinned spot (user-dragged) as viewport fractions; null = follow the default dock. */
429
514
  const [pinnedPos, setPinnedPos] = (0, react.useState)(() => readStoredFloatPos());
@@ -485,6 +570,11 @@ window.__ModuleLoader__.load({
485
570
  (0, react.useEffect)(() => {
486
571
  if (data !== null && readStoredInterval() === null && data.refreshSeconds !== intervalSec) setIntervalSec(data.refreshSeconds);
487
572
  }, [data]);
573
+ (0, react.useEffect)(() => {
574
+ if (data === null) return;
575
+ if (readStoredThreshold(THRESHOLD_RED_KEY) === null && typeof data.balanceRedThreshold === "number") setRedThreshold(String(data.balanceRedThreshold));
576
+ if (readStoredThreshold(THRESHOLD_YELLOW_KEY) === null && typeof data.balanceYellowThreshold === "number") setYellowThreshold(String(data.balanceYellowThreshold));
577
+ }, [data]);
488
578
  (0, react.useEffect)(() => {
489
579
  if (!open) return;
490
580
  setNow(Date.now());
@@ -533,7 +623,9 @@ window.__ModuleLoader__.load({
533
623
  window.addEventListener("resize", onResize);
534
624
  return () => window.removeEventListener("resize", onResize);
535
625
  }, []);
536
- const tone = healthTone(data, error);
626
+ const effYellow = parseThreshold(yellowThreshold) ?? DEFAULT_BALANCE_YELLOW;
627
+ const effRed = Math.min(parseThreshold(redThreshold) ?? DEFAULT_BALANCE_RED, effYellow);
628
+ const tone = healthTone(data, error, effRed, effYellow);
537
629
  const onKeyDown = (event) => {
538
630
  if (event.key !== "Escape" || !open) return;
539
631
  event.preventDefault();
@@ -682,6 +774,8 @@ window.__ModuleLoader__.load({
682
774
  (data?.providers ?? []).map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderCard, {
683
775
  provider,
684
776
  now,
777
+ red: effRed,
778
+ yellow: effYellow,
685
779
  t: tt
686
780
  }, provider.id)),
687
781
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -709,6 +803,53 @@ window.__ModuleLoader__.load({
709
803
  children: data === null ? tt("panel.never") : tt("panel.updated", { time: new Date(data.fetchedAt).toLocaleTimeString(lang === "zh" ? "zh-CN" : "en-US") })
710
804
  })
711
805
  ]
806
+ }),
807
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
808
+ className: "dsh-usage-thresholds",
809
+ children: [
810
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
811
+ className: "dsh-usage-footLabel",
812
+ children: tt("panel.thresholds")
813
+ }),
814
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
815
+ className: "dsh-usage-thresholdField",
816
+ title: tt("panel.thresholdRed"),
817
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
818
+ className: "dsh-usage-thresholdLabel dsh-usage-thresholdLabel--red",
819
+ children: tt("panel.thresholdRed")
820
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
821
+ className: "dsh-usage-thresholdInput",
822
+ type: "number",
823
+ min: 0,
824
+ step: .01,
825
+ inputMode: "decimal",
826
+ value: redThreshold,
827
+ onChange: (event) => {
828
+ setRedThreshold(event.target.value);
829
+ storeThreshold(THRESHOLD_RED_KEY, event.target.value);
830
+ }
831
+ })]
832
+ }),
833
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
834
+ className: "dsh-usage-thresholdField",
835
+ title: tt("panel.thresholdYellow"),
836
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
837
+ className: "dsh-usage-thresholdLabel dsh-usage-thresholdLabel--yellow",
838
+ children: tt("panel.thresholdYellow")
839
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
840
+ className: "dsh-usage-thresholdInput",
841
+ type: "number",
842
+ min: 0,
843
+ step: .01,
844
+ inputMode: "decimal",
845
+ value: yellowThreshold,
846
+ onChange: (event) => {
847
+ setYellowThreshold(event.target.value);
848
+ storeThreshold(THRESHOLD_YELLOW_KEY, event.target.value);
849
+ }
850
+ })]
851
+ })
852
+ ]
712
853
  })
713
854
  ]
714
855
  }), document.body) : null]
@@ -838,12 +979,19 @@ window.__ModuleLoader__.load({
838
979
  color: var(--dsw-alias-label-primary); font-size: 18px; font-weight: 600;
839
980
  font-variant-numeric: tabular-nums;
840
981
  }
982
+ /* Balance below the user-configured thresholds: red < red threshold, yellow
983
+ below the yellow threshold; a healthy balance keeps the default label color. */
984
+ .dsh-usage-balanceTotal--warn { color: #e2b93b; }
985
+ .dsh-usage-balanceTotal--danger { color: #d94f4f; }
841
986
  .dsh-usage-balanceCurrency { color: var(--dsw-alias-label-secondary); font-size: 12px; }
842
987
  .dsh-usage-balanceParts { color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
843
988
  .dsh-usage-row { display: flex; flex-direction: column; gap: 3px; }
844
989
  .dsh-usage-rowHead { display: flex; align-items: center; gap: 8px; font-size: 12px; line-height: 18px; }
845
990
  .dsh-usage-rowLabel { color: var(--dsw-alias-label-secondary); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
846
991
  .dsh-usage-rowValue { color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; flex: none; }
992
+ /* Credits rows show a remaining balance: tinted by the balance thresholds. */
993
+ .dsh-usage-rowValue--warn { color: #e2b93b; }
994
+ .dsh-usage-rowValue--danger { color: #d94f4f; }
847
995
  .dsh-usage-barTrack {
848
996
  height: 4px; border-radius: 2px; overflow: hidden;
849
997
  background: var(--dsw-alias-border-l2);
@@ -863,6 +1011,25 @@ window.__ModuleLoader__.load({
863
1011
  color: var(--dsw-alias-label-secondary); font-size: 11px; padding: 2px 4px; cursor: pointer;
864
1012
  }
865
1013
  .dsh-usage-updated { color: var(--dsw-alias-label-tertiary); font-size: 11px; margin-left: auto; }
1014
+ /* Balance threshold row under the footer: red/yellow inputs persisted locally. */
1015
+ .dsh-usage-thresholds {
1016
+ display: flex; align-items: center; gap: 10px; padding: 2px 4px 4px;
1017
+ }
1018
+ .dsh-usage-thresholdField { display: inline-flex; align-items: center; gap: 4px; }
1019
+ .dsh-usage-thresholdLabel { font-size: 11px; font-weight: 600; }
1020
+ .dsh-usage-thresholdLabel--red { color: #d94f4f; }
1021
+ .dsh-usage-thresholdLabel--yellow { color: #e2b93b; }
1022
+ .dsh-usage-thresholdInput {
1023
+ width: 58px; box-sizing: border-box;
1024
+ border: 1px solid var(--dsw-alias-border-l2); border-radius: 6px; background: 0;
1025
+ color: var(--dsw-alias-label-secondary); font-size: 11px; padding: 2px 4px;
1026
+ font-variant-numeric: tabular-nums;
1027
+ }
1028
+ .dsh-usage-thresholdInput:focus {
1029
+ outline: none;
1030
+ border-color: var(--dsw-alias-accent, #4f8cff);
1031
+ box-shadow: 0 0 0 1px color-mix(in srgb, var(--dsw-alias-accent, #4f8cff) 40%, transparent);
1032
+ }
866
1033
  `;
867
1034
  const TAG_ID = "dsh-provider-usage/panel.css";
868
1035
  /** Inject the panel stylesheet once; on HMR swap, refresh the existing tag's payload in place. */
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["useState","useMemo","useCallback","useRef","createPortal"],"sources":["../src/client/locales.ts","../src/client/Panel.tsx","../src/client/styles.ts","../src/client/index.ts"],"sourcesContent":["/** Simplified Chinese dictionary (the key-set source of truth). */\nexport const zh = {\n 'action.label': '用量',\n 'action.aria': 'Provider 用量',\n 'panel.title': 'Provider 用量',\n 'panel.refresh': '立即刷新',\n 'panel.refreshing': '刷新中…',\n 'lang.switch': 'Switch to English',\n 'panel.resetPos': '悬浮球归位',\n 'panel.interval': '刷新周期',\n 'panel.updated': '更新于 {time}',\n 'panel.never': '尚未获取',\n 'panel.empty': '未检测到已配置的 Provider',\n 'interval.15s': '15 秒',\n 'interval.30s': '30 秒',\n 'interval.60s': '1 分钟',\n 'interval.300s': '5 分钟',\n 'interval.900s': '15 分钟',\n 'interval.1800s': '30 分钟',\n 'balance.total': '总额',\n 'balance.granted': '赠送 {amount}',\n 'balance.toppedUp': '充值 {amount}',\n 'usage.weekly': '每周配额',\n 'usage.used': '已用 {used}',\n 'usage.remaining': '剩余 {remaining}',\n 'usage.unlimited': '不限量',\n 'usage.resetAt': '重置于 {time}',\n 'usage.resetIn': '{countdown} 后重置',\n 'status.unsupported': '该 Provider 暂不支持额度查询',\n 'status.missingCredential': '未配置密钥({ref})',\n 'status.missingAuthorization': '未完成 OAuth 授权({key})',\n 'status.error': '查询失败',\n 'status.inUse': '使用中',\n 'status.tone.ok': '当前 Provider 正常',\n 'status.tone.warn': '当前 Provider 用量接近上限',\n 'status.tone.danger': '当前 Provider 异常或用量将尽',\n 'countdown.days': '{days}天{hours}小时',\n 'countdown.hours': '{hours}小时{minutes}分',\n 'countdown.minutes': '{minutes}分钟',\n}\n\n/** English dictionary, key-identical to the Chinese source of truth. */\nexport const en = {\n 'action.label': 'Usage',\n 'action.aria': 'Provider usage',\n 'panel.title': 'Provider Usage',\n 'panel.refresh': 'Refresh now',\n 'panel.refreshing': 'Refreshing…',\n 'lang.switch': '切换到中文',\n 'panel.resetPos': 'Reset ball position',\n 'panel.interval': 'Refresh interval',\n 'panel.updated': 'Updated {time}',\n 'panel.never': 'Not fetched yet',\n 'panel.empty': 'No configured provider detected',\n 'interval.15s': '15s',\n 'interval.30s': '30s',\n 'interval.60s': '1 min',\n 'interval.300s': '5 min',\n 'interval.900s': '15 min',\n 'interval.1800s': '30 min',\n 'balance.total': 'Total',\n 'balance.granted': 'granted {amount}',\n 'balance.toppedUp': 'topped up {amount}',\n 'usage.weekly': 'Weekly quota',\n 'usage.used': 'used {used}',\n 'usage.remaining': '{remaining} left',\n 'usage.unlimited': 'Unlimited',\n 'usage.resetAt': 'Resets at {time}',\n 'usage.resetIn': 'Resets in {countdown}',\n 'status.unsupported': 'Usage query is not supported for this provider',\n 'status.missingCredential': 'No API key configured ({ref})',\n 'status.missingAuthorization': 'OAuth authorization missing ({key})',\n 'status.error': 'Query failed',\n 'status.inUse': 'In use',\n 'status.tone.ok': 'Current provider healthy',\n 'status.tone.warn': 'Current provider usage nearing its limit',\n 'status.tone.danger': 'Current provider error or usage almost exhausted',\n 'countdown.days': '{days}d {hours}h',\n 'countdown.hours': '{hours}h {minutes}m',\n 'countdown.minutes': '{minutes}m',\n}\n","/**\n * Floating-ball quota widget: a draggable ball portaled to document.body\n * (the sidebar slot only mounts the component; the button itself no longer\n * lives in the sidebar, avoiding slot layout contention with other plugins).\n * The popover panel lists every configured provider's live balance/quota.\n * Polling interval is user-selectable (persisted in localStorage) and falls\n * back to the deployment-suggested value from the host plugin config.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport { en, zh } from './locales'\n\n/* ------------------------------------------------------------------ *\n * Wire views (mirror of the host half)\n * ------------------------------------------------------------------ */\n\ninterface BalanceRow {\n currency: string\n total: string\n granted: string\n toppedUp: string\n}\n\ninterface UsageRow {\n label: string\n used: number | null\n limit: number | null\n remaining: number | null\n percent: number | null\n resetAt: string | null\n}\n\ninterface ProviderUsageView {\n id: string\n displayName: string\n kind: 'balance' | 'usage' | null\n status: 'ok' | 'error' | 'missing-credential' | 'missing-authorization' | 'unsupported'\n message: string | null\n balances: BalanceRow[] | null\n usages: UsageRow[] | null\n /** The provider in use (absent on older hosts); it alone drives the ball tone. */\n active?: boolean\n}\n\ninterface UsageListResult {\n fetchedAt: string\n refreshSeconds: number\n /** Plugin package version (absent on older hosts). */\n version?: string\n providers: ProviderUsageView[]\n}\n\nexport interface UsageActionProps {\n /** Sidebar expansion state, owned by the sidebar footer slot. Not read\n directly — a change re-renders us, which re-anchors the default dock. */\n wide?: boolean\n /** Namespace translator injected by the locale seat. */\n t: (key: string, params?: Record<string, unknown>) => string\n /** Business face: call the host `usage/list` remote. */\n fetchUsage: () => Promise<UsageListResult>\n}\n\n/* ------------------------------------------------------------------ *\n * Refresh interval\n * ------------------------------------------------------------------ */\n\nconst INTERVAL_OPTIONS = [15, 30, 60, 300, 900, 1800] as const\nconst STORAGE_KEY = 'dsh.provider-usage.refreshSeconds'\nconst LANG_KEY = 'dsh.provider-usage.lang'\nconst POS_KEY = 'dsh.provider-usage.floatPos'\nconst DEFAULT_INTERVAL = 60\n\n/** Floating ball diameter in px; drag math derives from it. */\nconst BALL_SIZE = 32\n\ntype Lang = 'zh' | 'en'\n/** Docked ball position: the ball CENTER as fractions of the viewport, so\n both the default spot and dragged spots follow window resizes. */\ninterface FloatPos {\n fx: number\n fy: number\n}\n\n/** Panel-level override stored in localStorage; null means follow the harness language. */\nfunction readStoredLang(): Lang | null {\n try {\n const raw = localStorage.getItem(LANG_KEY)\n return raw === 'zh' || raw === 'en' ? raw : null\n } catch {\n return null\n }\n}\n\nfunction storeLang(lang: Lang): void {\n try {\n localStorage.setItem(LANG_KEY, lang)\n } catch {\n /* storage unavailable: keep the in-memory value only */\n }\n}\n\n/** Local translator over the bundled dictionaries, mirroring the shell's `{param}` interpolation. */\nfunction makeT(dict: Record<string, string>): UsageActionProps['t'] {\n return (key, params) => {\n const template = dict[key] ?? key\n if (params === undefined) return template\n return template.replace(/\\{(\\w+)\\}/g, (raw, name: string) => (name in params ? String(params[name]) : raw))\n }\n}\n\nfunction readStoredInterval(): number | null {\n try {\n const raw = localStorage.getItem(STORAGE_KEY)\n if (raw === null) return null\n const value = Number(raw)\n return Number.isFinite(value) && value >= 5 ? value : null\n } catch {\n return null\n }\n}\n\nfunction storeInterval(value: number): void {\n try {\n localStorage.setItem(STORAGE_KEY, String(value))\n } catch {\n /* storage unavailable: keep the in-memory value only */\n }\n}\n\n/* ------------------------------------------------------------------ *\n * Floating ball position\n * ------------------------------------------------------------------ */\n\n/** Equal margin from the chat area's left edge and the window's bottom edge. */\nconst DOCK_MARGIN = 24\n\n/**\n * Default dock: bottom-left of the main chat area with equal margins on both\n * axes. Returned as viewport fractions of the ball CENTER, like every other\n * position. The chat area's left edge is the sidebar's right edge, derived\n * from the full-height ancestor of the settings slot (stable data-slot hook;\n * class names are build-hashed), so it adapts to sidebar collapse.\n */\nfunction defaultFloatPos(): FloatPos {\n let sidebarRight = 264\n let node = document.querySelector('[data-slot=\"sidebar.settings\"]')?.parentElement ?? null\n while (node) {\n const rect = node.getBoundingClientRect()\n if (rect.x <= 1 && rect.height >= window.innerHeight * 0.9) {\n sidebarRight = rect.right\n break\n }\n node = node.parentElement\n }\n return {\n fx: (sidebarRight + DOCK_MARGIN + BALL_SIZE / 2) / window.innerWidth,\n fy: (window.innerHeight - DOCK_MARGIN - BALL_SIZE / 2) / window.innerHeight,\n }\n}\n\nfunction readStoredFloatPos(): FloatPos | null {\n try {\n const raw = localStorage.getItem(POS_KEY)\n if (raw === null) return null\n const parsed = JSON.parse(raw) as Partial<FloatPos> | null\n if (parsed === null || typeof parsed.fx !== 'number' || !Number.isFinite(parsed.fx)) return null\n if (typeof parsed.fy !== 'number' || !Number.isFinite(parsed.fy)) return null\n return { fx: Math.min(1, Math.max(0, parsed.fx)), fy: Math.min(1, Math.max(0, parsed.fy)) }\n } catch {\n return null\n }\n}\n\nfunction storeFloatPos(pos: FloatPos): void {\n try {\n localStorage.setItem(POS_KEY, JSON.stringify(pos))\n } catch {\n /* storage unavailable: keep the in-memory value only */\n }\n}\n\n/** 归位: forget the pinned spot; the ball follows the default dock again. */\nfunction clearStoredFloatPos(): void {\n try {\n localStorage.removeItem(POS_KEY)\n } catch {\n /* storage unavailable */\n }\n}\n\n/* ------------------------------------------------------------------ *\n * Formatting helpers\n * ------------------------------------------------------------------ */\n\nfunction formatCountdown(resetAt: string, now: number, t: UsageActionProps['t']): string | null {\n const diff = new Date(resetAt).getTime() - now\n if (!Number.isFinite(diff) || diff <= 0) return null\n const minutes = Math.floor(diff / 60000)\n if (minutes >= 60 * 24) return t('countdown.days', { days: Math.floor(minutes / (60 * 24)), hours: Math.floor((minutes % (60 * 24)) / 60) })\n if (minutes >= 60) return t('countdown.hours', { hours: Math.floor(minutes / 60), minutes: minutes % 60 })\n return t('countdown.minutes', { minutes: Math.max(1, minutes) })\n}\n\nfunction barTone(percent: number | null): 'ok' | 'warn' | 'danger' {\n if (percent === null) return 'ok'\n if (percent >= 90) return 'danger'\n if (percent >= 70) return 'warn'\n return 'ok'\n}\n\n/** Worst health across the fetch + the provider IN USE: drives the trigger's\n status dot. Only the active provider (the composer's current selection)\n colors the ball, so an idle low-quota provider does not alarm; when the\n host reports no active provider, fall back to every provider. */\nfunction healthTone(data: UsageListResult | null, error: string | null): 'ok' | 'warn' | 'danger' {\n if (error !== null) return 'danger'\n const providers = data?.providers ?? []\n const active = providers.filter((provider) => provider.active)\n const relevant = active.length > 0 ? active : providers\n let tone: 'ok' | 'warn' | 'danger' = 'ok'\n for (const provider of relevant) {\n if (provider.status === 'error' || provider.status === 'missing-credential' || provider.status === 'missing-authorization') return 'danger'\n for (const row of provider.usages ?? []) {\n const rowTone = barTone(row.percent)\n if (rowTone === 'danger') return 'danger'\n if (rowTone === 'warn') tone = 'warn'\n }\n }\n return tone\n}\n\nfunction formatAmount(value: number | null): string {\n if (value === null) return '—'\n return Number.isInteger(value) ? String(value) : value.toFixed(2)\n}\n\n/* ------------------------------------------------------------------ *\n * Provider card\n * ------------------------------------------------------------------ */\n\nfunction UsageRows({ provider, now, t }: { provider: ProviderUsageView; now: number; t: UsageActionProps['t'] }) {\n const rows = provider.usages ?? []\n if (rows.length === 0) return null\n return (\n <>\n {rows.map((row, index) => {\n const percent = row.percent\n const reset = row.resetAt !== null ? formatCountdown(row.resetAt, now, t) : null\n return (\n <div className=\"dsh-usage-row\" key={`${row.label}-${index}`}>\n <div className=\"dsh-usage-rowHead\">\n <span className=\"dsh-usage-rowLabel\">{row.label === 'weekly' ? t('usage.weekly') : row.label}</span>\n <span className=\"dsh-usage-rowValue\">\n {row.limit === null\n ? row.used !== null\n ? t('usage.used', { used: formatAmount(row.used) })\n : row.remaining !== null\n ? formatAmount(row.remaining)\n : t('usage.unlimited')\n : percent !== null\n ? `${Math.round(percent)}%`\n : t('usage.used', { used: formatAmount(row.used) })}\n </span>\n </div>\n {percent !== null ? (\n <div className=\"dsh-usage-barTrack\">\n <div\n className={`dsh-usage-barFill dsh-usage-barFill--${barTone(percent)}`}\n style={{ width: `${Math.min(100, Math.max(0, percent))}%` }}\n />\n </div>\n ) : null}\n {reset !== null ? <span className=\"dsh-usage-rowReset\">{t('usage.resetIn', { countdown: reset })}</span> : null}\n </div>\n )\n })}\n </>\n )\n}\n\nfunction ProviderCard({ provider, now, t }: { provider: ProviderUsageView; now: number; t: UsageActionProps['t'] }) {\n return (\n <div className={`dsh-usage-card${provider.active ? ' dsh-usage-card--active' : ''}`}>\n <div className=\"dsh-usage-cardHead\">\n <span className=\"dsh-usage-providerName\">{provider.displayName}</span>\n {provider.active ? <span className=\"dsh-usage-inUse\">{t('status.inUse')}</span> : null}\n </div>\n {provider.status === 'unsupported' ? (\n <span className=\"dsh-usage-message\">{t('status.unsupported')}</span>\n ) : provider.status === 'missing-credential' ? (\n <span className=\"dsh-usage-message\">{t('status.missingCredential', { ref: provider.message })}</span>\n ) : provider.status === 'missing-authorization' ? (\n <span className=\"dsh-usage-message\">{t('status.missingAuthorization', { key: provider.message })}</span>\n ) : provider.status === 'error' ? (\n <span className=\"dsh-usage-message\">{t('status.error')}: {provider.message}</span>\n ) : provider.kind === 'balance' ? (\n (provider.balances ?? []).map((row, index) => (\n <div key={`${row.currency}-${index}`}>\n <div className=\"dsh-usage-balanceRow\">\n <span className=\"dsh-usage-balanceTotal\">{row.total}</span>\n <span className=\"dsh-usage-balanceCurrency\">{row.currency}</span>\n </div>\n <span className=\"dsh-usage-balanceParts\">\n {[row.granted !== '0' && row.granted !== '0.00' ? t('balance.granted', { amount: row.granted }) : null,\n row.toppedUp !== '0' && row.toppedUp !== '0.00' ? t('balance.toppedUp', { amount: row.toppedUp }) : null]\n .filter(Boolean)\n .join(' · ')}\n </span>\n </div>\n ))\n ) : (\n <UsageRows provider={provider} now={now} t={t} />\n )}\n </div>\n )\n}\n\n/* ------------------------------------------------------------------ *\n * Trigger + popover\n * ------------------------------------------------------------------ */\n\n/** Battery level: remaining charge reads as remaining quota. */\nfunction BatteryIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <rect width=\"16\" height=\"10\" x=\"2\" y=\"7\" rx=\"2\" ry=\"2\" />\n <path d=\"M22 11v2\" />\n <path d=\"M6 11v2\" />\n <path d=\"M10 11v2\" />\n </svg>\n )\n}\n\nfunction RefreshIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M20 12a8 8 0 1 1-2.34-5.66\" />\n <path d=\"M20 4v4h-4\" />\n </svg>\n )\n}\n\nfunction GlobeIcon() {\n return (\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M3 12h18\" />\n <path d=\"M12 3c2.5 2.6 3.9 5.7 3.9 9s-1.4 6.4-3.9 9c-2.5-2.6-3.9-5.7-3.9-9S9.5 5.6 12 3z\" />\n </svg>\n )\n}\n\n/** Crosshair for the \"reset ball to its default spot\" head button. */\nfunction HomeIcon() {\n return (\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"7\" />\n <path d=\"M12 2v3M12 19v3M2 12h3M19 12h3\" />\n <circle cx=\"12\" cy=\"12\" r=\"1.5\" fill=\"currentColor\" stroke=\"none\" />\n </svg>\n )\n}\n\nexport default function UsageAction({ t, fetchUsage }: UsageActionProps) {\n const [open, setOpen] = useState(false)\n const [data, setData] = useState<UsageListResult | null>(null)\n const [error, setError] = useState<string | null>(null)\n const [loading, setLoading] = useState(false)\n const [intervalSec, setIntervalSec] = useState(() => readStoredInterval() ?? DEFAULT_INTERVAL)\n const [langOverride, setLangOverride] = useState<Lang | null>(() => readStoredLang())\n const [now, setNow] = useState(() => Date.now())\n /** Pinned spot (user-dragged) as viewport fractions; null = follow the default dock. */\n const [pinnedPos, setPinnedPos] = useState<FloatPos | null>(() => readStoredFloatPos())\n /** Viewport size drives the fraction → px conversion and the default dock. */\n const [viewport, setViewport] = useState({ w: window.innerWidth, h: window.innerHeight })\n /** Transient ball position while dragging (cursor-centered); null when docked. */\n const [dragPos, setDragPos] = useState<{ x: number; y: number } | null>(null)\n\n // Follow the harness language by default: probe the shell-injected\n // translator with a known key, then let a panel-level override win.\n const harnessLang: Lang | null =\n t('lang.switch') === zh['lang.switch'] ? 'zh' : t('lang.switch') === en['lang.switch'] ? 'en' : null\n const lang: Lang = langOverride ?? harnessLang ?? 'en'\n const tt = useMemo(() => makeT(lang === 'zh' ? (zh as Record<string, string>) : (en as Record<string, string>)), [lang])\n const toggleLang = useCallback(() => {\n setLangOverride((current) => {\n const next: Lang = (current ?? harnessLang ?? 'en') === 'zh' ? 'en' : 'zh'\n storeLang(next)\n return next\n })\n }, [harnessLang])\n /** Panel-head home button: unpin the ball so it follows the default dock. */\n const resetFloatPos = () => {\n clearStoredFloatPos()\n setPinnedPos(null)\n }\n const [panelPos, setPanelPos] = useState<{ left: number; top?: number; bottom?: number } | null>(null)\n const rootRef = useRef<HTMLDivElement>(null)\n const ballRef = useRef<HTMLButtonElement>(null)\n const panelRef = useRef<HTMLDivElement>(null)\n const inFlight = useRef(false)\n /** Active ball-drag session; cleared on pointerup/pointercancel. */\n const dragRef = useRef<{ pointerId: number; startX: number; startY: number; dragging: boolean } | null>(null)\n /** Set when a drag ends so the trailing click does not toggle the panel. */\n const suppressClickRef = useRef(false)\n\n const refresh = useCallback(async () => {\n if (inFlight.current) return\n inFlight.current = true\n setLoading(true)\n try {\n const result = await fetchUsage()\n setData(result)\n setError(null)\n } catch (reason) {\n setError(reason instanceof Error ? reason.message : String(reason))\n } finally {\n inFlight.current = false\n setLoading(false)\n }\n }, [fetchUsage])\n\n // Initial fetch + fixed-interval polling; skip ticks while the tab is hidden.\n useEffect(() => {\n refresh()\n }, [refresh])\n useEffect(() => {\n const timer = setInterval(() => {\n if (document.visibilityState === 'visible') refresh()\n }, intervalSec * 1000)\n return () => clearInterval(timer)\n }, [intervalSec, refresh])\n\n // Adopt the deployment-suggested interval until the user picks one locally.\n useEffect(() => {\n if (data !== null && readStoredInterval() === null && data.refreshSeconds !== intervalSec) {\n setIntervalSec(data.refreshSeconds)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [data])\n\n // Slow tick so reset countdowns advance while the panel is open.\n useEffect(() => {\n if (!open) return\n setNow(Date.now())\n const timer = setInterval(() => setNow(Date.now()), 30000)\n return () => clearInterval(timer)\n }, [open])\n\n // Close on outside press / Escape, mirroring the shipped popover behavior.\n // The panel and the ball are portaled to document.body, so all trees count\n // as inside.\n useEffect(() => {\n if (!open) return\n const closeOutside = (event: PointerEvent) => {\n if (!(event.target instanceof Node)) return\n if (rootRef.current?.contains(event.target)) return\n if (ballRef.current?.contains(event.target)) return\n if (panelRef.current?.contains(event.target)) return\n setOpen(false)\n }\n document.addEventListener('pointerdown', closeOutside)\n return () => document.removeEventListener('pointerdown', closeOutside)\n }, [open])\n\n // Anchor the fixed-position panel to the ball; recompute while open so a\n // position reset or window resize keeps it attached. A ball in the lower\n // half of the viewport opens the panel above it, upper half below.\n useEffect(() => {\n if (!open) return\n const update = () => {\n const rect = ballRef.current?.getBoundingClientRect()\n if (!rect) return\n const left = Math.min(Math.max(8, rect.left), Math.max(8, window.innerWidth - 336))\n setPanelPos(\n rect.top > window.innerHeight / 2\n ? { left, bottom: window.innerHeight - rect.top + 6 }\n : { left, top: rect.bottom + 6 },\n )\n }\n update()\n window.addEventListener('resize', update)\n return () => window.removeEventListener('resize', update)\n }, [open, pinnedPos, viewport])\n\n // Track the viewport: fraction-based positions and the default dock follow\n // window resizes automatically.\n useEffect(() => {\n const onResize = () => setViewport({ w: window.innerWidth, h: window.innerHeight })\n window.addEventListener('resize', onResize)\n return () => window.removeEventListener('resize', onResize)\n }, [])\n\n const tone = healthTone(data, error)\n\n const onKeyDown = (event: React.KeyboardEvent) => {\n if (event.key !== 'Escape' || !open) return\n event.preventDefault()\n setOpen(false)\n }\n\n /* Ball dragging: pointer events only (touch included via touch-action:\n none). A press becomes a drag past a 5px dead zone; while dragging the\n ball follows the cursor centered on it, and on release it stays exactly\n where dropped (no edge snapping). A plain press falls through to the\n click handler. */\n const onBallPointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {\n event.currentTarget.setPointerCapture(event.pointerId)\n dragRef.current = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, dragging: false }\n }\n\n const onBallPointerMove = (event: React.PointerEvent<HTMLButtonElement>) => {\n const drag = dragRef.current\n if (drag === null || drag.pointerId !== event.pointerId) return\n if (!drag.dragging) {\n if (Math.abs(event.clientX - drag.startX) < 5 && Math.abs(event.clientY - drag.startY) < 5) return\n drag.dragging = true\n // The panel would detach from a moving anchor: close it on drag start.\n if (open) setOpen(false)\n }\n const half = BALL_SIZE / 2\n setDragPos({\n x: Math.min(Math.max(half, event.clientX), window.innerWidth - half),\n y: Math.min(Math.max(half, event.clientY), window.innerHeight - half),\n })\n }\n\n const endBallDrag = (event: React.PointerEvent<HTMLButtonElement>) => {\n const drag = dragRef.current\n dragRef.current = null\n if (drag === null || drag.pointerId !== event.pointerId || !drag.dragging) return\n const half = BALL_SIZE / 2\n const x = Math.min(Math.max(half, event.clientX), window.innerWidth - half)\n const y = Math.min(Math.max(half, event.clientY), window.innerHeight - half)\n // Pin the drop point as viewport fractions so it follows window resizes.\n const next: FloatPos = { fx: x / window.innerWidth, fy: y / window.innerHeight }\n setPinnedPos(next)\n storeFloatPos(next)\n setDragPos(null)\n suppressClickRef.current = true\n }\n\n const cancelBallDrag = (event: React.PointerEvent<HTMLButtonElement>) => {\n if (dragRef.current?.pointerId !== event.pointerId) return\n dragRef.current = null\n setDragPos(null)\n }\n\n const onBallClick = () => {\n if (suppressClickRef.current) {\n suppressClickRef.current = false\n return\n }\n setNow(Date.now())\n setOpen((current) => !current)\n if (!open) refresh()\n }\n\n // Ball center in px: transient drag point > pinned fractions > default dock.\n // (The default reads the sidebar edge from the DOM at render time; cheap.)\n const docked = pinnedPos ?? defaultFloatPos()\n const centerX = dragPos !== null ? dragPos.x : docked.fx * viewport.w\n const centerY = dragPos !== null ? dragPos.y : docked.fy * viewport.h\n\n return (\n <div ref={rootRef} className=\"dsh-usage-root\" onKeyDown={onKeyDown}>\n {createPortal(\n <button\n ref={ballRef}\n type=\"button\"\n className={`dsh-usage-ball dsh-usage-tone-${tone}`}\n style={{ left: centerX - BALL_SIZE / 2, top: centerY - BALL_SIZE / 2 }}\n aria-expanded={open}\n aria-label={tt('action.aria')}\n title={`${tt('action.aria')} · ${tt(`status.tone.${tone}`)}`}\n onPointerDown={onBallPointerDown}\n onPointerMove={onBallPointerMove}\n onPointerUp={endBallDrag}\n onPointerCancel={cancelBallDrag}\n onClick={onBallClick}\n onKeyDown={onKeyDown}\n >\n <BatteryIcon />\n </button>,\n document.body,\n )}\n {open && panelPos !== null\n ? createPortal(\n <div\n ref={panelRef}\n className=\"dsh-usage-panel\"\n role=\"dialog\"\n aria-label={tt('panel.title')}\n style={{ left: panelPos.left, top: panelPos.top, bottom: panelPos.bottom }}\n onKeyDown={onKeyDown}\n >\n <div className=\"dsh-usage-head\">\n <span className=\"dsh-usage-title\">{tt('panel.title')}</span>\n {data?.version ? <span className=\"dsh-usage-version\">v{data.version}</span> : null}\n <button\n type=\"button\"\n className=\"dsh-usage-home\"\n onClick={resetFloatPos}\n title={tt('panel.resetPos')}\n aria-label={tt('panel.resetPos')}\n >\n <HomeIcon />\n </button>\n <button\n type=\"button\"\n className=\"dsh-usage-lang\"\n onClick={toggleLang}\n title={tt('lang.switch')}\n aria-label={tt('lang.switch')}\n >\n <GlobeIcon />\n <span>{lang === 'zh' ? '中' : 'EN'}</span>\n </button>\n <button\n type=\"button\"\n className={`dsh-usage-refresh${loading ? ' dsh-usage-refresh--loading' : ''}`}\n disabled={loading}\n onClick={refresh}\n title={tt('panel.refresh')}\n aria-label={tt('panel.refresh')}\n >\n <RefreshIcon />\n </button>\n </div>\n {data === null && error !== null ? (\n <span className=\"dsh-usage-message\">{tt('status.error')}: {error}</span>\n ) : null}\n {data !== null && data.providers.length === 0 ? (\n <span className=\"dsh-usage-message\">{tt('panel.empty')}</span>\n ) : null}\n {(data?.providers ?? []).map((provider) => (\n <ProviderCard key={provider.id} provider={provider} now={now} t={tt} />\n ))}\n <div className=\"dsh-usage-foot\">\n <span className=\"dsh-usage-footLabel\">{tt('panel.interval')}</span>\n <select\n className=\"dsh-usage-select\"\n value={intervalSec}\n onChange={(event) => {\n const value = Number(event.target.value)\n setIntervalSec(value)\n storeInterval(value)\n }}\n >\n {INTERVAL_OPTIONS.map((seconds) => (\n <option key={seconds} value={seconds}>{tt(`interval.${seconds}s`)}</option>\n ))}\n </select>\n <span className=\"dsh-usage-updated\">\n {data === null\n ? tt('panel.never')\n : tt('panel.updated', { time: new Date(data.fetchedAt).toLocaleTimeString(lang === 'zh' ? 'zh-CN' : 'en-US') })}\n </span>\n </div>\n </div>,\n document.body,\n )\n : null}\n </div>\n )\n}\n","/**\n * Panel stylesheet, injected once per page. Follows the shipped plugins'\n * convention of a `<style data-plugin-css>` tag and design-token colors.\n */\nconst CSS = `\n.dsh-usage-root { position: relative; display: inline-flex; }\n/* Tri-tone health halo: only the shadow carries the state of the provider IN\n USE (green ok / amber remaining<30% / red error or usage>=90%) — the button\n face itself stays neutral. A 1px tone ring plus a soft halo keeps the\n state readable on both light and dark skins. */\n.dsh-usage-tone-ok { --dsh-usage-tone: #22a06b; }\n.dsh-usage-tone-warn { --dsh-usage-tone: #e2b93b; }\n.dsh-usage-tone-danger { --dsh-usage-tone: #d94f4f; }\n/* Floating ball trigger: a draggable fixed circle portaled to document.body,\n one layer below the panel (1000) so the popover always covers it. Position\n comes from inline style: docked it sits at its stored coordinates, while\n dragging it follows the cursor. Keeps the shell's lv3 lift shadow; the\n tone halo stacks on top. */\n.dsh-usage-ball {\n position: fixed; z-index: 999; width: 32px; height: 32px; border-radius: 50%;\n cursor: pointer; touch-action: none;\n display: inline-flex; align-items: center; justify-content: center;\n border: 1px solid var(--dsw-alias-border-l2);\n background-color: var(--dsw-specific-menu);\n color: var(--dsw-alias-label-tertiary);\n transition: box-shadow 0.15s;\n}\n.dsh-usage-ball:hover, .dsh-usage-ball:focus-visible { color: var(--dsw-alias-label-secondary); }\n.dsh-usage-ball svg { width: 15px; height: 15px; }\n.dsh-usage-ball.dsh-usage-tone-ok,\n.dsh-usage-ball.dsh-usage-tone-warn,\n.dsh-usage-ball.dsh-usage-tone-danger {\n box-shadow:\n var(--dsw-shadow-lv3),\n 0 0 0 1px color-mix(in srgb, var(--dsh-usage-tone) 40%, transparent),\n 0 0 14px color-mix(in srgb, var(--dsh-usage-tone) 55%, transparent);\n}\n.dsh-usage-ball.dsh-usage-tone-ok:hover,\n.dsh-usage-ball.dsh-usage-tone-warn:hover,\n.dsh-usage-ball.dsh-usage-tone-danger:hover,\n.dsh-usage-ball.dsh-usage-tone-ok:focus-visible,\n.dsh-usage-ball.dsh-usage-tone-warn:focus-visible,\n.dsh-usage-ball.dsh-usage-tone-danger:focus-visible {\n box-shadow:\n var(--dsw-shadow-lv3),\n 0 0 0 1px color-mix(in srgb, var(--dsh-usage-tone) 55%, transparent),\n 0 0 18px color-mix(in srgb, var(--dsh-usage-tone) 70%, transparent);\n}\n.dsh-usage-panel {\n z-index: 1000; box-sizing: border-box;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-specific-menu);\n box-shadow: var(--dsw-shadow-lv3);\n border-radius: 12px; margin: 0; padding: 8px;\n width: 320px; max-width: min(360px, 100vw - 16px);\n max-height: min(460px, 100vh - 60px); overflow: auto;\n display: flex; flex-direction: column; gap: 6px;\n /* position: fixed with viewport coordinates computed inline from the\n trigger rect — absolute positioning inside the sidebar gets clipped by\n the column's overflow. */\n position: fixed;\n --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);\n --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);\n}\n.dsh-usage-head {\n display: flex; align-items: center; gap: 8px; padding: 2px 4px 6px;\n border-bottom: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-usage-title { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 600; }\n.dsh-usage-version {\n color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px;\n padding: 0 5px; border-radius: 4px; background: var(--dsw-alias-fill-l2);\n font-variant-numeric: tabular-nums; user-select: none; margin-right: auto;\n}\n.dsh-usage-lang {\n cursor: pointer; display: inline-flex; align-items: center; gap: 4px;\n border: 0; border-radius: 6px; background: 0; padding: 3px 5px;\n color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px;\n}\n.dsh-usage-lang:hover { color: var(--dsw-alias-label-secondary); }\n.dsh-usage-refresh {\n cursor: pointer; display: inline-flex; align-items: center; justify-content: center;\n border: 0; border-radius: 6px; background: 0; padding: 4px;\n color: var(--dsw-alias-label-tertiary); line-height: 0;\n}\n.dsh-usage-refresh:hover { color: var(--dsw-alias-label-secondary); }\n.dsh-usage-refresh:disabled { opacity: 0.5; cursor: default; }\n/* Home button in the panel head: sends the ball back to its default spot. */\n.dsh-usage-home {\n cursor: pointer; display: inline-flex; align-items: center; justify-content: center;\n border: 0; border-radius: 6px; background: 0; padding: 4px;\n color: var(--dsw-alias-label-tertiary); line-height: 0;\n}\n.dsh-usage-home:hover { color: var(--dsw-alias-label-secondary); }\n@keyframes dsh-usage-spin { to { transform: rotate(360deg); } }\n.dsh-usage-refresh--loading svg { animation: dsh-usage-spin 0.9s linear infinite; }\n.dsh-usage-card {\n border-radius: 8px; padding: 8px 10px; display: flex; flex-direction: column; gap: 6px;\n background: var(--dsw-alias-fill-l2);\n}\n/* The provider in use, whose quota alone drives the ball tone. */\n.dsh-usage-card--active {\n outline: 1px solid color-mix(in srgb, var(--dsw-alias-accent, #4f8cff) 45%, transparent);\n outline-offset: -1px;\n}\n.dsh-usage-cardHead { display: flex; align-items: center; gap: 8px; }\n.dsh-usage-providerName {\n color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 600;\n min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;\n}\n.dsh-usage-inUse {\n flex: none; font-size: 11px; line-height: 16px; padding: 0 6px; border-radius: 999px;\n color: var(--dsw-alias-label-secondary);\n background: color-mix(in srgb, var(--dsw-alias-accent, #4f8cff) 18%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-accent, #4f8cff) 35%, transparent);\n}\n.dsh-usage-message { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; word-break: break-all; }\n.dsh-usage-balanceRow { display: flex; align-items: baseline; gap: 8px; }\n.dsh-usage-balanceTotal {\n color: var(--dsw-alias-label-primary); font-size: 18px; font-weight: 600;\n font-variant-numeric: tabular-nums;\n}\n.dsh-usage-balanceCurrency { color: var(--dsw-alias-label-secondary); font-size: 12px; }\n.dsh-usage-balanceParts { color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }\n.dsh-usage-row { display: flex; flex-direction: column; gap: 3px; }\n.dsh-usage-rowHead { display: flex; align-items: center; gap: 8px; font-size: 12px; line-height: 18px; }\n.dsh-usage-rowLabel { color: var(--dsw-alias-label-secondary); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.dsh-usage-rowValue { color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; flex: none; }\n.dsh-usage-barTrack {\n height: 4px; border-radius: 2px; overflow: hidden;\n background: var(--dsw-alias-border-l2);\n}\n.dsh-usage-barFill { height: 100%; border-radius: 2px; transition: width 0.3s; }\n.dsh-usage-barFill--ok { background: #22a06b; }\n.dsh-usage-barFill--warn { background: #e2b93b; }\n.dsh-usage-barFill--danger { background: #d94f4f; }\n.dsh-usage-rowReset { color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }\n.dsh-usage-foot {\n display: flex; align-items: center; gap: 8px; padding: 6px 4px 2px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-usage-footLabel { color: var(--dsw-alias-label-tertiary); font-size: 11px; }\n.dsh-usage-select {\n border: 1px solid var(--dsw-alias-border-l2); border-radius: 6px; background: 0;\n color: var(--dsw-alias-label-secondary); font-size: 11px; padding: 2px 4px; cursor: pointer;\n}\n.dsh-usage-updated { color: var(--dsw-alias-label-tertiary); font-size: 11px; margin-left: auto; }\n`\n\nconst TAG_ID = 'dsh-provider-usage/panel.css'\n\n/** Inject the panel stylesheet once; on HMR swap, refresh the existing tag's payload in place. */\nexport function ensureStyles(): void {\n if (typeof document === 'undefined') return\n const existing = document.querySelector('style[data-plugin-css=' + JSON.stringify(TAG_ID) + ']')\n if (existing !== null) {\n // A hot-swapped bundle re-runs this module while the old <style> tag is\n // still in the DOM — sync its content so the new CSS actually applies.\n if (existing.textContent !== CSS) existing.textContent = CSS\n return\n }\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-provider-usage'\n tag.dataset.pluginCss = TAG_ID\n tag.textContent = CSS\n document.head.appendChild(tag)\n}\n","/**\n * provider-usage — browser half.\n *\n * Registers the locale dictionaries and contributes the usage widget into the\n * sidebar footer's additive action slot. Data comes from the host `usage`\n * Typert Remote (SRC mode — no generated descriptors) through the raw RPC\n * caller on the connection service.\n */\nimport UsageAction, { type UsageActionProps } from './Panel'\nimport { en, zh } from './locales'\nimport { ensureStyles } from './styles'\n\n/** Required client services: slot registry, locale seats, and the connection RPC caller. */\nconst inject = ['slots', 'locale', 'connection']\n\ninterface ClientContext {\n effect: (fn: () => unknown, label: string) => void\n locale: { register: (ns: string, dicts: { zh: unknown; en: unknown }) => unknown }\n slots: {\n inject: (name: string, callback: () => unknown) => void\n register: (entry: Record<string, unknown>, component: unknown) => unknown\n }\n connection: {\n rpc: {\n call: (\n channel: string,\n endpoint: string,\n payload: { args: Record<string, unknown> },\n signal?: AbortSignal,\n ) => Promise<{ ok: true; value: unknown } | { ok: false; error: { code: string; message: string } }>\n }\n }\n}\n\nfunction apply(ctx: ClientContext) {\n ensureStyles()\n ctx.effect(() => ctx.locale.register('provider-usage', { zh, en }), 'provider-usage: dictionaries')\n\n const fetchUsage: UsageActionProps['fetchUsage'] = async () => {\n const result = await ctx.connection.rpc.call('/api', 'usage/list', { args: {} })\n if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`)\n return result.value as Awaited<ReturnType<UsageActionProps['fetchUsage']>>\n }\n\n ctx.slots.inject('sidebar.footer.action', () =>\n ctx.slots.register(\n {\n name: 'sidebar.footer.action',\n id: 'provider-usage',\n order: 10,\n locale: 'provider-usage',\n inject: () => ({ fetchUsage }),\n },\n UsageAction,\n ),\n )\n}\n\nexport { apply, inject }\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["useState","useMemo","useCallback","useRef","createPortal"],"sources":["../src/client/locales.ts","../src/client/Panel.tsx","../src/client/styles.ts","../src/client/index.ts"],"sourcesContent":["/** Simplified Chinese dictionary (the key-set source of truth). */\nexport const zh = {\n 'action.label': '用量',\n 'action.aria': 'Provider 用量',\n 'panel.title': 'Provider 用量',\n 'panel.refresh': '立即刷新',\n 'panel.refreshing': '刷新中…',\n 'lang.switch': 'Switch to English',\n 'panel.resetPos': '悬浮球归位',\n 'panel.interval': '刷新周期',\n 'panel.thresholds': '余额阈值',\n 'panel.thresholdRed': '红',\n 'panel.thresholdYellow': '黄',\n 'panel.updated': '更新于 {time}',\n 'panel.never': '尚未获取',\n 'panel.empty': '未检测到已配置的 Provider',\n 'interval.15s': '15 秒',\n 'interval.30s': '30 秒',\n 'interval.60s': '1 分钟',\n 'interval.300s': '5 分钟',\n 'interval.900s': '15 分钟',\n 'interval.1800s': '30 分钟',\n 'balance.total': '总额',\n 'balance.granted': '赠送 {amount}',\n 'balance.toppedUp': '充值 {amount}',\n 'usage.weekly': '每周配额',\n 'usage.used': '已用 {used}',\n 'usage.remaining': '剩余 {remaining}',\n 'usage.unlimited': '不限量',\n 'usage.resetAt': '重置于 {time}',\n 'usage.resetIn': '{countdown} 后重置',\n 'status.unsupported': '该 Provider 暂不支持额度查询',\n 'status.missingCredential': '未配置密钥({ref})',\n 'status.missingAuthorization': '未完成 OAuth 授权({key})',\n 'status.error': '查询失败',\n 'status.inUse': '使用中',\n 'status.tone.ok': '当前 Provider 正常',\n 'status.tone.warn': '当前 Provider 用量接近上限或余额偏低',\n 'status.tone.danger': '当前 Provider 异常、用量将尽或余额过低',\n 'countdown.days': '{days}天{hours}小时',\n 'countdown.hours': '{hours}小时{minutes}分',\n 'countdown.minutes': '{minutes}分钟',\n}\n\n/** English dictionary, key-identical to the Chinese source of truth. */\nexport const en = {\n 'action.label': 'Usage',\n 'action.aria': 'Provider usage',\n 'panel.title': 'Provider Usage',\n 'panel.refresh': 'Refresh now',\n 'panel.refreshing': 'Refreshing…',\n 'lang.switch': '切换到中文',\n 'panel.resetPos': 'Reset ball position',\n 'panel.interval': 'Refresh interval',\n 'panel.thresholds': 'Balance thresholds',\n 'panel.thresholdRed': 'Red',\n 'panel.thresholdYellow': 'Yellow',\n 'panel.updated': 'Updated {time}',\n 'panel.never': 'Not fetched yet',\n 'panel.empty': 'No configured provider detected',\n 'interval.15s': '15s',\n 'interval.30s': '30s',\n 'interval.60s': '1 min',\n 'interval.300s': '5 min',\n 'interval.900s': '15 min',\n 'interval.1800s': '30 min',\n 'balance.total': 'Total',\n 'balance.granted': 'granted {amount}',\n 'balance.toppedUp': 'topped up {amount}',\n 'usage.weekly': 'Weekly quota',\n 'usage.used': 'used {used}',\n 'usage.remaining': '{remaining} left',\n 'usage.unlimited': 'Unlimited',\n 'usage.resetAt': 'Resets at {time}',\n 'usage.resetIn': 'Resets in {countdown}',\n 'status.unsupported': 'Usage query is not supported for this provider',\n 'status.missingCredential': 'No API key configured ({ref})',\n 'status.missingAuthorization': 'OAuth authorization missing ({key})',\n 'status.error': 'Query failed',\n 'status.inUse': 'In use',\n 'status.tone.ok': 'Current provider healthy',\n 'status.tone.warn': 'Current provider usage nearing its limit or balance running low',\n 'status.tone.danger': 'Current provider error, usage almost exhausted, or balance too low',\n 'countdown.days': '{days}d {hours}h',\n 'countdown.hours': '{hours}h {minutes}m',\n 'countdown.minutes': '{minutes}m',\n}\n","/**\n * Floating-ball quota widget: a draggable ball portaled to document.body\n * (the sidebar slot only mounts the component; the button itself no longer\n * lives in the sidebar, avoiding slot layout contention with other plugins).\n * The popover panel lists every configured provider's live balance/quota.\n * Polling interval is user-selectable (persisted in localStorage) and falls\n * back to the deployment-suggested value from the host plugin config.\n */\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport { en, zh } from './locales'\n\n/* ------------------------------------------------------------------ *\n * Wire views (mirror of the host half)\n * ------------------------------------------------------------------ */\n\ninterface BalanceRow {\n currency: string\n total: string\n granted: string\n toppedUp: string\n}\n\ninterface UsageRow {\n label: string\n used: number | null\n limit: number | null\n remaining: number | null\n percent: number | null\n resetAt: string | null\n}\n\ninterface ProviderUsageView {\n id: string\n displayName: string\n kind: 'balance' | 'usage' | null\n status: 'ok' | 'error' | 'missing-credential' | 'missing-authorization' | 'unsupported'\n message: string | null\n balances: BalanceRow[] | null\n usages: UsageRow[] | null\n /** The provider in use (absent on older hosts); it alone drives the ball tone. */\n active?: boolean\n}\n\ninterface UsageListResult {\n fetchedAt: string\n refreshSeconds: number\n /** Deployment-suggested balance thresholds (absent on older hosts);\n the panel may override them locally. */\n balanceRedThreshold?: number\n balanceYellowThreshold?: number\n /** Plugin package version (absent on older hosts). */\n version?: string\n providers: ProviderUsageView[]\n}\n\nexport interface UsageActionProps {\n /** Sidebar expansion state, owned by the sidebar footer slot. Not read\n directly — a change re-renders us, which re-anchors the default dock. */\n wide?: boolean\n /** Namespace translator injected by the locale seat. */\n t: (key: string, params?: Record<string, unknown>) => string\n /** Business face: call the host `usage/list` remote. */\n fetchUsage: () => Promise<UsageListResult>\n}\n\n/* ------------------------------------------------------------------ *\n * Refresh interval\n * ------------------------------------------------------------------ */\n\nconst INTERVAL_OPTIONS = [15, 30, 60, 300, 900, 1800] as const\nconst STORAGE_KEY = 'dsh.provider-usage.refreshSeconds'\nconst LANG_KEY = 'dsh.provider-usage.lang'\nconst POS_KEY = 'dsh.provider-usage.floatPos'\nconst THRESHOLD_RED_KEY = 'dsh.provider-usage.balanceRedThreshold'\nconst THRESHOLD_YELLOW_KEY = 'dsh.provider-usage.balanceYellowThreshold'\nconst DEFAULT_INTERVAL = 60\n/** Balance thresholds fall back to these hardcoded defaults when neither the\n host config nor a stored override provides them. */\nconst DEFAULT_BALANCE_RED = 10\nconst DEFAULT_BALANCE_YELLOW = 30\n\n/** Floating ball diameter in px; drag math derives from it. */\nconst BALL_SIZE = 32\n\ntype Lang = 'zh' | 'en'\n/** Docked ball position: the ball CENTER as fractions of the viewport, so\n both the default spot and dragged spots follow window resizes. */\ninterface FloatPos {\n fx: number\n fy: number\n}\n\n/** Panel-level override stored in localStorage; null means follow the harness language. */\nfunction readStoredLang(): Lang | null {\n try {\n const raw = localStorage.getItem(LANG_KEY)\n return raw === 'zh' || raw === 'en' ? raw : null\n } catch {\n return null\n }\n}\n\nfunction storeLang(lang: Lang): void {\n try {\n localStorage.setItem(LANG_KEY, lang)\n } catch {\n /* storage unavailable: keep the in-memory value only */\n }\n}\n\n/** Local translator over the bundled dictionaries, mirroring the shell's `{param}` interpolation. */\nfunction makeT(dict: Record<string, string>): UsageActionProps['t'] {\n return (key, params) => {\n const template = dict[key] ?? key\n if (params === undefined) return template\n return template.replace(/\\{(\\w+)\\}/g, (raw, name: string) => (name in params ? String(params[name]) : raw))\n }\n}\n\nfunction readStoredInterval(): number | null {\n try {\n const raw = localStorage.getItem(STORAGE_KEY)\n if (raw === null) return null\n const value = Number(raw)\n return Number.isFinite(value) && value >= 5 ? value : null\n } catch {\n return null\n }\n}\n\nfunction storeInterval(value: number): void {\n try {\n localStorage.setItem(STORAGE_KEY, String(value))\n } catch {\n /* storage unavailable: keep the in-memory value only */\n }\n}\n\n/* ------------------------------------------------------------------ *\n * Balance thresholds (panel overrides, persisted in localStorage)\n * ------------------------------------------------------------------ */\n\n/** Parse a threshold input: a finite number ≥ 0, or null while the field is\n empty/transiently invalid (the string is kept in state so typing never\n fights the controlled input). */\nfunction parseThreshold(raw: string): number | null {\n const trimmed = raw.trim()\n if (trimmed === '') return null\n const value = Number(trimmed)\n return Number.isFinite(value) && value >= 0 ? value : null\n}\n\nfunction readStoredThreshold(key: string): string | null {\n try {\n const raw = localStorage.getItem(key)\n return raw !== null && parseThreshold(raw) !== null ? raw : null\n } catch {\n return null\n }\n}\n\nfunction storeThreshold(key: string, value: string): void {\n try {\n localStorage.setItem(key, value)\n } catch {\n /* storage unavailable: keep the in-memory value only */\n }\n}\n\n/* ------------------------------------------------------------------ *\n * Floating ball position\n * ------------------------------------------------------------------ */\n\n/** Equal margin from the chat area's left edge and the window's bottom edge. */\nconst DOCK_MARGIN = 24\n\n/**\n * Default dock: bottom-left of the main chat area with equal margins on both\n * axes. Returned as viewport fractions of the ball CENTER, like every other\n * position. The chat area's left edge is the sidebar's right edge, derived\n * from the full-height ancestor of the settings slot (stable data-slot hook;\n * class names are build-hashed), so it adapts to sidebar collapse.\n */\nfunction defaultFloatPos(): FloatPos {\n let sidebarRight = 264\n let node = document.querySelector('[data-slot=\"sidebar.settings\"]')?.parentElement ?? null\n while (node) {\n const rect = node.getBoundingClientRect()\n if (rect.x <= 1 && rect.height >= window.innerHeight * 0.9) {\n sidebarRight = rect.right\n break\n }\n node = node.parentElement\n }\n return {\n fx: (sidebarRight + DOCK_MARGIN + BALL_SIZE / 2) / window.innerWidth,\n fy: (window.innerHeight - DOCK_MARGIN - BALL_SIZE / 2) / window.innerHeight,\n }\n}\n\nfunction readStoredFloatPos(): FloatPos | null {\n try {\n const raw = localStorage.getItem(POS_KEY)\n if (raw === null) return null\n const parsed = JSON.parse(raw) as Partial<FloatPos> | null\n if (parsed === null || typeof parsed.fx !== 'number' || !Number.isFinite(parsed.fx)) return null\n if (typeof parsed.fy !== 'number' || !Number.isFinite(parsed.fy)) return null\n return { fx: Math.min(1, Math.max(0, parsed.fx)), fy: Math.min(1, Math.max(0, parsed.fy)) }\n } catch {\n return null\n }\n}\n\nfunction storeFloatPos(pos: FloatPos): void {\n try {\n localStorage.setItem(POS_KEY, JSON.stringify(pos))\n } catch {\n /* storage unavailable: keep the in-memory value only */\n }\n}\n\n/** 归位: forget the pinned spot; the ball follows the default dock again. */\nfunction clearStoredFloatPos(): void {\n try {\n localStorage.removeItem(POS_KEY)\n } catch {\n /* storage unavailable */\n }\n}\n\n/* ------------------------------------------------------------------ *\n * Formatting helpers\n * ------------------------------------------------------------------ */\n\nfunction formatCountdown(resetAt: string, now: number, t: UsageActionProps['t']): string | null {\n const diff = new Date(resetAt).getTime() - now\n if (!Number.isFinite(diff) || diff <= 0) return null\n const minutes = Math.floor(diff / 60000)\n if (minutes >= 60 * 24) return t('countdown.days', { days: Math.floor(minutes / (60 * 24)), hours: Math.floor((minutes % (60 * 24)) / 60) })\n if (minutes >= 60) return t('countdown.hours', { hours: Math.floor(minutes / 60), minutes: minutes % 60 })\n return t('countdown.minutes', { minutes: Math.max(1, minutes) })\n}\n\nfunction barTone(percent: number | null): 'ok' | 'warn' | 'danger' {\n if (percent === null) return 'ok'\n if (percent >= 90) return 'danger'\n if (percent >= 70) return 'warn'\n return 'ok'\n}\n\n/** Parse a balance amount string (\"12.34\", \"0.00\") into a number. */\nfunction toAmount(value: string): number | null {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : null\n}\n\n/**\n * Tone for a monetary balance, compared in the balance's own currency:\n * below the red threshold → danger, below the yellow threshold → warn,\n * otherwise ok. The effective red threshold never exceeds the yellow one,\n * so the zones stay ordered no matter how the panel is configured.\n */\nfunction balanceTone(value: number | null, red: number, yellow: number): 'ok' | 'warn' | 'danger' {\n if (value === null) return 'ok'\n const effRed = Math.min(red, yellow)\n if (value < effRed) return 'danger'\n if (value < yellow) return 'warn'\n return 'ok'\n}\n\n/**\n * Tone for one usage row. Windows with a percent (5h/weekly limits) keep the\n * percent-based tone; rows labeled 'credits' carry a remaining balance and\n * are judged by the balance thresholds instead.\n */\nfunction usageRowTone(row: UsageRow, red: number, yellow: number): 'ok' | 'warn' | 'danger' {\n if (row.label === 'credits') return balanceTone(row.remaining, red, yellow)\n return barTone(row.percent)\n}\n\n/** Worst balance/usage tone across one provider's balances and usage rows. */\nfunction providerTone(provider: ProviderUsageView, red: number, yellow: number): 'ok' | 'warn' | 'danger' {\n let tone: 'ok' | 'warn' | 'danger' = 'ok'\n if (provider.kind === 'balance') {\n for (const row of provider.balances ?? []) {\n const rowTone = balanceTone(toAmount(row.total), red, yellow)\n if (rowTone === 'danger') return 'danger'\n if (rowTone === 'warn') tone = 'warn'\n }\n }\n for (const row of provider.usages ?? []) {\n const rowTone = usageRowTone(row, red, yellow)\n if (rowTone === 'danger') return 'danger'\n if (rowTone === 'warn') tone = 'warn'\n }\n return tone\n}\n\n/** Worst health across the fetch + the provider IN USE: drives the trigger's\n status dot. Only the active provider (the composer's current selection)\n colors the ball, so an idle low-quota provider does not alarm; when the\n host reports no active provider, fall back to every provider. */\nfunction healthTone(data: UsageListResult | null, error: string | null, red: number, yellow: number): 'ok' | 'warn' | 'danger' {\n if (error !== null) return 'danger'\n const providers = data?.providers ?? []\n const active = providers.filter((provider) => provider.active)\n const relevant = active.length > 0 ? active : providers\n let tone: 'ok' | 'warn' | 'danger' = 'ok'\n for (const provider of relevant) {\n if (provider.status === 'error' || provider.status === 'missing-credential' || provider.status === 'missing-authorization') return 'danger'\n const providerToneValue = providerTone(provider, red, yellow)\n if (providerToneValue === 'danger') return 'danger'\n if (providerToneValue === 'warn') tone = 'warn'\n }\n return tone\n}\n\nfunction formatAmount(value: number | null): string {\n if (value === null) return '—'\n return Number.isInteger(value) ? String(value) : value.toFixed(2)\n}\n\n/* ------------------------------------------------------------------ *\n * Provider card\n * ------------------------------------------------------------------ */\n\nfunction UsageRows({ provider, now, red, yellow, t }: { provider: ProviderUsageView; now: number; red: number; yellow: number; t: UsageActionProps['t'] }) {\n const rows = provider.usages ?? []\n if (rows.length === 0) return null\n return (\n <>\n {rows.map((row, index) => {\n const percent = row.percent\n const isCredits = row.label === 'credits'\n // Credits rows carry a remaining balance and are judged by the amount;\n // quota windows keep the percent-based tone.\n const tone = isCredits ? balanceTone(row.remaining, red, yellow) : barTone(row.percent)\n const reset = row.resetAt !== null ? formatCountdown(row.resetAt, now, t) : null\n return (\n <div className=\"dsh-usage-row\" key={`${row.label}-${index}`}>\n <div className=\"dsh-usage-rowHead\">\n <span className=\"dsh-usage-rowLabel\">{row.label === 'weekly' ? t('usage.weekly') : row.label}</span>\n <span className={`dsh-usage-rowValue${tone === 'ok' ? '' : ` dsh-usage-rowValue--${tone}`}`}>\n {isCredits && row.remaining !== null\n ? formatAmount(row.remaining)\n : row.limit === null\n ? row.used !== null\n ? t('usage.used', { used: formatAmount(row.used) })\n : row.remaining !== null\n ? formatAmount(row.remaining)\n : t('usage.unlimited')\n : percent !== null\n ? `${Math.round(percent)}%`\n : t('usage.used', { used: formatAmount(row.used) })}\n </span>\n </div>\n {percent !== null ? (\n <div className=\"dsh-usage-barTrack\">\n <div\n className={`dsh-usage-barFill dsh-usage-barFill--${tone}`}\n style={{ width: `${Math.min(100, Math.max(0, percent))}%` }}\n />\n </div>\n ) : null}\n {reset !== null ? <span className=\"dsh-usage-rowReset\">{t('usage.resetIn', { countdown: reset })}</span> : null}\n </div>\n )\n })}\n </>\n )\n}\n\nfunction ProviderCard({ provider, now, red, yellow, t }: { provider: ProviderUsageView; now: number; red: number; yellow: number; t: UsageActionProps['t'] }) {\n return (\n <div className={`dsh-usage-card${provider.active ? ' dsh-usage-card--active' : ''}`}>\n <div className=\"dsh-usage-cardHead\">\n <span className=\"dsh-usage-providerName\">{provider.displayName}</span>\n {provider.active ? <span className=\"dsh-usage-inUse\">{t('status.inUse')}</span> : null}\n </div>\n {provider.status === 'unsupported' ? (\n <span className=\"dsh-usage-message\">{t('status.unsupported')}</span>\n ) : provider.status === 'missing-credential' ? (\n <span className=\"dsh-usage-message\">{t('status.missingCredential', { ref: provider.message })}</span>\n ) : provider.status === 'missing-authorization' ? (\n <span className=\"dsh-usage-message\">{t('status.missingAuthorization', { key: provider.message })}</span>\n ) : provider.status === 'error' ? (\n <span className=\"dsh-usage-message\">{t('status.error')}: {provider.message}</span>\n ) : provider.kind === 'balance' ? (\n (provider.balances ?? []).map((row, index) => {\n const tone = balanceTone(toAmount(row.total), red, yellow)\n return (\n <div key={`${row.currency}-${index}`}>\n <div className=\"dsh-usage-balanceRow\">\n <span className={`dsh-usage-balanceTotal${tone === 'ok' ? '' : ` dsh-usage-balanceTotal--${tone}`}`}>{row.total}</span>\n <span className=\"dsh-usage-balanceCurrency\">{row.currency}</span>\n </div>\n <span className=\"dsh-usage-balanceParts\">\n {[row.granted !== '0' && row.granted !== '0.00' ? t('balance.granted', { amount: row.granted }) : null,\n row.toppedUp !== '0' && row.toppedUp !== '0.00' ? t('balance.toppedUp', { amount: row.toppedUp }) : null]\n .filter(Boolean)\n .join(' · ')}\n </span>\n </div>\n )\n })\n ) : (\n <UsageRows provider={provider} now={now} red={red} yellow={yellow} t={t} />\n )}\n </div>\n )\n}\n\n/* ------------------------------------------------------------------ *\n * Trigger + popover\n * ------------------------------------------------------------------ */\n\n/** Battery level: remaining charge reads as remaining quota. */\nfunction BatteryIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <rect width=\"16\" height=\"10\" x=\"2\" y=\"7\" rx=\"2\" ry=\"2\" />\n <path d=\"M22 11v2\" />\n <path d=\"M6 11v2\" />\n <path d=\"M10 11v2\" />\n </svg>\n )\n}\n\nfunction RefreshIcon() {\n return (\n <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M20 12a8 8 0 1 1-2.34-5.66\" />\n <path d=\"M20 4v4h-4\" />\n </svg>\n )\n}\n\nfunction GlobeIcon() {\n return (\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"9\" />\n <path d=\"M3 12h18\" />\n <path d=\"M12 3c2.5 2.6 3.9 5.7 3.9 9s-1.4 6.4-3.9 9c-2.5-2.6-3.9-5.7-3.9-9S9.5 5.6 12 3z\" />\n </svg>\n )\n}\n\n/** Crosshair for the \"reset ball to its default spot\" head button. */\nfunction HomeIcon() {\n return (\n <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"7\" />\n <path d=\"M12 2v3M12 19v3M2 12h3M19 12h3\" />\n <circle cx=\"12\" cy=\"12\" r=\"1.5\" fill=\"currentColor\" stroke=\"none\" />\n </svg>\n )\n}\n\nexport default function UsageAction({ t, fetchUsage }: UsageActionProps) {\n const [open, setOpen] = useState(false)\n const [data, setData] = useState<UsageListResult | null>(null)\n const [error, setError] = useState<string | null>(null)\n const [loading, setLoading] = useState(false)\n const [intervalSec, setIntervalSec] = useState(() => readStoredInterval() ?? DEFAULT_INTERVAL)\n const [langOverride, setLangOverride] = useState<Lang | null>(() => readStoredLang())\n /** Balance thresholds as raw input strings (kept as text so typing never\n fights the controlled input); parsed on use, overridden locally only\n after the user edits them. */\n const [redThreshold, setRedThreshold] = useState<string>(() => readStoredThreshold(THRESHOLD_RED_KEY) ?? String(DEFAULT_BALANCE_RED))\n const [yellowThreshold, setYellowThreshold] = useState<string>(() => readStoredThreshold(THRESHOLD_YELLOW_KEY) ?? String(DEFAULT_BALANCE_YELLOW))\n const [now, setNow] = useState(() => Date.now())\n /** Pinned spot (user-dragged) as viewport fractions; null = follow the default dock. */\n const [pinnedPos, setPinnedPos] = useState<FloatPos | null>(() => readStoredFloatPos())\n /** Viewport size drives the fraction → px conversion and the default dock. */\n const [viewport, setViewport] = useState({ w: window.innerWidth, h: window.innerHeight })\n /** Transient ball position while dragging (cursor-centered); null when docked. */\n const [dragPos, setDragPos] = useState<{ x: number; y: number } | null>(null)\n\n // Follow the harness language by default: probe the shell-injected\n // translator with a known key, then let a panel-level override win.\n const harnessLang: Lang | null =\n t('lang.switch') === zh['lang.switch'] ? 'zh' : t('lang.switch') === en['lang.switch'] ? 'en' : null\n const lang: Lang = langOverride ?? harnessLang ?? 'en'\n const tt = useMemo(() => makeT(lang === 'zh' ? (zh as Record<string, string>) : (en as Record<string, string>)), [lang])\n const toggleLang = useCallback(() => {\n setLangOverride((current) => {\n const next: Lang = (current ?? harnessLang ?? 'en') === 'zh' ? 'en' : 'zh'\n storeLang(next)\n return next\n })\n }, [harnessLang])\n /** Panel-head home button: unpin the ball so it follows the default dock. */\n const resetFloatPos = () => {\n clearStoredFloatPos()\n setPinnedPos(null)\n }\n const [panelPos, setPanelPos] = useState<{ left: number; top?: number; bottom?: number } | null>(null)\n const rootRef = useRef<HTMLDivElement>(null)\n const ballRef = useRef<HTMLButtonElement>(null)\n const panelRef = useRef<HTMLDivElement>(null)\n const inFlight = useRef(false)\n /** Active ball-drag session; cleared on pointerup/pointercancel. */\n const dragRef = useRef<{ pointerId: number; startX: number; startY: number; dragging: boolean } | null>(null)\n /** Set when a drag ends so the trailing click does not toggle the panel. */\n const suppressClickRef = useRef(false)\n\n const refresh = useCallback(async () => {\n if (inFlight.current) return\n inFlight.current = true\n setLoading(true)\n try {\n const result = await fetchUsage()\n setData(result)\n setError(null)\n } catch (reason) {\n setError(reason instanceof Error ? reason.message : String(reason))\n } finally {\n inFlight.current = false\n setLoading(false)\n }\n }, [fetchUsage])\n\n // Initial fetch + fixed-interval polling; skip ticks while the tab is hidden.\n useEffect(() => {\n refresh()\n }, [refresh])\n useEffect(() => {\n const timer = setInterval(() => {\n if (document.visibilityState === 'visible') refresh()\n }, intervalSec * 1000)\n return () => clearInterval(timer)\n }, [intervalSec, refresh])\n\n // Adopt the deployment-suggested interval until the user picks one locally.\n useEffect(() => {\n if (data !== null && readStoredInterval() === null && data.refreshSeconds !== intervalSec) {\n setIntervalSec(data.refreshSeconds)\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [data])\n\n // Adopt the deployment-suggested balance thresholds until the user edits\n // them locally (a stored override always wins, like the refresh interval).\n useEffect(() => {\n if (data === null) return\n if (readStoredThreshold(THRESHOLD_RED_KEY) === null && typeof data.balanceRedThreshold === 'number') {\n setRedThreshold(String(data.balanceRedThreshold))\n }\n if (readStoredThreshold(THRESHOLD_YELLOW_KEY) === null && typeof data.balanceYellowThreshold === 'number') {\n setYellowThreshold(String(data.balanceYellowThreshold))\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [data])\n\n // Slow tick so reset countdowns advance while the panel is open.\n useEffect(() => {\n if (!open) return\n setNow(Date.now())\n const timer = setInterval(() => setNow(Date.now()), 30000)\n return () => clearInterval(timer)\n }, [open])\n\n // Close on outside press / Escape, mirroring the shipped popover behavior.\n // The panel and the ball are portaled to document.body, so all trees count\n // as inside.\n useEffect(() => {\n if (!open) return\n const closeOutside = (event: PointerEvent) => {\n if (!(event.target instanceof Node)) return\n if (rootRef.current?.contains(event.target)) return\n if (ballRef.current?.contains(event.target)) return\n if (panelRef.current?.contains(event.target)) return\n setOpen(false)\n }\n document.addEventListener('pointerdown', closeOutside)\n return () => document.removeEventListener('pointerdown', closeOutside)\n }, [open])\n\n // Anchor the fixed-position panel to the ball; recompute while open so a\n // position reset or window resize keeps it attached. A ball in the lower\n // half of the viewport opens the panel above it, upper half below.\n useEffect(() => {\n if (!open) return\n const update = () => {\n const rect = ballRef.current?.getBoundingClientRect()\n if (!rect) return\n const left = Math.min(Math.max(8, rect.left), Math.max(8, window.innerWidth - 336))\n setPanelPos(\n rect.top > window.innerHeight / 2\n ? { left, bottom: window.innerHeight - rect.top + 6 }\n : { left, top: rect.bottom + 6 },\n )\n }\n update()\n window.addEventListener('resize', update)\n return () => window.removeEventListener('resize', update)\n }, [open, pinnedPos, viewport])\n\n // Track the viewport: fraction-based positions and the default dock follow\n // window resizes automatically.\n useEffect(() => {\n const onResize = () => setViewport({ w: window.innerWidth, h: window.innerHeight })\n window.addEventListener('resize', onResize)\n return () => window.removeEventListener('resize', onResize)\n }, [])\n\n // Effective thresholds: parse the inputs, fall back to defaults, and keep\n // the red zone at or below the yellow one so both stay meaningful.\n const effYellow = parseThreshold(yellowThreshold) ?? DEFAULT_BALANCE_YELLOW\n const effRed = Math.min(parseThreshold(redThreshold) ?? DEFAULT_BALANCE_RED, effYellow)\n\n const tone = healthTone(data, error, effRed, effYellow)\n\n const onKeyDown = (event: React.KeyboardEvent) => {\n if (event.key !== 'Escape' || !open) return\n event.preventDefault()\n setOpen(false)\n }\n\n /* Ball dragging: pointer events only (touch included via touch-action:\n none). A press becomes a drag past a 5px dead zone; while dragging the\n ball follows the cursor centered on it, and on release it stays exactly\n where dropped (no edge snapping). A plain press falls through to the\n click handler. */\n const onBallPointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {\n event.currentTarget.setPointerCapture(event.pointerId)\n dragRef.current = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, dragging: false }\n }\n\n const onBallPointerMove = (event: React.PointerEvent<HTMLButtonElement>) => {\n const drag = dragRef.current\n if (drag === null || drag.pointerId !== event.pointerId) return\n if (!drag.dragging) {\n if (Math.abs(event.clientX - drag.startX) < 5 && Math.abs(event.clientY - drag.startY) < 5) return\n drag.dragging = true\n // The panel would detach from a moving anchor: close it on drag start.\n if (open) setOpen(false)\n }\n const half = BALL_SIZE / 2\n setDragPos({\n x: Math.min(Math.max(half, event.clientX), window.innerWidth - half),\n y: Math.min(Math.max(half, event.clientY), window.innerHeight - half),\n })\n }\n\n const endBallDrag = (event: React.PointerEvent<HTMLButtonElement>) => {\n const drag = dragRef.current\n dragRef.current = null\n if (drag === null || drag.pointerId !== event.pointerId || !drag.dragging) return\n const half = BALL_SIZE / 2\n const x = Math.min(Math.max(half, event.clientX), window.innerWidth - half)\n const y = Math.min(Math.max(half, event.clientY), window.innerHeight - half)\n // Pin the drop point as viewport fractions so it follows window resizes.\n const next: FloatPos = { fx: x / window.innerWidth, fy: y / window.innerHeight }\n setPinnedPos(next)\n storeFloatPos(next)\n setDragPos(null)\n suppressClickRef.current = true\n }\n\n const cancelBallDrag = (event: React.PointerEvent<HTMLButtonElement>) => {\n if (dragRef.current?.pointerId !== event.pointerId) return\n dragRef.current = null\n setDragPos(null)\n }\n\n const onBallClick = () => {\n if (suppressClickRef.current) {\n suppressClickRef.current = false\n return\n }\n setNow(Date.now())\n setOpen((current) => !current)\n if (!open) refresh()\n }\n\n // Ball center in px: transient drag point > pinned fractions > default dock.\n // (The default reads the sidebar edge from the DOM at render time; cheap.)\n const docked = pinnedPos ?? defaultFloatPos()\n const centerX = dragPos !== null ? dragPos.x : docked.fx * viewport.w\n const centerY = dragPos !== null ? dragPos.y : docked.fy * viewport.h\n\n return (\n <div ref={rootRef} className=\"dsh-usage-root\" onKeyDown={onKeyDown}>\n {createPortal(\n <button\n ref={ballRef}\n type=\"button\"\n className={`dsh-usage-ball dsh-usage-tone-${tone}`}\n style={{ left: centerX - BALL_SIZE / 2, top: centerY - BALL_SIZE / 2 }}\n aria-expanded={open}\n aria-label={tt('action.aria')}\n title={`${tt('action.aria')} · ${tt(`status.tone.${tone}`)}`}\n onPointerDown={onBallPointerDown}\n onPointerMove={onBallPointerMove}\n onPointerUp={endBallDrag}\n onPointerCancel={cancelBallDrag}\n onClick={onBallClick}\n onKeyDown={onKeyDown}\n >\n <BatteryIcon />\n </button>,\n document.body,\n )}\n {open && panelPos !== null\n ? createPortal(\n <div\n ref={panelRef}\n className=\"dsh-usage-panel\"\n role=\"dialog\"\n aria-label={tt('panel.title')}\n style={{ left: panelPos.left, top: panelPos.top, bottom: panelPos.bottom }}\n onKeyDown={onKeyDown}\n >\n <div className=\"dsh-usage-head\">\n <span className=\"dsh-usage-title\">{tt('panel.title')}</span>\n {data?.version ? <span className=\"dsh-usage-version\">v{data.version}</span> : null}\n <button\n type=\"button\"\n className=\"dsh-usage-home\"\n onClick={resetFloatPos}\n title={tt('panel.resetPos')}\n aria-label={tt('panel.resetPos')}\n >\n <HomeIcon />\n </button>\n <button\n type=\"button\"\n className=\"dsh-usage-lang\"\n onClick={toggleLang}\n title={tt('lang.switch')}\n aria-label={tt('lang.switch')}\n >\n <GlobeIcon />\n <span>{lang === 'zh' ? '中' : 'EN'}</span>\n </button>\n <button\n type=\"button\"\n className={`dsh-usage-refresh${loading ? ' dsh-usage-refresh--loading' : ''}`}\n disabled={loading}\n onClick={refresh}\n title={tt('panel.refresh')}\n aria-label={tt('panel.refresh')}\n >\n <RefreshIcon />\n </button>\n </div>\n {data === null && error !== null ? (\n <span className=\"dsh-usage-message\">{tt('status.error')}: {error}</span>\n ) : null}\n {data !== null && data.providers.length === 0 ? (\n <span className=\"dsh-usage-message\">{tt('panel.empty')}</span>\n ) : null}\n {(data?.providers ?? []).map((provider) => (\n <ProviderCard key={provider.id} provider={provider} now={now} red={effRed} yellow={effYellow} t={tt} />\n ))}\n <div className=\"dsh-usage-foot\">\n <span className=\"dsh-usage-footLabel\">{tt('panel.interval')}</span>\n <select\n className=\"dsh-usage-select\"\n value={intervalSec}\n onChange={(event) => {\n const value = Number(event.target.value)\n setIntervalSec(value)\n storeInterval(value)\n }}\n >\n {INTERVAL_OPTIONS.map((seconds) => (\n <option key={seconds} value={seconds}>{tt(`interval.${seconds}s`)}</option>\n ))}\n </select>\n <span className=\"dsh-usage-updated\">\n {data === null\n ? tt('panel.never')\n : tt('panel.updated', { time: new Date(data.fetchedAt).toLocaleTimeString(lang === 'zh' ? 'zh-CN' : 'en-US') })}\n </span>\n </div>\n <div className=\"dsh-usage-thresholds\">\n <span className=\"dsh-usage-footLabel\">{tt('panel.thresholds')}</span>\n <label className=\"dsh-usage-thresholdField\" title={tt('panel.thresholdRed')}>\n <span className=\"dsh-usage-thresholdLabel dsh-usage-thresholdLabel--red\">{tt('panel.thresholdRed')}</span>\n <input\n className=\"dsh-usage-thresholdInput\"\n type=\"number\"\n min={0}\n step={0.01}\n inputMode=\"decimal\"\n value={redThreshold}\n onChange={(event) => {\n setRedThreshold(event.target.value)\n storeThreshold(THRESHOLD_RED_KEY, event.target.value)\n }}\n />\n </label>\n <label className=\"dsh-usage-thresholdField\" title={tt('panel.thresholdYellow')}>\n <span className=\"dsh-usage-thresholdLabel dsh-usage-thresholdLabel--yellow\">{tt('panel.thresholdYellow')}</span>\n <input\n className=\"dsh-usage-thresholdInput\"\n type=\"number\"\n min={0}\n step={0.01}\n inputMode=\"decimal\"\n value={yellowThreshold}\n onChange={(event) => {\n setYellowThreshold(event.target.value)\n storeThreshold(THRESHOLD_YELLOW_KEY, event.target.value)\n }}\n />\n </label>\n </div>\n </div>,\n document.body,\n )\n : null}\n </div>\n )\n}\n","/**\n * Panel stylesheet, injected once per page. Follows the shipped plugins'\n * convention of a `<style data-plugin-css>` tag and design-token colors.\n */\nconst CSS = `\n.dsh-usage-root { position: relative; display: inline-flex; }\n/* Tri-tone health halo: only the shadow carries the state of the provider IN\n USE (green ok / amber remaining<30% / red error or usage>=90%) — the button\n face itself stays neutral. A 1px tone ring plus a soft halo keeps the\n state readable on both light and dark skins. */\n.dsh-usage-tone-ok { --dsh-usage-tone: #22a06b; }\n.dsh-usage-tone-warn { --dsh-usage-tone: #e2b93b; }\n.dsh-usage-tone-danger { --dsh-usage-tone: #d94f4f; }\n/* Floating ball trigger: a draggable fixed circle portaled to document.body,\n one layer below the panel (1000) so the popover always covers it. Position\n comes from inline style: docked it sits at its stored coordinates, while\n dragging it follows the cursor. Keeps the shell's lv3 lift shadow; the\n tone halo stacks on top. */\n.dsh-usage-ball {\n position: fixed; z-index: 999; width: 32px; height: 32px; border-radius: 50%;\n cursor: pointer; touch-action: none;\n display: inline-flex; align-items: center; justify-content: center;\n border: 1px solid var(--dsw-alias-border-l2);\n background-color: var(--dsw-specific-menu);\n color: var(--dsw-alias-label-tertiary);\n transition: box-shadow 0.15s;\n}\n.dsh-usage-ball:hover, .dsh-usage-ball:focus-visible { color: var(--dsw-alias-label-secondary); }\n.dsh-usage-ball svg { width: 15px; height: 15px; }\n.dsh-usage-ball.dsh-usage-tone-ok,\n.dsh-usage-ball.dsh-usage-tone-warn,\n.dsh-usage-ball.dsh-usage-tone-danger {\n box-shadow:\n var(--dsw-shadow-lv3),\n 0 0 0 1px color-mix(in srgb, var(--dsh-usage-tone) 40%, transparent),\n 0 0 14px color-mix(in srgb, var(--dsh-usage-tone) 55%, transparent);\n}\n.dsh-usage-ball.dsh-usage-tone-ok:hover,\n.dsh-usage-ball.dsh-usage-tone-warn:hover,\n.dsh-usage-ball.dsh-usage-tone-danger:hover,\n.dsh-usage-ball.dsh-usage-tone-ok:focus-visible,\n.dsh-usage-ball.dsh-usage-tone-warn:focus-visible,\n.dsh-usage-ball.dsh-usage-tone-danger:focus-visible {\n box-shadow:\n var(--dsw-shadow-lv3),\n 0 0 0 1px color-mix(in srgb, var(--dsh-usage-tone) 55%, transparent),\n 0 0 18px color-mix(in srgb, var(--dsh-usage-tone) 70%, transparent);\n}\n.dsh-usage-panel {\n z-index: 1000; box-sizing: border-box;\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-specific-menu);\n box-shadow: var(--dsw-shadow-lv3);\n border-radius: 12px; margin: 0; padding: 8px;\n width: 320px; max-width: min(360px, 100vw - 16px);\n max-height: min(460px, 100vh - 60px); overflow: auto;\n display: flex; flex-direction: column; gap: 6px;\n /* position: fixed with viewport coordinates computed inline from the\n trigger rect — absolute positioning inside the sidebar gets clipped by\n the column's overflow. */\n position: fixed;\n --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);\n --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);\n}\n.dsh-usage-head {\n display: flex; align-items: center; gap: 8px; padding: 2px 4px 6px;\n border-bottom: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-usage-title { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 600; }\n.dsh-usage-version {\n color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px;\n padding: 0 5px; border-radius: 4px; background: var(--dsw-alias-fill-l2);\n font-variant-numeric: tabular-nums; user-select: none; margin-right: auto;\n}\n.dsh-usage-lang {\n cursor: pointer; display: inline-flex; align-items: center; gap: 4px;\n border: 0; border-radius: 6px; background: 0; padding: 3px 5px;\n color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px;\n}\n.dsh-usage-lang:hover { color: var(--dsw-alias-label-secondary); }\n.dsh-usage-refresh {\n cursor: pointer; display: inline-flex; align-items: center; justify-content: center;\n border: 0; border-radius: 6px; background: 0; padding: 4px;\n color: var(--dsw-alias-label-tertiary); line-height: 0;\n}\n.dsh-usage-refresh:hover { color: var(--dsw-alias-label-secondary); }\n.dsh-usage-refresh:disabled { opacity: 0.5; cursor: default; }\n/* Home button in the panel head: sends the ball back to its default spot. */\n.dsh-usage-home {\n cursor: pointer; display: inline-flex; align-items: center; justify-content: center;\n border: 0; border-radius: 6px; background: 0; padding: 4px;\n color: var(--dsw-alias-label-tertiary); line-height: 0;\n}\n.dsh-usage-home:hover { color: var(--dsw-alias-label-secondary); }\n@keyframes dsh-usage-spin { to { transform: rotate(360deg); } }\n.dsh-usage-refresh--loading svg { animation: dsh-usage-spin 0.9s linear infinite; }\n.dsh-usage-card {\n border-radius: 8px; padding: 8px 10px; display: flex; flex-direction: column; gap: 6px;\n background: var(--dsw-alias-fill-l2);\n}\n/* The provider in use, whose quota alone drives the ball tone. */\n.dsh-usage-card--active {\n outline: 1px solid color-mix(in srgb, var(--dsw-alias-accent, #4f8cff) 45%, transparent);\n outline-offset: -1px;\n}\n.dsh-usage-cardHead { display: flex; align-items: center; gap: 8px; }\n.dsh-usage-providerName {\n color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 600;\n min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;\n}\n.dsh-usage-inUse {\n flex: none; font-size: 11px; line-height: 16px; padding: 0 6px; border-radius: 999px;\n color: var(--dsw-alias-label-secondary);\n background: color-mix(in srgb, var(--dsw-alias-accent, #4f8cff) 18%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-accent, #4f8cff) 35%, transparent);\n}\n.dsh-usage-message { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; word-break: break-all; }\n.dsh-usage-balanceRow { display: flex; align-items: baseline; gap: 8px; }\n.dsh-usage-balanceTotal {\n color: var(--dsw-alias-label-primary); font-size: 18px; font-weight: 600;\n font-variant-numeric: tabular-nums;\n}\n/* Balance below the user-configured thresholds: red < red threshold, yellow\n below the yellow threshold; a healthy balance keeps the default label color. */\n.dsh-usage-balanceTotal--warn { color: #e2b93b; }\n.dsh-usage-balanceTotal--danger { color: #d94f4f; }\n.dsh-usage-balanceCurrency { color: var(--dsw-alias-label-secondary); font-size: 12px; }\n.dsh-usage-balanceParts { color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }\n.dsh-usage-row { display: flex; flex-direction: column; gap: 3px; }\n.dsh-usage-rowHead { display: flex; align-items: center; gap: 8px; font-size: 12px; line-height: 18px; }\n.dsh-usage-rowLabel { color: var(--dsw-alias-label-secondary); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.dsh-usage-rowValue { color: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; flex: none; }\n/* Credits rows show a remaining balance: tinted by the balance thresholds. */\n.dsh-usage-rowValue--warn { color: #e2b93b; }\n.dsh-usage-rowValue--danger { color: #d94f4f; }\n.dsh-usage-barTrack {\n height: 4px; border-radius: 2px; overflow: hidden;\n background: var(--dsw-alias-border-l2);\n}\n.dsh-usage-barFill { height: 100%; border-radius: 2px; transition: width 0.3s; }\n.dsh-usage-barFill--ok { background: #22a06b; }\n.dsh-usage-barFill--warn { background: #e2b93b; }\n.dsh-usage-barFill--danger { background: #d94f4f; }\n.dsh-usage-rowReset { color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }\n.dsh-usage-foot {\n display: flex; align-items: center; gap: 8px; padding: 6px 4px 2px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-usage-footLabel { color: var(--dsw-alias-label-tertiary); font-size: 11px; }\n.dsh-usage-select {\n border: 1px solid var(--dsw-alias-border-l2); border-radius: 6px; background: 0;\n color: var(--dsw-alias-label-secondary); font-size: 11px; padding: 2px 4px; cursor: pointer;\n}\n.dsh-usage-updated { color: var(--dsw-alias-label-tertiary); font-size: 11px; margin-left: auto; }\n/* Balance threshold row under the footer: red/yellow inputs persisted locally. */\n.dsh-usage-thresholds {\n display: flex; align-items: center; gap: 10px; padding: 2px 4px 4px;\n}\n.dsh-usage-thresholdField { display: inline-flex; align-items: center; gap: 4px; }\n.dsh-usage-thresholdLabel { font-size: 11px; font-weight: 600; }\n.dsh-usage-thresholdLabel--red { color: #d94f4f; }\n.dsh-usage-thresholdLabel--yellow { color: #e2b93b; }\n.dsh-usage-thresholdInput {\n width: 58px; box-sizing: border-box;\n border: 1px solid var(--dsw-alias-border-l2); border-radius: 6px; background: 0;\n color: var(--dsw-alias-label-secondary); font-size: 11px; padding: 2px 4px;\n font-variant-numeric: tabular-nums;\n}\n.dsh-usage-thresholdInput:focus {\n outline: none;\n border-color: var(--dsw-alias-accent, #4f8cff);\n box-shadow: 0 0 0 1px color-mix(in srgb, var(--dsw-alias-accent, #4f8cff) 40%, transparent);\n}\n`\n\nconst TAG_ID = 'dsh-provider-usage/panel.css'\n\n/** Inject the panel stylesheet once; on HMR swap, refresh the existing tag's payload in place. */\nexport function ensureStyles(): void {\n if (typeof document === 'undefined') return\n const existing = document.querySelector('style[data-plugin-css=' + JSON.stringify(TAG_ID) + ']')\n if (existing !== null) {\n // A hot-swapped bundle re-runs this module while the old <style> tag is\n // still in the DOM — sync its content so the new CSS actually applies.\n if (existing.textContent !== CSS) existing.textContent = CSS\n return\n }\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-provider-usage'\n tag.dataset.pluginCss = TAG_ID\n tag.textContent = CSS\n document.head.appendChild(tag)\n}\n","/**\n * provider-usage — browser half.\n *\n * Registers the locale dictionaries and contributes the usage widget into the\n * sidebar footer's additive action slot. Data comes from the host `usage`\n * Typert Remote (SRC mode — no generated descriptors) through the raw RPC\n * caller on the connection service.\n */\nimport UsageAction, { type UsageActionProps } from './Panel'\nimport { en, zh } from './locales'\nimport { ensureStyles } from './styles'\n\n/** Required client services: slot registry, locale seats, and the connection RPC caller. */\nconst inject = ['slots', 'locale', 'connection']\n\ninterface ClientContext {\n effect: (fn: () => unknown, label: string) => void\n locale: { register: (ns: string, dicts: { zh: unknown; en: unknown }) => unknown }\n slots: {\n inject: (name: string, callback: () => unknown) => void\n register: (entry: Record<string, unknown>, component: unknown) => unknown\n }\n connection: {\n rpc: {\n call: (\n channel: string,\n endpoint: string,\n payload: { args: Record<string, unknown> },\n signal?: AbortSignal,\n ) => Promise<{ ok: true; value: unknown } | { ok: false; error: { code: string; message: string } }>\n }\n }\n}\n\nfunction apply(ctx: ClientContext) {\n ensureStyles()\n ctx.effect(() => ctx.locale.register('provider-usage', { zh, en }), 'provider-usage: dictionaries')\n\n const fetchUsage: UsageActionProps['fetchUsage'] = async () => {\n const result = await ctx.connection.rpc.call('/api', 'usage/list', { args: {} })\n if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`)\n return result.value as Awaited<ReturnType<UsageActionProps['fetchUsage']>>\n }\n\n ctx.slots.inject('sidebar.footer.action', () =>\n ctx.slots.register(\n {\n name: 'sidebar.footer.action',\n id: 'provider-usage',\n order: 10,\n locale: 'provider-usage',\n inject: () => ({ fetchUsage }),\n },\n UsageAction,\n ),\n )\n}\n\nexport { apply, inject }\n"],"mappings":"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"}
package/lib/index.js CHANGED
@@ -93,7 +93,7 @@ async function refreshOpenAiCodexGrant(refreshToken, signal) {
93
93
  const name = "provider-usage";
94
94
  /** Own package version, baked in by tsdown `define` at build time (the
95
95
  fallback only fires if the sources are compiled some other way). */
96
- const version = "0.3.8";
96
+ const version = "0.3.9";
97
97
  /** Settings namespace this plugin owns (registered through `ctx.settings`). */
98
98
  const NS = "provider-usage";
99
99
  const PROVIDER_KINDS = [
@@ -123,6 +123,12 @@ const ProviderSpec = z.object({
123
123
  const Config = z.object({
124
124
  /** Suggested widget refresh interval in seconds (the panel may override it locally). */
125
125
  refreshSeconds: z.number().step(1).min(5).max(86400).default(60),
126
+ /** Balance below this amount turns red, compared in the balance's own currency
127
+ (the panel may override it locally). */
128
+ balanceRedThreshold: z.number().step(.01).min(0).default(10),
129
+ /** Balance below this amount turns yellow, compared in the balance's own currency
130
+ (the panel may override it locally). */
131
+ balanceYellowThreshold: z.number().step(.01).min(0).default(30),
126
132
  /** Enumerate live provider routes from the llm registry. */
127
133
  autoDetect: z.boolean().default(true),
128
134
  /** Manual provider specs; an id matching a detected route overrides it. */
@@ -867,6 +873,8 @@ var UsageService = class extends TypertRemoteService {
867
873
  return {
868
874
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
869
875
  refreshSeconds: this.options().refreshSeconds,
876
+ balanceRedThreshold: this.options().balanceRedThreshold,
877
+ balanceYellowThreshold: this.options().balanceYellowThreshold,
870
878
  version,
871
879
  providers
872
880
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-provider-usage",
3
- "version": "0.3.8",
4
- "description": "DeepSeek Harness plugin: live account balance/usage panel for every configured LLM provider (draggable floating-ball widget, configurable refresh interval)",
3
+ "version": "0.3.9",
4
+ "description": "DeepSeek Harness plugin: live account balance/usage panel for every configured LLM provider (draggable floating-ball widget, configurable refresh interval and balance thresholds)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {