dsh-usage-state 0.4.2 → 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 +25 -3
- package/README.md +25 -3
- package/lib/client.js +93 -17
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -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
|
|
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
|
-

|
|
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
|
|
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.
|
|
@@ -124,6 +124,28 @@ npm run build # tsdown → lib/ (host index.js + typert.js, browser clien
|
|
|
124
124
|
npm run watch # rebuilds client.js only; client changes hot-reload, no page refresh
|
|
125
125
|
```
|
|
126
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
|
+
|
|
127
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.
|
|
128
150
|
|
|
129
151
|
## Docs
|
package/README.md
CHANGED
|
@@ -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
|
-

|
|
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)已用百分比 · 重置倒计时 ·
|
|
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
|
|
|
@@ -148,6 +148,28 @@ npm run build # tsdown → lib/(宿主 index.js + typert.js,浏览器
|
|
|
148
148
|
npm run watch # 只重建 client.js;客户端会被 HMR 热替换,无需刷新页面
|
|
149
149
|
```
|
|
150
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
|
+
|
|
151
173
|
宿主机改动需要重启 DSH;客户端改动 `npm run watch` 即可。`lib/` 产物**必须提交进仓库**——`dsh plugin add github:...` 直接装仓库、没有构建步骤(`npm test` 里的构建守卫会检查信封、require 白名单与 `exports` 指向)。
|
|
152
174
|
|
|
153
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.
|
|
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.
|
|
1243
|
+
...segment.progress === void 0 ? {} : { progress: segment.progress },
|
|
1249
1244
|
...tooltipOf([resetHint, context]) === void 0 ? {} : { tooltip: tooltipOf([resetHint, context]) }
|
|
1250
1245
|
});
|
|
1251
1246
|
break;
|
|
@@ -1283,8 +1278,21 @@ window.__ModuleLoader__.load({
|
|
|
1283
1278
|
alignItems: "center",
|
|
1284
1279
|
justifyContent: "center",
|
|
1285
1280
|
gap: "6px",
|
|
1286
|
-
|
|
1287
|
-
|
|
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)",
|
|
1295
|
+
lineHeight: "calc(20px + var(--dsh-content-font-delta-secondary, 0px))",
|
|
1288
1296
|
fontVariantNumeric: "tabular-nums",
|
|
1289
1297
|
whiteSpace: "nowrap",
|
|
1290
1298
|
flexWrap: "wrap",
|
|
@@ -1296,6 +1304,65 @@ window.__ModuleLoader__.load({
|
|
|
1296
1304
|
};
|
|
1297
1305
|
const LABEL_STYLE = { color: "var(--dsw-alias-label-tertiary)" };
|
|
1298
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
|
+
}
|
|
1299
1366
|
function severityColor(severity) {
|
|
1300
1367
|
if (severity === "critical") return "var(--dsw-alias-state-error-primary)";
|
|
1301
1368
|
if (severity === "warn") return "var(--dsw-alias-state-warn-primary)";
|
|
@@ -1325,11 +1392,16 @@ window.__ModuleLoader__.load({
|
|
|
1325
1392
|
children: part.text
|
|
1326
1393
|
}, key);
|
|
1327
1394
|
case "window": return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1328
|
-
style: {
|
|
1395
|
+
style: {
|
|
1396
|
+
color: severityColor(part.severity),
|
|
1397
|
+
display: "inline-flex",
|
|
1398
|
+
alignItems: "center",
|
|
1399
|
+
gap: "4px"
|
|
1400
|
+
},
|
|
1329
1401
|
children: [
|
|
1402
|
+
part.progress === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProgressRing, { percent: part.progress }),
|
|
1330
1403
|
part.text,
|
|
1331
|
-
part.countdown === void 0 ? "" : ` (${part.countdown})
|
|
1332
|
-
part.bar === void 0 ? "" : ` ${part.bar}`
|
|
1404
|
+
part.countdown === void 0 ? "" : ` (${part.countdown})`
|
|
1333
1405
|
]
|
|
1334
1406
|
}, key);
|
|
1335
1407
|
case "state": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1402,11 +1474,15 @@ window.__ModuleLoader__.load({
|
|
|
1402
1474
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1403
1475
|
"data-usage-state": "dock",
|
|
1404
1476
|
style: DOCK_STYLE,
|
|
1405
|
-
children: fullParts.map((part, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
children: "
|
|
1409
|
-
|
|
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))
|
|
1410
1486
|
});
|
|
1411
1487
|
}
|
|
1412
1488
|
//#endregion
|