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 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
- ![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,24 @@ window.__ModuleLoader__.load({
122
122
  "card.hideRaw": "隐藏原始返回",
123
123
  "card.unrecognized": "接口返回的字段还没被识别,下面是原始返回(可以发给我做校准)",
124
124
  "history.title": "余额消耗趋势",
125
- "history.range.30m": "最近半小时",
126
- "history.range.12h": "最近半天",
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.range.30m": "Last 30 min",
234
- "history.range.12h": "Last 12 hours",
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: 18px; color: var(--dsw-alias-label-primary); background: transparent; font-size: 13px; font-weight: 400; line-height: 20px; cursor: pointer; }",
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 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); }",
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: 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; }",
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: 0 16px; overflow: hidden; }",
310
- ".dsh-usage-modal-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px 14px; }",
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: 10px; }",
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: 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; }",
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: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-state-success-primary); }",
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: 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); }",
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: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); font-variant-numeric: tabular-nums; }",
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: 10px; display: flex; flex-direction: column; gap: 4px; }",
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: transparent; color: var(--dsw-alias-label-secondary); font-size: 13px; line-height: 20px; cursor: pointer; }",
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: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }",
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: 4px; flex-wrap: wrap; }",
359
- ".dsh-usage-history-ranges .dsh-usage-mode-btn { height: 26px; padding: 0 8px; font-size: 11px; }",
360
- ".dsh-usage-history-chart { width: 100%; height: 148px; overflow: visible; }",
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 - 300px)); overflow-y: auto; overscroll-behavior: contain; }",
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 = 30 * 60 * 1000;
384
- const HISTORY_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
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 - HISTORY_RETENTION_MS;
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 && now - last.at < HISTORY_INTERVAL_MS) return points;
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 - HISTORY_RETENTION_MS);
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 BalanceHistory({ points, range, onRange, t }) {
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
- 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();
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 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)}`),
770
- React.createElement("text", { key: "y-zero", x: 36, y: 108, textAnchor: "end", fill: "var(--dsw-alias-label-secondary)", fontSize: 10 }, "$0"),
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
- 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
- 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",
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: 3.5,
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, `${labelTime(point.at)} · $${point.spend.toFixed(2)}`),
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
- 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}`)))),
976
+ rangeControls,
977
+ customRangeForm,
810
978
  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)
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("12h");
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) return;
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 nextHistory = recordHistorySample(result.value, sampledAt);
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 on a fixed cadence even when the user disables display auto-refresh.
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 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;
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
- retryId = setInterval(() => {
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 (retryId !== undefined) clearInterval(retryId);
1566
+ if (intervalId !== undefined) clearInterval(intervalId);
1346
1567
  };
1347
- }, [view.data, history, refresh]);
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: 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.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