dsh-model-usage 0.8.3 → 0.8.4

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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to this package are documented in this file. Versions follow Semantic Versioning.
4
4
 
5
+ ## 0.8.4
6
+
7
+ - Command Code balance history: sample on ten-minute clock marks, retain the current and two
8
+ preceding calendar months, support custom date ranges and daily week/month aggregation, and add
9
+ month deletion and full reset controls.
10
+
5
11
  ## 0.8.3
6
12
 
7
13
  - 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
- ![Usage panel](docs/screenshots/usage-panel.png)
11
+ ![Usage panel](docs/screenshots/u1.png)
12
+
13
+ ![Usage panel](docs/screenshots/u2.png)
14
+
15
+ ![Usage panel](docs/screenshots/u3.png)
16
+
17
+ ![Usage panel](docs/screenshots/u4.png)
18
+
19
+ ![Usage panel](docs/screenshots/u5.png)
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
- *Command Code 订阅的用量面板:5 小时 / 7 天窗口、信用余额、已花费、请求数,以及本次查询使用的
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.** Successful balance queries save a baseline and then record
30
- balance changes every 30 minutes, independently of the display refresh setting. Samples are kept
31
- locally per account for up to seven days and shown as a spend line chart with last 30 minutes,
32
- last 12 hours, today and yesterday filters.
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 余额历史独立于面板刷新开关,每 30 分钟采样并按账户保存在浏览器本地,最多保留
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 32 32" width="32" height="32" role="img" aria-label="Usage">
2
- <rect x="1" y="1" width="30" height="30" rx="8" fill="#1f6feb"/>
3
- <path d="M7.5 21.5a8.5 8.5 0 0 1 17 0" fill="none" stroke="#ffffff" stroke-width="2.4" stroke-linecap="round"/>
4
- <path d="M16 21.5 21.4 16.4" fill="none" stroke="#ffffff" stroke-width="2.4" stroke-linecap="round"/>
5
- <circle cx="16" cy="21.5" r="2" fill="#ffffff"/>
6
- <path d="M9.5 26h13" fill="none" stroke="#ffffff" stroke-width="2.2" stroke-linecap="round" opacity="0.7"/>
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,22 @@ window.__ModuleLoader__.load({
122
122
  "card.hideRaw": "隐藏原始返回",
123
123
  "card.unrecognized": "接口返回的字段还没被识别,下面是原始返回(可以发给我做校准)",
124
124
  "history.title": "余额消耗趋势",
125
- "history.range.30m": "最近半小时",
126
- "history.range.12h": "最近半天",
127
125
  "history.range.today": "今天",
128
126
  "history.range.yesterday": "昨天",
127
+ "history.range.week": "近一周",
128
+ "history.range.month": "近一月",
129
+ "history.range.custom": "自定义",
130
+ "history.start": "开始日期",
131
+ "history.end": "结束日期",
132
+ "history.apply": "应用时间段",
129
133
  "history.empty": "该时间段暂无足够的余额记录",
130
134
  "history.amount": "消耗",
135
+ "history.clearMonth": "清除所选月份",
136
+ "history.confirmClearMonth": "确定清除 {month} 的余额记录吗?",
137
+ "history.reset": "重置当前记录",
138
+ "history.month": "数据月份",
139
+ "history.noMonths": "暂无可清理的月份",
140
+ "history.confirmReset": "确定清除当前账户的全部余额记录吗?",
131
141
  "card.used": "已用",
132
142
  "card.total": "总额",
133
143
  "window.fiveHour": "5 小时窗口",
@@ -230,12 +240,22 @@ window.__ModuleLoader__.load({
230
240
  "card.hideRaw": "Hide raw response",
231
241
  "card.unrecognized": "These fields are not recognized yet — the raw response is below (send it over to calibrate)",
232
242
  "history.title": "Balance spend trend",
233
- "history.range.30m": "Last 30 min",
234
- "history.range.12h": "Last 12 hours",
235
243
  "history.range.today": "Today",
236
244
  "history.range.yesterday": "Yesterday",
245
+ "history.range.week": "Last 7 days",
246
+ "history.range.month": "Last 30 days",
247
+ "history.range.custom": "Custom",
248
+ "history.start": "Start date",
249
+ "history.end": "End date",
250
+ "history.apply": "Apply range",
237
251
  "history.empty": "Not enough balance samples for this period",
238
252
  "history.amount": "Spent",
253
+ "history.clearMonth": "Clear selected month",
254
+ "history.confirmClearMonth": "Clear balance records for {month}?",
255
+ "history.reset": "Reset current records",
256
+ "history.month": "Data month",
257
+ "history.noMonths": "No stored months to clear",
258
+ "history.confirmReset": "Clear all balance records for this account?",
239
259
  "card.used": "Used",
240
260
  "card.total": "Total",
241
261
  "window.fiveHour": "5-hour window",
@@ -291,47 +311,49 @@ window.__ModuleLoader__.load({
291
311
  const STYLES = [
292
312
  "@keyframes dsh-usage-spin { to { transform: rotate(360deg); } }",
293
313
  "@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: 18px; color: var(--dsw-alias-label-primary); background: transparent; font-size: 13px; font-weight: 400; line-height: 20px; cursor: pointer; }",
314
+ ".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
315
  ".dsh-usage-btn:hover { background: var(--dsw-alias-bg-layer-2); }",
316
+ ".dsh-usage-btn:active { transform: scale(0.98); }",
296
317
  ".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 14px; border: 1px solid transparent; border-radius: 8px; font-size: 13px; font-weight: 400; line-height: 20px; cursor: pointer; }",
298
- ".dsh-usage-action-primary { background: var(--dsw-alias-brand-primary); border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-bg-base); }",
299
- ".dsh-usage-action-outline { background: transparent; border-color: var(--dsw-alias-border-l2); color: var(--dsw-alias-label-primary); }",
318
+ ".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; }",
319
+ ".dsh-usage-action:active { transform: scale(0.98); }",
320
+ ".dsh-usage-action-primary { background: #0A84FF; border-color: #0A84FF; color: #FFFFFF; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12); }",
321
+ ".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
322
  ".dsh-usage-action-ghost { background: transparent; border-color: transparent; color: var(--dsw-alias-label-secondary); }",
301
323
  ".dsh-usage-action-outline:hover, .dsh-usage-action-ghost:hover { background: var(--dsw-alias-bg-layer-2); }",
302
324
  ".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: 24px; background: rgba(0, 0, 0, 0.28); }",
304
- ".dsh-usage-modal-dialog { display: flex; flex-direction: column; width: min(560px, 100%); max-height: 100%; border: 1px solid var(--dsw-alias-border-l2); border-radius: 14px; background: var(--dsw-alias-bg-layer-1); box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22); outline: none; }",
305
- ".dsh-usage-modal-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px 10px; }",
306
- ".dsh-usage-modal-title { font-size: 14px; font-weight: 500; line-height: 22px; color: var(--dsw-alias-label-primary); }",
307
- ".dsh-usage-modal-close { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border: none; border-radius: 8px; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 14px; line-height: 1; cursor: pointer; }",
325
+ ".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%); }",
326
+ ".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; }",
327
+ ".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); }",
328
+ ".dsh-usage-modal-title { font-size: 15px; font-weight: 600; line-height: 22px; color: var(--dsw-alias-label-primary); }",
329
+ ".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
330
  ".dsh-usage-modal-close:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }",
309
- ".dsh-usage-modal-content { padding: 0 16px; overflow: hidden; }",
310
- ".dsh-usage-modal-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px 14px; }",
331
+ ".dsh-usage-modal-content { padding: 14px 18px; overflow: hidden; }",
332
+ ".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
333
  ".dsh-usage-state { padding: 12px 0; font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-secondary); }",
312
334
  ".dsh-usage-error { display: flex; flex-direction: column; align-items: flex-start; gap: 12px; padding: 4px 0; }",
313
335
  ".dsh-usage-error-text { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); word-break: break-all; }",
314
336
  ".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: 10px; }",
337
+ ".dsh-usage-stack { display: flex; flex-direction: column; gap: 12px; }",
316
338
  ".dsh-usage-status-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }",
317
339
  ".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
340
  ".dsh-usage-status-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
319
341
  ".dsh-usage-status-actions { display: inline-flex; align-items: center; gap: 6px; }",
320
342
  ".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: 26px; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 13px; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; cursor: pointer; }",
343
+ ".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
344
  ".dsh-usage-chip:hover { background: var(--dsw-alias-bg-layer-2); }",
323
- ".dsh-usage-chip-active { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-state-success-primary); }",
345
+ ".dsh-usage-chip-active { color: #FFFFFF; border-color: #0A84FF; background: #0A84FF; }",
324
346
  ".dsh-usage-chip-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 230px; }",
325
347
  ".dsh-usage-modelbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }",
326
348
  ".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: 12px; padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; }",
328
- ".dsh-usage-balance-card-title { display: flex; align-items: center; justify-content: space-between; font-size: 14px; font-weight: 500; line-height: 22px; color: var(--dsw-alias-label-primary); }",
349
+ ".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); }",
350
+ ".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
351
  ".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; }",
352
+ ".dsh-usage-balance-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 25px; }",
331
353
  ".dsh-usage-balance-label { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }",
332
- ".dsh-usage-balance-value { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); font-variant-numeric: tabular-nums; }",
354
+ ".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
355
  ".dsh-usage-refresh { display: flex; justify-content: flex-end; }",
334
- ".dsh-usage-meta { border-top: 1px solid var(--dsw-alias-border-l2); padding-top: 10px; display: flex; flex-direction: column; gap: 4px; }",
356
+ ".dsh-usage-meta { border-top: 1px solid var(--dsw-alias-border-l2); padding-top: 12px; display: flex; flex-direction: column; gap: 6px; }",
335
357
  ".dsh-usage-meta-row { display: flex; gap: 8px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }",
336
358
  ".dsh-usage-meta-key { flex: 0 0 auto; }",
337
359
  ".dsh-usage-meta-val { flex: 1 1 auto; word-break: break-all; font-variant-numeric: tabular-nums; }",
@@ -339,11 +361,11 @@ window.__ModuleLoader__.load({
339
361
  ".dsh-usage-field { display: flex; flex-direction: column; gap: 6px; }",
340
362
  ".dsh-usage-field-label { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }",
341
363
  ".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; }",
364
+ ".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
365
  ".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: transparent; color: var(--dsw-alias-label-secondary); font-size: 13px; line-height: 20px; cursor: pointer; }",
366
+ ".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
367
  ".dsh-usage-mode-btn:hover { background: var(--dsw-alias-bg-layer-2); }",
346
- ".dsh-usage-mode-btn-active { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }",
368
+ ".dsh-usage-mode-btn-active { background: #0A84FF; border-color: #0A84FF; color: #FFFFFF; }",
347
369
  ".dsh-usage-cc-window { display: flex; flex-direction: column; gap: 6px; }",
348
370
  ".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
371
  ".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 +377,18 @@ window.__ModuleLoader__.load({
355
377
  ".dsh-usage-history { display: flex; flex-direction: column; gap: 10px; }",
356
378
  ".dsh-usage-history-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; }",
357
379
  ".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: 4px; flex-wrap: wrap; }",
359
- ".dsh-usage-history-ranges .dsh-usage-mode-btn { height: 26px; padding: 0 8px; font-size: 11px; }",
380
+ ".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; }",
381
+ ".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; }",
382
+ ".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); }",
383
+ ".dsh-usage-history-form { display: flex; align-items: end; gap: 6px; flex-wrap: wrap; }",
384
+ ".dsh-usage-history-date { display: flex; flex-direction: column; gap: 3px; color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; }",
385
+ ".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; }",
386
+ ".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; }",
387
+ ".dsh-usage-history-manage { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }",
388
+ ".dsh-usage-history-manage .dsh-usage-action { height: 28px; padding: 0 9px; font-size: 11px; }",
360
389
  ".dsh-usage-history-chart { width: 100%; height: 148px; overflow: visible; }",
361
390
  ".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 - 300px)); overflow-y: auto; overscroll-behavior: contain; }",
391
+ ".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
392
  ".dsh-usage-provider-list { display: flex; flex-direction: column; border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; overflow: hidden; }",
364
393
  ".dsh-usage-provider-row { display: flex; flex-direction: column; gap: 2px; padding: 8px 12px; }",
365
394
  ".dsh-usage-provider-row + .dsh-usage-provider-row { border-top: 1px solid var(--dsw-alias-border-l2); }",
@@ -380,9 +409,8 @@ window.__ModuleLoader__.load({
380
409
  /** How often the open panel re-reads the clock and re-checks staleness. */
381
410
  const TICK_MS = 5000;
382
411
  const HISTORY_KEY = "dsh-model-usage:commandcode-history";
383
- const HISTORY_INTERVAL_MS = 30 * 60 * 1000;
384
- const HISTORY_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
385
- const HISTORY_RANGES = ["30m", "12h", "today", "yesterday"];
412
+ const HISTORY_INTERVAL_MS = 10 * 60 * 1000;
413
+ const HISTORY_RANGES = ["today", "yesterday", "week", "month", "custom"];
386
414
  /** Everything Tab may land on inside a dialog, for the focus trap. */
387
415
  const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
388
416
 
@@ -428,6 +456,13 @@ window.__ModuleLoader__.load({
428
456
  return `${HISTORY_KEY}:${encodeURIComponent(account)}`;
429
457
  }
430
458
 
459
+ // Keep the current calendar month plus the two preceding complete months.
460
+ // In October this retains October, September and August, and removes July.
461
+ function historyCutoff(value = Date.now()) {
462
+ const date = new Date(value);
463
+ return new Date(date.getFullYear(), date.getMonth() - 2, 1).getTime();
464
+ }
465
+
431
466
  function readHistory(data) {
432
467
  const key = historyStorageKey(data);
433
468
  if (key === null) return [];
@@ -435,7 +470,7 @@ window.__ModuleLoader__.load({
435
470
  const parsed = JSON.parse(localStorage.getItem(key) ?? "[]");
436
471
  if (!Array.isArray(parsed)) return [];
437
472
  const now = Date.now();
438
- const cutoff = now - HISTORY_RETENTION_MS;
473
+ const cutoff = historyCutoff(now);
439
474
  const retained = parsed.filter((point) => point !== null && typeof point === "object"
440
475
  && Number.isFinite(point.at) && point.at >= cutoff && point.at <= now
441
476
  && Number.isFinite(point.balance)
@@ -454,19 +489,43 @@ window.__ModuleLoader__.load({
454
489
  }
455
490
  }
456
491
 
492
+ function pruneStoredHistories(now = Date.now()) {
493
+ const prefix = `${HISTORY_KEY}:`;
494
+ const cutoff = historyCutoff(now);
495
+ try {
496
+ for (let index = localStorage.length - 1; index >= 0; index -= 1) {
497
+ const key = localStorage.key(index);
498
+ if (typeof key !== "string" || !key.startsWith(prefix)) continue;
499
+ let parsed;
500
+ try { parsed = JSON.parse(localStorage.getItem(key) ?? "[]"); } catch { parsed = []; }
501
+ const retained = Array.isArray(parsed)
502
+ ? parsed.filter((point) => point !== null && typeof point === "object"
503
+ && Number.isFinite(point.at) && point.at >= cutoff && point.at <= now
504
+ && Number.isFinite(point.balance)
505
+ && (point.spend === null || Number.isFinite(point.spend)))
506
+ : [];
507
+ if (retained.length === 0) localStorage.removeItem(key);
508
+ else if (retained.length !== parsed.length) localStorage.setItem(key, JSON.stringify(retained));
509
+ }
510
+ } catch {
511
+ // A storage restriction must not stop balance queries.
512
+ }
513
+ }
514
+
457
515
  function recordHistorySample(data, now = Date.now()) {
458
516
  const key = historyStorageKey(data);
459
517
  const balance = Number(data?.data?.credits?.totalRemaining);
460
518
  if (key === null || !Number.isFinite(balance)) return [];
519
+ pruneStoredHistories(now);
461
520
  const points = readHistory(data);
462
521
  const last = points[points.length - 1];
463
- if (last !== undefined && now - last.at < HISTORY_INTERVAL_MS) return points;
522
+ if (last !== undefined && Math.floor(last.at / HISTORY_INTERVAL_MS) === Math.floor(now / HISTORY_INTERVAL_MS)) return points;
464
523
  points.push({
465
524
  at: now,
466
525
  balance,
467
526
  spend: last === undefined ? null : Math.max(0, last.balance - balance),
468
527
  });
469
- const retained = points.filter((point) => point.at >= now - HISTORY_RETENTION_MS);
528
+ const retained = points.filter((point) => point.at >= historyCutoff(now));
470
529
  try {
471
530
  localStorage.setItem(key, JSON.stringify(retained));
472
531
  } catch {
@@ -475,6 +534,33 @@ window.__ModuleLoader__.load({
475
534
  return retained;
476
535
  }
477
536
 
537
+ function storedMonths(points) {
538
+ return [...new Set(points.map((point) => {
539
+ const date = new Date(point.at);
540
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
541
+ }))].sort().reverse();
542
+ }
543
+
544
+ function clearHistoryMonth(data, month) {
545
+ const key = historyStorageKey(data);
546
+ if (key === null || !/^\d{4}-\d{2}$/.test(month)) return [];
547
+ const points = readHistory(data).filter((point) => {
548
+ const date = new Date(point.at);
549
+ const value = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
550
+ return value !== month;
551
+ });
552
+ try { localStorage.setItem(key, JSON.stringify(points)); } catch { /* memory view remains usable */ }
553
+ return points;
554
+ }
555
+
556
+ function resetHistoryStorage(data) {
557
+ const key = historyStorageKey(data);
558
+ if (key !== null) {
559
+ try { localStorage.removeItem(key); } catch { /* memory view remains usable */ }
560
+ }
561
+ return [];
562
+ }
563
+
478
564
  const MODEL_KEY = "dsh-model-usage:model";
479
565
  /** Key this plugin used before it was renamed to dsh-model-usage. */
480
566
  const LEGACY_MODEL_KEY = "dsh-desktop-usage:model";
@@ -737,82 +823,118 @@ window.__ModuleLoader__.load({
737
823
  );
738
824
  }
739
825
 
740
- function BalanceHistory({ points, range, onRange, t }) {
826
+ function dateInputValue(value) {
827
+ const date = new Date(value);
828
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
829
+ }
830
+
831
+ function localDateStart(value) {
832
+ const [year, month, day] = String(value).split("-").map(Number);
833
+ return Number.isFinite(year) && Number.isFinite(month) && Number.isFinite(day)
834
+ ? new Date(year, month - 1, day).getTime()
835
+ : NaN;
836
+ }
837
+
838
+ function BalanceHistory({ points, range, customRange, appliedCustomRange, selectedMonth, onRange, onCustomRange, onApplyCustomRange, onSelectMonth, onClearMonth, onReset, t }) {
741
839
  const now = Date.now();
742
- let start;
743
- let end = now;
744
- if (range === "30m") start = now - HISTORY_INTERVAL_MS;
745
- else if (range === "12h") start = now - 12 * 60 * 60 * 1000;
746
- else if (range === "yesterday") {
747
- const today = new Date(now);
748
- today.setHours(0, 0, 0, 0);
749
- end = today.getTime();
750
- start = end - 24 * 60 * 60 * 1000;
751
- } else {
752
- const today = new Date(now);
753
- today.setHours(0, 0, 0, 0);
754
- start = today.getTime();
840
+ const todayStart = new Date(new Date(now).setHours(0, 0, 0, 0)).getTime();
841
+ let start = todayStart;
842
+ let end = todayStart + 24 * 60 * 60 * 1000;
843
+ let daily = false;
844
+ if (range === "yesterday") {
845
+ start -= 24 * 60 * 60 * 1000;
846
+ end -= 24 * 60 * 60 * 1000;
847
+ } else if (range === "week") {
848
+ start -= 6 * 24 * 60 * 60 * 1000;
849
+ daily = true;
850
+ } else if (range === "month") {
851
+ start -= 29 * 24 * 60 * 60 * 1000;
852
+ daily = true;
853
+ } else if (range === "custom") {
854
+ const customStart = localDateStart(appliedCustomRange.start);
855
+ const customEnd = localDateStart(appliedCustomRange.end);
856
+ if (Number.isFinite(customStart) && Number.isFinite(customEnd) && customEnd >= customStart) {
857
+ start = customStart;
858
+ end = customEnd + 24 * 60 * 60 * 1000;
859
+ daily = end - start >= 7 * 24 * 60 * 60 * 1000;
860
+ }
861
+ }
862
+ const rawVisible = points.filter((point) => point.spend !== null && point.at >= start && point.at < end);
863
+ let visible = rawVisible;
864
+ if (daily) {
865
+ const buckets = new Map();
866
+ for (const point of rawVisible) {
867
+ const date = new Date(point.at);
868
+ const bucket = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
869
+ buckets.set(bucket, (buckets.get(bucket) ?? 0) + point.spend);
870
+ }
871
+ visible = [...buckets.entries()].map(([at, spend]) => ({ at: at + 12 * 60 * 60 * 1000, spend })).sort((a, b) => a.at - b.at);
755
872
  }
756
- const visible = points.filter((point) => point.spend !== null && point.at >= start && point.at < end);
757
873
  const maxSpend = Math.max(0.01, ...visible.map((point) => point.spend));
758
874
  const chartPoints = visible.map((point) => ({
759
- x: 42 + ((point.at - start) / (end - start)) * 306,
875
+ x: 42 + Math.max(0, Math.min(1, (point.at - start) / (end - start))) * 306,
760
876
  y: 104 - (point.spend / maxSpend) * 78,
761
877
  point,
762
878
  }));
763
879
  const sum = visible.reduce((total, point) => total + point.spend, 0);
764
- const labelTime = (value) => new Date(value).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
765
- const children = [
766
- React.createElement("line", { key: "grid-1", x1: 42, x2: 348, y1: 26, y2: 26, stroke: "var(--dsw-alias-border-l2)", strokeWidth: 1 }),
767
- React.createElement("line", { key: "grid-2", x1: 42, x2: 348, y1: 65, y2: 65, stroke: "var(--dsw-alias-border-l2)", strokeWidth: 1 }),
768
- React.createElement("line", { key: "grid-3", x1: 42, x2: 348, y1: 104, y2: 104, stroke: "var(--dsw-alias-border-l2)", strokeWidth: 1 }),
769
- React.createElement("text", { key: "y-max", x: 36, y: 30, textAnchor: "end", fill: "var(--dsw-alias-label-secondary)", fontSize: 10 }, `$${maxSpend.toFixed(2)}`),
880
+ const span = end - start;
881
+ const tickCount = span <= 2 * 24 * 60 * 60 * 1000 ? 7 : span <= 8 * 24 * 60 * 60 * 1000 ? 8 : 7;
882
+ const tickLabel = (value) => span <= 2 * 24 * 60 * 60 * 1000
883
+ ? new Date(value).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
884
+ : new Date(value).toLocaleDateString([], { month: "2-digit", day: "2-digit" });
885
+ const children = [];
886
+ for (let index = 0; index < tickCount; index += 1) {
887
+ const ratio = index / (tickCount - 1);
888
+ const x = 42 + ratio * 306;
889
+ const value = start + ratio * span;
890
+ children.push(
891
+ React.createElement("line", { key: `grid-${index}`, x1: x, x2: x, y1: 20, y2: 108, stroke: "var(--dsw-alias-border-l2)", strokeWidth: 1 }),
892
+ 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)),
893
+ );
894
+ }
895
+ children.push(
896
+ React.createElement("text", { key: "y-max", x: 36, y: 25, textAnchor: "end", fill: "var(--dsw-alias-label-secondary)", fontSize: 10 }, `$${maxSpend.toFixed(2)}`),
770
897
  React.createElement("text", { key: "y-zero", x: 36, y: 108, textAnchor: "end", fill: "var(--dsw-alias-label-secondary)", fontSize: 10 }, "$0"),
771
- ];
898
+ );
772
899
  if (chartPoints.length > 0) {
773
- const linePoints = chartPoints.length === 1
774
- ? `${Math.max(42, chartPoints[0].x - 16)},${chartPoints[0].y} ${chartPoints[0].x},${chartPoints[0].y}`
775
- : chartPoints.map(({ x, y }) => `${x},${y}`).join(" ");
776
900
  children.push(React.createElement("polyline", {
777
- key: "line", points: linePoints, fill: "none",
778
- stroke: "var(--dsw-alias-state-success-primary)", strokeWidth: 2.5,
779
- strokeLinecap: "round", strokeLinejoin: "round",
901
+ key: "line", points: chartPoints.map(({ x, y }) => `${x},${y}`).join(" "), fill: "none",
902
+ stroke: "var(--dsw-alias-state-success-primary)", strokeWidth: 2.5, strokeLinecap: "round", strokeLinejoin: "round",
780
903
  }));
781
904
  chartPoints.forEach(({ x, y, point }, index) => children.push(
782
905
  React.createElement("g", { key: `point-${index}` },
783
- React.createElement("circle", {
784
- cx: x, cy: y, r: 3.5,
785
- fill: "var(--dsw-alias-state-success-primary)",
786
- }),
787
- React.createElement("title", null, `${labelTime(point.at)} · $${point.spend.toFixed(2)}`),
906
+ React.createElement("circle", { cx: x, cy: y, r: 3.5, fill: "var(--dsw-alias-state-success-primary)" }),
907
+ React.createElement("title", null, `${new Date(point.at).toLocaleString()} · $${point.spend.toFixed(2)}`),
788
908
  ),
789
909
  ));
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
910
  }
911
+ const months = storedMonths(points);
912
+ const activeMonth = months.includes(selectedMonth) ? selectedMonth : (months[0] ?? "");
797
913
  return React.createElement("div", { className: "dsh-usage-balance-card dsh-usage-history" },
798
914
  React.createElement("div", { className: "dsh-usage-history-head" },
799
915
  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,
916
+ visible.length > 0 ? React.createElement("span", { className: "dsh-usage-balance-status" }, `${t("history.amount")} $${sum.toFixed(2)}`) : null,
803
917
  ),
804
918
  React.createElement("div", { className: "dsh-usage-history-ranges" }, HISTORY_RANGES.map((item) =>
805
- React.createElement("button", {
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}`)))),
919
+ 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}`)))),
920
+ range === "custom"
921
+ ? React.createElement("div", { className: "dsh-usage-history-form" },
922
+ 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 }) })),
923
+ 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 }) })),
924
+ React.createElement(ActionButton, { variant: "outline", onClick: onApplyCustomRange }, t("history.apply")),
925
+ )
926
+ : null,
810
927
  visible.length > 0
811
- ? React.createElement("svg", {
812
- className: "dsh-usage-history-chart", viewBox: "0 0 360 132",
813
- role: "img", "aria-label": t("history.title"),
814
- }, children)
928
+ ? React.createElement("svg", { className: "dsh-usage-history-chart", viewBox: "0 0 360 132", role: "img", "aria-label": t("history.title") }, children)
815
929
  : React.createElement("div", { className: "dsh-usage-history-empty" }, t("history.empty")),
930
+ React.createElement("div", { className: "dsh-usage-history-manage" },
931
+ React.createElement("span", { className: "dsh-usage-history-title" }, t("history.month")),
932
+ months.length > 0
933
+ ? 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)))
934
+ : React.createElement("span", { className: "dsh-usage-history-empty" }, t("history.noMonths")),
935
+ months.length > 0 ? React.createElement(ActionButton, { variant: "ghost", onClick: () => onClearMonth(activeMonth) }, t("history.clearMonth")) : null,
936
+ React.createElement(ActionButton, { variant: "ghost", onClick: onReset }, t("history.reset")),
937
+ ),
816
938
  );
817
939
  }
818
940
 
@@ -972,7 +1094,7 @@ window.__ModuleLoader__.load({
972
1094
  );
973
1095
  }
974
1096
 
975
- function UsageBalance({ data, history, historyRange, onHistoryRange, onRefresh, t }) {
1097
+ function UsageBalance({ data, history, historyRange, customRange, appliedCustomRange, selectedMonth, onHistoryRange, onCustomRange, onApplyCustomRange, onSelectMonth, onClearMonth, onResetHistory, onRefresh, t }) {
976
1098
  let card;
977
1099
  if (data.mode === "commandcode") card = React.createElement(CommandCodeCard, { data, t });
978
1100
  else if (data.mode === "newapi") card = React.createElement(NewApiCard, { data, t });
@@ -986,6 +1108,8 @@ window.__ModuleLoader__.load({
986
1108
  data.mode === "commandcode"
987
1109
  ? React.createElement(BalanceHistory, {
988
1110
  points: history, range: historyRange, onRange: onHistoryRange, t,
1111
+ customRange, appliedCustomRange, selectedMonth, onCustomRange, onApplyCustomRange, onSelectMonth,
1112
+ onClearMonth, onReset: onResetHistory,
989
1113
  })
990
1114
  : null,
991
1115
  React.createElement(UsageMeta, { data, t }),
@@ -1256,16 +1380,29 @@ window.__ModuleLoader__.load({
1256
1380
  const [form, setForm] = React.useState({ auto: true, minutes: "5" });
1257
1381
  const [view, setView] = React.useState({ refreshing: false, data: null, error: "", updatedAt: 0 });
1258
1382
  const [history, setHistory] = React.useState([]);
1259
- const [historyRange, setHistoryRange] = React.useState("12h");
1383
+ const [historyRange, setHistoryRange] = React.useState("today");
1384
+ const [customRange, setCustomRange] = React.useState(() => ({
1385
+ start: dateInputValue(Date.now() - 6 * 24 * 60 * 60 * 1000),
1386
+ end: dateInputValue(Date.now()),
1387
+ }));
1388
+ const [appliedCustomRange, setAppliedCustomRange] = React.useState(() => ({
1389
+ start: dateInputValue(Date.now() - 6 * 24 * 60 * 60 * 1000),
1390
+ end: dateInputValue(Date.now()),
1391
+ }));
1392
+ const [selectedMonth, setSelectedMonth] = React.useState("");
1260
1393
  const [, forceRender] = React.useState(0);
1261
1394
  const viewRef = React.useRef(view);
1262
1395
  const inFlight = React.useRef(false);
1396
+ const samplePending = React.useRef(false);
1263
1397
  const generation = React.useRef(0);
1264
1398
  viewRef.current = view;
1265
1399
 
1266
1400
  /** One usage query; concurrent calls collapse into the first one. */
1267
- const refresh = React.useCallback(() => {
1268
- if (inFlight.current) return;
1401
+ const refresh = React.useCallback((sample = false) => {
1402
+ if (inFlight.current) {
1403
+ if (sample) samplePending.current = true;
1404
+ return;
1405
+ }
1269
1406
  inFlight.current = true;
1270
1407
  const gen = generation.current;
1271
1408
  // A missing host route binding is reported in the panel, never thrown:
@@ -1282,9 +1419,17 @@ window.__ModuleLoader__.load({
1282
1419
  // the panel no longer shows, so it must not write anything.
1283
1420
  if (gen !== generation.current) return;
1284
1421
  inFlight.current = false;
1422
+ const pendingSample = samplePending.current;
1423
+ samplePending.current = false;
1285
1424
  if (result.ok) {
1286
1425
  const sampledAt = Date.now();
1287
- const nextHistory = recordHistorySample(result.value, sampledAt);
1426
+ const previousHistory = result.value.mode === "commandcode" ? readHistory(result.value) : [];
1427
+ const shouldRecord = sample || (!pendingSample && previousHistory.length === 0);
1428
+ const nextHistory = result.value.mode === "commandcode"
1429
+ ? shouldRecord
1430
+ ? recordHistorySample(result.value, sampledAt)
1431
+ : previousHistory
1432
+ : [];
1288
1433
  if (result.value.mode === "commandcode") setHistory(nextHistory);
1289
1434
  setView((s) => ({ ...s, refreshing: false, data: result.value, error: "", updatedAt: sampledAt }));
1290
1435
  } else {
@@ -1292,11 +1437,16 @@ window.__ModuleLoader__.load({
1292
1437
  // blank out what the user was reading.
1293
1438
  setView((s) => ({ ...s, refreshing: false, error: result.error.message }));
1294
1439
  }
1440
+ if (pendingSample && !sample) setTimeout(() => { refresh(true); }, 0);
1295
1441
  })
1296
1442
  .catch((err) => {
1297
1443
  if (gen !== generation.current) return;
1298
1444
  inFlight.current = false;
1299
1445
  setView((s) => ({ ...s, refreshing: false, error: String(err) }));
1446
+ if (samplePending.current) {
1447
+ samplePending.current = false;
1448
+ setTimeout(() => { refresh(true); }, 0);
1449
+ }
1300
1450
  });
1301
1451
  }, [fetchBalance, provider, model, effort, sessionId]);
1302
1452
 
@@ -1312,8 +1462,10 @@ window.__ModuleLoader__.load({
1312
1462
  React.useEffect(() => {
1313
1463
  generation.current += 1;
1314
1464
  inFlight.current = false;
1465
+ samplePending.current = false;
1315
1466
  setView({ refreshing: false, data: null, error: "", updatedAt: 0 });
1316
1467
  setHistory([]);
1468
+ setSelectedMonth("");
1317
1469
  refresh();
1318
1470
  }, [refresh]);
1319
1471
 
@@ -1325,26 +1477,23 @@ window.__ModuleLoader__.load({
1325
1477
  return () => { clearInterval(id); };
1326
1478
  }, [config.auto, config.intervalMs, refresh]);
1327
1479
 
1328
- // Sample on a fixed cadence even when the user disables display auto-refresh.
1480
+ // Sample at the system clock's 00/10/20/30/40/50 minute marks, even when
1481
+ // the configurable display auto-refresh is disabled.
1329
1482
  React.useEffect(() => {
1330
1483
  if (view.data?.mode !== "commandcode") return undefined;
1331
- const points = readHistory(view.data);
1332
- const last = points[points.length - 1];
1333
- const wait = last === undefined
1334
- ? 0
1335
- : Math.max(0, HISTORY_INTERVAL_MS - (Date.now() - last.at));
1336
- let retryId;
1484
+ const now = new Date();
1485
+ const elapsed = (now.getMinutes() % 10) * 60 * 1000 + now.getSeconds() * 1000 + now.getMilliseconds();
1486
+ const wait = HISTORY_INTERVAL_MS - elapsed;
1487
+ let intervalId;
1337
1488
  const sampleId = setTimeout(() => {
1338
- refresh();
1339
- retryId = setInterval(() => {
1340
- if (!inFlight.current) refresh();
1341
- }, 5 * 60 * 1000);
1489
+ refresh(true);
1490
+ intervalId = setInterval(() => { if (!inFlight.current) refresh(true); }, HISTORY_INTERVAL_MS);
1342
1491
  }, wait);
1343
1492
  return () => {
1344
1493
  clearTimeout(sampleId);
1345
- if (retryId !== undefined) clearInterval(retryId);
1494
+ if (intervalId !== undefined) clearInterval(intervalId);
1346
1495
  };
1347
- }, [view.data, history, refresh]);
1496
+ }, [view.data, refresh]);
1348
1497
 
1349
1498
  // Opening the panel: show what is known, and only pay for a query when the
1350
1499
  // numbers are stale (which also renders as 刷新中 over those numbers).
@@ -1433,6 +1582,28 @@ window.__ModuleLoader__.load({
1433
1582
  setOpen(true);
1434
1583
  };
1435
1584
 
1585
+ const clearMonth = (month) => {
1586
+ if (view.data?.mode !== "commandcode" || month === "") return;
1587
+ const message = fill(t("history.confirmClearMonth"), { month });
1588
+ if (typeof window !== "undefined" && typeof window.confirm === "function" && !window.confirm(message)) return;
1589
+ const next = clearHistoryMonth(view.data, month);
1590
+ setHistory(next);
1591
+ setSelectedMonth(storedMonths(next)[0] ?? "");
1592
+ };
1593
+ const resetHistory = () => {
1594
+ if (view.data?.mode !== "commandcode") return;
1595
+ if (typeof window !== "undefined" && typeof window.confirm === "function" && !window.confirm(t("history.confirmReset"))) return;
1596
+ setHistory(resetHistoryStorage(view.data));
1597
+ setSelectedMonth("");
1598
+ };
1599
+ const applyCustomRange = () => {
1600
+ const start = localDateStart(customRange.start);
1601
+ const end = localDateStart(customRange.end);
1602
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return;
1603
+ setAppliedCustomRange({ ...customRange });
1604
+ setHistoryRange("custom");
1605
+ };
1606
+
1436
1607
  let body;
1437
1608
  if (view.data !== null) {
1438
1609
  body = React.createElement("div", { className: "dsh-usage-stack" },
@@ -1440,7 +1611,15 @@ window.__ModuleLoader__.load({
1440
1611
  data: view.data,
1441
1612
  history,
1442
1613
  historyRange,
1614
+ customRange,
1615
+ appliedCustomRange,
1616
+ selectedMonth,
1443
1617
  onHistoryRange: setHistoryRange,
1618
+ onCustomRange: setCustomRange,
1619
+ onApplyCustomRange: applyCustomRange,
1620
+ onSelectMonth: setSelectedMonth,
1621
+ onClearMonth: clearMonth,
1622
+ onResetHistory: resetHistory,
1444
1623
  onRefresh: refresh,
1445
1624
  t,
1446
1625
  }),
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: provider account usage, manual query-target selection, background refresh, and a seven-day Command Code balance-spend trend."
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": "在会话头部加一个「用量」入口:查看当前对话所用模型对应供应商账户的用量(余额 / 套餐窗口 / 请求数),可手动切换查询对象、后台自动刷新,并查看 Command Code 七天余额消耗趋势。"
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",
3
+ "version": "0.8.4",
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