dsh-model-usage 0.8.3 → 0.8.5
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/CHANGELOG.md +10 -0
- package/README.md +20 -10
- package/docs/screenshots/u1.png +0 -0
- package/docs/screenshots/u2.png +0 -0
- package/docs/screenshots/u3.png +0 -0
- package/docs/screenshots/u4.png +0 -0
- package/docs/screenshots/u5.png +0 -0
- package/icon.svg +28 -6
- package/lib/client.js +359 -108
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +1 -1
- package/docs/screenshots/usage-panel.png +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented in this file. Versions follow Semantic Versioning.
|
|
4
4
|
|
|
5
|
+
## 0.8.5
|
|
6
|
+
|
|
7
|
+
- Balance spend trend: add a centered enlarged chart view, a points-only toggle, and clearer spacing between dense ten-minute samples.
|
|
8
|
+
|
|
9
|
+
## 0.8.4
|
|
10
|
+
|
|
11
|
+
- Command Code balance history: sample on ten-minute clock marks, retain the current and two
|
|
12
|
+
preceding calendar months, support custom date ranges and daily week/month aggregation, and add
|
|
13
|
+
month deletion and full reset controls.
|
|
14
|
+
|
|
5
15
|
## 0.8.3
|
|
6
16
|
|
|
7
17
|
- Command Code balance history: record account balance snapshots every 30 minutes, retain up to
|
package/README.md
CHANGED
|
@@ -8,13 +8,20 @@ using.
|
|
|
8
8
|
Queries run in the host process with the provider's own credential reference. The browser receives
|
|
9
9
|
normalized values and the credential reference name; credential values never leave the host.
|
|
10
10
|
|
|
11
|
-

|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+

