dsh-usage-state 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # dsh-usage-state
2
2
 
3
- See your **account balance** or **coding-plan quota** at a glance in [DSH (DeepSeek Harness)](https://github.com/deepseek-ai) — right under the composer stats row.
3
+ See your **account balance** or **coding-plan quota** at a glance in [DSH (DeepSeek Harness)](https://github.com/deepseek-ai) — on its own line above the composer input, as wide as the input card.
4
4
 
5
5
  > 中文说明见 [README.md](README.md)。
6
6
 
7
7
  ```
8
- below the composer stats: z.ai / GLM · 5h 12% (4h0m) ▓▓▓░░░░░ · 7d 59% (3d17h) ▓▓▓▓▓░░░
8
+ its own line above the composer input: z.ai / GLM · 5h 12% (4h0m) ▓▓▓░░░░░ · 7d 59% (3d17h) ▓▓▓▓▓░░░
9
9
  DeepSeek · ¥58.13
10
10
  hover any segment: Source DeepSeek · Mode API balance · Granted 0 · Topped up 58.13
11
11
  ```
@@ -14,7 +14,7 @@ hover any segment: Source DeepSeek · Mode API balance · Granted 0 ·
14
14
 
15
15
  - **Works with zero configuration**: the plugin figures out which data source and mode a provider needs, and reuses the API key DSH already has.
16
16
  - **Configured per provider, not per model** — readings are account-level, so each provider gets one setting: `Auto` / `API` / `Coding Plan` / `Hidden`. The model list is informational.
17
- - **One line, always visible**: directly below the composer's stats row (the native stats row and its popover are left untouched, not replaced) — no hover, no click.
17
+ - **One line, always visible**: its own line above the composer input (DSH 0.2 mounts it at `conversation.input.dock`, width-matched to the input card; the native stats row and context meter are left untouched, not replaced) — no hover, no click.
18
18
  - **Never invents data**: a failed refresh keeps the last good value and marks it stale (`12m ago ⚠`); rejected keys, endpoint errors and network problems each get a readable reason.
19
19
  - **Hover details**: source and mode, the window's absolute reset time, the granted/topped-up split of a balance, the failure reason with the provider's own message.
20
20
  - **Bilingual** (zh / en), following the DSH locale setting.
@@ -55,7 +55,7 @@ Nothing showing up after installing? See the troubleshooting table at the end of
55
55
 
56
56
  1. Open **Settings → Usage state**: one row per provider configured in DSH.
57
57
  2. Leave it on **Auto** (it detects the data source and its primary mode), or pick `API` / `Coding Plan` / `Hidden`; use ↑↓ to reorder.
58
- 3. The reading appears directly below the composer's stats row.
58
+ 3. The reading appears on its own line above the composer input, as wide as the input card.
59
59
 
60
60
  If a source needs an endpoint or a key (a self-hosted Sub2API, or a provider without a credential yet), expand that row's **Advanced** block to override the source, set the endpoint, name the credential, or paste a key (written to the DSH credential store).
61
61
 
@@ -63,9 +63,9 @@ A very long provider name never pushes the controls around: the card header stay
63
63
 
64
64
  ## Screenshots
65
65
 
66
- The reading directly below the composer's stats row (shown here with OpenCode Zen Go's three windows; hover any segment for source, mode and the absolute reset time):
66
+ The reading on its own line above the composer (shown here with OpenCode Zen Go's three windows; the row under the card is DSH's own statistics, untouched by this plugin — hover any segment for source, mode and the absolute reset time):
67
67
 
68
- ![The reading below the composer](https://raw.githubusercontent.com/takboo/dsh-usage-state/main/assets/screenshots/status-line.webp)
68
+ ![The reading on its own line above the composer](https://raw.githubusercontent.com/takboo/dsh-usage-state/main/assets/screenshots/status-line.webp)
69
69
 
70
70
  Settings: one row per provider; **Auto** detects the data source and its primary mode:
71
71
 
@@ -92,7 +92,7 @@ Expanding **Advanced** lets you override the source and endpoint, name a credent
92
92
  ## Display, refresh, credentials
93
93
 
94
94
  - **Placement**: directly below the composer's stats row, aligned with the native row's geometry. The line is always visible — it never depends on hover or a click.
95
- - **Elements**: provider label · balance + currency · each window (5h / 7d / 30d) used % · reset countdown · mini progress bar · threshold colours (defaults: amber ≥80%, red ≥95%).
95
+ - **Elements**: provider label · balance + currency · each window (5h / 7d / 30d) used % · reset countdown · mini progress ring · threshold colours (defaults: amber ≥80%, red ≥95%).
96
96
  - **Semantics**: percentages are always *used*; balances only appear in API mode, and coding-plan mode shows the windows the source actually has (5h / 7d for z.ai and Sub2API, plus 30d for OpenCode Zen Go); a stale reading shows its age instead of hiding.
97
97
  - **Refresh**: 2s after a turn ends, plus a 5-minute idle fallback; at most one real request per source per 60s, in-flight calls are shared, failures are not throttled.
98
98
  - **Credentials**: override → the provider's declared `apiKeyEnv` → the source's built-in ref → DSH credential store. Keys are written to `~/.dsh/.credentials.yaml`; **this plugin never stores a plaintext key** and the browser never receives a key value.
@@ -101,6 +101,7 @@ Expanding **Advanced** lets you override the source and endpoint, name a credent
101
101
 
102
102
  - **0.2 line only**: `engines.dsh` = `>=0.2.0-rc.2 <0.3.0-0` (this is what dsh-market's badge and install gate read). On an earlier host, install `0.3.2`.
103
103
  - `peerDependencies` carry `@deepseek-ai/dsh-settings` (`^0.2.0-rc.2`) and `@deepseek-ai/schemastery` (`^3.18.2`). The first one is deliberate: the runtime install gate only reads `@deepseek-ai/dsh*` peers, so declaring it makes a 0.1.x host **refuse the install** instead of accepting it and then failing to boot. Both are platform-provided; pnpm may print a `missing peer` warning for them, which is expected.
104
+ - Version `0.4.2` moves the status line to `conversation.input.dock`, giving it **its own line above the composer input** — DSH 0.2 turned the old below-the-input position into a shared row of pills next to the platform's stats and context meter (revisions 19/20).
104
105
  - Version `0.4.1` fixes `0.4.0`'s missing readings: the browser half's RPC contribution lacked the `create()` factory 0.2 requires, so `remote.usageState` never mounted — the UI showed **no readings at all** and mislabelled the cause as `Mode not supported`. The same release stops swallowing a rejected mount, says "reading" while the catalog is absent, and adds **real 0.2 registry contract tests** (revision 18). **Use `0.4.1`, not `0.4.0`.**
105
106
  - Version `0.4.0` migrates to the 0.2 settings model (`Config` + `configForms`, with cross-entry config read through `configEditor`). Verification and results: [`docs/implementation.md`](docs/implementation.md) §10; rationale: [`docs/design-consensus.md`](docs/design-consensus.md) revision 17.
106
107
  - Version `0.3.2` (last of the 0.1 line) only widened `engines.dsh`; **its claimed 0.2 compatibility was wrong** — on `0.2.0-rc.2` it fails the entire web boot because `settingsScope` does not exist there.
@@ -123,6 +124,28 @@ npm run build # tsdown → lib/ (host index.js + typert.js, browser clien
123
124
  npm run watch # rebuilds client.js only; client changes hot-reload, no page refresh
124
125
  ```
125
126
 
127
+ **Local loop (no release needed)**: `scripts/dev-local.sh` creates a throwaway profile under `/tmp/dsh-dev` (override with `DSH_HOME`), installs this repository into it as a `link:` and boots a host on its own port with the Desktop app's bundled CLI. Your real profiles are never touched.
128
+
129
+ ```bash
130
+ npm run build # lib/ must exist
131
+ scripts/dev-local.sh # creates/reuses the dev profile, prints the token URL
132
+ # second terminal:
133
+ npm run watch # rebuilds client.js on save
134
+ ```
135
+
136
+ Because the profile links this working tree, a rebuild is picked up by the running host: client changes are hot-swapped by `dsh-client-hmr` (the host stat-polls every bundle at 500ms and tells the browser to reload the module over `/plugins/events`) — no restart, no release. Host-side changes (`src/host/**`, `src/index.ts`, `cordis.patch.yml`) need this script restarted.
137
+
138
+ ⚠️ **This profile has no credentials**: `$DSH_HOME/.credentials.yaml` is home-level, and a fresh home starts without keys and without your other plugins (a font plugin changes the very glyph metrics a layout depends on). It is for **structural / host-side** checks only. To judge how something *looks*, install the same build into a profile that already has your keys and plugins — still without publishing:
139
+
140
+ ```bash
141
+ npm pack --pack-destination /tmp --cache /tmp/npm-cache
142
+ dsh plugin --profile web add /tmp/dsh-usage-state-<version>.tgz # file: install
143
+ dsh plugin --profile web add "$PWD" # link: install, for hot reload
144
+ dsh plugin --profile web add dsh-usage-state@<published> # back to the published build
145
+ ```
146
+
147
+ Bump the version and publish only after you have **seen** the change work.
148
+
126
149
  Host-side changes need a DSH restart; client-side changes do not. The `lib/` output is **committed on purpose**: `dsh plugin add github:...` installs straight from the repository with no build step, and `npm test` guards the bundle envelope, the require allow-list and the `exports` targets.
127
150
 
128
151
  ## Docs
package/README.md CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  **中文** | [English](README.en.md)
4
4
 
5
- 在 [DSH(DeepSeek Harness)](https://github.com/deepseek-ai) 里一眼看到你的**账户余额**或**套餐额度**——就在输入框统计行的正下方。
5
+ 在 [DSH(DeepSeek Harness)](https://github.com/deepseek-ai) 里一眼看到你的**账户余额**或**套餐额度**——就在输入框上方、与输入卡片同宽的一行(原生统计行与其弹窗原样保留、不做替换)。
6
6
 
7
7
  > A minimal DSH plugin that shows your account **balance** (API mode) or **coding-plan quota** (5h / 7d / 30d) for the model you are using, right under the composer.
8
8
 
9
9
  ```
10
- 输入框统计行下方: z.ai / GLM · 5h 12% (4h0m) ▓▓▓░░░░░ · 7d 59% (3d17h) ▓▓▓▓▓░░░
10
+ 输入框上方独立一行: z.ai / GLM · 5h 12% (4h0m) ▓▓▓░░░░░ · 7d 59% (3d17h) ▓▓▓▓▓░░░
11
11
  DeepSeek · ¥58.13
12
12
  悬停任意一段: Source DeepSeek · Mode API balance · Granted 0 · Topped up 58.13
13
13
  ```
@@ -16,7 +16,7 @@
16
16
 
17
17
  - **零配置可用**:按 provider 自动识别该用哪个数据源与模式,DSH 里配过的密钥会被自动复用,不填任何东西就能看到读数。
18
18
  - **账户级配置**:余额与额度是账户级的,所以每个供应商只配一次(`自动` / `API` / `Coding Plan` / `隐藏`),模型清单只作展示。
19
- - **始终可见一行**:固定在输入框统计行的正下方(原生统计行与其弹窗原样保留、不做替换),不需要悬停或点击。
19
+ - **始终可见一行**:独占一行、位于输入框上方(DSH 0.2 把它挂在 `conversation.input.dock`,与输入卡片同宽居中;原生统计行与上下文计量器原样保留、不做替换),不需要悬停或点击。
20
20
  - **失败不撒谎**:请求失败时保留上次成功值并标明「多久之前 + ⚠」,**绝不显示 0 或伪造数据**;密钥无效、接口报错、网络不可达会分别给出可读原因。
21
21
  - **悬浮提示**:每段文字悬停显示一行放不下的信息——数据源与模式、窗口的绝对重置时刻、余额的赠送/充值构成、失败原因与原始消息。
22
22
  - **中英双语**:跟随 DSH 的语言设置(`locale.preference`),设置页与状态行都不含硬编码文案。
@@ -57,7 +57,7 @@ dsh plugin --profile web add /path/to/dsh-usage-state
57
57
 
58
58
  1. 打开 **设置 → 用量状态**:DSH 里配置的每个供应商一行。
59
59
  2. 保持默认的 **自动** 即可(它会识别数据源与主模式);需要时改成 `API` / `Coding Plan` / `隐藏`,或用 ↑↓ 调整顺序。
60
- 3. 该供应商的账户读数会出现在输入框统计行的正下方。
60
+ 3. 该供应商的账户读数会出现在输入框上方、与输入卡片同宽的一行。
61
61
 
62
62
  若某个数据源需要端点或密钥(例如自建的 Sub2API、或尚未配置的 z.ai),展开该行的 **高级**:可覆盖数据源、填接口地址、指定凭据名、粘贴密钥(写入 DSH 凭据库)。
63
63
 
@@ -65,9 +65,9 @@ dsh plugin --profile web add /path/to/dsh-usage-state
65
65
 
66
66
  ## 截图
67
67
 
68
- 输入框统计行正下方的账户读数(图示为 OpenCode Zen Go 的三个窗口,悬停任意一段可看数据源、模式与绝对重置时刻):
68
+ 输入框上方独占一行的账户读数(图示为 OpenCode Zen Go 的三个窗口;卡片下方那排是 DSH 原生统计,两者互不干扰。悬停任意一段可看数据源、模式与绝对重置时刻):
69
69
 
70
- ![输入框下方的账户读数](https://raw.githubusercontent.com/takboo/dsh-usage-state/main/assets/screenshots/status-line.webp)
70
+ ![输入框上方独占一行的账户读数](https://raw.githubusercontent.com/takboo/dsh-usage-state/main/assets/screenshots/status-line.webp)
71
71
 
72
72
  设置页:每个 provider 一行,默认「自动」会识别数据源与主模式:
73
73
 
@@ -95,7 +95,7 @@ dsh plugin --profile web add /path/to/dsh-usage-state
95
95
 
96
96
  **位置**:输入框统计行的正下方(与原生统计行的几何对齐,跟随 DSH 的会话内容宽度)。读数**始终可见**,不依赖悬停,也不需要点击。
97
97
 
98
- **元素**:供应商标签 · 余额金额 + 币种 · 各窗口(5h / 7d / 30d)已用百分比 · 重置倒计时 · 迷你进度条 · 阈值变色(默认 ≥80% 黄、≥95% 红,可在设置里改)。
98
+ **元素**:供应商标签 · 余额金额 + 币种 · 各窗口(5h / 7d / 30d)已用百分比 · 重置倒计时 · 迷你进度环 · 阈值变色(默认 ≥80% 黄、≥95% 红,可在设置里改)。
99
99
 
100
100
  **口径**:百分比一律是**已用**(与 z.ai / Claude 官方一致);API 模式显示余额,Coding Plan 模式显示该数据源实际提供的窗口(z.ai 与 Sub2API 是 5h / 7d,OpenCode Zen Go 多一个 30d)。
101
101
 
@@ -125,6 +125,7 @@ dsh plugin --profile web add /path/to/dsh-usage-state
125
125
 
126
126
  - **只支持 DSH 0.2 线**:`engines.dsh` = `>=0.2.0-rc.2 <0.3.0-0`(市场读它做徽标与"可安装"判定)。0.2 之前的宿主请装 `0.3.2`。
127
127
  - `peerDependencies` 里有 `@deepseek-ai/dsh-settings`(`^0.2.0-rc.2`)与 `@deepseek-ai/schemastery`(`^3.18.2`):前者是**故意的**——平台的运行时安装闸门只读 `@deepseek-ai/dsh*` 的 peer,声明它可以让 0.1.x 宿主在**安装时**就拒绝,而不是装上之后把启动搞崩;两者都由平台提供,pnpm 可能为此打一行 `missing peer` 警告,属预期。
128
+ - 版本 `0.4.2`:状态行迁到 `conversation.input.dock`,成为**输入框上方的独立一行**(0.1 时代它在输入框之下,而 0.2 把那个位置改成了与平台统计、上下文计量器共排的一排 pill——见修订 19/20)。
128
129
  - 版本 `0.4.1`:修 `0.4.0` 的读数缺失——客户端 RPC contribution 的参数 codec 少了 0.2 要求的 `create()`,`remote.usageState` 因此从未挂载,界面表现为**没有任何读数**并误报 `Mode not supported`。同一轮还让挂载失败不再被静默吞掉、catalog 未到时改说「读取中」,并把**真实 0.2 registry 契约测试**写进单测(修订 18)。**请用 `0.4.1`,不要用 `0.4.0`。**
129
130
  - 版本 `0.4.0`:迁到 0.2 原生设置模型(`Config` + `configForms`,宿主侧 `configEditor` 读跨条目配置)。验证方式与结论见 [`docs/implementation.md`](docs/implementation.md) §10、决策见 [`docs/design-consensus.md`](docs/design-consensus.md) 修订 17。
130
131
  - 版本 `0.3.2`(0.1 线末版):仅放宽 `engines.dsh`;**它声称的 0.2 兼容是错的**(0.3.2 在 0.2.0-rc.2 上会因 `settingsScope` 不存在而让整个 web 启动失败)。
@@ -147,6 +148,28 @@ npm run build # tsdown → lib/(宿主 index.js + typert.js,浏览器
147
148
  npm run watch # 只重建 client.js;客户端会被 HMR 热替换,无需刷新页面
148
149
  ```
149
150
 
151
+ **本地回路(不需要发版)**:`scripts/dev-local.sh` 会在 `/tmp/dsh-dev` 建一个**一次性 profile**,把本仓库以 `link:` 装进去并起一个独立端口的宿主;你的真实 profile 完全不受影响。
152
+
153
+ ```bash
154
+ npm run build # 先有 lib/
155
+ scripts/dev-local.sh # 建/复用 dev profile + 起宿主,打印带 token 的 URL
156
+ # 另一个终端:
157
+ npm run watch # 保存即重建 client.js
158
+ ```
159
+
160
+ 客户端改动由 `dsh-client-hmr` 热替换(宿主每 500ms 轮询 bundle,经 `/plugins/events` 通知浏览器重载模块)——**不用重启、不用发版**;宿主改动(`src/host/**`、`src/index.ts`、`cordis.patch.yml`)需要重启该脚本。
161
+
162
+ ⚠️ **这个 profile 没有凭据**:`$DSH_HOME/.credentials.yaml` 是 home 级的,全新 home 里没有 key,也没有你其它插件(**字体插件会改变排版所依赖的字形度量**)。所以它只适合**结构/宿主侧**核对。要看**实际观感**,把同一份构建装进已有 key 与插件的 profile,同样不用发版:
163
+
164
+ ```bash
165
+ npm pack --pack-destination /tmp --cache /tmp/npm-cache
166
+ dsh plugin --profile web add /tmp/dsh-usage-state-<version>.tgz # file: 安装
167
+ dsh plugin --profile web add "$PWD" # 想热迭代就用 link:
168
+ dsh plugin --profile web add dsh-usage-state@<已发布版本> # 回到线上版本
169
+ ```
170
+
171
+ 只有当你**亲眼确认**改动可用之后,才值得动版本号与 npm 发布。
172
+
150
173
  宿主机改动需要重启 DSH;客户端改动 `npm run watch` 即可。`lib/` 产物**必须提交进仓库**——`dsh plugin add github:...` 直接装仓库、没有构建步骤(`npm test` 里的构建守卫会检查信封、require 白名单与 `exports` 指向)。
151
174
 
152
175
  ```
package/lib/client.js CHANGED
@@ -1056,7 +1056,6 @@ window.__ModuleLoader__.load({
1056
1056
  CNY: "¥",
1057
1057
  USD: "$"
1058
1058
  };
1059
- const PROGRESS_WIDTH = 8;
1060
1059
  /** `¥66.28`, `$6.80`, `EUR 1.50`; an unknown code keeps its numeric form. */
1061
1060
  function formatBalance(balance) {
1062
1061
  const amount = balance.amount.toFixed(2);
@@ -1088,10 +1087,6 @@ window.__ModuleLoader__.load({
1088
1087
  const minutes = Math.floor(seconds / 60);
1089
1088
  return minutes >= 1 ? `${minutes}m` : `${seconds}s`;
1090
1089
  }
1091
- function progressBar(percent, width = PROGRESS_WIDTH) {
1092
- const filled = Math.min(width, Math.max(0, Math.round(percent / 100 * width)));
1093
- return "█".repeat(filled) + "░".repeat(width - filled);
1094
- }
1095
1090
  function severityOf(usedPercent, display) {
1096
1091
  if (usedPercent >= display.thresholdCriticalPercent) return "critical";
1097
1092
  if (usedPercent >= display.thresholdWarnPercent) return "warn";
@@ -1162,7 +1157,7 @@ window.__ModuleLoader__.load({
1162
1157
  severity: severityOf(window.usedPercent, display)
1163
1158
  };
1164
1159
  if (window.resetsAt !== void 0 && window.resetsAt > now) segment.resetsAt = window.resetsAt;
1165
- if (display.progressBar) segment.bar = progressBar(window.usedPercent);
1160
+ if (display.progressBar) segment.progress = Math.min(100, Math.max(0, window.usedPercent));
1166
1161
  segments.push(segment);
1167
1162
  }
1168
1163
  return segments;
@@ -1245,7 +1240,7 @@ window.__ModuleLoader__.load({
1245
1240
  percent: segment.percent,
1246
1241
  severity: segment.severity,
1247
1242
  ...countdown === void 0 ? {} : { countdown },
1248
- ...segment.bar === void 0 ? {} : { bar: segment.bar },
1243
+ ...segment.progress === void 0 ? {} : { progress: segment.progress },
1249
1244
  ...tooltipOf([resetHint, context]) === void 0 ? {} : { tooltip: tooltipOf([resetHint, context]) }
1250
1245
  });
1251
1246
  break;
@@ -1268,27 +1263,106 @@ window.__ModuleLoader__.load({
1268
1263
  //#endregion
1269
1264
  //#region src/client/StatusLine.tsx
1270
1265
  /**
1271
- * The composer mount point: one line directly under the platform's stats row, so
1272
- * it reads as that row's second line rather than a stray block.
1266
+ * The composer mount point: one full-width line in the composer stack, directly
1267
+ * above the input card.
1268
+ *
1269
+ * `conversation.input.dock` is a stretched column child (`.composerStack` is a column
1270
+ * flex with no `align-items`), so the width comes from the card's own custom
1271
+ * property, exactly like the platform's queued-message dock in that slot. It is
1272
+ * **not** the row below the card: `conversation.composer.dock` there is one centered
1273
+ * flex row shared with the platform's stats pill and the context meter, where a
1274
+ * contribution can only ever be a pill beside them (revisions 19 and 20).
1273
1275
  */
1274
1276
  const DOCK_STYLE = {
1275
1277
  display: "flex",
1276
1278
  alignItems: "center",
1277
1279
  justifyContent: "center",
1278
- gap: "8px",
1279
- fontSize: "var(--dsh-content-font-size-secondary, 13px)",
1280
+ gap: "6px",
1281
+ /**
1282
+ * Exactly the typography the platform's own stats row uses under the card
1283
+ * (`StatsPills.module.css`, `.root`), copied expression for expression:
1284
+ *
1285
+ * font-size: calc(var(--dsh-content-font-size-secondary, 13px) - 1px)
1286
+ * line-height: calc(20px + var(--dsh-content-font-delta-secondary, 0px))
1287
+ *
1288
+ * `font: inherit` looked right in the dock row (the platform's pills inherit there)
1289
+ * but not above the card: this element is a child of `.composerStack`, so it
1290
+ * inherited the *input card's* `--dsh-content-font-size` (14px) and rendered a size
1291
+ * larger than the row below it. Font family is deliberately left alone, so a
1292
+ * UI-font plugin still applies.
1293
+ */
1294
+ fontSize: "calc(var(--dsh-content-font-size-secondary, 13px) - 1px)",
1280
1295
  lineHeight: "calc(20px + var(--dsh-content-font-delta-secondary, 0px))",
1281
1296
  fontVariantNumeric: "tabular-nums",
1282
1297
  whiteSpace: "nowrap",
1283
- overflow: "hidden",
1284
- textOverflow: "ellipsis",
1298
+ flexWrap: "wrap",
1299
+ rowGap: "2px",
1285
1300
  width: "100%",
1286
- maxWidth: "var(--dsh-chat-content-width)",
1301
+ maxWidth: "var(--dsh-composer-card-max-width)",
1287
1302
  margin: "0 auto",
1288
- padding: "4px calc(var(--dsh-composer-side-clearance) + 16px) 0"
1303
+ padding: "0 var(--dsh-composer-side-clearance)"
1289
1304
  };
1290
1305
  const LABEL_STYLE = { color: "var(--dsw-alias-label-tertiary)" };
1291
1306
  const SEPARATOR_STYLE = { color: "var(--dsw-alias-separator-primary, var(--dsw-alias-label-dimmed))" };
1307
+ /**
1308
+ * One segment and the separator that introduces it, boxed together.
1309
+ *
1310
+ * The line wraps between these boxes, never inside one: with the separator as a
1311
+ * sibling of the segments, a wrap ended a line on a dangling `·` while the segment it
1312
+ * introduces moved to the next line.
1313
+ */
1314
+ const PART_STYLE = {
1315
+ display: "inline-flex",
1316
+ alignItems: "center",
1317
+ gap: "6px"
1318
+ };
1319
+ /**
1320
+ * The platform's own ring geometry (`ContextMeter.module.css`): a 14x14 viewBox with
1321
+ * two circles of radius 5.5 and a 2px stroke, the filled one rotated so the arc
1322
+ * starts at twelve o'clock.
1323
+ */
1324
+ const RING_RADIUS = 5.5;
1325
+ const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS;
1326
+ /**
1327
+ * How full a window is, drawn rather than typed.
1328
+ *
1329
+ * This replaced an eight-cell `█`/`░` text bar. Those glyphs are not covered by the
1330
+ * shell's UI font, so a fallback served them at roughly twice the advance width and
1331
+ * three bars wrapped the line — and pinning them to a monospace stack only made the
1332
+ * width predictable, not small. An SVG ring has no glyph metrics at all, and it
1333
+ * matches the context meter sitting in the platform's stats row.
1334
+ *
1335
+ * The stroke uses `currentColor`, so the severity colour on the segment carries into
1336
+ * the ring; the track copies the platform's `--dsw-alias-border-l3`.
1337
+ */
1338
+ function ProgressRing({ percent }) {
1339
+ const filled = RING_CIRCUMFERENCE * Math.min(100, Math.max(0, percent)) / 100;
1340
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1341
+ viewBox: "0 0 14 14",
1342
+ width: "14",
1343
+ height: "14",
1344
+ "aria-hidden": "true",
1345
+ style: { flex: "none" },
1346
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1347
+ cx: "7",
1348
+ cy: "7",
1349
+ r: RING_RADIUS,
1350
+ fill: "none",
1351
+ stroke: "var(--dsw-alias-border-l3)",
1352
+ strokeWidth: "2"
1353
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1354
+ cx: "7",
1355
+ cy: "7",
1356
+ r: RING_RADIUS,
1357
+ fill: "none",
1358
+ stroke: "currentColor",
1359
+ strokeWidth: "2",
1360
+ strokeLinecap: "round",
1361
+ strokeDasharray: `${filled} ${RING_CIRCUMFERENCE}`,
1362
+ transform: "rotate(-90 7 7)"
1363
+ })]
1364
+ });
1365
+ }
1292
1366
  function severityColor(severity) {
1293
1367
  if (severity === "critical") return "var(--dsw-alias-state-error-primary)";
1294
1368
  if (severity === "warn") return "var(--dsw-alias-state-warn-primary)";
@@ -1318,11 +1392,16 @@ window.__ModuleLoader__.load({
1318
1392
  children: part.text
1319
1393
  }, key);
1320
1394
  case "window": return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1321
- style: { color: severityColor(part.severity) },
1395
+ style: {
1396
+ color: severityColor(part.severity),
1397
+ display: "inline-flex",
1398
+ alignItems: "center",
1399
+ gap: "4px"
1400
+ },
1322
1401
  children: [
1402
+ part.progress === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProgressRing, { percent: part.progress }),
1323
1403
  part.text,
1324
- part.countdown === void 0 ? "" : ` (${part.countdown})`,
1325
- part.bar === void 0 ? "" : ` ${part.bar}`
1404
+ part.countdown === void 0 ? "" : ` (${part.countdown})`
1326
1405
  ]
1327
1406
  }, key);
1328
1407
  case "state": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1395,11 +1474,15 @@ window.__ModuleLoader__.load({
1395
1474
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1396
1475
  "data-usage-state": "dock",
1397
1476
  style: DOCK_STYLE,
1398
- children: fullParts.map((part, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [index > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1399
- style: SEPARATOR_STYLE,
1400
- "aria-hidden": "true",
1401
- children: "·"
1402
- }) : null, withTooltip(renderPart(part, t, index), part.tooltip, index)] }, index))
1477
+ children: fullParts.map((part, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1478
+ "data-usage-part": index === 0 ? "first" : "following",
1479
+ style: PART_STYLE,
1480
+ children: [index > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1481
+ style: SEPARATOR_STYLE,
1482
+ "aria-hidden": "true",
1483
+ children: "·"
1484
+ }) : null, withTooltip(renderPart(part, t, index), part.tooltip, index)]
1485
+ }, index))
1403
1486
  });
1404
1487
  }
1405
1488
  //#endregion
@@ -1626,9 +1709,9 @@ window.__ModuleLoader__.load({
1626
1709
  //#endregion
1627
1710
  //#region src/client/slots.ts
1628
1711
  const STATUS_LINE_SLOTS = [{
1629
- name: "conversation.composer.dock",
1712
+ name: "conversation.input.dock",
1630
1713
  id: "usage-state",
1631
- order: 1,
1714
+ order: 200,
1632
1715
  locale: "usage-state"
1633
1716
  }];
1634
1717
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-usage-state",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
5
  "description": "DSH plugin: show account balance (API mode) or coding-plan quota (5h/7d/30d) for the model in use",
6
6
  "license": "MIT",