dsh-provider-usage 0.3.6 → 0.3.8

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
@@ -24,7 +24,7 @@
24
24
  | `kimi-coding` | `kimi-coding` | `GET {baseURL}/v1/usages` | weekly quota + rate-limit windows, reset countdown |
25
25
  | `openrouter` | `openrouter` | `GET {origin}/api/v1/credits` | credits used / total |
26
26
  | `github-copilot` | `github-copilot` | `GET api.github.com/copilot_internal/user` | plan quota snapshots (paid) or monthly quotas (free) |
27
- | `openai-codex` | `openai-codex` | `GET {baseURL}/wham/usage` | ChatGPT subscription 5h / weekly windows + credits |
27
+ | `openai-codex` | `openai-codex` | `GET {baseURL}/wham/usage` | ChatGPT subscription 5h / weekly windows + credits + spend control (**OAuth login**, not an API key — see [OpenAI Codex via OAuth](#openai-codex-via-oauth)) |
28
28
  | `openai` | `openai` | `GET {origin}/v1/organization/costs` | current-month spend (**admin key required**; a regular key fails with 403) |
29
29
  | `anthropic` | `anthropic` | `GET {baseURL}/v1/organizations/cost_report` | current-month spend (**admin key required**, `x-api-key` auth) |
30
30
  | `minimax` | `minimax`, `minimax-cn` | `GET {origin}/v1/api/openplatform/coding_plan/remains` | Coding Plan 5h / weekly remaining % |
@@ -32,8 +32,8 @@
32
32
  | `opencode` | `opencode`, `opencode-go` | `GET {baseURL}/usage` | Zen Go rolling / weekly / monthly windows |
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
- - **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.
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 provider health: green all good, amber some quota below 30% left, red on query failure / missing key / usage ≥90%.
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.
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.
@@ -45,6 +45,48 @@ The floating ball (bottom-left, with the green healthy halo) and the open usage
45
45
 
46
46
  ![Usage panel in English](docs/panel-en.png)
47
47
 
48
+ ## OpenAI Codex via OAuth
49
+
50
+ OpenAI Codex is a **ChatGPT-subscription** provider: it authenticates with an OAuth access token, not an API key, so there is nothing to paste into a key field. dsh itself has no OAuth *button* for this route — but this plugin can still query its quota, because it reads the OAuth grant out of the harness credential store. Set the provider up once, and the usage panel shows your real 5-hour / weekly Codex windows.
51
+
52
+ ### 1. Make sure the route exists
53
+
54
+ The sign-in writes a grant record addressed `llm-pi-ai/openai-codex`, and the route has to be configured for `ctx.llm` to list it. With the stock `llm-pi-ai` adapter mounted (default in the web profile), an empty profile is enough — e.g. in `~/.dsh/settings.yaml`:
55
+
56
+ ```yaml
57
+ llm-pi-ai:
58
+ providers:
59
+ openai-codex: {}
60
+ ```
61
+
62
+ ### 2. Sign in through the harness authorization seam
63
+
64
+ `dsh-llm-pi-ai` registers an "OpenAI (ChatGPT Plus/Pro)" OAuth flow for `openai-codex` on the harness authorization seam (`ctx.authorization`, credential key `llm-pi-ai/openai-codex`). Complete it from any surface that runs that flow — a sign-in entry on the harness model/authorization UI, or any pi-ai client whose login persists through the harness credential store. Finish the browser (or device-code) flow with your ChatGPT account. The harness then stores the grant under `~/.dsh/.credentials.yaml` as `llm-pi-ai/openai-codex`:
65
+
66
+ ```yaml
67
+ records:
68
+ llm-pi-ai/openai-codex:
69
+ kind: grant
70
+ payload:
71
+ type: oauth
72
+ access: <access token>
73
+ refresh: <refresh token>
74
+ expires: <epoch ms>
75
+ accountId: <chatgpt account id>
76
+ ```
77
+
78
+ > The grant must land in the harness credential store (the record above). Codex clients that keep their own credential file (e.g. `dsh-codex`'s `$DSH_HOME/.openai-codex-auth.json`, or the Codex CLI's `~/.codex/auth.json`) do not write this record, so this plugin cannot see them.
79
+
80
+ ### 3. Watch the quota
81
+
82
+ That's it. The route is auto-detected (`ctx.llm` lists `openai-codex`), and the plugin:
83
+
84
+ 1. reads the grant record from the credential store on every poll (no caching),
85
+ 2. refreshes the OAuth token automatically when it is within 30 s of expiry (and persists the rotated grant back through the store's locked `modifyRecord`),
86
+ 3. calls `GET https://chatgpt.com/backend-api/wham/usage` with `Authorization: Bearer <access>` and the `ChatGPT-Account-Id` header derived from the token's own JWT claim, retrying once after a refresh if the endpoint answers `401`.
87
+
88
+ The panel then shows your subscription's **5h limit**, **weekly** windows (used %, reset countdown) plus **credits** and **spend control** balances when the plan reports them. If the grant is missing the card reads "OAuth authorization missing (llm-pi-ai/openai-codex)" — sign in again with step 2.
89
+
48
90
  ## Install
49
91
 
50
92
  > [!NOTE]
package/README_ZH.md CHANGED
@@ -24,7 +24,7 @@
24
24
  | `kimi-coding` | `kimi-coding` | `GET {baseURL}/v1/usages` | 每周配额及各限速窗口,含重置倒计时 |
25
25
  | `openrouter` | `openrouter` | `GET {origin}/api/v1/credits` | credit 已用/总额 |
26
26
  | `github-copilot` | `github-copilot` | `GET api.github.com/copilot_internal/user` | 付费档配额快照 / 免费档月度配额 |
27
- | `openai-codex` | `openai-codex` | `GET {baseURL}/wham/usage` | ChatGPT 订阅 5h/周窗口 + credits |
27
+ | `openai-codex` | `openai-codex` | `GET {baseURL}/wham/usage` | ChatGPT 订阅 5h/周窗口 + credits + spend control(**OAuth 登录**,非 API key,见 [通过 OAuth 添加 OpenAI Codex](#通过-oauth-添加-openai-codex)) |
28
28
  | `openai` | `openai` | `GET {origin}/v1/organization/costs` | 当月花费(**需 Admin key**,普通 key 会 403) |
29
29
  | `anthropic` | `anthropic` | `GET {baseURL}/v1/organizations/cost_report` | 当月花费(**需 Admin key**,`x-api-key` 头) |
30
30
  | `minimax` | `minimax`、`minimax-cn` | `GET {origin}/v1/api/openplatform/coding_plan/remains` | Coding Plan 5h/周剩余百分比 |
@@ -32,8 +32,8 @@
32
32
  | `opencode` | `opencode`、`opencode-go` | `GET {baseURL}/usage` | Zen Go 滚动/周/月窗口 |
33
33
  | `vercel-ai-gateway` | `vercel-ai-gateway` | `GET {baseURL}/v1/credits` | 团队 credit 余额 |
34
34
  | `xai` | `xai` | `GET {baseURL}/billing/credits` | 预付余额(USD) |
35
- - **密钥安全** —— 通过 harness 凭据服务按次解析(环境变量 / `~/.dsh/.credentials.yaml`),不缓存、不落地。
36
- - **悬浮球入口** —— 可任意拖动的悬浮球点击弹出用量面板,位置持久化;默认停靠在主对话区域左下角(左边距 = 底边距),面板头部的归位按钮一键回到默认位置;球体光晕表达健康度:绿色全部正常、黄色有配额剩余不足 30%、红色查询失败/缺密钥/用量 ≥90%。
35
+ - **密钥安全** —— 通过 harness 凭据服务按次解析(环境变量 / `~/.dsh/.credentials.yaml`),不缓存、不落地。对 OAuth 类 Provider(OpenAI Codex)则直接读取登录流程存入的授权记录,并在令牌临近过期时自动刷新。
36
+ - **悬浮球入口** —— 可任意拖动的悬浮球点击弹出用量面板,位置持久化;默认停靠在主对话区域左下角(左边距 = 底边距),面板头部的归位按钮一键回到默认位置;球体光晕表达**当前正在使用**的 Provider(composer 中选中的模型)的健康度:绿色正常、黄色配额剩余不足 30%、红色查询失败/缺密钥/用量 ≥90%。闲置的 Provider 余量不足不再影响悬浮球颜色——切换到余量充足的另一个 Provider 后球体会恢复绿色;面板会标注"使用中"的 Provider,并照常列出所有 Provider 的用量明细。
37
37
  - **版本徽章** —— 面板标题旁显示当前运行的插件版本,一眼确认加载的是哪个发布版。
38
38
  - **中英双语** —— 面板内置中英文界面,默认跟随 harness 系统语言,标题栏按钮一键切换(localStorage 持久化)。
39
39
  - **刷新周期可调** —— 面板内调整(15s–30min,localStorage 持久化),默认值由插件配置提供。
@@ -45,6 +45,48 @@
45
45
 
46
46
  ![用量面板(中文)](docs/panel-zh.png)
47
47
 
48
+ ## 通过 OAuth 添加 OpenAI Codex
49
+
50
+ OpenAI Codex 是 **ChatGPT 订阅制** Provider:它用 OAuth access token 认证,而不是 API key,所以没有任何密钥可填。dsh 本身没有为该路由内置 OAuth 登录按钮——但本插件依然能查询它的余量,因为插件会直接从 harness 凭据库读取登录后存入的授权记录。按下面步骤配置一次,用量面板就能展示真实的 Codex 5 小时 / 每周窗口。
51
+
52
+ ### 1. 确认路由已配置
53
+
54
+ 登录会把授权记录写入 `llm-pi-ai/openai-codex`,且路由需要已配置,`ctx.llm` 才会列出它。默认 web profile 已挂载 `llm-pi-ai` 适配器,空 profile 即可——例如在 `~/.dsh/settings.yaml` 中:
55
+
56
+ ```yaml
57
+ llm-pi-ai:
58
+ providers:
59
+ openai-codex: {}
60
+ ```
61
+
62
+ ### 2. 通过 harness 授权 seam 登录
63
+
64
+ `dsh-llm-pi-ai` 在 harness 授权 seam(`ctx.authorization`,凭据键 `llm-pi-ai/openai-codex`)上为 `openai-codex` 注册了 "OpenAI (ChatGPT Plus/Pro)" 的 OAuth 流程。从任何调起该流程的入口完成登录——harness 模型/授权界面上的登录入口,或任何会把登录结果持久化到 harness 凭据库的 pi-ai 客户端。用你的 ChatGPT 账号完成浏览器(或设备码)流程后,harness 会把授权记录存到 `~/.dsh/.credentials.yaml` 的 `llm-pi-ai/openai-codex` 下:
65
+
66
+ ```yaml
67
+ records:
68
+ llm-pi-ai/openai-codex:
69
+ kind: grant
70
+ payload:
71
+ type: oauth
72
+ access: <access token>
73
+ refresh: <refresh token>
74
+ expires: <epoch ms>
75
+ accountId: <chatgpt account id>
76
+ ```
77
+
78
+ > 授权记录必须落在 harness 凭据库(即上面的记录)。使用独立凭据文件的 Codex 客户端(如 `dsh-codex` 的 `$DSH_HOME/.openai-codex-auth.json`,或 Codex CLI 的 `~/.codex/auth.json`)不会写入该记录,本插件无法读取。
79
+
80
+ ### 3. 查看余量
81
+
82
+ 配置完成。路由会被自动探测(`ctx.llm` 会列出 `openai-codex`),插件会:
83
+
84
+ 1. 每次轮询都从凭据库**实时读取**授权记录(不缓存);
85
+ 2. access token 距过期不足 30 秒时**自动刷新** OAuth 令牌(并通过凭据库带锁的 `modifyRecord` 回写旋转后的授权);
86
+ 3. 用 `Authorization: Bearer <access>` + 从令牌 JWT 中解析出的 `ChatGPT-Account-Id` 头请求 `GET https://chatgpt.com/backend-api/wham/usage`;若返回 `401` 会自动刷新一次并重试。
87
+
88
+ 面板随即展示订阅的 **5h 上限**、**每周**窗口(已用百分比 + 重置倒计时),以及计划上报的 **credits** 与 **spend control** 余额。若授权记录缺失,卡片会显示「未完成 OAuth 授权(llm-pi-ai/openai-codex)」,按第 2 步重新登录即可。
89
+
48
90
  ## 安装
49
91
 
50
92
  > [!NOTE]
package/lib/client.js CHANGED
@@ -38,10 +38,12 @@ window.__ModuleLoader__.load({
38
38
  "usage.resetIn": "{countdown} 后重置",
39
39
  "status.unsupported": "该 Provider 暂不支持额度查询",
40
40
  "status.missingCredential": "未配置密钥({ref})",
41
+ "status.missingAuthorization": "未完成 OAuth 授权({key})",
41
42
  "status.error": "查询失败",
42
- "status.tone.ok": "全部正常",
43
- "status.tone.warn": "有用量接近上限",
44
- "status.tone.danger": " Provider 异常或用量将尽",
43
+ "status.inUse": "使用中",
44
+ "status.tone.ok": "当前 Provider 正常",
45
+ "status.tone.warn": "当前 Provider 用量接近上限",
46
+ "status.tone.danger": "当前 Provider 异常或用量将尽",
45
47
  "countdown.days": "{days}天{hours}小时",
46
48
  "countdown.hours": "{hours}小时{minutes}分",
47
49
  "countdown.minutes": "{minutes}分钟"
@@ -76,10 +78,12 @@ window.__ModuleLoader__.load({
76
78
  "usage.resetIn": "Resets in {countdown}",
77
79
  "status.unsupported": "Usage query is not supported for this provider",
78
80
  "status.missingCredential": "No API key configured ({ref})",
81
+ "status.missingAuthorization": "OAuth authorization missing ({key})",
79
82
  "status.error": "Query failed",
80
- "status.tone.ok": "All providers healthy",
81
- "status.tone.warn": "Some usage nearing its limit",
82
- "status.tone.danger": "Provider error or usage almost exhausted",
83
+ "status.inUse": "In use",
84
+ "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",
83
87
  "countdown.days": "{days}d {hours}h",
84
88
  "countdown.hours": "{hours}h {minutes}m",
85
89
  "countdown.minutes": "{minutes}m"
@@ -216,12 +220,18 @@ window.__ModuleLoader__.load({
216
220
  if (percent >= 70) return "warn";
217
221
  return "ok";
218
222
  }
219
- /** Worst health across the fetch + every provider: drives the trigger's status dot. */
223
+ /** Worst health across the fetch + the provider IN USE: drives the trigger's
224
+ status dot. Only the active provider (the composer's current selection)
225
+ colors the ball, so an idle low-quota provider does not alarm; when the
226
+ host reports no active provider, fall back to every provider. */
220
227
  function healthTone(data, error) {
221
228
  if (error !== null) return "danger";
229
+ const providers = data?.providers ?? [];
230
+ const active = providers.filter((provider) => provider.active);
231
+ const relevant = active.length > 0 ? active : providers;
222
232
  let tone = "ok";
223
- for (const provider of data?.providers ?? []) {
224
- if (provider.status === "error" || provider.status === "missing-credential") return "danger";
233
+ for (const provider of relevant) {
234
+ if (provider.status === "error" || provider.status === "missing-credential" || provider.status === "missing-authorization") return "danger";
225
235
  for (const row of provider.usages ?? []) {
226
236
  const rowTone = barTone(row.percent);
227
237
  if (rowTone === "danger") return "danger";
@@ -270,19 +280,25 @@ window.__ModuleLoader__.load({
270
280
  }
271
281
  function ProviderCard({ provider, now, t }) {
272
282
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
273
- className: "dsh-usage-card",
274
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
283
+ className: `dsh-usage-card${provider.active ? " dsh-usage-card--active" : ""}`,
284
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
275
285
  className: "dsh-usage-cardHead",
276
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
286
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
277
287
  className: "dsh-usage-providerName",
278
288
  children: provider.displayName
279
- })
289
+ }), provider.active ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
290
+ className: "dsh-usage-inUse",
291
+ children: t("status.inUse")
292
+ }) : null]
280
293
  }), provider.status === "unsupported" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
281
294
  className: "dsh-usage-message",
282
295
  children: t("status.unsupported")
283
296
  }) : provider.status === "missing-credential" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
284
297
  className: "dsh-usage-message",
285
298
  children: t("status.missingCredential", { ref: provider.message })
299
+ }) : provider.status === "missing-authorization" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
300
+ className: "dsh-usage-message",
301
+ children: t("status.missingAuthorization", { key: provider.message })
286
302
  }) : provider.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
287
303
  className: "dsh-usage-message",
288
304
  children: [
@@ -706,8 +722,8 @@ window.__ModuleLoader__.load({
706
722
  */
707
723
  const CSS = `
708
724
  .dsh-usage-root { position: relative; display: inline-flex; }
709
- /* Tri-tone health halo: only the shadow carries the worst provider state
710
- (green ok / amber remaining<30% / red error or usage>=90%) — the button
725
+ /* Tri-tone health halo: only the shadow carries the state of the provider IN
726
+ USE (green ok / amber remaining<30% / red error or usage>=90%) — the button
711
727
  face itself stays neutral. A 1px tone ring plus a soft halo keeps the
712
728
  state readable on both light and dark skins. */
713
729
  .dsh-usage-tone-ok { --dsh-usage-tone: #22a06b; }
@@ -800,11 +816,22 @@ window.__ModuleLoader__.load({
800
816
  border-radius: 8px; padding: 8px 10px; display: flex; flex-direction: column; gap: 6px;
801
817
  background: var(--dsw-alias-fill-l2);
802
818
  }
819
+ /* The provider in use, whose quota alone drives the ball tone. */
820
+ .dsh-usage-card--active {
821
+ outline: 1px solid color-mix(in srgb, var(--dsw-alias-accent, #4f8cff) 45%, transparent);
822
+ outline-offset: -1px;
823
+ }
803
824
  .dsh-usage-cardHead { display: flex; align-items: center; gap: 8px; }
804
825
  .dsh-usage-providerName {
805
826
  color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 600;
806
827
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;
807
828
  }
829
+ .dsh-usage-inUse {
830
+ flex: none; font-size: 11px; line-height: 16px; padding: 0 6px; border-radius: 999px;
831
+ color: var(--dsw-alias-label-secondary);
832
+ background: color-mix(in srgb, var(--dsw-alias-accent, #4f8cff) 18%, transparent);
833
+ border: 1px solid color-mix(in srgb, var(--dsw-alias-accent, #4f8cff) 35%, transparent);
834
+ }
808
835
  .dsh-usage-message { color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 18px; word-break: break-all; }
809
836
  .dsh-usage-balanceRow { display: flex; align-items: baseline; gap: 8px; }
810
837
  .dsh-usage-balanceTotal {
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.error': '查询失败',\n 'status.tone.ok': '全部正常',\n 'status.tone.warn': '有用量接近上限',\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.error': 'Query failed',\n 'status.tone.ok': 'All providers healthy',\n 'status.tone.warn': 'Some usage nearing its limit',\n 'status.tone.danger': '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' | 'unsupported'\n message: string | null\n balances: BalanceRow[] | null\n usages: UsageRow[] | null\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 + every provider: drives the trigger's status dot. */\nfunction healthTone(data: UsageListResult | null, error: string | null): 'ok' | 'warn' | 'danger' {\n if (error !== null) return 'danger'\n let tone: 'ok' | 'warn' | 'danger' = 'ok'\n for (const provider of data?.providers ?? []) {\n if (provider.status === 'error' || provider.status === 'missing-credential') 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\">\n <div className=\"dsh-usage-cardHead\">\n <span className=\"dsh-usage-providerName\">{provider.displayName}</span>\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 === '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 worst provider state\n (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.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-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.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":";;;;;;;;;;;EACA,MAAa,KAAK;GAChB,gBAAgB;GAChB,eAAe;GACf,eAAe;GACf,iBAAiB;GACjB,oBAAoB;GACpB,eAAe;GACf,kBAAkB;GAClB,kBAAkB;GAClB,iBAAiB;GACjB,eAAe;GACf,eAAe;GACf,gBAAgB;GAChB,gBAAgB;GAChB,gBAAgB;GAChB,iBAAiB;GACjB,iBAAiB;GACjB,kBAAkB;GAClB,iBAAiB;GACjB,mBAAmB;GACnB,oBAAoB;GACpB,gBAAgB;GAChB,cAAc;GACd,mBAAmB;GACnB,mBAAmB;GACnB,iBAAiB;GACjB,iBAAiB;GACjB,sBAAsB;GACtB,4BAA4B;GAC5B,+BAA+B;GAC/B,gBAAgB;GAChB,gBAAgB;GAChB,kBAAkB;GAClB,oBAAoB;GACpB,sBAAsB;GACtB,kBAAkB;GAClB,mBAAmB;GACnB,qBAAqB;EACvB;;EAGA,MAAa,KAAK;GAChB,gBAAgB;GAChB,eAAe;GACf,eAAe;GACf,iBAAiB;GACjB,oBAAoB;GACpB,eAAe;GACf,kBAAkB;GAClB,kBAAkB;GAClB,iBAAiB;GACjB,eAAe;GACf,eAAe;GACf,gBAAgB;GAChB,gBAAgB;GAChB,gBAAgB;GAChB,iBAAiB;GACjB,iBAAiB;GACjB,kBAAkB;GAClB,iBAAiB;GACjB,mBAAmB;GACnB,oBAAoB;GACpB,gBAAgB;GAChB,cAAc;GACd,mBAAmB;GACnB,mBAAmB;GACnB,iBAAiB;GACjB,iBAAiB;GACjB,sBAAsB;GACtB,4BAA4B;GAC5B,+BAA+B;GAC/B,gBAAgB;GAChB,gBAAgB;GAChB,kBAAkB;GAClB,oBAAoB;GACpB,sBAAsB;GACtB,kBAAkB;GAClB,mBAAmB;GACnB,qBAAqB;EACvB;;;;;;;;;;;ECdA,MAAM,mBAAmB;GAAC;GAAI;GAAI;GAAI;GAAK;GAAK;EAAI;EACpD,MAAM,cAAc;EACpB,MAAM,WAAW;EACjB,MAAM,UAAU;EAChB,MAAM,mBAAmB;;EAGzB,MAAM,YAAY;;EAWlB,SAAS,iBAA8B;GACrC,IAAI;IACF,MAAM,MAAM,aAAa,QAAQ,QAAQ;IACzC,OAAO,QAAQ,QAAQ,QAAQ,OAAO,MAAM;GAC9C,QAAQ;IACN,OAAO;GACT;EACF;EAEA,SAAS,UAAU,MAAkB;GACnC,IAAI;IACF,aAAa,QAAQ,UAAU,IAAI;GACrC,QAAQ,CAER;EACF;;EAGA,SAAS,MAAM,MAAqD;GAClE,QAAQ,KAAK,WAAW;IACtB,MAAM,WAAW,KAAK,QAAQ;IAC9B,IAAI,WAAW,KAAA,GAAW,OAAO;IACjC,OAAO,SAAS,QAAQ,eAAe,KAAK,SAAkB,QAAQ,SAAS,OAAO,OAAO,KAAK,IAAI,GAAI;GAC5G;EACF;EAEA,SAAS,qBAAoC;GAC3C,IAAI;IACF,MAAM,MAAM,aAAa,QAAQ,WAAW;IAC5C,IAAI,QAAQ,MAAM,OAAO;IACzB,MAAM,QAAQ,OAAO,GAAG;IACxB,OAAO,OAAO,SAAS,KAAK,KAAK,SAAS,IAAI,QAAQ;GACxD,QAAQ;IACN,OAAO;GACT;EACF;EAEA,SAAS,cAAc,OAAqB;GAC1C,IAAI;IACF,aAAa,QAAQ,aAAa,OAAO,KAAK,CAAC;GACjD,QAAQ,CAER;EACF;;EAOA,MAAM,cAAc;;;;;;;;EASpB,SAAS,kBAA4B;GACnC,IAAI,eAAe;GACnB,IAAI,OAAO,SAAS,cAAc,kCAAgC,CAAC,EAAE,iBAAiB;GACtF,OAAO,MAAM;IACX,MAAM,OAAO,KAAK,sBAAsB;IACxC,IAAI,KAAK,KAAK,KAAK,KAAK,UAAU,OAAO,cAAc,IAAK;KAC1D,eAAe,KAAK;KACpB;IACF;IACA,OAAO,KAAK;GACd;GACA,OAAO;IACL,KAAK,eAAe,cAAc,YAAY,KAAK,OAAO;IAC1D,KAAK,OAAO,cAAc,cAAc,YAAY,KAAK,OAAO;GAClE;EACF;EAEA,SAAS,qBAAsC;GAC7C,IAAI;IACF,MAAM,MAAM,aAAa,QAAQ,OAAO;IACxC,IAAI,QAAQ,MAAM,OAAO;IACzB,MAAM,SAAS,KAAK,MAAM,GAAG;IAC7B,IAAI,WAAW,QAAQ,OAAO,OAAO,OAAO,YAAY,CAAC,OAAO,SAAS,OAAO,EAAE,GAAG,OAAO;IAC5F,IAAI,OAAO,OAAO,OAAO,YAAY,CAAC,OAAO,SAAS,OAAO,EAAE,GAAG,OAAO;IACzE,OAAO;KAAE,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,OAAO,EAAE,CAAC;KAAG,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,OAAO,EAAE,CAAC;IAAE;GAC5F,QAAQ;IACN,OAAO;GACT;EACF;EAEA,SAAS,cAAc,KAAqB;GAC1C,IAAI;IACF,aAAa,QAAQ,SAAS,KAAK,UAAU,GAAG,CAAC;GACnD,QAAQ,CAER;EACF;;EAGA,SAAS,sBAA4B;GACnC,IAAI;IACF,aAAa,WAAW,OAAO;GACjC,QAAQ,CAER;EACF;EAMA,SAAS,gBAAgB,SAAiB,KAAa,GAAyC;GAC9F,MAAM,OAAO,IAAI,KAAK,OAAO,CAAC,CAAC,QAAQ,IAAI;GAC3C,IAAI,CAAC,OAAO,SAAS,IAAI,KAAK,QAAQ,GAAG,OAAO;GAChD,MAAM,UAAU,KAAK,MAAM,OAAO,GAAK;GACvC,IAAI,WAAW,MAAS,OAAO,EAAE,kBAAkB;IAAE,MAAM,KAAK,MAAM,UAAW,IAAQ;IAAG,OAAO,KAAK,MAAO,UAAW,OAAY,EAAE;GAAE,CAAC;GAC3I,IAAI,WAAW,IAAI,OAAO,EAAE,mBAAmB;IAAE,OAAO,KAAK,MAAM,UAAU,EAAE;IAAG,SAAS,UAAU;GAAG,CAAC;GACzG,OAAO,EAAE,qBAAqB,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,EAAE,CAAC;EACjE;EAEA,SAAS,QAAQ,SAAkD;GACjE,IAAI,YAAY,MAAM,OAAO;GAC7B,IAAI,WAAW,IAAI,OAAO;GAC1B,IAAI,WAAW,IAAI,OAAO;GAC1B,OAAO;EACT;;;;;EAMA,SAAS,WAAW,MAA8B,OAAgD;GAChG,IAAI,UAAU,MAAM,OAAO;GAC3B,MAAM,YAAY,MAAM,aAAa,CAAC;GACtC,MAAM,SAAS,UAAU,QAAQ,aAAa,SAAS,MAAM;GAC7D,MAAM,WAAW,OAAO,SAAS,IAAI,SAAS;GAC9C,IAAI,OAAiC;GACrC,KAAK,MAAM,YAAY,UAAU;IAC/B,IAAI,SAAS,WAAW,WAAW,SAAS,WAAW,wBAAwB,SAAS,WAAW,yBAAyB,OAAO;IACnI,KAAK,MAAM,OAAO,SAAS,UAAU,CAAC,GAAG;KACvC,MAAM,UAAU,QAAQ,IAAI,OAAO;KACnC,IAAI,YAAY,UAAU,OAAO;KACjC,IAAI,YAAY,QAAQ,OAAO;IACjC;GACF;GACA,OAAO;EACT;EAEA,SAAS,aAAa,OAA8B;GAClD,IAAI,UAAU,MAAM,OAAO;GAC3B,OAAO,OAAO,UAAU,KAAK,IAAI,OAAO,KAAK,IAAI,MAAM,QAAQ,CAAC;EAClE;EAMA,SAAS,UAAU,EAAE,UAAU,KAAK,KAA6E;GAC/G,MAAM,OAAO,SAAS,UAAU,CAAC;GACjC,IAAI,KAAK,WAAW,GAAG,OAAO;GAC9B,OACE,iBAAA,GAAA,kBAAA,IAAA,CAAA,kBAAA,UAAA,EAAA,UACG,KAAK,KAAK,KAAK,UAAU;IACxB,MAAM,UAAU,IAAI;IACpB,MAAM,QAAQ,IAAI,YAAY,OAAO,gBAAgB,IAAI,SAAS,KAAK,CAAC,IAAI;IAC5E,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;KAAK,WAAU;KAAf,UAAA;MACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;OAAK,WAAU;OAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;QAAM,WAAU;QAAsB,UAAA,IAAI,UAAU,WAAW,EAAE,cAAc,IAAI,IAAI;OAAY,CAAA,GACnG,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;QAAM,WAAU;QACb,UAAA,IAAI,UAAU,OACX,IAAI,SAAS,OACX,EAAE,cAAc,EAAE,MAAM,aAAa,IAAI,IAAI,EAAE,CAAC,IAChD,IAAI,cAAc,OAChB,aAAa,IAAI,SAAS,IAC1B,EAAE,iBAAiB,IACvB,YAAY,OACV,GAAG,KAAK,MAAM,OAAO,EAAE,KACvB,EAAE,cAAc,EAAE,MAAM,aAAa,IAAI,IAAI,EAAE,CAAC;OAClD,CAAA,CACH;;MACJ,YAAY,OACX,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;OAAK,WAAU;OACb,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,OAAD;QACE,WAAW,wCAAwC,QAAQ,OAAO;QAClE,OAAO,EAAE,OAAO,GAAG,KAAK,IAAI,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC,EAAE,GAAG;OAC3D,CAAA;MACE,CAAA,IACH;MACH,UAAU,OAAO,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;OAAM,WAAU;OAAsB,UAAA,EAAE,iBAAiB,EAAE,WAAW,MAAM,CAAC;MAAQ,CAAA,IAAI;KACxG;IAxB+B,GAAA,GAAG,IAAI,MAAM,GAAG,OAwB/C;GAET,CAAC,EACD,CAAA;EAEN;EAEA,SAAS,aAAa,EAAE,UAAU,KAAK,KAA6E;GAClH,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,WAAW,iBAAiB,SAAS,SAAS,4BAA4B;IAA/E,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;KAAK,WAAU;KAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,WAAU;MAA0B,UAAA,SAAS;KAAkB,CAAA,GACpE,SAAS,SAAS,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,WAAU;MAAmB,UAAA,EAAE,cAAc;KAAQ,CAAA,IAAI,IAC/E;IACJ,CAAA,GAAA,SAAS,WAAW,gBACnB,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;KAAM,WAAU;KAAqB,UAAA,EAAE,oBAAoB;IAAQ,CAAA,IACjE,SAAS,WAAW,uBACtB,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;KAAM,WAAU;KAAqB,UAAA,EAAE,4BAA4B,EAAE,KAAK,SAAS,QAAQ,CAAC;IAAQ,CAAA,IAClG,SAAS,WAAW,0BACtB,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;KAAM,WAAU;KAAqB,UAAA,EAAE,+BAA+B,EAAE,KAAK,SAAS,QAAQ,CAAC;IAAQ,CAAA,IACrG,SAAS,WAAW,UACtB,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD;KAAM,WAAU;KAAhB,UAAA;MAAqC,EAAE,cAAc;MAAE;MAAG,SAAS;KAAc;IAC/E,CAAA,IAAA,SAAS,SAAS,aACnB,SAAS,YAAY,CAAC,EAAA,CAAG,KAAK,KAAK,UAClC,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD,EAAA,UAAA,CACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;KAAK,WAAU;KAAf,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,WAAU;MAA0B,UAAA,IAAI;KAAY,CAAA,GAC1D,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,WAAU;MAA6B,UAAA,IAAI;KAAe,CAAA,CAC7D;IACL,CAAA,GAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;KAAM,WAAU;KACb,UAAA,CAAC,IAAI,YAAY,OAAO,IAAI,YAAY,SAAS,EAAE,mBAAmB,EAAE,QAAQ,IAAI,QAAQ,CAAC,IAAI,MAChG,IAAI,aAAa,OAAO,IAAI,aAAa,SAAS,EAAE,oBAAoB,EAAE,QAAQ,IAAI,SAAS,CAAC,IAAI,IAAI,CAAC,CACxG,OAAO,OAAO,CAAC,CACf,KAAK,KAAK;IACT,CAAA,CACH,EAAA,GAXK,GAAG,IAAI,SAAS,GAAG,OAWxB,CACN,IAED,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD;KAAqB;KAAe;KAAQ;IAAI,CAAA,CAE/C;;EAET;;EAOA,SAAS,cAAc;GACrB,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,OAAM;IAAK,QAAO;IAAK,SAAQ;IAAY,MAAK;IAAO,QAAO;IAAe,aAAY;IAAI,eAAc;IAAQ,gBAAe;IAAQ,eAAY;IAA3J,UAAA;KACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;MAAM,OAAM;MAAK,QAAO;MAAK,GAAE;MAAI,GAAE;MAAI,IAAG;MAAI,IAAG;KAAK,CAAA;KACxD,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAM,GAAE,WAAY,CAAA;KACpB,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAM,GAAE,UAAW,CAAA;KACnB,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAM,GAAE,WAAY,CAAA;IACjB;;EAET;EAEA,SAAS,cAAc;GACrB,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,OAAM;IAAK,QAAO;IAAK,SAAQ;IAAY,MAAK;IAAO,QAAO;IAAe,aAAY;IAAI,eAAc;IAAQ,gBAAe;IAAQ,eAAY;IAA3J,UAAA,CACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAM,GAAE,6BAA8B,CAAA,GACtC,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAM,GAAE,aAAc,CAAA,CACnB;;EAET;EAEA,SAAS,YAAY;GACnB,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,OAAM;IAAK,QAAO;IAAK,SAAQ;IAAY,MAAK;IAAO,QAAO;IAAe,aAAY;IAAI,eAAc;IAAQ,gBAAe;IAAQ,eAAY;IAA3J,UAAA;KACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;MAAQ,IAAG;MAAK,IAAG;MAAK,GAAE;KAAK,CAAA;KAC/B,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAM,GAAE,WAAY,CAAA;KACpB,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAM,GAAE,kFAAmF,CAAA;IACxF;;EAET;;EAGA,SAAS,WAAW;GAClB,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,OAAM;IAAK,QAAO;IAAK,SAAQ;IAAY,MAAK;IAAO,QAAO;IAAe,aAAY;IAAI,eAAc;IAAQ,gBAAe;IAAQ,eAAY;IAA3J,UAAA;KACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;MAAQ,IAAG;MAAK,IAAG;MAAK,GAAE;KAAK,CAAA;KAC/B,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAM,GAAE,iCAAkC,CAAA;KAC1C,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;MAAQ,IAAG;MAAK,IAAG;MAAK,GAAE;MAAM,MAAK;MAAe,QAAO;KAAQ,CAAA;IAChE;;EAET;EAEA,SAAwB,YAAY,EAAE,GAAG,cAAgC;GACvE,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CAAS,KAAK;GACtC,MAAM,CAAC,MAAM,YAAA,GAAWA,MAAAA,SAAAA,CAAiC,IAAI;GAC7D,MAAM,CAAC,OAAO,aAAA,GAAYA,MAAAA,SAAAA,CAAwB,IAAI;GACtD,MAAM,CAAC,SAAS,eAAA,GAAcA,MAAAA,SAAAA,CAAS,KAAK;GAC5C,MAAM,CAAC,aAAa,mBAAA,GAAkBA,MAAAA,SAAAA,OAAe,mBAAmB,KAAK,gBAAgB;GAC7F,MAAM,CAAC,cAAc,oBAAA,GAAmBA,MAAAA,SAAAA,OAA4B,eAAe,CAAC;GACpF,MAAM,CAAC,KAAK,WAAA,GAAUA,MAAAA,SAAAA,OAAe,KAAK,IAAI,CAAC;;GAE/C,MAAM,CAAC,WAAW,iBAAA,GAAgBA,MAAAA,SAAAA,OAAgC,mBAAmB,CAAC;;GAEtF,MAAM,CAAC,UAAU,gBAAA,GAAeA,MAAAA,SAAAA,CAAS;IAAE,GAAG,OAAO;IAAY,GAAG,OAAO;GAAY,CAAC;;GAExF,MAAM,CAAC,SAAS,eAAA,GAAcA,MAAAA,SAAAA,CAA0C,IAAI;GAI5E,MAAM,cACJ,EAAE,aAAa,MAAM,GAAG,iBAAiB,OAAO,EAAE,aAAa,MAAM,GAAG,iBAAiB,OAAO;GAClG,MAAM,OAAa,gBAAgB,eAAe;GAClD,MAAM,MAAA,GAAKC,MAAAA,QAAAA,OAAc,MAAM,SAAS,OAAQ,KAAiC,EAA6B,GAAG,CAAC,IAAI,CAAC;GACvH,MAAM,cAAA,GAAaC,MAAAA,YAAAA,OAAkB;IACnC,iBAAiB,YAAY;KAC3B,MAAM,QAAc,WAAW,eAAe,UAAU,OAAO,OAAO;KACtE,UAAU,IAAI;KACd,OAAO;IACT,CAAC;GACH,GAAG,CAAC,WAAW,CAAC;;GAEhB,MAAM,sBAAsB;IAC1B,oBAAoB;IACpB,aAAa,IAAI;GACnB;GACA,MAAM,CAAC,UAAU,gBAAA,GAAeF,MAAAA,SAAAA,CAAiE,IAAI;GACrG,MAAM,WAAA,GAAUG,MAAAA,OAAAA,CAAuB,IAAI;GAC3C,MAAM,WAAA,GAAUA,MAAAA,OAAAA,CAA0B,IAAI;GAC9C,MAAM,YAAA,GAAWA,MAAAA,OAAAA,CAAuB,IAAI;GAC5C,MAAM,YAAA,GAAWA,MAAAA,OAAAA,CAAO,KAAK;;GAE7B,MAAM,WAAA,GAAUA,MAAAA,OAAAA,CAAwF,IAAI;;GAE5G,MAAM,oBAAA,GAAmBA,MAAAA,OAAAA,CAAO,KAAK;GAErC,MAAM,WAAA,GAAUD,MAAAA,YAAAA,CAAY,YAAY;IACtC,IAAI,SAAS,SAAS;IACtB,SAAS,UAAU;IACnB,WAAW,IAAI;IACf,IAAI;KACF,MAAM,SAAS,MAAM,WAAW;KAChC,QAAQ,MAAM;KACd,SAAS,IAAI;IACf,SAAS,QAAQ;KACf,SAAS,kBAAkB,QAAQ,OAAO,UAAU,OAAO,MAAM,CAAC;IACpE,UAAU;KACR,SAAS,UAAU;KACnB,WAAW,KAAK;IAClB;GACF,GAAG,CAAC,UAAU,CAAC;GAGf,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,QAAQ;GACV,GAAG,CAAC,OAAO,CAAC;GACZ,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,MAAM,QAAQ,kBAAkB;KAC9B,IAAI,SAAS,oBAAoB,WAAW,QAAQ;IACtD,GAAG,cAAc,GAAI;IACrB,aAAa,cAAc,KAAK;GAClC,GAAG,CAAC,aAAa,OAAO,CAAC;GAGzB,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,SAAS,QAAQ,mBAAmB,MAAM,QAAQ,KAAK,mBAAmB,aAC5E,eAAe,KAAK,cAAc;GAGtC,GAAG,CAAC,IAAI,CAAC;GAGT,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,CAAC,MAAM;IACX,OAAO,KAAK,IAAI,CAAC;IACjB,MAAM,QAAQ,kBAAkB,OAAO,KAAK,IAAI,CAAC,GAAG,GAAK;IACzD,aAAa,cAAc,KAAK;GAClC,GAAG,CAAC,IAAI,CAAC;GAKT,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,CAAC,MAAM;IACX,MAAM,gBAAgB,UAAwB;KAC5C,IAAI,EAAE,MAAM,kBAAkB,OAAO;KACrC,IAAI,QAAQ,SAAS,SAAS,MAAM,MAAM,GAAG;KAC7C,IAAI,QAAQ,SAAS,SAAS,MAAM,MAAM,GAAG;KAC7C,IAAI,SAAS,SAAS,SAAS,MAAM,MAAM,GAAG;KAC9C,QAAQ,KAAK;IACf;IACA,SAAS,iBAAiB,eAAe,YAAY;IACrD,aAAa,SAAS,oBAAoB,eAAe,YAAY;GACvE,GAAG,CAAC,IAAI,CAAC;GAKT,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,IAAI,CAAC,MAAM;IACX,MAAM,eAAe;KACnB,MAAM,OAAO,QAAQ,SAAS,sBAAsB;KACpD,IAAI,CAAC,MAAM;KACX,MAAM,OAAO,KAAK,IAAI,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,OAAO,aAAa,GAAG,CAAC;KAClF,YACE,KAAK,MAAM,OAAO,cAAc,IAC5B;MAAE;MAAM,QAAQ,OAAO,cAAc,KAAK,MAAM;KAAE,IAClD;MAAE;MAAM,KAAK,KAAK,SAAS;KAAE,CACnC;IACF;IACA,OAAO;IACP,OAAO,iBAAiB,UAAU,MAAM;IACxC,aAAa,OAAO,oBAAoB,UAAU,MAAM;GAC1D,GAAG;IAAC;IAAM;IAAW;GAAQ,CAAC;GAI9B,CAAA,GAAA,MAAA,UAAA,OAAgB;IACd,MAAM,iBAAiB,YAAY;KAAE,GAAG,OAAO;KAAY,GAAG,OAAO;IAAY,CAAC;IAClF,OAAO,iBAAiB,UAAU,QAAQ;IAC1C,aAAa,OAAO,oBAAoB,UAAU,QAAQ;GAC5D,GAAG,CAAC,CAAC;GAEL,MAAM,OAAO,WAAW,MAAM,KAAK;GAEnC,MAAM,aAAa,UAA+B;IAChD,IAAI,MAAM,QAAQ,YAAY,CAAC,MAAM;IACrC,MAAM,eAAe;IACrB,QAAQ,KAAK;GACf;GAOA,MAAM,qBAAqB,UAAiD;IAC1E,MAAM,cAAc,kBAAkB,MAAM,SAAS;IACrD,QAAQ,UAAU;KAAE,WAAW,MAAM;KAAW,QAAQ,MAAM;KAAS,QAAQ,MAAM;KAAS,UAAU;IAAM;GAChH;GAEA,MAAM,qBAAqB,UAAiD;IAC1E,MAAM,OAAO,QAAQ;IACrB,IAAI,SAAS,QAAQ,KAAK,cAAc,MAAM,WAAW;IACzD,IAAI,CAAC,KAAK,UAAU;KAClB,IAAI,KAAK,IAAI,MAAM,UAAU,KAAK,MAAM,IAAI,KAAK,KAAK,IAAI,MAAM,UAAU,KAAK,MAAM,IAAI,GAAG;KAC5F,KAAK,WAAW;KAEhB,IAAI,MAAM,QAAQ,KAAK;IACzB;IACA,MAAM,OAAO,YAAY;IACzB,WAAW;KACT,GAAG,KAAK,IAAI,KAAK,IAAI,MAAM,MAAM,OAAO,GAAG,OAAO,aAAa,IAAI;KACnE,GAAG,KAAK,IAAI,KAAK,IAAI,MAAM,MAAM,OAAO,GAAG,OAAO,cAAc,IAAI;IACtE,CAAC;GACH;GAEA,MAAM,eAAe,UAAiD;IACpE,MAAM,OAAO,QAAQ;IACrB,QAAQ,UAAU;IAClB,IAAI,SAAS,QAAQ,KAAK,cAAc,MAAM,aAAa,CAAC,KAAK,UAAU;IAC3E,MAAM,OAAO,YAAY;IACzB,MAAM,IAAI,KAAK,IAAI,KAAK,IAAI,MAAM,MAAM,OAAO,GAAG,OAAO,aAAa,IAAI;IAC1E,MAAM,IAAI,KAAK,IAAI,KAAK,IAAI,MAAM,MAAM,OAAO,GAAG,OAAO,cAAc,IAAI;IAE3E,MAAM,OAAiB;KAAE,IAAI,IAAI,OAAO;KAAY,IAAI,IAAI,OAAO;IAAY;IAC/E,aAAa,IAAI;IACjB,cAAc,IAAI;IAClB,WAAW,IAAI;IACf,iBAAiB,UAAU;GAC7B;GAEA,MAAM,kBAAkB,UAAiD;IACvE,IAAI,QAAQ,SAAS,cAAc,MAAM,WAAW;IACpD,QAAQ,UAAU;IAClB,WAAW,IAAI;GACjB;GAEA,MAAM,oBAAoB;IACxB,IAAI,iBAAiB,SAAS;KAC5B,iBAAiB,UAAU;KAC3B;IACF;IACA,OAAO,KAAK,IAAI,CAAC;IACjB,SAAS,YAAY,CAAC,OAAO;IAC7B,IAAI,CAAC,MAAM,QAAQ;GACrB;GAIA,MAAM,SAAS,aAAa,gBAAgB;GAC5C,MAAM,UAAU,YAAY,OAAO,QAAQ,IAAI,OAAO,KAAK,SAAS;GACpE,MAAM,UAAU,YAAY,OAAO,QAAQ,IAAI,OAAO,KAAK,SAAS;GAEpE,OACE,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;IAAK,KAAK;IAAS,WAAU;IAA4B;IAAzD,UAAA,EAAA,GACGE,UAAAA,aAAAA,CACC,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;KACE,KAAK;KACL,MAAK;KACL,WAAW,iCAAiC;KAC5C,OAAO;MAAE,MAAM,UAAU,YAAY;MAAG,KAAK,UAAU,YAAY;KAAE;KACrE,iBAAe;KACf,cAAY,GAAG,aAAa;KAC5B,OAAO,GAAG,GAAG,aAAa,EAAE,KAAK,GAAG,eAAe,MAAM;KACzD,eAAe;KACf,eAAe;KACf,aAAa;KACb,iBAAiB;KACjB,SAAS;KACE;KAEX,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,aAAD,CAAc,CAAA;IACR,CAAA,GACR,SAAS,IACX,GACC,QAAQ,aAAa,QAAA,GAClBA,UAAAA,aAAAA,CACA,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;KACE,KAAK;KACL,WAAU;KACV,MAAK;KACL,cAAY,GAAG,aAAa;KAC5B,OAAO;MAAE,MAAM,SAAS;MAAM,KAAK,SAAS;MAAK,QAAQ,SAAS;KAAO;KAC9D;KANb,UAAA;MAQA,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;OAAK,WAAU;OAAf,UAAA;QACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;SAAM,WAAU;SAAmB,UAAA,GAAG,aAAa;QAAQ,CAAA;QAC1D,MAAM,UAAU,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD;SAAM,WAAU;SAAhB,UAAA,CAAoC,KAAE,KAAK,OAAc;QAAI,CAAA,IAAA;QAC9E,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;SACE,MAAK;SACL,WAAU;SACV,SAAS;SACT,OAAO,GAAG,gBAAgB;SAC1B,cAAY,GAAG,gBAAgB;SAE/B,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD,CAAW,CAAA;QACL,CAAA;QACR,iBAAA,GAAA,kBAAA,KAAA,CAAC,UAAD;SACE,MAAK;SACL,WAAU;SACV,SAAS;SACT,OAAO,GAAG,aAAa;SACvB,cAAY,GAAG,aAAa;SAL9B,UAAA,CAOE,iBAAA,GAAA,kBAAA,IAAA,CAAC,WAAD,CAAY,CAAA,GACZ,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD,EAAA,UAAO,SAAS,OAAO,MAAM,KAAW,CAAA,CAClC;;QACR,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;SACE,MAAK;SACL,WAAW,oBAAoB,UAAU,gCAAgC;SACzE,UAAU;SACV,SAAS;SACT,OAAO,GAAG,eAAe;SACzB,cAAY,GAAG,eAAe;SAE9B,UAAA,iBAAA,GAAA,kBAAA,IAAA,CAAC,aAAD,CAAc,CAAA;QACR,CAAA;OACL;;MACJ,SAAS,QAAQ,UAAU,OAC1B,iBAAA,GAAA,kBAAA,KAAA,CAAC,QAAD;OAAM,WAAU;OAAhB,UAAA;QAAqC,GAAG,cAAc;QAAE;QAAG;OAAY;MACrE,CAAA,IAAA;MACH,SAAS,QAAQ,KAAK,UAAU,WAAW,IAC1C,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;OAAM,WAAU;OAAqB,UAAA,GAAG,aAAa;MAAQ,CAAA,IAC3D;OACF,MAAM,aAAa,CAAC,EAAA,CAAG,KAAK,aAC5B,iBAAA,GAAA,kBAAA,IAAA,CAAC,cAAD;OAA0C;OAAe;OAAK,GAAG;MAAK,GAAnD,SAAS,EAA0C,CACvE;MACD,iBAAA,GAAA,kBAAA,KAAA,CAAC,OAAD;OAAK,WAAU;OAAf,UAAA;QACE,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;SAAM,WAAU;SAAuB,UAAA,GAAG,gBAAgB;QAAQ,CAAA;QAClE,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;SACE,WAAU;SACV,OAAO;SACP,WAAW,UAAU;UACnB,MAAM,QAAQ,OAAO,MAAM,OAAO,KAAK;UACvC,eAAe,KAAK;UACpB,cAAc,KAAK;SACrB;SAEC,UAAA,iBAAiB,KAAK,YACrB,iBAAA,GAAA,kBAAA,IAAA,CAAC,UAAD;UAAsB,OAAO;UAAU,UAAA,GAAG,YAAY,QAAQ,EAAE;SAAU,GAA7D,OAA6D,CAC3E;QACK,CAAA;QACR,iBAAA,GAAA,kBAAA,IAAA,CAAC,QAAD;SAAM,WAAU;SACb,UAAA,SAAS,OACN,GAAG,aAAa,IAChB,GAAG,iBAAiB,EAAE,MAAM,IAAI,KAAK,KAAK,SAAS,CAAC,CAAC,mBAAmB,SAAS,OAAO,UAAU,OAAO,EAAE,CAAC;QAC5G,CAAA;OACH;;KACA;IACL,CAAA,GAAA,SAAS,IACX,IACE,IACD;;EAET;;;;;;;ECrpBA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiJZ,MAAM,SAAS;;EAGf,SAAgB,eAAqB;GACnC,IAAI,OAAO,aAAa,aAAa;GACrC,MAAM,WAAW,SAAS,cAAc,2BAA2B,KAAK,UAAU,MAAM,IAAI,GAAG;GAC/F,IAAI,aAAa,MAAM;IAGrB,IAAI,SAAS,gBAAgB,KAAK,SAAS,cAAc;IACzD;GACF;GACA,MAAM,MAAM,SAAS,cAAc,OAAO;GAC1C,IAAI,QAAQ,SAAS;GACrB,IAAI,QAAQ,YAAY;GACxB,IAAI,cAAc;GAClB,SAAS,KAAK,YAAY,GAAG;EAC/B;;;;;;;;;;;;ECzJA,MAAM,SAAS;GAAC;GAAS;GAAU;EAAY;EAqB/C,SAAS,MAAM,KAAoB;GACjC,aAAa;GACb,IAAI,aAAa,IAAI,OAAO,SAAS,kBAAkB;IAAE;IAAI;GAAG,CAAC,GAAG,8BAA8B;GAElG,MAAM,aAA6C,YAAY;IAC7D,MAAM,SAAS,MAAM,IAAI,WAAW,IAAI,KAAK,QAAQ,cAAc,EAAE,MAAM,CAAC,EAAE,CAAC;IAC/E,IAAI,CAAC,OAAO,IAAI,MAAM,IAAI,MAAM,GAAG,OAAO,MAAM,KAAK,IAAI,OAAO,MAAM,SAAS;IAC/E,OAAO,OAAO;GAChB;GAEA,IAAI,MAAM,OAAO,+BACf,IAAI,MAAM,SACR;IACE,MAAM;IACN,IAAI;IACJ,OAAO;IACP,QAAQ;IACR,eAAe,EAAE,WAAW;GAC9B,GACA,WACF,CACF;EACF"}
package/lib/index.js CHANGED
@@ -1,12 +1,99 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import { credentialRef } from "@deepseek-ai/dsh-credentials";
2
+ import { credentialKey, credentialRef } from "@deepseek-ai/dsh-credentials";
3
3
  import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
4
4
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
5
+ //#region src/openai-codex.ts
6
+ /** OpenAI Codex OAuth grant parsing and refresh helpers. */
7
+ const OPENAI_CODEX_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
8
+ const OPENAI_CODEX_TOKEN_URL = "https://auth.openai.com/oauth/token";
9
+ const OPENAI_AUTH_CLAIM = "https://api.openai.com/auth";
10
+ function isObject(value) {
11
+ return value !== null && typeof value === "object" && !Array.isArray(value);
12
+ }
13
+ /** Decode the ChatGPT account id embedded in a Codex OAuth access token. */
14
+ function openAiCodexAccountId(accessToken) {
15
+ try {
16
+ const parts = accessToken.split(".");
17
+ if (parts.length !== 3 || parts[1] === "") return null;
18
+ const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
19
+ if (!isObject(payload)) return null;
20
+ const auth = payload[OPENAI_AUTH_CLAIM];
21
+ if (!isObject(auth)) return null;
22
+ const accountId = auth.chatgpt_account_id;
23
+ return typeof accountId === "string" && accountId !== "" ? accountId : null;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ /** Validate the opaque grant payload written by dsh-llm-pi-ai. */
29
+ function parseOpenAiCodexGrant(payload) {
30
+ if (!isObject(payload) || payload.type !== "oauth") return null;
31
+ if (typeof payload.access !== "string" || payload.access === "") return null;
32
+ if (typeof payload.refresh !== "string" || payload.refresh === "") return null;
33
+ if (typeof payload.expires !== "number" || !Number.isFinite(payload.expires)) return null;
34
+ const accountId = (typeof payload.accountId === "string" && payload.accountId !== "" ? payload.accountId : null) ?? openAiCodexAccountId(payload.access);
35
+ return accountId === null ? null : {
36
+ type: "oauth",
37
+ access: payload.access,
38
+ refresh: payload.refresh,
39
+ expires: payload.expires,
40
+ accountId
41
+ };
42
+ }
43
+ /** Refresh slightly early so the token cannot expire during the quota request. */
44
+ function openAiCodexGrantNeedsRefresh(grant, now = Date.now()) {
45
+ return grant.expires <= now + 3e4;
46
+ }
47
+ /** Refresh a Codex OAuth grant using the same public client identity as pi-ai. */
48
+ async function refreshOpenAiCodexGrant(refreshToken, signal) {
49
+ let response;
50
+ try {
51
+ response = await fetch(OPENAI_CODEX_TOKEN_URL, {
52
+ method: "POST",
53
+ headers: { "content-type": "application/x-www-form-urlencoded" },
54
+ body: new URLSearchParams({
55
+ grant_type: "refresh_token",
56
+ refresh_token: refreshToken,
57
+ client_id: OPENAI_CODEX_CLIENT_ID
58
+ }),
59
+ signal
60
+ });
61
+ } catch (error) {
62
+ if (signal.aborted) throw error;
63
+ throw new Error(`OpenAI Codex OAuth refresh failed: ${error instanceof Error ? error.message : String(error)}`);
64
+ }
65
+ let body;
66
+ try {
67
+ body = await response.json();
68
+ } catch {
69
+ body = void 0;
70
+ }
71
+ if (!response.ok) {
72
+ const record = isObject(body) ? body : void 0;
73
+ const detail = record?.error_description ?? record?.error;
74
+ throw new Error(`OpenAI Codex OAuth refresh failed (HTTP ${response.status})${typeof detail === "string" ? `: ${detail}` : ""}`);
75
+ }
76
+ if (!isObject(body)) throw new Error("OpenAI Codex OAuth refresh returned an invalid response");
77
+ const access = body.access_token;
78
+ const refresh = body.refresh_token;
79
+ const expiresIn = body.expires_in;
80
+ if (typeof access !== "string" || access === "" || typeof refresh !== "string" || refresh === "" || typeof expiresIn !== "number" || !Number.isFinite(expiresIn)) throw new Error("OpenAI Codex OAuth refresh response is missing token fields");
81
+ const accountId = openAiCodexAccountId(access);
82
+ if (accountId === null) throw new Error("OpenAI Codex OAuth access token has no ChatGPT account id");
83
+ return {
84
+ type: "oauth",
85
+ access,
86
+ refresh,
87
+ expires: Date.now() + expiresIn * 1e3,
88
+ accountId
89
+ };
90
+ }
91
+ //#endregion
5
92
  //#region src/index.ts
6
93
  const name = "provider-usage";
7
94
  /** Own package version, baked in by tsdown `define` at build time (the
8
95
  fallback only fires if the sources are compiled some other way). */
9
- const version = "0.3.6";
96
+ const version = "0.3.8";
10
97
  /** Settings namespace this plugin owns (registered through `ctx.settings`). */
11
98
  const NS = "provider-usage";
12
99
  const PROVIDER_KINDS = [
@@ -271,6 +358,16 @@ function usageRow(raw, fallbackLabel) {
271
358
  resetAt
272
359
  };
273
360
  }
361
+ /**
362
+ * Display priority for quota windows: the rolling 5h window comes first,
363
+ * then the weekly window, then provider-ordered extras (stable sort).
364
+ * Every adapter with a 5h/weekly pair (Kimi, Codex, MiniMax, z.ai,
365
+ * OpenCode) renders in this order.
366
+ */
367
+ function orderUsageRows(rows) {
368
+ const priority = (label) => label === "5h limit" ? 0 : label === "weekly" ? 1 : 2;
369
+ return rows.sort((a, b) => priority(a.label) - priority(b.label));
370
+ }
274
371
  /** Parse the Kimi Code `/v1/usages` payload (both observed shapes). */
275
372
  function parseKimiUsages(payload) {
276
373
  const rows = [];
@@ -284,7 +381,7 @@ function parseKimiUsages(payload) {
284
381
  label: "weekly"
285
382
  } : row);
286
383
  }
287
- return rows;
384
+ return orderUsageRows(rows);
288
385
  }
289
386
  const usage = usageRow(payload?.usage, "weekly");
290
387
  if (usage) rows.push({
@@ -308,7 +405,7 @@ function parseKimiUsages(payload) {
308
405
  const row = usageRow(detail, fallback);
309
406
  if (row) rows.push(row);
310
407
  }
311
- return rows;
408
+ return orderUsageRows(rows);
312
409
  }
313
410
  async function fetchJson(url, apiKey, signal, extraHeaders = {}) {
314
411
  const hasAuth = Object.keys(extraHeaders).some((h) => {
@@ -344,7 +441,7 @@ function okView(base) {
344
441
  message: null
345
442
  };
346
443
  }
347
- function failView(id, displayName, kind, status, message) {
444
+ function failView(id, displayName, kind, status, message, active) {
348
445
  return {
349
446
  id,
350
447
  displayName,
@@ -352,7 +449,8 @@ function failView(id, displayName, kind, status, message) {
352
449
  status,
353
450
  message,
354
451
  balances: null,
355
- usages: null
452
+ usages: null,
453
+ active
356
454
  };
357
455
  }
358
456
  /** Strip trailing slashes from a route baseURL. */
@@ -482,8 +580,29 @@ const ADAPTERS = {
482
580
  "openai-codex": {
483
581
  view: "usage",
484
582
  match: /chatgpt\.com\/backend-api/i,
485
- async fetch(baseURL, apiKey, signal) {
486
- const body = await fetchJson(`${root(baseURL)}/wham/usage`, apiKey, signal);
583
+ headers: {
584
+ originator: "pi",
585
+ "user-agent": `dsh-provider-usage/${version}`
586
+ },
587
+ async fetch(baseURL, apiKey, signal, refresh) {
588
+ const headers = (token) => {
589
+ const accountId = openAiCodexAccountId(token);
590
+ if (accountId === null) throw new Error("OpenAI Codex OAuth token has no ChatGPT account id");
591
+ return {
592
+ "chatgpt-account-id": accountId,
593
+ ...this.headers
594
+ };
595
+ };
596
+ const attempt = async (token) => {
597
+ return await fetchJson(`${root(baseURL)}/wham/usage`, token, signal, headers(token));
598
+ };
599
+ let body;
600
+ try {
601
+ body = await attempt(apiKey);
602
+ } catch (error) {
603
+ if (!((error instanceof Error ? Number(/^HTTP (\d+)/.exec(error.message)?.[1] ?? NaN) : NaN) === 401 && refresh !== void 0)) throw error;
604
+ body = await attempt(await refresh(signal));
605
+ }
487
606
  const rows = [];
488
607
  const windowRow = (label, win) => {
489
608
  if (win === null || typeof win !== "object") return;
@@ -502,18 +621,35 @@ const ADAPTERS = {
502
621
  windowRow("weekly", body?.rate_limit?.secondary_window);
503
622
  const extra = body?.additional_rate_limits;
504
623
  if (Array.isArray(extra)) for (const item of extra) windowRow(String(item?.limit_name ?? item?.metered_feature ?? "limit"), item?.rate_limit?.primary_window);
505
- const credits = toNumber(body?.credits?.balance);
506
- if (credits !== null) rows.push({
507
- label: "credits",
508
- used: null,
509
- limit: null,
510
- remaining: credits,
511
- percent: null,
512
- resetAt: null
513
- });
624
+ const credits = body?.credits;
625
+ if (credits !== null && typeof credits === "object") {
626
+ const balance = toNumber(credits.balance);
627
+ if (balance !== null) rows.push({
628
+ label: "credits",
629
+ used: null,
630
+ limit: null,
631
+ remaining: balance,
632
+ percent: null,
633
+ resetAt: null
634
+ });
635
+ }
636
+ const spend = body?.spend_control?.individual_limit;
637
+ if (spend !== null && typeof spend === "object") {
638
+ const spendLimit = toNumber(spend.limit);
639
+ const spendUsed = toNumber(spend.used);
640
+ const spendRemaining = toNumber(spend.remaining);
641
+ if (spendLimit !== null || spendUsed !== null || spendRemaining !== null) rows.push({
642
+ label: "spend control",
643
+ used: spendUsed,
644
+ limit: spendLimit,
645
+ remaining: spendRemaining,
646
+ percent: spendLimit !== null && spendLimit > 0 && spendUsed !== null ? spendUsed / spendLimit * 100 : null,
647
+ resetAt: toResetAt(spend.reset_at)
648
+ });
649
+ }
514
650
  return {
515
651
  balances: null,
516
- usages: rows
652
+ usages: orderUsageRows(rows)
517
653
  };
518
654
  }
519
655
  },
@@ -610,7 +746,7 @@ const ADAPTERS = {
610
746
  }
611
747
  return {
612
748
  balances: null,
613
- usages: rows
749
+ usages: orderUsageRows(rows)
614
750
  };
615
751
  }
616
752
  },
@@ -641,7 +777,7 @@ const ADAPTERS = {
641
777
  });
642
778
  return {
643
779
  balances: null,
644
- usages: rows
780
+ usages: orderUsageRows(rows)
645
781
  };
646
782
  }
647
783
  },
@@ -672,7 +808,7 @@ const ADAPTERS = {
672
808
  }
673
809
  return {
674
810
  balances: null,
675
- usages: rows
811
+ usages: orderUsageRows(rows)
676
812
  };
677
813
  }
678
814
  },
@@ -726,7 +862,8 @@ var UsageService = class extends TypertRemoteService {
726
862
  */
727
863
  async list(signal) {
728
864
  const specs = await this.collectSpecs();
729
- const providers = await Promise.all(specs.map((spec) => this.fetchProvider(spec, signal)));
865
+ const activeProviderId = this.currentProviderId();
866
+ const providers = await Promise.all(specs.map((spec) => this.fetchProvider(spec, activeProviderId, signal)));
730
867
  return {
731
868
  fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
732
869
  refreshSeconds: this.options().refreshSeconds,
@@ -734,6 +871,17 @@ var UsageService = class extends TypertRemoteService {
734
871
  providers
735
872
  };
736
873
  }
874
+ /**
875
+ * The provider route in use: the harness's current default model selection,
876
+ * kept in sync whenever the composer's model seat selects a provider/model
877
+ * (`session/selectModel` → `agent-default-model` settings). Null when the
878
+ * service is not mounted or no selection is stored yet — the client then
879
+ * falls back to judging every provider.
880
+ */
881
+ currentProviderId() {
882
+ const provider = this.ctx.get("agentDefaultModel")?.currentSelection?.().provider;
883
+ return typeof provider === "string" && provider !== "" ? provider : null;
884
+ }
737
885
  /** Enumerate provider routes (registry + settings), then apply manual overrides. */
738
886
  async collectSpecs() {
739
887
  const options = this.options();
@@ -782,12 +930,12 @@ var UsageService = class extends TypertRemoteService {
782
930
  }
783
931
  if (baseURL !== void 0) {
784
932
  const kind = kindOfBaseURL(baseURL);
785
- if (kind !== null && apiKeyEnv !== void 0) return {
933
+ if (kind !== null && (apiKeyEnv !== void 0 || kind === "openai-codex")) return {
786
934
  id: route.id,
787
935
  displayName: route.name || known?.displayName || route.id,
788
936
  kind,
789
937
  baseURL,
790
- apiKeyEnv
938
+ ...apiKeyEnv === void 0 ? {} : { apiKeyEnv }
791
939
  };
792
940
  }
793
941
  return {
@@ -805,21 +953,54 @@ var UsageService = class extends TypertRemoteService {
805
953
  const ambient = launchEnvironmentOf(this.ctx).get(ref);
806
954
  if (ambient !== void 0 && ambient.value.length > 0) return ambient.value;
807
955
  }
808
- async fetchProvider(spec, outerSignal) {
809
- if ("unsupported" in spec) return failView(spec.route.id, spec.route.name || spec.route.id, null, "unsupported", spec.route.id);
956
+ /** Resolve the Codex OAuth grant from the harness credential records,
957
+ refreshing it when close to expiry (and persisting the rotated grant). */
958
+ async resolveOpenAiCodexGrant(signal) {
959
+ const credentials = this.ctx.get("credentials");
960
+ if (credentials === void 0) return void 0;
961
+ const record = await credentials.readRecord(credentialKey("llm-pi-ai", "openai-codex"));
962
+ if (record === void 0) return void 0;
963
+ const current = record.kind === "grant" ? parseOpenAiCodexGrant(record.payload) : null;
964
+ if (current === null) return void 0;
965
+ if (!openAiCodexGrantNeedsRefresh(current)) return current;
966
+ const refreshed = await refreshOpenAiCodexGrant(current.refresh, signal);
967
+ await credentials.modifyRecord(credentialKey("llm-pi-ai", "openai-codex"), async (existing) => existing === void 0 ? existing : {
968
+ kind: "grant",
969
+ payload: refreshed
970
+ }).catch(() => {});
971
+ return refreshed;
972
+ }
973
+ async fetchProvider(spec, activeProviderId, outerSignal) {
974
+ if ("unsupported" in spec) return failView(spec.route.id, spec.route.name || spec.route.id, null, "unsupported", spec.route.id, spec.route.id === activeProviderId);
975
+ const active = spec.id === activeProviderId;
810
976
  const adapter = ADAPTERS[spec.kind];
811
977
  const base = {
812
978
  id: spec.id,
813
979
  displayName: spec.displayName,
814
980
  kind: adapter.view,
815
981
  balances: null,
816
- usages: null
982
+ usages: null,
983
+ active
817
984
  };
818
- const apiKey = await this.resolveApiKey(spec.apiKeyEnv);
819
- if (apiKey === void 0) return failView(spec.id, spec.displayName, adapter.view, "missing-credential", spec.apiKeyEnv);
820
- const signal = AbortSignal.any([AbortSignal.timeout(15e3), ...outerSignal ? [outerSignal] : []]);
985
+ const signal = AbortSignal.any([AbortSignal.timeout(2e4), ...outerSignal ? [outerSignal] : []]);
821
986
  try {
822
- const payload = await adapter.fetch(spec.baseURL, apiKey, signal);
987
+ let apiKey;
988
+ let refresh;
989
+ if (spec.kind === "openai-codex" && spec.apiKeyEnv === void 0) {
990
+ refresh = async (refreshSignal) => {
991
+ const grant = await this.resolveOpenAiCodexGrant(refreshSignal);
992
+ if (grant === void 0) throw new Error("OpenAI Codex OAuth authorization missing");
993
+ return grant.access;
994
+ };
995
+ const grant = await this.resolveOpenAiCodexGrant(signal);
996
+ if (grant === void 0) return failView(spec.id, spec.displayName, adapter.view, "missing-authorization", "llm-pi-ai/openai-codex", active);
997
+ apiKey = grant.access;
998
+ } else {
999
+ const resolved = await this.resolveApiKey(spec.apiKeyEnv);
1000
+ if (resolved === void 0) return failView(spec.id, spec.displayName, adapter.view, "missing-credential", spec.apiKeyEnv, active);
1001
+ apiKey = resolved;
1002
+ }
1003
+ const payload = await adapter.fetch(spec.baseURL, apiKey, signal, refresh);
823
1004
  return okView({
824
1005
  ...base,
825
1006
  ...payload
@@ -827,7 +1008,7 @@ var UsageService = class extends TypertRemoteService {
827
1008
  } catch (error) {
828
1009
  if (outerSignal?.aborted) throw error;
829
1010
  const message = error instanceof Error ? error.message : String(error);
830
- return failView(spec.id, spec.displayName, adapter.view, "error", message);
1011
+ return failView(spec.id, spec.displayName, adapter.view, "error", message, active);
831
1012
  }
832
1013
  }
833
1014
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-provider-usage",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
4
4
  "description": "DeepSeek Harness plugin: live account balance/usage panel for every configured LLM provider (draggable floating-ball widget, configurable refresh interval)",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",