|
|
12
20
|
|
|
13
21
|
*Usage panel for a Command Code subscription (Chinese UI): five-hour and weekly quota windows,
|
|
14
22
|
credit balance, spend, request count, and the model and credential reference used for the query.*
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
模型与凭据引用。*
|
|
24
|
+
*用量面板:兼容深浅色模式*
|
|
18
25
|
|
|
19
26
|
Published on npm as [`dsh-model-usage`](https://www.npmjs.com/package/dsh-model-usage).
|
|
20
27
|
|
|
@@ -26,10 +33,11 @@ Published on npm as [`dsh-model-usage`](https://www.npmjs.com/package/dsh-model-
|
|
|
26
33
|
model the conversation runs on.
|
|
27
34
|
- **Background refresh.** The mounted component refreshes on an interval, including while the panel
|
|
28
35
|
is closed.
|
|
29
|
-
- **Command Code balance history.**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
- **Command Code balance history.** The plugin queries on the system clock's ten-minute marks
|
|
37
|
+
(`:00`, `:10`, `:20`, …), independently of the display refresh setting. Samples are kept locally
|
|
38
|
+
per account for the current calendar month and the two preceding calendar months. The chart has
|
|
39
|
+
uniform time-axis ticks, actual sample positions for short ranges, daily totals for week/month
|
|
40
|
+
views, custom start/end dates, and month deletion/reset controls.
|
|
33
41
|
- **Provider directory.** The deployment's provider routes are listed with their usage adapter,
|
|
34
42
|
credential reference name and configuration state. Values are never read out.
|
|
35
43
|
|
|
@@ -254,9 +262,11 @@ OpenCode Zen(按量付费)没有用量接口(`/zen/v1/usage`、`/me`、`/c
|
|
|
254
262
|
5 分钟。策略保存在 `dsh-model-usage:refresh`。
|
|
255
263
|
- 数据仍在间隔内时打开面板不会发起请求;「刷新」按钮可强制查询。
|
|
256
264
|
- 查询失败时保留上次数据并追加 `上次刷新失败:…`;仅在从未成功时显示整屏错误与「重试」。
|
|
257
|
-
- Command Code
|
|
258
|
-
|
|
259
|
-
|
|
265
|
+
- Command Code 余额历史独立于面板刷新开关,在系统时间的每小时 `00 / 10 / 20 / 30 / 40 / 50`
|
|
266
|
+
分自动查询。数据按账户保存在浏览器本地,保留当前自然月及前两个月;例如十月会保留十月、
|
|
267
|
+
九月、八月,七月数据自动清除。用量卡片提供「今天 / 昨天 / 近一周 / 近一月 / 自定义」筛选,
|
|
268
|
+
周和月按每天合计,短时间段按真实查询时间定位折线点,并可清除指定月份或重置当前账户记录。
|
|
269
|
+
首次记录作为余额基线,后续每段消耗按前后余额差计算,余额增加时该段显示为零。
|
|
260
270
|
- host 侧单次用量请求超时 30 秒,浏览器侧另有 45 秒外层超时。
|
|
261
271
|
|
|
262
272
|
### 安全
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/icon.svg
CHANGED
|
@@ -1,7 +1,29 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Usage">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="usage-bg" x1="7" y1="4" x2="57" y2="60" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop offset="0" stop-color="#0A84FF"/>
|
|
5
|
+
<stop offset="0.55" stop-color="#147EFB"/>
|
|
6
|
+
<stop offset="1" stop-color="#30D158"/>
|
|
7
|
+
</linearGradient>
|
|
8
|
+
<linearGradient id="usage-glass" x1="13" y1="11" x2="51" y2="55" gradientUnits="userSpaceOnUse">
|
|
9
|
+
<stop stop-color="#FFFFFF" stop-opacity="0.24"/>
|
|
10
|
+
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0.08"/>
|
|
11
|
+
</linearGradient>
|
|
12
|
+
<filter id="usage-shadow" x="-20%" y="-20%" width="140%" height="140%">
|
|
13
|
+
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#003C7A" flood-opacity="0.22"/>
|
|
14
|
+
</filter>
|
|
15
|
+
</defs>
|
|
16
|
+
|
|
17
|
+
<rect x="2" y="2" width="60" height="60" rx="17" fill="url(#usage-bg)"/>
|
|
18
|
+
<rect x="8" y="8" width="48" height="48" rx="13" fill="url(#usage-glass)" stroke="#FFFFFF" stroke-opacity="0.22"/>
|
|
19
|
+
|
|
20
|
+
<g filter="url(#usage-shadow)" fill="none" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round">
|
|
21
|
+
<path d="M16 45.5h32" stroke-width="2" stroke-opacity="0.42"/>
|
|
22
|
+
<path d="M16 40.5 25 33l7 5 13-15" stroke-width="4"/>
|
|
23
|
+
<path d="m40.5 23 4.5 0 0 4.5" stroke-width="3"/>
|
|
24
|
+
</g>
|
|
25
|
+
|
|
26
|
+
<circle cx="16" cy="40.5" r="3.5" fill="#FFFFFF"/>
|
|
27
|
+
<circle cx="45" cy="23" r="3.5" fill="#30D158" stroke="#FFFFFF" stroke-width="2"/>
|
|
28
|
+
<circle cx="45" cy="23" r="1.2" fill="#FFFFFF"/>
|
|
7
29
|
</svg>
|
package/lib/client.js
CHANGED
|
@@ -122,12 +122,24 @@ window.__ModuleLoader__.load({
|
|
|
122
122
|
"card.hideRaw": "隐藏原始返回",
|
|
123
123
|
"card.unrecognized": "接口返回的字段还没被识别,下面是原始返回(可以发给我做校准)",
|
|
124
124
|
"history.title": "余额消耗趋势",
|
|
125
|
-
"history.
|
|
126
|
-
"history.
|
|
125
|
+
"history.expand": "全屏查看余额消耗趋势",
|
|
126
|
+
"history.pointsOnly": "仅显示消耗点",
|
|
127
127
|
"history.range.today": "今天",
|
|
128
128
|
"history.range.yesterday": "昨天",
|
|
129
|
+
"history.range.week": "近一周",
|
|
130
|
+
"history.range.month": "近一月",
|
|
131
|
+
"history.range.custom": "自定义",
|
|
132
|
+
"history.start": "开始日期",
|
|
133
|
+
"history.end": "结束日期",
|
|
134
|
+
"history.apply": "应用时间段",
|
|
129
135
|
"history.empty": "该时间段暂无足够的余额记录",
|
|
130
136
|
"history.amount": "消耗",
|
|
137
|
+
"history.clearMonth": "清除所选月份",
|
|
138
|
+
"history.confirmClearMonth": "确定清除 {month} 的余额记录吗?",
|
|
139
|
+
"history.reset": "重置当前记录",
|
|
140
|
+
"history.month": "数据月份",
|
|
141
|
+
"history.noMonths": "暂无可清理的月份",
|
|
142
|
+
"history.confirmReset": "确定清除当前账户的全部余额记录吗?",
|
|
131
143
|
"card.used": "已用",
|
|
132
144
|
"card.total": "总额",
|
|
133
145
|
"window.fiveHour": "5 小时窗口",
|
|
@@ -230,12 +242,24 @@ window.__ModuleLoader__.load({
|
|
|
230
242
|
"card.hideRaw": "Hide raw response",
|
|
231
243
|
"card.unrecognized": "These fields are not recognized yet — the raw response is below (send it over to calibrate)",
|
|
232
244
|
"history.title": "Balance spend trend",
|
|
233
|
-
"history.
|
|
234
|
-
"history.
|
|
245
|
+
"history.expand": "View balance spend trend full screen",
|
|
246
|
+
"history.pointsOnly": "Show points only",
|
|
235
247
|
"history.range.today": "Today",
|
|
236
248
|
"history.range.yesterday": "Yesterday",
|
|
249
|
+
"history.range.week": "Last 7 days",
|
|
250
|
+
"history.range.month": "Last 30 days",
|
|
251
|
+
"history.range.custom": "Custom",
|
|
252
|
+
"history.start": "Start date",
|
|
253
|
+
"history.end": "End date",
|
|
254
|
+
"history.apply": "Apply range",
|
|
237
255
|
"history.empty": "Not enough balance samples for this period",
|
|
238
256
|
"history.amount": "Spent",
|
|
257
|
+
"history.clearMonth": "Clear selected month",
|
|
258
|
+
"history.confirmClearMonth": "Clear balance records for {month}?",
|
|
259
|
+
"history.reset": "Reset current records",
|
|
260
|
+
"history.month": "Data month",
|
|
261
|
+
"history.noMonths": "No stored months to clear",
|
|
262
|
+
"history.confirmReset": "Clear all balance records for this account?",
|
|
239
263
|
"card.used": "Used",
|
|
240
264
|
"card.total": "Total",
|
|
241
265
|
"window.fiveHour": "5-hour window",
|
|
@@ -291,47 +315,49 @@ window.__ModuleLoader__.load({
|
|
|
291
315
|
const STYLES = [
|
|
292
316
|
"@keyframes dsh-usage-spin { to { transform: rotate(360deg); } }",
|
|
293
317
|
"@keyframes dsh-usage-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }",
|
|
294
|
-
".dsh-usage-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 32px; padding: 6px 14px; border: 1px solid var(--dsw-alias-border-l2); border-radius:
|
|
318
|
+
".dsh-usage-btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 32px; padding: 6px 14px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-1); font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', sans-serif; font-size: 13px; font-weight: 500; line-height: 20px; cursor: pointer; transition: background 140ms ease, border-color 140ms ease, transform 140ms ease; }",
|
|
295
319
|
".dsh-usage-btn:hover { background: var(--dsw-alias-bg-layer-2); }",
|
|
320
|
+
".dsh-usage-btn:active { transform: scale(0.98); }",
|
|
296
321
|
".dsh-usage-btn-busy::after { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-state-success-primary); animation: dsh-usage-pulse 1.2s ease-in-out infinite; }",
|
|
297
|
-
".dsh-usage-action { display: inline-flex; align-items: center; justify-content: center; height: 32px; padding: 0
|
|
298
|
-
".dsh-usage-action
|
|
299
|
-
".dsh-usage-action-
|
|
322
|
+
".dsh-usage-action { display: inline-flex; align-items: center; justify-content: center; min-height: 32px; padding: 0 13px; border: 1px solid transparent; border-radius: 9px; font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', sans-serif; font-size: 13px; font-weight: 500; line-height: 20px; cursor: pointer; transition: background 140ms ease, border-color 140ms ease, transform 140ms ease; }",
|
|
323
|
+
".dsh-usage-action:active { transform: scale(0.98); }",
|
|
324
|
+
".dsh-usage-action-primary { background: #0A84FF; border-color: #0A84FF; color: #FFFFFF; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12); }",
|
|
325
|
+
".dsh-usage-action-outline { background: var(--dsw-alias-bg-layer-1); border-color: var(--dsw-alias-border-l2); color: var(--dsw-alias-label-primary); }",
|
|
300
326
|
".dsh-usage-action-ghost { background: transparent; border-color: transparent; color: var(--dsw-alias-label-secondary); }",
|
|
301
327
|
".dsh-usage-action-outline:hover, .dsh-usage-action-ghost:hover { background: var(--dsw-alias-bg-layer-2); }",
|
|
302
328
|
".dsh-usage-action-ghost:hover { color: var(--dsw-alias-label-primary); }",
|
|
303
|
-
".dsh-usage-modal-overlay { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; padding:
|
|
304
|
-
".dsh-usage-modal-dialog { display: flex; flex-direction: column; width: min(
|
|
305
|
-
".dsh-usage-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding:
|
|
306
|
-
".dsh-usage-modal-title { font-size:
|
|
307
|
-
".dsh-usage-modal-close { display: inline-flex; align-items: center; justify-content: center; width:
|
|
329
|
+
".dsh-usage-modal-overlay { position: fixed; inset: 0; z-index: 60; display: flex; align-items: center; justify-content: center; padding: 20px; background: rgba(8, 12, 18, 0.34); backdrop-filter: blur(12px) saturate(125%); -webkit-backdrop-filter: blur(12px) saturate(125%); }",
|
|
330
|
+
".dsh-usage-modal-dialog { display: flex; flex-direction: column; width: min(620px, 100%); max-height: 100%; border: 1px solid var(--dsw-alias-border-l2); border-radius: 16px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', sans-serif; box-shadow: 0 24px 70px rgba(0, 0, 0, 0.28), 0 2px 8px rgba(0, 0, 0, 0.1); outline: none; overflow: hidden; }",
|
|
331
|
+
".dsh-usage-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px 13px; border-bottom: 1px solid var(--dsw-alias-border-l2); }",
|
|
332
|
+
".dsh-usage-modal-title { font-size: 15px; font-weight: 600; line-height: 22px; color: var(--dsw-alias-label-primary); }",
|
|
333
|
+
".dsh-usage-modal-close { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: 1px solid transparent; border-radius: 8px; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 14px; line-height: 1; cursor: pointer; transition: background 140ms ease; }",
|
|
308
334
|
".dsh-usage-modal-close:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }",
|
|
309
|
-
".dsh-usage-modal-content { padding:
|
|
310
|
-
".dsh-usage-modal-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px
|
|
335
|
+
".dsh-usage-modal-content { padding: 14px 18px; overflow: hidden; }",
|
|
336
|
+
".dsh-usage-modal-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 18px 16px; border-top: 1px solid var(--dsw-alias-border-l2); }",
|
|
311
337
|
".dsh-usage-state { padding: 12px 0; font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-secondary); }",
|
|
312
338
|
".dsh-usage-error { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; padding: 4px 0; }",
|
|
313
339
|
".dsh-usage-error-text { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); word-break: break-all; }",
|
|
314
340
|
".dsh-usage-inline-error { font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-error-primary); word-break: break-all; }",
|
|
315
|
-
".dsh-usage-stack { display: flex; flex-direction: column; gap:
|
|
341
|
+
".dsh-usage-stack { display: flex; flex-direction: column; gap: 12px; }",
|
|
316
342
|
".dsh-usage-status-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }",
|
|
317
343
|
".dsh-usage-status { display: inline-flex; align-items: center; gap: 6px; min-width: 0; flex: 1 1 auto; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; }",
|
|
318
344
|
".dsh-usage-status-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
|
|
319
345
|
".dsh-usage-status-actions { display: inline-flex; align-items: center; gap: 6px; }",
|
|
320
346
|
".dsh-usage-spinner { width: 10px; height: 10px; border-radius: 50%; border: 1.5px solid var(--dsw-alias-border-l2); border-top-color: var(--dsw-alias-label-primary); animation: dsh-usage-spin 0.8s linear infinite; }",
|
|
321
|
-
".dsh-usage-chip { display: inline-flex; align-items: center; height:
|
|
347
|
+
".dsh-usage-chip { display: inline-flex; align-items: center; height: 28px; padding: 0 11px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 9px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; cursor: pointer; transition: background 140ms ease, border-color 140ms ease; }",
|
|
322
348
|
".dsh-usage-chip:hover { background: var(--dsw-alias-bg-layer-2); }",
|
|
323
|
-
".dsh-usage-chip-active { color:
|
|
349
|
+
".dsh-usage-chip-active { color: #FFFFFF; border-color: #0A84FF; background: #0A84FF; }",
|
|
324
350
|
".dsh-usage-chip-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 230px; }",
|
|
325
351
|
".dsh-usage-modelbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }",
|
|
326
352
|
".dsh-usage-modelbar-label { flex: 0 0 auto; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }",
|
|
327
|
-
".dsh-usage-balance-card { border: 1px solid var(--dsw-alias-border-l2); border-radius:
|
|
328
|
-
".dsh-usage-balance-card-title { display: flex; align-items: center; justify-content: space-between; font-size: 14px; font-weight:
|
|
353
|
+
".dsh-usage-balance-card { border: 1px solid var(--dsw-alias-border-l2); border-radius: 11px; padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; background: var(--dsw-alias-bg-layer-1); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.035); }",
|
|
354
|
+
".dsh-usage-balance-card-title { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 14px; font-weight: 600; line-height: 22px; color: var(--dsw-alias-label-primary); }",
|
|
329
355
|
".dsh-usage-balance-status { font-size: 12px; font-weight: 400; line-height: 18px; color: var(--dsw-alias-label-secondary); }",
|
|
330
|
-
".dsh-usage-balance-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }",
|
|
356
|
+
".dsh-usage-balance-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 25px; }",
|
|
331
357
|
".dsh-usage-balance-label { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }",
|
|
332
|
-
".dsh-usage-balance-value { font-size:
|
|
358
|
+
".dsh-usage-balance-value { font-size: 13px; line-height: 20px; font-weight: 600; color: var(--dsw-alias-label-primary); font-variant-numeric: tabular-nums; }",
|
|
333
359
|
".dsh-usage-refresh { display: flex; justify-content: flex-end; }",
|
|
334
|
-
".dsh-usage-meta { border-top: 1px solid var(--dsw-alias-border-l2); padding-top:
|
|
360
|
+
".dsh-usage-meta { border-top: 1px solid var(--dsw-alias-border-l2); padding-top: 12px; display: flex; flex-direction: column; gap: 6px; }",
|
|
335
361
|
".dsh-usage-meta-row { display: flex; gap: 8px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }",
|
|
336
362
|
".dsh-usage-meta-key { flex: 0 0 auto; }",
|
|
337
363
|
".dsh-usage-meta-val { flex: 1 1 auto; word-break: break-all; font-variant-numeric: tabular-nums; }",
|
|
@@ -339,11 +365,11 @@ window.__ModuleLoader__.load({
|
|
|
339
365
|
".dsh-usage-field { display: flex; flex-direction: column; gap: 6px; }",
|
|
340
366
|
".dsh-usage-field-label { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }",
|
|
341
367
|
".dsh-usage-hint { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }",
|
|
342
|
-
".dsh-usage-input { height: 34px; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 20px; }",
|
|
368
|
+
".dsh-usage-input { height: 34px; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-base); color: var(--dsw-alias-label-primary); font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', sans-serif; font-size: 13px; line-height: 20px; }",
|
|
343
369
|
".dsh-usage-mode-toggle { display: flex; flex-wrap: wrap; gap: 8px; }",
|
|
344
|
-
".dsh-usage-mode-btn { display: inline-flex; align-items: center; justify-content: center; height: 30px; padding: 0 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background:
|
|
370
|
+
".dsh-usage-mode-btn { display: inline-flex; align-items: center; justify-content: center; min-height: 30px; padding: 0 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', sans-serif; font-size: 12px; font-weight: 500; line-height: 18px; cursor: pointer; transition: background 140ms ease, color 140ms ease, border-color 140ms ease; }",
|
|
345
371
|
".dsh-usage-mode-btn:hover { background: var(--dsw-alias-bg-layer-2); }",
|
|
346
|
-
".dsh-usage-mode-btn-active { background:
|
|
372
|
+
".dsh-usage-mode-btn-active { background: #0A84FF; border-color: #0A84FF; color: #FFFFFF; }",
|
|
347
373
|
".dsh-usage-cc-window { display: flex; flex-direction: column; gap: 6px; }",
|
|
348
374
|
".dsh-usage-cc-window-head { display: flex; align-items: baseline; justify-content: space-between; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }",
|
|
349
375
|
".dsh-usage-cc-window-val { font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-primary); font-variant-numeric: tabular-nums; }",
|
|
@@ -355,11 +381,32 @@ window.__ModuleLoader__.load({
|
|
|
355
381
|
".dsh-usage-history { display: flex; flex-direction: column; gap: 10px; }",
|
|
356
382
|
".dsh-usage-history-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }",
|
|
357
383
|
".dsh-usage-history-title { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 500; line-height: 20px; }",
|
|
358
|
-
".dsh-usage-history-ranges { display: flex; gap:
|
|
359
|
-
".dsh-usage-history-ranges .dsh-usage-mode-btn { height:
|
|
360
|
-
".dsh-usage-history-
|
|
384
|
+
".dsh-usage-history-ranges { display: inline-flex; align-self: flex-start; gap: 2px; padding: 3px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 10px; background: var(--dsw-alias-bg-layer-2); flex-wrap: wrap; }",
|
|
385
|
+
".dsh-usage-history-ranges .dsh-usage-mode-btn { min-height: 27px; padding: 0 10px; border-color: transparent; border-radius: 7px; background: transparent; font-size: 11px; }",
|
|
386
|
+
".dsh-usage-history-ranges .dsh-usage-mode-btn-active { background: var(--dsw-alias-bg-layer-1); border-color: var(--dsw-alias-border-l2); color: var(--dsw-alias-label-primary); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.14); }",
|
|
387
|
+
".dsh-usage-history-controls { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }",
|
|
388
|
+
".dsh-usage-history-points-toggle { display: inline-flex; align-items: center; gap: 7px; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; cursor: pointer; user-select: none; }",
|
|
389
|
+
".dsh-usage-history-points-toggle input { appearance: none; position: relative; width: 30px; height: 18px; flex: 0 0 30px; margin: 0; border: 1px solid var(--dsw-alias-border-l2); border-radius: 999px; background: var(--dsw-alias-bg-layer-2); cursor: pointer; transition: background 140ms ease, border-color 140ms ease; }",
|
|
390
|
+
".dsh-usage-history-points-toggle input::after { content: ''; position: absolute; top: 2px; left: 2px; width: 12px; height: 12px; border-radius: 50%; background: var(--dsw-alias-bg-layer-1); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); transition: transform 140ms ease; }",
|
|
391
|
+
".dsh-usage-history-points-toggle input:checked { border-color: #0A84FF; background: #0A84FF; }",
|
|
392
|
+
".dsh-usage-history-points-toggle input:checked::after { transform: translateX(12px); }",
|
|
393
|
+
".dsh-usage-history-points-toggle input:focus-visible { outline: 2px solid rgba(10, 132, 255, 0.45); outline-offset: 2px; }",
|
|
394
|
+
".dsh-usage-history-form { display: flex; align-items: end; gap: 6px; flex-wrap: wrap; }",
|
|
395
|
+
".dsh-usage-history-date { display: flex; flex-direction: column; gap: 3px; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; }",
|
|
396
|
+
".dsh-usage-history-date input, .dsh-usage-history-month { height: 30px; min-width: 112px; padding: 0 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', sans-serif; font-size: 12px; }",
|
|
397
|
+
".dsh-usage-history-date input:focus, .dsh-usage-history-month:focus, .dsh-usage-input:focus { outline: 2px solid rgba(10, 132, 255, 0.45); outline-offset: 1px; }",
|
|
398
|
+
".dsh-usage-history-manage { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }",
|
|
399
|
+
".dsh-usage-history-manage .dsh-usage-action { height: 28px; padding: 0 9px; font-size: 11px; }",
|
|
400
|
+
".dsh-usage-history-chart { display: block; width: 100%; height: auto; aspect-ratio: 360 / 132; max-height: 220px; overflow: visible; }",
|
|
401
|
+
".dsh-usage-history-chart-trigger { display: block; width: 100%; padding: 0; border: 0; border-radius: 8px; background: transparent; cursor: zoom-in; }",
|
|
402
|
+
".dsh-usage-history-chart-trigger:focus-visible { outline: 2px solid #0A84FF; outline-offset: 3px; }",
|
|
403
|
+
".dsh-usage-history-fullscreen-dialog { width: min(960px, 88vw); height: min(480px, 60vh); max-height: calc(100dvh - 40px); }",
|
|
404
|
+
".dsh-usage-history-fullscreen-content { display: flex; flex: 1 1 auto; min-height: 0; flex-direction: column; gap: 12px; padding: 14px 18px 18px; overflow: auto; }",
|
|
405
|
+
".dsh-usage-history-fullscreen-content .dsh-usage-history-chart { width: 100%; height: min(34vh, 300px); min-height: 200px; max-height: none; aspect-ratio: auto; }",
|
|
406
|
+
".dsh-usage-history-fullscreen-content .dsh-usage-history-ranges { align-self: flex-start; }",
|
|
407
|
+
"@media (max-width: 640px) { .dsh-usage-history-fullscreen-dialog { width: calc(100vw - 24px); height: min(70vh, 560px); } .dsh-usage-history-fullscreen-content { padding: 12px 14px 16px; } .dsh-usage-history-fullscreen-content .dsh-usage-history-chart { height: max(32vh, 180px); min-height: 180px; } }",
|
|
361
408
|
".dsh-usage-history-empty { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; padding: 8px 0; }",
|
|
362
|
-
".dsh-usage-modal-body { max-height: max(140px, calc(100vh -
|
|
409
|
+
".dsh-usage-modal-body { max-height: max(140px, calc(100vh - 260px)); overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; scrollbar-color: var(--dsw-alias-border-l2) transparent; }",
|
|
363
410
|
".dsh-usage-provider-list { display: flex; flex-direction: column; border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; overflow: hidden; }",
|
|
364
411
|
".dsh-usage-provider-row { display: flex; flex-direction: column; gap: 2px; padding: 8px 12px; }",
|
|
365
412
|
".dsh-usage-provider-row + .dsh-usage-provider-row { border-top: 1px solid var(--dsw-alias-border-l2); }",
|
|
@@ -380,9 +427,8 @@ window.__ModuleLoader__.load({
|
|
|
380
427
|
/** How often the open panel re-reads the clock and re-checks staleness. */
|
|
381
428
|
const TICK_MS = 5000;
|
|
382
429
|
const HISTORY_KEY = "dsh-model-usage:commandcode-history";
|
|
383
|
-
const HISTORY_INTERVAL_MS =
|
|
384
|
-
const
|
|
385
|
-
const HISTORY_RANGES = ["30m", "12h", "today", "yesterday"];
|
|
430
|
+
const HISTORY_INTERVAL_MS = 10 * 60 * 1000;
|
|
431
|
+
const HISTORY_RANGES = ["today", "yesterday", "week", "month", "custom"];
|
|
386
432
|
/** Everything Tab may land on inside a dialog, for the focus trap. */
|
|
387
433
|
const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
388
434
|
|
|
@@ -428,6 +474,13 @@ window.__ModuleLoader__.load({
|
|
|
428
474
|
return `${HISTORY_KEY}:${encodeURIComponent(account)}`;
|
|
429
475
|
}
|
|
430
476
|
|
|
477
|
+
// Keep the current calendar month plus the two preceding complete months.
|
|
478
|
+
// In October this retains October, September and August, and removes July.
|
|
479
|
+
function historyCutoff(value = Date.now()) {
|
|
480
|
+
const date = new Date(value);
|
|
481
|
+
return new Date(date.getFullYear(), date.getMonth() - 2, 1).getTime();
|
|
482
|
+
}
|
|
483
|
+
|
|
431
484
|
function readHistory(data) {
|
|
432
485
|
const key = historyStorageKey(data);
|
|
433
486
|
if (key === null) return [];
|
|
@@ -435,7 +488,7 @@ window.__ModuleLoader__.load({
|
|
|
435
488
|
const parsed = JSON.parse(localStorage.getItem(key) ?? "[]");
|
|
436
489
|
if (!Array.isArray(parsed)) return [];
|
|
437
490
|
const now = Date.now();
|
|
438
|
-
const cutoff = now
|
|
491
|
+
const cutoff = historyCutoff(now);
|
|
439
492
|
const retained = parsed.filter((point) => point !== null && typeof point === "object"
|
|
440
493
|
&& Number.isFinite(point.at) && point.at >= cutoff && point.at <= now
|
|
441
494
|
&& Number.isFinite(point.balance)
|
|
@@ -454,19 +507,43 @@ window.__ModuleLoader__.load({
|
|
|
454
507
|
}
|
|
455
508
|
}
|
|
456
509
|
|
|
510
|
+
function pruneStoredHistories(now = Date.now()) {
|
|
511
|
+
const prefix = `${HISTORY_KEY}:`;
|
|
512
|
+
const cutoff = historyCutoff(now);
|
|
513
|
+
try {
|
|
514
|
+
for (let index = localStorage.length - 1; index >= 0; index -= 1) {
|
|
515
|
+
const key = localStorage.key(index);
|
|
516
|
+
if (typeof key !== "string" || !key.startsWith(prefix)) continue;
|
|
517
|
+
let parsed;
|
|
518
|
+
try { parsed = JSON.parse(localStorage.getItem(key) ?? "[]"); } catch { parsed = []; }
|
|
519
|
+
const retained = Array.isArray(parsed)
|
|
520
|
+
? parsed.filter((point) => point !== null && typeof point === "object"
|
|
521
|
+
&& Number.isFinite(point.at) && point.at >= cutoff && point.at <= now
|
|
522
|
+
&& Number.isFinite(point.balance)
|
|
523
|
+
&& (point.spend === null || Number.isFinite(point.spend)))
|
|
524
|
+
: [];
|
|
525
|
+
if (retained.length === 0) localStorage.removeItem(key);
|
|
526
|
+
else if (retained.length !== parsed.length) localStorage.setItem(key, JSON.stringify(retained));
|
|
527
|
+
}
|
|
528
|
+
} catch {
|
|
529
|
+
// A storage restriction must not stop balance queries.
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
457
533
|
function recordHistorySample(data, now = Date.now()) {
|
|
458
534
|
const key = historyStorageKey(data);
|
|
459
535
|
const balance = Number(data?.data?.credits?.totalRemaining);
|
|
460
536
|
if (key === null || !Number.isFinite(balance)) return [];
|
|
537
|
+
pruneStoredHistories(now);
|
|
461
538
|
const points = readHistory(data);
|
|
462
539
|
const last = points[points.length - 1];
|
|
463
|
-
if (last !== undefined &&
|
|
540
|
+
if (last !== undefined && Math.floor(last.at / HISTORY_INTERVAL_MS) === Math.floor(now / HISTORY_INTERVAL_MS)) return points;
|
|
464
541
|
points.push({
|
|
465
542
|
at: now,
|
|
466
543
|
balance,
|
|
467
544
|
spend: last === undefined ? null : Math.max(0, last.balance - balance),
|
|
468
545
|
});
|
|
469
|
-
const retained = points.filter((point) => point.at >= now
|
|
546
|
+
const retained = points.filter((point) => point.at >= historyCutoff(now));
|
|
470
547
|
try {
|
|
471
548
|
localStorage.setItem(key, JSON.stringify(retained));
|
|
472
549
|
} catch {
|
|
@@ -475,6 +552,33 @@ window.__ModuleLoader__.load({
|
|
|
475
552
|
return retained;
|
|
476
553
|
}
|
|
477
554
|
|
|
555
|
+
function storedMonths(points) {
|
|
556
|
+
return [...new Set(points.map((point) => {
|
|
557
|
+
const date = new Date(point.at);
|
|
558
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
559
|
+
}))].sort().reverse();
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function clearHistoryMonth(data, month) {
|
|
563
|
+
const key = historyStorageKey(data);
|
|
564
|
+
if (key === null || !/^\d{4}-\d{2}$/.test(month)) return [];
|
|
565
|
+
const points = readHistory(data).filter((point) => {
|
|
566
|
+
const date = new Date(point.at);
|
|
567
|
+
const value = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
|
|
568
|
+
return value !== month;
|
|
569
|
+
});
|
|
570
|
+
try { localStorage.setItem(key, JSON.stringify(points)); } catch { /* memory view remains usable */ }
|
|
571
|
+
return points;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function resetHistoryStorage(data) {
|
|
575
|
+
const key = historyStorageKey(data);
|
|
576
|
+
if (key !== null) {
|
|
577
|
+
try { localStorage.removeItem(key); } catch { /* memory view remains usable */ }
|
|
578
|
+
}
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
|
|
478
582
|
const MODEL_KEY = "dsh-model-usage:model";
|
|
479
583
|
/** Key this plugin used before it was renamed to dsh-model-usage. */
|
|
480
584
|
const LEGACY_MODEL_KEY = "dsh-desktop-usage:model";
|
|
@@ -578,7 +682,7 @@ window.__ModuleLoader__.load({
|
|
|
578
682
|
* re-running it there would steal focus every few seconds.
|
|
579
683
|
*/
|
|
580
684
|
function UsageModal(props) {
|
|
581
|
-
const { open, onClose, title, closeLabel, className, footer, children } = props;
|
|
685
|
+
const { open, onClose, title, closeLabel, className, overlayClassName, footer, children } = props;
|
|
582
686
|
const dialogRef = React.useRef(null);
|
|
583
687
|
const restoreRef = React.useRef(null);
|
|
584
688
|
const closeRef = React.useRef(onClose);
|
|
@@ -636,7 +740,7 @@ window.__ModuleLoader__.load({
|
|
|
636
740
|
|
|
637
741
|
if (!open) return null;
|
|
638
742
|
return React.createElement("div", {
|
|
639
|
-
className: "dsh-usage-modal-overlay",
|
|
743
|
+
className: "dsh-usage-modal-overlay" + (overlayClassName ? ` ${overlayClassName}` : ""),
|
|
640
744
|
onMouseDown: (event) => { if (event.target === event.currentTarget) closeRef.current(); },
|
|
641
745
|
},
|
|
642
746
|
React.createElement("div", {
|
|
@@ -737,82 +841,172 @@ window.__ModuleLoader__.load({
|
|
|
737
841
|
);
|
|
738
842
|
}
|
|
739
843
|
|
|
740
|
-
function
|
|
844
|
+
function dateInputValue(value) {
|
|
845
|
+
const date = new Date(value);
|
|
846
|
+
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function localDateStart(value) {
|
|
850
|
+
const [year, month, day] = String(value).split("-").map(Number);
|
|
851
|
+
return Number.isFinite(year) && Number.isFinite(month) && Number.isFinite(day)
|
|
852
|
+
? new Date(year, month - 1, day).getTime()
|
|
853
|
+
: NaN;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function BalanceHistory({ points, range, customRange, appliedCustomRange, selectedMonth, onRange, onCustomRange, onApplyCustomRange, onSelectMonth, onClearMonth, onReset, t }) {
|
|
857
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
858
|
+
const [pointsOnly, setPointsOnly] = React.useState(false);
|
|
741
859
|
const now = Date.now();
|
|
742
|
-
|
|
743
|
-
let
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
start
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
860
|
+
const todayStart = new Date(new Date(now).setHours(0, 0, 0, 0)).getTime();
|
|
861
|
+
let start = todayStart;
|
|
862
|
+
let end = todayStart + 24 * 60 * 60 * 1000;
|
|
863
|
+
let daily = false;
|
|
864
|
+
if (range === "yesterday") {
|
|
865
|
+
start -= 24 * 60 * 60 * 1000;
|
|
866
|
+
end -= 24 * 60 * 60 * 1000;
|
|
867
|
+
} else if (range === "week") {
|
|
868
|
+
start -= 6 * 24 * 60 * 60 * 1000;
|
|
869
|
+
daily = true;
|
|
870
|
+
} else if (range === "month") {
|
|
871
|
+
start -= 29 * 24 * 60 * 60 * 1000;
|
|
872
|
+
daily = true;
|
|
873
|
+
} else if (range === "custom") {
|
|
874
|
+
const customStart = localDateStart(appliedCustomRange.start);
|
|
875
|
+
const customEnd = localDateStart(appliedCustomRange.end);
|
|
876
|
+
if (Number.isFinite(customStart) && Number.isFinite(customEnd) && customEnd >= customStart) {
|
|
877
|
+
start = customStart;
|
|
878
|
+
end = customEnd + 24 * 60 * 60 * 1000;
|
|
879
|
+
daily = end - start >= 7 * 24 * 60 * 60 * 1000;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
const rawVisible = points.filter((point) => point.spend !== null && point.at >= start && point.at < end);
|
|
883
|
+
let visible = rawVisible;
|
|
884
|
+
if (daily) {
|
|
885
|
+
const buckets = new Map();
|
|
886
|
+
for (const point of rawVisible) {
|
|
887
|
+
const date = new Date(point.at);
|
|
888
|
+
const bucket = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
889
|
+
buckets.set(bucket, (buckets.get(bucket) ?? 0) + point.spend);
|
|
890
|
+
}
|
|
891
|
+
visible = [...buckets.entries()].map(([at, spend]) => ({ at: at + 12 * 60 * 60 * 1000, spend })).sort((a, b) => a.at - b.at);
|
|
755
892
|
}
|
|
756
|
-
const visible = points.filter((point) => point.spend !== null && point.at >= start && point.at < end);
|
|
757
893
|
const maxSpend = Math.max(0.01, ...visible.map((point) => point.spend));
|
|
758
894
|
const chartPoints = visible.map((point) => ({
|
|
759
|
-
x: 42 + ((point.at - start) / (end - start)) * 306,
|
|
895
|
+
x: 42 + Math.max(0, Math.min(1, (point.at - start) / (end - start))) * 306,
|
|
760
896
|
y: 104 - (point.spend / maxSpend) * 78,
|
|
761
897
|
point,
|
|
762
898
|
}));
|
|
763
899
|
const sum = visible.reduce((total, point) => total + point.spend, 0);
|
|
764
|
-
const
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
900
|
+
const span = end - start;
|
|
901
|
+
const tickCount = span <= 2 * 24 * 60 * 60 * 1000 ? 7 : span <= 8 * 24 * 60 * 60 * 1000 ? 8 : 7;
|
|
902
|
+
const tickLabel = (value) => span <= 2 * 24 * 60 * 60 * 1000
|
|
903
|
+
? new Date(value).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
|
|
904
|
+
: new Date(value).toLocaleDateString([], { month: "2-digit", day: "2-digit" });
|
|
905
|
+
const children = [];
|
|
906
|
+
for (let index = 0; index < tickCount; index += 1) {
|
|
907
|
+
const ratio = index / (tickCount - 1);
|
|
908
|
+
const x = 42 + ratio * 306;
|
|
909
|
+
const value = start + ratio * span;
|
|
910
|
+
children.push(
|
|
911
|
+
React.createElement("line", { key: `grid-${index}`, x1: x, x2: x, y1: 26, y2: 104, stroke: "var(--dsw-alias-border-l2)", strokeWidth: 1, strokeOpacity: 0.32, strokeDasharray: "2 4" }),
|
|
912
|
+
React.createElement("text", { key: `tick-${index}`, x, y: 126, textAnchor: index === 0 ? "start" : index === tickCount - 1 ? "end" : "middle", fill: "var(--dsw-alias-label-secondary)", fontSize: 10 }, tickLabel(value)),
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
for (let index = 0; index <= 4; index += 1) {
|
|
916
|
+
const ratio = index / 4;
|
|
917
|
+
const y = 104 - ratio * 78;
|
|
918
|
+
children.push(
|
|
919
|
+
React.createElement("line", { key: `amount-grid-${index}`, x1: 42, x2: 348, y1: y, y2: y, stroke: "var(--dsw-alias-border-l2)", strokeWidth: 1, strokeOpacity: 0.62 }),
|
|
920
|
+
React.createElement("text", { key: `amount-${index}`, x: 36, y: y + 3, textAnchor: "end", fill: "var(--dsw-alias-label-secondary)", fontSize: 9 }, `$${(maxSpend * ratio).toFixed(2)}`),
|
|
921
|
+
);
|
|
922
|
+
}
|
|
772
923
|
if (chartPoints.length > 0) {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
:
|
|
776
|
-
|
|
777
|
-
key: "line", points: linePoints, fill: "none",
|
|
778
|
-
stroke: "var(--dsw-alias-state-success-primary)", strokeWidth: 2.5,
|
|
779
|
-
strokeLinecap: "round", strokeLinejoin: "round",
|
|
924
|
+
if (!pointsOnly) children.push(React.createElement("polyline", {
|
|
925
|
+
key: "line", points: chartPoints.map(({ x, y }) => `${x},${y}`).join(" "), fill: "none",
|
|
926
|
+
stroke: "var(--dsw-alias-state-success-primary)", strokeWidth: 1.35,
|
|
927
|
+
vectorEffect: "non-scaling-stroke", strokeLinecap: "round", strokeLinejoin: "round",
|
|
780
928
|
}));
|
|
781
929
|
chartPoints.forEach(({ x, y, point }, index) => children.push(
|
|
782
930
|
React.createElement("g", { key: `point-${index}` },
|
|
783
931
|
React.createElement("circle", {
|
|
784
|
-
cx: x, cy: y, r:
|
|
932
|
+
cx: x, cy: y, r: 1.8,
|
|
785
933
|
fill: "var(--dsw-alias-state-success-primary)",
|
|
934
|
+
stroke: "var(--dsw-alias-bg-layer-1)", strokeWidth: 1.4,
|
|
935
|
+
vectorEffect: "non-scaling-stroke",
|
|
786
936
|
}),
|
|
787
|
-
React.createElement("title", null, `${
|
|
937
|
+
React.createElement("title", null, `${new Date(point.at).toLocaleString()} · $${point.spend.toFixed(2)}`),
|
|
788
938
|
),
|
|
789
939
|
));
|
|
790
|
-
const first = chartPoints[0].point.at;
|
|
791
|
-
const last = chartPoints[chartPoints.length - 1].point.at;
|
|
792
|
-
children.push(
|
|
793
|
-
React.createElement("text", { key: "x-first", x: 42, y: 126, fill: "var(--dsw-alias-label-secondary)", fontSize: 10 }, labelTime(first)),
|
|
794
|
-
React.createElement("text", { key: "x-last", x: 348, y: 126, textAnchor: "end", fill: "var(--dsw-alias-label-secondary)", fontSize: 10 }, labelTime(last)),
|
|
795
|
-
);
|
|
796
940
|
}
|
|
941
|
+
const months = storedMonths(points);
|
|
942
|
+
const activeMonth = months.includes(selectedMonth) ? selectedMonth : (months[0] ?? "");
|
|
943
|
+
const rangeControls = React.createElement("div", { className: "dsh-usage-history-controls" },
|
|
944
|
+
React.createElement("div", { className: "dsh-usage-history-ranges" }, HISTORY_RANGES.map((item) =>
|
|
945
|
+
React.createElement("button", { key: item, type: "button", className: "dsh-usage-mode-btn" + (range === item ? " dsh-usage-mode-btn-active" : ""), onClick: () => onRange(item) }, t(`history.range.${item}`)))),
|
|
946
|
+
React.createElement("label", { className: "dsh-usage-history-points-toggle" },
|
|
947
|
+
React.createElement("input", {
|
|
948
|
+
type: "checkbox",
|
|
949
|
+
role: "switch",
|
|
950
|
+
checked: pointsOnly,
|
|
951
|
+
onChange: (event) => setPointsOnly(event.target.checked),
|
|
952
|
+
"aria-label": t("history.pointsOnly"),
|
|
953
|
+
}),
|
|
954
|
+
t("history.pointsOnly"),
|
|
955
|
+
),
|
|
956
|
+
);
|
|
957
|
+
const customRangeForm = range === "custom"
|
|
958
|
+
? React.createElement("div", { className: "dsh-usage-history-form" },
|
|
959
|
+
React.createElement("label", { className: "dsh-usage-history-date" }, t("history.start"), React.createElement("input", { type: "date", value: customRange.start, onChange: (event) => onCustomRange({ ...customRange, start: event.target.value }) })),
|
|
960
|
+
React.createElement("label", { className: "dsh-usage-history-date" }, t("history.end"), React.createElement("input", { type: "date", value: customRange.end, onChange: (event) => onCustomRange({ ...customRange, end: event.target.value }) })),
|
|
961
|
+
React.createElement(ActionButton, { variant: "outline", onClick: onApplyCustomRange }, t("history.apply")),
|
|
962
|
+
)
|
|
963
|
+
: null;
|
|
964
|
+
const chart = (className, isExpanded = false) => React.createElement("svg", {
|
|
965
|
+
className,
|
|
966
|
+
viewBox: "0 0 360 132",
|
|
967
|
+
preserveAspectRatio: isExpanded ? "none" : "xMidYMid meet",
|
|
968
|
+
role: "img",
|
|
969
|
+
"aria-label": t("history.title"),
|
|
970
|
+
}, children);
|
|
797
971
|
return React.createElement("div", { className: "dsh-usage-balance-card dsh-usage-history" },
|
|
798
972
|
React.createElement("div", { className: "dsh-usage-history-head" },
|
|
799
973
|
React.createElement("span", { className: "dsh-usage-history-title" }, t("history.title")),
|
|
800
|
-
visible.length > 0
|
|
801
|
-
? React.createElement("span", { className: "dsh-usage-balance-status" }, `${t("history.amount")} $${sum.toFixed(2)}`)
|
|
802
|
-
: null,
|
|
974
|
+
visible.length > 0 ? React.createElement("span", { className: "dsh-usage-balance-status" }, `${t("history.amount")} $${sum.toFixed(2)}`) : null,
|
|
803
975
|
),
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
key: item, type: "button",
|
|
807
|
-
className: "dsh-usage-mode-btn" + (range === item ? " dsh-usage-mode-btn-active" : ""),
|
|
808
|
-
onClick: () => onRange(item),
|
|
809
|
-
}, t(`history.range.${item}`)))),
|
|
976
|
+
rangeControls,
|
|
977
|
+
customRangeForm,
|
|
810
978
|
visible.length > 0
|
|
811
|
-
? React.createElement("
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
979
|
+
? React.createElement("button", {
|
|
980
|
+
type: "button",
|
|
981
|
+
className: "dsh-usage-history-chart-trigger",
|
|
982
|
+
title: t("history.expand"),
|
|
983
|
+
"aria-label": t("history.expand"),
|
|
984
|
+
onClick: () => setExpanded(true),
|
|
985
|
+
}, chart("dsh-usage-history-chart"))
|
|
815
986
|
: React.createElement("div", { className: "dsh-usage-history-empty" }, t("history.empty")),
|
|
987
|
+
React.createElement("div", { className: "dsh-usage-history-manage" },
|
|
988
|
+
React.createElement("span", { className: "dsh-usage-history-title" }, t("history.month")),
|
|
989
|
+
months.length > 0
|
|
990
|
+
? React.createElement("select", { className: "dsh-usage-history-month", value: activeMonth, onChange: (event) => onSelectMonth(event.target.value) }, months.map((month) => React.createElement("option", { key: month, value: month }, month)))
|
|
991
|
+
: React.createElement("span", { className: "dsh-usage-history-empty" }, t("history.noMonths")),
|
|
992
|
+
months.length > 0 ? React.createElement(ActionButton, { variant: "ghost", onClick: () => onClearMonth(activeMonth) }, t("history.clearMonth")) : null,
|
|
993
|
+
React.createElement(ActionButton, { variant: "ghost", onClick: onReset }, t("history.reset")),
|
|
994
|
+
),
|
|
995
|
+
React.createElement(UsageModal, {
|
|
996
|
+
open: expanded,
|
|
997
|
+
onClose: () => setExpanded(false),
|
|
998
|
+
title: t("history.title"),
|
|
999
|
+
closeLabel: t("action.close"),
|
|
1000
|
+
className: "dsh-usage-history-fullscreen-dialog",
|
|
1001
|
+
overlayClassName: "dsh-usage-history-fullscreen-overlay",
|
|
1002
|
+
},
|
|
1003
|
+
React.createElement("div", { className: "dsh-usage-history-fullscreen-content" },
|
|
1004
|
+
rangeControls,
|
|
1005
|
+
customRangeForm,
|
|
1006
|
+
React.createElement("span", { className: "dsh-usage-balance-status" }, `${t("history.amount")} $${sum.toFixed(2)}`),
|
|
1007
|
+
visible.length > 0 ? chart("dsh-usage-history-chart", true) : React.createElement("div", { className: "dsh-usage-history-empty" }, t("history.empty")),
|
|
1008
|
+
),
|
|
1009
|
+
),
|
|
816
1010
|
);
|
|
817
1011
|
}
|
|
818
1012
|
|
|
@@ -972,7 +1166,7 @@ window.__ModuleLoader__.load({
|
|
|
972
1166
|
);
|
|
973
1167
|
}
|
|
974
1168
|
|
|
975
|
-
function UsageBalance({ data, history, historyRange, onHistoryRange, onRefresh, t }) {
|
|
1169
|
+
function UsageBalance({ data, history, historyRange, customRange, appliedCustomRange, selectedMonth, onHistoryRange, onCustomRange, onApplyCustomRange, onSelectMonth, onClearMonth, onResetHistory, onRefresh, t }) {
|
|
976
1170
|
let card;
|
|
977
1171
|
if (data.mode === "commandcode") card = React.createElement(CommandCodeCard, { data, t });
|
|
978
1172
|
else if (data.mode === "newapi") card = React.createElement(NewApiCard, { data, t });
|
|
@@ -986,6 +1180,8 @@ window.__ModuleLoader__.load({
|
|
|
986
1180
|
data.mode === "commandcode"
|
|
987
1181
|
? React.createElement(BalanceHistory, {
|
|
988
1182
|
points: history, range: historyRange, onRange: onHistoryRange, t,
|
|
1183
|
+
customRange, appliedCustomRange, selectedMonth, onCustomRange, onApplyCustomRange, onSelectMonth,
|
|
1184
|
+
onClearMonth, onReset: onResetHistory,
|
|
989
1185
|
})
|
|
990
1186
|
: null,
|
|
991
1187
|
React.createElement(UsageMeta, { data, t }),
|
|
@@ -1256,16 +1452,29 @@ window.__ModuleLoader__.load({
|
|
|
1256
1452
|
const [form, setForm] = React.useState({ auto: true, minutes: "5" });
|
|
1257
1453
|
const [view, setView] = React.useState({ refreshing: false, data: null, error: "", updatedAt: 0 });
|
|
1258
1454
|
const [history, setHistory] = React.useState([]);
|
|
1259
|
-
const [historyRange, setHistoryRange] = React.useState("
|
|
1455
|
+
const [historyRange, setHistoryRange] = React.useState("today");
|
|
1456
|
+
const [customRange, setCustomRange] = React.useState(() => ({
|
|
1457
|
+
start: dateInputValue(Date.now() - 6 * 24 * 60 * 60 * 1000),
|
|
1458
|
+
end: dateInputValue(Date.now()),
|
|
1459
|
+
}));
|
|
1460
|
+
const [appliedCustomRange, setAppliedCustomRange] = React.useState(() => ({
|
|
1461
|
+
start: dateInputValue(Date.now() - 6 * 24 * 60 * 60 * 1000),
|
|
1462
|
+
end: dateInputValue(Date.now()),
|
|
1463
|
+
}));
|
|
1464
|
+
const [selectedMonth, setSelectedMonth] = React.useState("");
|
|
1260
1465
|
const [, forceRender] = React.useState(0);
|
|
1261
1466
|
const viewRef = React.useRef(view);
|
|
1262
1467
|
const inFlight = React.useRef(false);
|
|
1468
|
+
const samplePending = React.useRef(false);
|
|
1263
1469
|
const generation = React.useRef(0);
|
|
1264
1470
|
viewRef.current = view;
|
|
1265
1471
|
|
|
1266
1472
|
/** One usage query; concurrent calls collapse into the first one. */
|
|
1267
|
-
const refresh = React.useCallback(() => {
|
|
1268
|
-
if (inFlight.current)
|
|
1473
|
+
const refresh = React.useCallback((sample = false) => {
|
|
1474
|
+
if (inFlight.current) {
|
|
1475
|
+
if (sample) samplePending.current = true;
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1269
1478
|
inFlight.current = true;
|
|
1270
1479
|
const gen = generation.current;
|
|
1271
1480
|
// A missing host route binding is reported in the panel, never thrown:
|
|
@@ -1282,9 +1491,17 @@ window.__ModuleLoader__.load({
|
|
|
1282
1491
|
// the panel no longer shows, so it must not write anything.
|
|
1283
1492
|
if (gen !== generation.current) return;
|
|
1284
1493
|
inFlight.current = false;
|
|
1494
|
+
const pendingSample = samplePending.current;
|
|
1495
|
+
samplePending.current = false;
|
|
1285
1496
|
if (result.ok) {
|
|
1286
1497
|
const sampledAt = Date.now();
|
|
1287
|
-
const
|
|
1498
|
+
const previousHistory = result.value.mode === "commandcode" ? readHistory(result.value) : [];
|
|
1499
|
+
const shouldRecord = sample || (!pendingSample && previousHistory.length === 0);
|
|
1500
|
+
const nextHistory = result.value.mode === "commandcode"
|
|
1501
|
+
? shouldRecord
|
|
1502
|
+
? recordHistorySample(result.value, sampledAt)
|
|
1503
|
+
: previousHistory
|
|
1504
|
+
: [];
|
|
1288
1505
|
if (result.value.mode === "commandcode") setHistory(nextHistory);
|
|
1289
1506
|
setView((s) => ({ ...s, refreshing: false, data: result.value, error: "", updatedAt: sampledAt }));
|
|
1290
1507
|
} else {
|
|
@@ -1292,11 +1509,16 @@ window.__ModuleLoader__.load({
|
|
|
1292
1509
|
// blank out what the user was reading.
|
|
1293
1510
|
setView((s) => ({ ...s, refreshing: false, error: result.error.message }));
|
|
1294
1511
|
}
|
|
1512
|
+
if (pendingSample && !sample) setTimeout(() => { refresh(true); }, 0);
|
|
1295
1513
|
})
|
|
1296
1514
|
.catch((err) => {
|
|
1297
1515
|
if (gen !== generation.current) return;
|
|
1298
1516
|
inFlight.current = false;
|
|
1299
1517
|
setView((s) => ({ ...s, refreshing: false, error: String(err) }));
|
|
1518
|
+
if (samplePending.current) {
|
|
1519
|
+
samplePending.current = false;
|
|
1520
|
+
setTimeout(() => { refresh(true); }, 0);
|
|
1521
|
+
}
|
|
1300
1522
|
});
|
|
1301
1523
|
}, [fetchBalance, provider, model, effort, sessionId]);
|
|
1302
1524
|
|
|
@@ -1312,8 +1534,10 @@ window.__ModuleLoader__.load({
|
|
|
1312
1534
|
React.useEffect(() => {
|
|
1313
1535
|
generation.current += 1;
|
|
1314
1536
|
inFlight.current = false;
|
|
1537
|
+
samplePending.current = false;
|
|
1315
1538
|
setView({ refreshing: false, data: null, error: "", updatedAt: 0 });
|
|
1316
1539
|
setHistory([]);
|
|
1540
|
+
setSelectedMonth("");
|
|
1317
1541
|
refresh();
|
|
1318
1542
|
}, [refresh]);
|
|
1319
1543
|
|
|
@@ -1325,26 +1549,23 @@ window.__ModuleLoader__.load({
|
|
|
1325
1549
|
return () => { clearInterval(id); };
|
|
1326
1550
|
}, [config.auto, config.intervalMs, refresh]);
|
|
1327
1551
|
|
|
1328
|
-
// Sample
|
|
1552
|
+
// Sample at the system clock's 00/10/20/30/40/50 minute marks, even when
|
|
1553
|
+
// the configurable display auto-refresh is disabled.
|
|
1329
1554
|
React.useEffect(() => {
|
|
1330
1555
|
if (view.data?.mode !== "commandcode") return undefined;
|
|
1331
|
-
const
|
|
1332
|
-
const
|
|
1333
|
-
const wait =
|
|
1334
|
-
|
|
1335
|
-
: Math.max(0, HISTORY_INTERVAL_MS - (Date.now() - last.at));
|
|
1336
|
-
let retryId;
|
|
1556
|
+
const now = new Date();
|
|
1557
|
+
const elapsed = (now.getMinutes() % 10) * 60 * 1000 + now.getSeconds() * 1000 + now.getMilliseconds();
|
|
1558
|
+
const wait = HISTORY_INTERVAL_MS - elapsed;
|
|
1559
|
+
let intervalId;
|
|
1337
1560
|
const sampleId = setTimeout(() => {
|
|
1338
|
-
refresh();
|
|
1339
|
-
|
|
1340
|
-
if (!inFlight.current) refresh();
|
|
1341
|
-
}, 5 * 60 * 1000);
|
|
1561
|
+
refresh(true);
|
|
1562
|
+
intervalId = setInterval(() => { if (!inFlight.current) refresh(true); }, HISTORY_INTERVAL_MS);
|
|
1342
1563
|
}, wait);
|
|
1343
1564
|
return () => {
|
|
1344
1565
|
clearTimeout(sampleId);
|
|
1345
|
-
if (
|
|
1566
|
+
if (intervalId !== undefined) clearInterval(intervalId);
|
|
1346
1567
|
};
|
|
1347
|
-
}, [view.data,
|
|
1568
|
+
}, [view.data, refresh]);
|
|
1348
1569
|
|
|
1349
1570
|
// Opening the panel: show what is known, and only pay for a query when the
|
|
1350
1571
|
// numbers are stale (which also renders as 刷新中 over those numbers).
|
|
@@ -1433,6 +1654,28 @@ window.__ModuleLoader__.load({
|
|
|
1433
1654
|
setOpen(true);
|
|
1434
1655
|
};
|
|
1435
1656
|
|
|
1657
|
+
const clearMonth = (month) => {
|
|
1658
|
+
if (view.data?.mode !== "commandcode" || month === "") return;
|
|
1659
|
+
const message = fill(t("history.confirmClearMonth"), { month });
|
|
1660
|
+
if (typeof window !== "undefined" && typeof window.confirm === "function" && !window.confirm(message)) return;
|
|
1661
|
+
const next = clearHistoryMonth(view.data, month);
|
|
1662
|
+
setHistory(next);
|
|
1663
|
+
setSelectedMonth(storedMonths(next)[0] ?? "");
|
|
1664
|
+
};
|
|
1665
|
+
const resetHistory = () => {
|
|
1666
|
+
if (view.data?.mode !== "commandcode") return;
|
|
1667
|
+
if (typeof window !== "undefined" && typeof window.confirm === "function" && !window.confirm(t("history.confirmReset"))) return;
|
|
1668
|
+
setHistory(resetHistoryStorage(view.data));
|
|
1669
|
+
setSelectedMonth("");
|
|
1670
|
+
};
|
|
1671
|
+
const applyCustomRange = () => {
|
|
1672
|
+
const start = localDateStart(customRange.start);
|
|
1673
|
+
const end = localDateStart(customRange.end);
|
|
1674
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return;
|
|
1675
|
+
setAppliedCustomRange({ ...customRange });
|
|
1676
|
+
setHistoryRange("custom");
|
|
1677
|
+
};
|
|
1678
|
+
|
|
1436
1679
|
let body;
|
|
1437
1680
|
if (view.data !== null) {
|
|
1438
1681
|
body = React.createElement("div", { className: "dsh-usage-stack" },
|
|
@@ -1440,7 +1683,15 @@ window.__ModuleLoader__.load({
|
|
|
1440
1683
|
data: view.data,
|
|
1441
1684
|
history,
|
|
1442
1685
|
historyRange,
|
|
1686
|
+
customRange,
|
|
1687
|
+
appliedCustomRange,
|
|
1688
|
+
selectedMonth,
|
|
1443
1689
|
onHistoryRange: setHistoryRange,
|
|
1690
|
+
onCustomRange: setCustomRange,
|
|
1691
|
+
onApplyCustomRange: applyCustomRange,
|
|
1692
|
+
onSelectMonth: setSelectedMonth,
|
|
1693
|
+
onClearMonth: clearMonth,
|
|
1694
|
+
onResetHistory: resetHistory,
|
|
1444
1695
|
onRefresh: refresh,
|
|
1445
1696
|
t,
|
|
1446
1697
|
}),
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Model usage",
|
|
4
|
-
"description": "Adds a Usage entry to the session header
|
|
4
|
+
"description": "Adds a Usage entry to the session header with provider usage, ten-minute Command Code sampling, calendar-month trends, custom ranges and history cleanup."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "用量",
|
|
4
|
-
"description": "在会话头部加一个「用量」入口:查看当前对话所用模型对应供应商账户的用量(余额 / 套餐窗口 /
|
|
4
|
+
"description": "在会话头部加一个「用量」入口:查看当前对话所用模型对应供应商账户的用量(余额 / 套餐窗口 / 请求数),支持 Command Code 十分钟采样、日历月历史趋势、自定义时间段和记录清理。"
|
|
5
5
|
}
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-model-usage",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.5",
|
|
4
4
|
"description": "Provider-account usage for DeepSeek Harness: quota windows, credit balance and request counts for the model a conversation runs on, with per-provider adapters and manual target selection.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "liuchaoxu <ischaoxu@163.com>",
|
|
Binary file
|