@cassius0924/dsh-usage-dashboard 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -26,7 +26,7 @@ __export(index_exports, {
26
26
  name: () => name
27
27
  });
28
28
  module.exports = __toCommonJS(index_exports);
29
- var import_react8 = require("react");
29
+ var import_react10 = require("react");
30
30
 
31
31
  // src/client/boundary.tsx
32
32
  var import_react = require("react");
@@ -89,9 +89,9 @@ function loadPersisted(storageKey, isUsable) {
89
89
  function pruneOldVersions(storageKey) {
90
90
  if (typeof localStorage === "undefined") return;
91
91
  try {
92
- const current = `${storageKey}:v${LS_VERSION}`;
92
+ const current2 = `${storageKey}:v${LS_VERSION}`;
93
93
  for (const key of Object.keys(localStorage)) {
94
- if (key.startsWith(`${storageKey}:v`) && key !== current) localStorage.removeItem(key);
94
+ if (key.startsWith(`${storageKey}:v`) && key !== current2) localStorage.removeItem(key);
95
95
  }
96
96
  } catch {
97
97
  }
@@ -118,6 +118,17 @@ function createCache(ttlMs, storageKey, isUsable) {
118
118
 
119
119
  // src/contract.ts
120
120
  var USAGE_WINDOW_DAYS = [7, 30, 90, 365];
121
+ var SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
122
+ var isValidSessionId = (id) => SESSION_ID_PATTERN.test(id);
123
+ var BREAKER_API = {
124
+ state: "/api/dsh-usage-dashboard/breaker",
125
+ release: "/api/dsh-usage-dashboard/breaker/release"
126
+ };
127
+ var isBreakerSettings = (value) => {
128
+ if (typeof value !== "object" || value === null) return false;
129
+ const candidate = value;
130
+ return typeof candidate.enabled === "boolean" && Array.isArray(candidate.sessions) && candidate.sessions.every((session) => typeof session === "string" && isValidSessionId(session));
131
+ };
121
132
 
122
133
  // src/client/api.ts
123
134
  var BALANCE_TTL_MS = 6e4;
@@ -195,15 +206,15 @@ var sessionUsageCache = /* @__PURE__ */ new Map();
195
206
  var sessionUsageListeners = /* @__PURE__ */ new Map();
196
207
  var getCachedSessionUsage = (sessionId) => sessionUsageCache.get(sessionId) ?? null;
197
208
  var subscribeSessionUsage = (sessionId, fn) => {
198
- let listeners = sessionUsageListeners.get(sessionId);
199
- if (listeners === void 0) {
200
- listeners = /* @__PURE__ */ new Set();
201
- sessionUsageListeners.set(sessionId, listeners);
209
+ let listeners2 = sessionUsageListeners.get(sessionId);
210
+ if (listeners2 === void 0) {
211
+ listeners2 = /* @__PURE__ */ new Set();
212
+ sessionUsageListeners.set(sessionId, listeners2);
202
213
  }
203
- listeners.add(fn);
214
+ listeners2.add(fn);
204
215
  return () => {
205
- listeners?.delete(fn);
206
- if (listeners?.size === 0) sessionUsageListeners.delete(sessionId);
216
+ listeners2?.delete(fn);
217
+ if (listeners2?.size === 0) sessionUsageListeners.delete(sessionId);
207
218
  };
208
219
  };
209
220
  var publishSessionUsage = (sessionId, response) => {
@@ -230,20 +241,118 @@ async function fetchSessionUsage(sessionId, force = false) {
230
241
  sessionUsageInflight.set(sessionId, tracked);
231
242
  return tracked;
232
243
  }
244
+ async function breakerRequest(url, init) {
245
+ try {
246
+ const response = await fetch(url, {
247
+ method: init.method,
248
+ cache: "no-store",
249
+ headers: init.body === void 0 ? { accept: "application/json" } : { accept: "application/json", "content-type": "application/json" },
250
+ ...init.body === void 0 ? {} : { body: init.body }
251
+ });
252
+ if (!response.ok) return { ok: false, error: `HTTP ${response.status}` };
253
+ return await response.json();
254
+ } catch (error) {
255
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
256
+ }
257
+ }
258
+ async function fetchBreaker() {
259
+ return breakerRequest(BREAKER_API.state, { method: "GET" });
260
+ }
261
+ async function updateBreaker(request) {
262
+ return breakerRequest(BREAKER_API.state, { method: "PUT", body: JSON.stringify(request) });
263
+ }
264
+ async function releaseBreaker(request) {
265
+ return breakerRequest(
266
+ BREAKER_API.release,
267
+ request === void 0 ? { method: "POST" } : { method: "POST", body: JSON.stringify(request) }
268
+ );
269
+ }
233
270
 
234
271
  // src/client/dashboard.tsx
235
- var import_react4 = require("react");
272
+ var import_react5 = require("react");
236
273
 
237
- // src/client/budget.ts
274
+ // src/client/breaker.ts
238
275
  var BEIJING_OFFSET_MS = 8 * 60 * 60 * 1e3;
276
+ var SHORT_SESSION_ID = 8;
277
+ function breakerIsUsable(res) {
278
+ if (res === null || res === void 0 || res.ok !== true) return false;
279
+ const data = res.data;
280
+ if (data === void 0 || data === null) return false;
281
+ return isBreakerSettings(data) && Array.isArray(data.holding) && data.holding.every((id) => typeof id === "string") && typeof data.now === "string" && typeof data.peak === "boolean" && (data.resumeAt === null || typeof data.resumeAt === "string") && Array.isArray(data.peakWindows) && data.peakWindows.every((window2) => typeof window2 === "string");
282
+ }
283
+ function beijingClock(iso) {
284
+ if (typeof iso !== "string" || iso === "") return null;
285
+ const ms = Date.parse(iso);
286
+ if (!Number.isFinite(ms)) return null;
287
+ const beijing = new Date(ms + BEIJING_OFFSET_MS);
288
+ const hours = String(beijing.getUTCHours()).padStart(2, "0");
289
+ const minutes = String(beijing.getUTCMinutes()).padStart(2, "0");
290
+ return `${hours}:${minutes}`;
291
+ }
292
+ function isSessionOptedIn(data, sessionId) {
293
+ if (data === null || sessionId === null || sessionId === void 0 || sessionId === "") return false;
294
+ return Array.isArray(data.sessions) && data.sessions.includes(sessionId);
295
+ }
296
+ function isSessionEnabled(data, sessionId) {
297
+ if (data === null || data.enabled !== true) return false;
298
+ return isSessionOptedIn(data, sessionId);
299
+ }
300
+ function sessionBreakerState(data, sessionId) {
301
+ if (!isSessionEnabled(data, sessionId)) return "off";
302
+ return data !== null && Array.isArray(data.holding) && data.holding.includes(sessionId) ? "holding" : "on";
303
+ }
304
+ function withSessionToggled(data, sessionId, enabled) {
305
+ if (data === null) return null;
306
+ const sessions = enabled ? data.sessions.includes(sessionId) ? data.sessions : [...data.sessions, sessionId] : data.sessions.filter((id) => id !== sessionId);
307
+ const holding = enabled ? data.holding : data.holding.filter((id) => id !== sessionId);
308
+ return { ...data, sessions, holding };
309
+ }
310
+ function withMasterToggled(data, enabled) {
311
+ if (data === null) return null;
312
+ return { ...data, enabled, holding: enabled ? data.holding : [] };
313
+ }
314
+ function shortSessionId(sessionId) {
315
+ const trimmed = sessionId.trim();
316
+ return trimmed.length <= SHORT_SESSION_ID ? trimmed : `${trimmed.slice(0, SHORT_SESSION_ID)}\u2026`;
317
+ }
318
+ function breakerPhase(data) {
319
+ if (data === null) return "unavailable";
320
+ if (!data.enabled) return "masterOff";
321
+ if (!data.peak) return "offPeak";
322
+ return data.holding.length > 0 ? "peakHolding" : "peak";
323
+ }
324
+
325
+ // src/client/breaker-state.ts
326
+ var import_react2 = require("react");
327
+ var current = null;
328
+ var listeners = /* @__PURE__ */ new Set();
329
+ function getBreakerData() {
330
+ return current;
331
+ }
332
+ function publishBreakerData(data) {
333
+ current = data;
334
+ for (const listener of [...listeners]) listener();
335
+ }
336
+ function subscribeBreakerData(listener) {
337
+ listeners.add(listener);
338
+ return () => {
339
+ listeners.delete(listener);
340
+ };
341
+ }
342
+ function useBreakerData() {
343
+ return (0, import_react2.useSyncExternalStore)(subscribeBreakerData, getBreakerData, getBreakerData);
344
+ }
345
+
346
+ // src/client/budget.ts
347
+ var BEIJING_OFFSET_MS2 = 8 * 60 * 60 * 1e3;
239
348
  function budgetSnapshot(spentValue, budgetValue, nowMs = Date.now()) {
240
349
  const spent = Number.isFinite(spentValue) ? Math.max(0, spentValue) : 0;
241
350
  const budget = Number.isFinite(budgetValue) ? Math.max(0, budgetValue) : 0;
242
- const beijingNow = new Date(nowMs + BEIJING_OFFSET_MS);
351
+ const beijingNow = new Date(nowMs + BEIJING_OFFSET_MS2);
243
352
  const year = beijingNow.getUTCFullYear();
244
353
  const month = beijingNow.getUTCMonth();
245
- const start = Date.UTC(year, month, 1) - BEIJING_OFFSET_MS;
246
- const end = Date.UTC(year, month + 1, 1) - BEIJING_OFFSET_MS;
354
+ const start = Date.UTC(year, month, 1) - BEIJING_OFFSET_MS2;
355
+ const end = Date.UTC(year, month + 1, 1) - BEIJING_OFFSET_MS2;
247
356
  const elapsedRatio = Math.min(1, Math.max(1 / (end - start), (nowMs - start) / (end - start)));
248
357
  const forecast = spent / elapsedRatio;
249
358
  const ratio = budget > 0 ? spent / budget : 0;
@@ -254,12 +363,12 @@ function budgetSnapshot(spentValue, budgetValue, nowMs = Date.now()) {
254
363
  }
255
364
 
256
365
  // src/client/charts.tsx
257
- var import_react3 = require("react");
366
+ var import_react4 = require("react");
258
367
 
259
368
  // src/client/chart-focus.ts
260
- function nextChartFocus(current, count, key, horizontalStep = 1) {
369
+ function nextChartFocus(current2, count, key, horizontalStep = 1) {
261
370
  if (count <= 0) return 0;
262
- const index = Math.min(Math.max(0, current), count - 1);
371
+ const index = Math.min(Math.max(0, current2), count - 1);
263
372
  if (key === "Home") return 0;
264
373
  if (key === "End") return count - 1;
265
374
  const step = key === "ArrowLeft" || key === "ArrowRight" ? Math.max(1, horizontalStep) : 1;
@@ -272,8 +381,8 @@ function lastPopulatedIndex(values) {
272
381
  }
273
382
  return Math.max(0, values.length - 1);
274
383
  }
275
- function nextPinnedIndex(current, tapped) {
276
- return current === tapped ? null : tapped;
384
+ function nextPinnedIndex(current2, tapped) {
385
+ return current2 === tapped ? null : tapped;
277
386
  }
278
387
  function isTapGesture(dx, dy, dt, moveThreshold = 10, timeThreshold = 500) {
279
388
  return Math.hypot(dx, dy) <= moveThreshold && dt <= timeThreshold;
@@ -288,7 +397,7 @@ function maxOrOne(values) {
288
397
  }
289
398
 
290
399
  // src/client/i18n.tsx
291
- var import_react2 = require("react");
400
+ var import_react3 = require("react");
292
401
 
293
402
  // src/client/locales.ts
294
403
  var NS = "dsh-usage-dashboard";
@@ -481,6 +590,7 @@ var zh = {
481
590
  "balance.granted": "\u8D60\u9001\u989D\u5EA6 {amount}",
482
591
  "balance.runway": "\u9884\u8BA1\u53EF\u7528",
483
592
  "balance.runwayBreakdownTitle": "\u67E5\u770B\u9884\u8BA1\u53EF\u7528\u5929\u6570\u8BF4\u660E",
593
+ "balance.topUpTitle": "\u4F59\u989D\u504F\u4F4E\uFF0C\u524D\u5F80 DeepSeek \u5F00\u653E\u5E73\u53F0\u5145\u503C\uFF08\u65B0\u6807\u7B7E\u9875\u6253\u5F00\uFF09",
484
594
  "balance.days": "{count} \u5929",
485
595
  "balance.overYear": "> 365 \u5929",
486
596
  "balance.noRunway": "\u8FD1 {days} \u5929\u6CA1\u6709\u7528\u91CF\uFF0C\u65E0\u6CD5\u4F30\u7B97",
@@ -519,6 +629,9 @@ var zh = {
519
629
  "settings.widgetTabs": "\u5C55\u793A\u6D6E\u7A97\u7684\u9875\u9762",
520
630
  "settings.widgetTabsHint": "\u5217\u8868\u4F1A\u968F\u5DF2\u5B89\u88C5\u7684\u9875\u9762\u81EA\u52A8\u66F4\u65B0\u3002",
521
631
  "settings.widgetTabsEmpty": "\u5F53\u524D\u6CA1\u6709\u53EF\u914D\u7F6E\u7684\u4F1A\u8BDD\u9875\u9762\u3002",
632
+ "settings.peakPause": "\u9AD8\u5CF0\u6682\u505C",
633
+ "settings.peakPauseHint": "\u603B\u5F00\u5173\uFF1B\u5173\u95ED\u540E\u6240\u6709\u4F1A\u8BDD\u90FD\u4E0D\u6682\u505C\u3002\u6BCF\u4E2A\u4F1A\u8BDD\u7684\u5F00\u5173\u4F1A\u4FDD\u7559\uFF0C\u53EF\u968F\u65F6\u91CD\u65B0\u6253\u5F00\u3002",
634
+ "settings.peakPauseLoading": "\u6B63\u5728\u8BFB\u53D6\u603B\u5F00\u5173\u72B6\u6001\u2026",
522
635
  "settings.lowBalance": "\u4F59\u989D\u9884\u8B66\u7EBF",
523
636
  "settings.lowBalanceOn": "\u4F59\u989D\u4F4E\u4E8E {amount} {currency} \u65F6\uFF0C\u8FD9\u91CC\u548C\u60AC\u6D6E\u7A97\u90FD\u4F1A\u8F6C\u4E3A\u8B66\u793A\u8272\u3002\u586B 0 \u5173\u95ED\u3002",
524
637
  "settings.lowBalanceOff": "\u5DF2\u5173\u95ED\u4F59\u989D\u9884\u8B66\u3002\u586B\u4E00\u4E2A\u5927\u4E8E 0 \u7684\u6570\u5F00\u542F\u3002",
@@ -535,6 +648,32 @@ var zh = {
535
648
  "settings.tabDragAria": "\u62D6\u52A8\u300C\u989D\u5EA6\u300D\u8C03\u6574\u6807\u7B7E\u4F4D\u7F6E",
536
649
  "settings.tabMoveEarlier": "\u524D\u79FB",
537
650
  "settings.tabMoveLater": "\u540E\u79FB",
651
+ "breaker.title": "\u9AD8\u5CF0\u6682\u505C",
652
+ "breaker.desc": "\u5728\u5BF9\u8BDD\u8F93\u5165\u6846\u65C1\u6309\u4F1A\u8BDD\u5F00\u542F\uFF1B\u9AD8\u5CF0\u65F6\u6BB5\u81EA\u52A8\u6682\u505C\u8BE5\u4F1A\u8BDD\uFF0C18:00 \u7EE7\u7EED\u3002\u81EA\u52A8\u5316\u4EFB\u52A1\u4E0D\u53D7\u5F71\u54CD\uFF1B\u603B\u5F00\u5173\u5728\u300C\u8BBE\u7F6E\u300D\u5361\u91CC\u3002",
653
+ "breaker.pillOff": "\u9AD8\u5CF0\u6682\u505C \xB7 \u5173",
654
+ "breaker.pillOn": "\u9AD8\u5CF0\u6682\u505C \xB7 \u5F00",
655
+ "breaker.pillHolding": "\u5DF2\u6682\u505C \xB7 {clock}",
656
+ "breaker.pillAriaOff": "\u9AD8\u5CF0\u6682\u505C\uFF1A\u672C\u4F1A\u8BDD\u5173\u95ED\uFF0C\u70B9\u51FB\u5F00\u542F",
657
+ "breaker.pillAriaOn": "\u9AD8\u5CF0\u6682\u505C\uFF1A\u672C\u4F1A\u8BDD\u5F00\u542F\uFF0C{clock} \u7EE7\u7EED\uFF0C\u70B9\u51FB\u5173\u95ED",
658
+ "breaker.pillAriaHolding": "\u9AD8\u5CF0\u6682\u505C\uFF1A\u672C\u4F1A\u8BDD\u6B63\u5728\u6682\u505C\uFF0C{clock} \u7EE7\u7EED\uFF0C\u70B9\u51FB\u5173\u95ED",
659
+ "breaker.pillAriaMasterOff": "\u9AD8\u5CF0\u6682\u505C\uFF1A\u603B\u5F00\u5173\u5DF2\u5173\u95ED\uFF0C\u672C\u4F1A\u8BDD\u4E0D\u4F1A\u6682\u505C\uFF1B\u70B9\u51FB\u53EA\u5207\u6362\u672C\u4F1A\u8BDD\u5F00\u5173",
660
+ "breaker.pillTitleOff": "\u9AD8\u5CF0\u65F6\u6BB5\u6682\u505C\u672C\u4F1A\u8BDD\uFF1A\u5F53\u524D\u5173\u95ED\uFF0C\u70B9\u51FB\u5F00\u542F",
661
+ "breaker.pillTitleOn": "\u9AD8\u5CF0\u65F6\u6BB5\u6682\u505C\u672C\u4F1A\u8BDD\uFF1A\u5F53\u524D\u5F00\u542F\uFF0C{clock} \u7EE7\u7EED\uFF1B\u70B9\u51FB\u5173\u95ED",
662
+ "breaker.pillTitleHolding": "\u672C\u4F1A\u8BDD\u6B63\u5728\u6682\u505C\uFF0C{clock} \u7EE7\u7EED\uFF1B\u70B9\u51FB\u5173\u95ED",
663
+ "breaker.pillTitleMasterOff": "\u9AD8\u5CF0\u6682\u505C\u7684\u603B\u5F00\u5173\u5DF2\u5728\u989D\u5EA6\u9875\u5173\u95ED\uFF0C\u672C\u4F1A\u8BDD\u4E0D\u4F1A\u6682\u505C\uFF1B\u70B9\u51FB\u53EA\u5207\u6362\u672C\u4F1A\u8BDD\u5F00\u5173",
664
+ "breaker.actionError": "\u64CD\u4F5C\u5931\u8D25\uFF1A{error}",
665
+ "breaker.loading": "\u8F7D\u5165\u4E2D\u2026",
666
+ "breaker.statusUnavailable": "\u72B6\u6001\u6682\u4E0D\u53EF\u7528",
667
+ "breaker.statusMasterOff": "\u603B\u5F00\u5173\u5DF2\u5173\u95ED\uFF0C\u6240\u6709\u4F1A\u8BDD\u90FD\u4E0D\u6682\u505C\uFF1B\u53EF\u5728\u300C\u8BBE\u7F6E\u300D\u5361\u91CD\u65B0\u6253\u5F00",
668
+ "breaker.statusOffPeak": "\u95F2\u65F6 \xB7 \u4E0D\u6682\u505C\u4F1A\u8BDD",
669
+ "breaker.statusPeak": "\u9AD8\u5CF0\u65F6\u6BB5 \xB7 \u5F00\u542F\u7684\u4F1A\u8BDD\u4F1A\u81EA\u52A8\u6682\u505C",
670
+ "breaker.statusHolding": "\u5DF2\u6682\u505C {count} \u4E2A\u4F1A\u8BDD \xB7 {clock} \u7EE7\u7EED",
671
+ "breaker.statusHoldingNoClock": "\u5DF2\u6682\u505C {count} \u4E2A\u4F1A\u8BDD",
672
+ "breaker.release": "\u7ACB\u5373\u5168\u90E8\u7EE7\u7EED",
673
+ "breaker.releaseBusy": "\u6B63\u5728\u7EE7\u7EED\u2026",
674
+ "breaker.empty": "\u8FD8\u6CA1\u6709\u4F1A\u8BDD\u5F00\u542F\u9AD8\u5CF0\u6682\u505C\u3002\u5728\u5BF9\u8BDD\u8F93\u5165\u6846\u65C1\u7684\u300C\u9AD8\u5CF0\u6682\u505C\u300D\u5F00\u5173\u91CC\u5F00\u542F\u3002",
675
+ "breaker.rowDisableAria": "\u5173\u95ED\u300C{title}\u300D\u7684\u9AD8\u5CF0\u6682\u505C",
676
+ "breaker.holding": "\u6682\u505C\u4E2D \xB7 {clock}",
538
677
  "panel.caption": "\u8D26\u6237\u7EA7 \xB7 \u8DE8\u5168\u90E8\u4F1A\u8BDD",
539
678
  "panel.openAria": "\u6253\u5F00\u989D\u5EA6\u9875",
540
679
  "widget.updatedNow": "\u521A\u521A\u66F4\u65B0",
@@ -752,6 +891,7 @@ var en = {
752
891
  "balance.granted": "Granted {amount}",
753
892
  "balance.runway": "Estimated runway",
754
893
  "balance.runwayBreakdownTitle": "View estimated runway details",
894
+ "balance.topUpTitle": "Low balance \u2014 top up on the DeepSeek platform (opens a new tab)",
755
895
  "balance.days": "{count} days",
756
896
  "balance.overYear": "> 365 days",
757
897
  "balance.noRunway": "No usage in the last {days} days, so runway cannot be estimated",
@@ -790,6 +930,9 @@ var en = {
790
930
  "settings.widgetTabs": "Show widget on",
791
931
  "settings.widgetTabsHint": "Updates automatically as views are installed.",
792
932
  "settings.widgetTabsEmpty": "There are no configurable conversation views right now.",
933
+ "settings.peakPause": "Peak pause",
934
+ "settings.peakPauseHint": "Master switch; when off no conversation pauses. Per-conversation switches are kept and can be turned back on at any time.",
935
+ "settings.peakPauseLoading": "Reading the master switch\u2026",
793
936
  "settings.lowBalance": "Balance warning",
794
937
  "settings.lowBalanceOn": "This page and the widget turn amber below {amount} {currency}. Enter 0 to disable.",
795
938
  "settings.lowBalanceOff": "Balance warnings are off. Enter an amount above 0 to enable them.",
@@ -806,6 +949,32 @@ var en = {
806
949
  "settings.tabDragAria": "Drag Usage to change its tab position",
807
950
  "settings.tabMoveEarlier": "Move earlier",
808
951
  "settings.tabMoveLater": "Move later",
952
+ "breaker.title": "Peak pause",
953
+ "breaker.desc": "Turn it on per conversation beside the composer; during peak hours that session pauses and resumes at 18:00. Automated tasks are unaffected; the master switch is in Settings.",
954
+ "breaker.pillOff": "Peak pause \xB7 off",
955
+ "breaker.pillOn": "Peak pause \xB7 on",
956
+ "breaker.pillHolding": "Paused \xB7 {clock}",
957
+ "breaker.pillAriaOff": "Peak pause: off for this conversation, click to turn on",
958
+ "breaker.pillAriaOn": "Peak pause: on for this conversation, resumes at {clock}, click to turn off",
959
+ "breaker.pillAriaHolding": "Peak pause: this conversation is paused now, resumes at {clock}, click to turn off",
960
+ "breaker.pillAriaMasterOff": "Peak pause: the master switch is off, so this conversation will not pause; clicking only flips this conversation",
961
+ "breaker.pillTitleOff": "Pause this conversation during peak hours: currently off, click to turn on",
962
+ "breaker.pillTitleOn": "Pause this conversation during peak hours: currently on, resumes at {clock}; click to turn off",
963
+ "breaker.pillTitleHolding": "This conversation is paused and resumes at {clock}; click to turn off",
964
+ "breaker.pillTitleMasterOff": "The Peak pause master switch is off on the Usage page, so this conversation will not pause; clicking only flips this conversation",
965
+ "breaker.actionError": "Action failed: {error}",
966
+ "breaker.loading": "Loading\u2026",
967
+ "breaker.statusUnavailable": "Status unavailable",
968
+ "breaker.statusMasterOff": "Master switch is off; no conversation pauses. Turn it back on in Settings.",
969
+ "breaker.statusOffPeak": "Off-peak \xB7 nothing is paused",
970
+ "breaker.statusPeak": "Peak hours \xB7 conversations with it on pause automatically",
971
+ "breaker.statusHolding": "{count} conversations paused \xB7 resumes at {clock}",
972
+ "breaker.statusHoldingNoClock": "{count} conversations paused",
973
+ "breaker.release": "Resume all now",
974
+ "breaker.releaseBusy": "Resuming\u2026",
975
+ "breaker.empty": "No conversation has Peak pause on yet. Turn it on with the Peak pause switch beside the composer.",
976
+ "breaker.rowDisableAria": "Turn off Peak pause for {title}",
977
+ "breaker.holding": "Paused \xB7 {clock}",
809
978
  "panel.caption": "Account-wide \xB7 across all sessions",
810
979
  "panel.openAria": "Open Usage",
811
980
  "widget.updatedNow": "Updated just now",
@@ -837,17 +1006,24 @@ var en = {
837
1006
 
838
1007
  // src/client/i18n.tsx
839
1008
  var import_jsx_runtime2 = require("react/jsx-runtime");
1009
+ var INTL_LOCALES = {
1010
+ zh: "zh-CN",
1011
+ en: "en-US"
1012
+ };
1013
+ function intlLocale(locale) {
1014
+ return INTL_LOCALES[locale];
1015
+ }
840
1016
  var fallbackT = (key, params) => {
841
1017
  const template = zh[key];
842
1018
  if (params === void 0) return template;
843
1019
  return template.replace(/\{(\w+)\}/g, (match, name2) => name2 in params ? String(params[name2]) : match);
844
1020
  };
845
- var TranslationContext = (0, import_react2.createContext)(fallbackT);
1021
+ var TranslationContext = (0, import_react3.createContext)(fallbackT);
846
1022
  function LocaleProvider(props) {
847
1023
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TranslationContext.Provider, { value: props.t, children: props.children });
848
1024
  }
849
1025
  function useI18n() {
850
- const t = (0, import_react2.useContext)(TranslationContext);
1026
+ const t = (0, import_react3.useContext)(TranslationContext);
851
1027
  return { t, locale: t("meta.locale") === "en" ? "en" : "zh" };
852
1028
  }
853
1029
  function localizeApiError(message, t, fallback) {
@@ -878,11 +1054,11 @@ function localizeApiError(message, t, fallback) {
878
1054
  // src/client/charts.tsx
879
1055
  var import_jsx_runtime3 = require("react/jsx-runtime");
880
1056
  function useChartTip() {
881
- const wrapRef = (0, import_react3.useRef)(null);
882
- const [tip, setTip] = (0, import_react3.useState)(null);
883
- const [pinned, setPinned] = (0, import_react3.useState)(null);
884
- const pinnedRef = (0, import_react3.useRef)(null);
885
- const touchStart = (0, import_react3.useRef)(null);
1057
+ const wrapRef = (0, import_react4.useRef)(null);
1058
+ const [tip, setTip] = (0, import_react4.useState)(null);
1059
+ const [pinned, setPinned] = (0, import_react4.useState)(null);
1060
+ const pinnedRef = (0, import_react4.useRef)(null);
1061
+ const touchStart = (0, import_react4.useRef)(null);
886
1062
  const setPin = (next) => {
887
1063
  pinnedRef.current = next;
888
1064
  setPinned(next);
@@ -921,7 +1097,7 @@ function useChartTip() {
921
1097
  if (next === null) setTip(null);
922
1098
  else showAt(e.currentTarget, text, e.clientX, e.clientY);
923
1099
  };
924
- (0, import_react3.useEffect)(() => {
1100
+ (0, import_react4.useEffect)(() => {
925
1101
  if (pinned === null) return void 0;
926
1102
  const onDocPointerDown = (e) => {
927
1103
  const wrap = wrapRef.current;
@@ -935,9 +1111,9 @@ function useChartTip() {
935
1111
  return { wrapRef, tip, pinned, show, showFocused, hide, onPointStart, onPointEnd };
936
1112
  }
937
1113
  function useRovingChartPoints(count, initial, horizontalStep = 1) {
938
- const refs = (0, import_react3.useRef)([]);
939
- const [active, setActive] = (0, import_react3.useState)(initial);
940
- (0, import_react3.useEffect)(() => setActive(Math.min(Math.max(0, initial), Math.max(0, count - 1))), [count, initial]);
1114
+ const refs = (0, import_react4.useRef)([]);
1115
+ const [active, setActive] = (0, import_react4.useState)(initial);
1116
+ (0, import_react4.useEffect)(() => setActive(Math.min(Math.max(0, initial), Math.max(0, count - 1))), [count, initial]);
941
1117
  const onKeyDown = (event, index) => {
942
1118
  if (!["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) return;
943
1119
  event.preventDefault();
@@ -962,10 +1138,10 @@ var MODEL_COLORS = ["#4176e6", "#2da44e", "#e16f24", "#8250df", "#bf8700", "#cf2
962
1138
  function Bars(props) {
963
1139
  const { data, height = 120, labelEvery = 1, minWidth } = props;
964
1140
  const { wrapRef, tip, pinned, show, showFocused, hide, onPointStart, onPointEnd } = useChartTip();
965
- const scrollRef = (0, import_react3.useRef)(null);
1141
+ const scrollRef = (0, import_react4.useRef)(null);
966
1142
  const initialFocus = lastPopulatedIndex(data.map((datum) => datum.value));
967
1143
  const roving = useRovingChartPoints(data.length, initialFocus);
968
- (0, import_react3.useEffect)(() => {
1144
+ (0, import_react4.useEffect)(() => {
969
1145
  const scroll = scrollRef.current;
970
1146
  if (scroll !== null && minWidth !== void 0) scroll.scrollLeft = scroll.scrollWidth;
971
1147
  }, [data.length, minWidth]);
@@ -1007,11 +1183,11 @@ function Bars(props) {
1007
1183
  function GroupedBars(props) {
1008
1184
  const { series, height = 120, labelEvery = 1, minWidth } = props;
1009
1185
  const { wrapRef, tip, pinned, show, showFocused, hide, onPointStart, onPointEnd } = useChartTip();
1010
- const scrollRef = (0, import_react3.useRef)(null);
1186
+ const scrollRef = (0, import_react4.useRef)(null);
1011
1187
  const count = series.length > 0 ? series[0].bars.length : 0;
1012
1188
  const pointValues = Array.from({ length: count }, (_, i) => series.map((s) => s.bars[i]?.value ?? 0)).flat();
1013
1189
  const roving = useRovingChartPoints(pointValues.length, lastPopulatedIndex(pointValues));
1014
- (0, import_react3.useEffect)(() => {
1190
+ (0, import_react4.useEffect)(() => {
1015
1191
  const scroll = scrollRef.current;
1016
1192
  if (scroll !== null && minWidth !== void 0) scroll.scrollLeft = scroll.scrollWidth;
1017
1193
  }, [count, minWidth]);
@@ -1102,7 +1278,7 @@ function Heatmap(props) {
1102
1278
  if (datum === null) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "dq-heat-cell dq-heat-cell--pad" }, d);
1103
1279
  const text = t("chart.tooltip", {
1104
1280
  head: datum.date,
1105
- tokens: datum.total.toLocaleString(locale === "en" ? "en-US" : "zh-CN"),
1281
+ tokens: datum.total.toLocaleString(intlLocale(locale)),
1106
1282
  cost: datum.cost.toFixed(2),
1107
1283
  calls: t("common.calls", { count: datum.calls })
1108
1284
  });
@@ -1213,9 +1389,9 @@ function getActiveConversationViewId(frame, viewIds) {
1213
1389
  }
1214
1390
 
1215
1391
  // src/client/export.ts
1216
- var BEIJING_OFFSET_MS2 = 8 * 60 * 60 * 1e3;
1392
+ var BEIJING_OFFSET_MS3 = 8 * 60 * 60 * 1e3;
1217
1393
  function exportDateStamp(nowMs = Date.now()) {
1218
- const date = new Date(nowMs + BEIJING_OFFSET_MS2);
1394
+ const date = new Date(nowMs + BEIJING_OFFSET_MS3);
1219
1395
  return [
1220
1396
  date.getUTCFullYear(),
1221
1397
  String(date.getUTCMonth() + 1).padStart(2, "0"),
@@ -1331,19 +1507,19 @@ var isBoolean = (value) => typeof value === "boolean";
1331
1507
 
1332
1508
  // src/client/store.ts
1333
1509
  function createStore(key, isValid, fallback) {
1334
- let current = loadPref(key, isValid, fallback);
1335
- const listeners = /* @__PURE__ */ new Set();
1510
+ let current2 = loadPref(key, isValid, fallback);
1511
+ const listeners2 = /* @__PURE__ */ new Set();
1336
1512
  return {
1337
- get: () => current,
1513
+ get: () => current2,
1338
1514
  set: (value) => {
1339
- current = value;
1515
+ current2 = value;
1340
1516
  savePref(key, value);
1341
- for (const listener of [...listeners]) listener();
1517
+ for (const listener of [...listeners2]) listener();
1342
1518
  },
1343
1519
  subscribe: (fn) => {
1344
- listeners.add(fn);
1520
+ listeners2.add(fn);
1345
1521
  return () => {
1346
- listeners.delete(fn);
1522
+ listeners2.delete(fn);
1347
1523
  };
1348
1524
  }
1349
1525
  };
@@ -1395,9 +1571,27 @@ function viewOrderForIndex(entries, index, selfId = "balance", fallback = 20) {
1395
1571
  return previous >= next ? previous + 0.5 : (previous + next) / 2;
1396
1572
  }
1397
1573
 
1574
+ // src/client/top-up.ts
1575
+ var TOP_UP_URL = "https://platform.deepseek.com/top_up";
1576
+ var TOP_UP_FLOOR = 10;
1577
+ function needsTopUp(balance, warningLine) {
1578
+ if (!Number.isFinite(balance)) return false;
1579
+ return balance < TOP_UP_FLOOR || warningLine > 0 && balance < warningLine;
1580
+ }
1581
+
1398
1582
  // src/client/dashboard.tsx
1399
1583
  var import_jsx_runtime4 = require("react/jsx-runtime");
1400
1584
  var COMPOSER_FALLBACK_PX = 126;
1585
+ function usageTitleMap(usage) {
1586
+ const titles = /* @__PURE__ */ new Map();
1587
+ for (const session of usage?.sessions ?? []) titles.set(session.id, session.title);
1588
+ for (const window2 of usage?.windows ?? []) {
1589
+ for (const session of window2.sessions) {
1590
+ if (!titles.has(session.id)) titles.set(session.id, session.title);
1591
+ }
1592
+ }
1593
+ return titles;
1594
+ }
1401
1595
  function Skel(props) {
1402
1596
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-skel", style: { width: `${props.w}px`, height: `${props.h}px` } });
1403
1597
  }
@@ -1447,12 +1641,12 @@ function HeatmapSkeleton() {
1447
1641
  }
1448
1642
  function Delta(props) {
1449
1643
  const { t } = useI18n();
1450
- const { current, previous, label } = props;
1644
+ const { current: current2, previous, label } = props;
1451
1645
  if (previous <= 0) {
1452
- if (current <= 0) return null;
1646
+ if (current2 <= 0) return null;
1453
1647
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-delta dq-delta--new", title: t("delta.noBaselineTitle", { label }), children: t("delta.noBaseline") });
1454
1648
  }
1455
- const percent = Math.round((current - previous) / previous * 100);
1649
+ const percent = Math.round((current2 - previous) / previous * 100);
1456
1650
  const title = `${label} \xA5${fmt(previous)}`;
1457
1651
  if (percent === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-delta dq-delta--flat", title, children: t("delta.flat") });
1458
1652
  const up = percent > 0;
@@ -1540,7 +1734,7 @@ function PricingNote(props) {
1540
1734
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.inputCacheMiss") }),
1541
1735
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("th", { children: t("pricing.output") })
1542
1736
  ] }) }),
1543
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("tbody", { children: pricing.tiers.map((tier) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react4.Fragment, { children: [
1737
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("tbody", { children: pricing.tiers.map((tier) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react5.Fragment, { children: [
1544
1738
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("tr", { children: [
1545
1739
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { rowSpan: 2, children: tier.model }),
1546
1740
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("td", { children: t("pricing.peak") }),
@@ -1569,7 +1763,7 @@ function CoverageDiagnostics(props) {
1569
1763
  const { t, locale } = useI18n();
1570
1764
  const { coverage } = props;
1571
1765
  const hasGaps = coverage.failedSessions > 0 || coverage.skippedRecords > 0 || coverage.scannedSessions < coverage.listedSessions;
1572
- const coverageTime = new Intl.DateTimeFormat(locale === "en" ? "en-US" : "zh-CN", {
1766
+ const coverageTime = new Intl.DateTimeFormat(intlLocale(locale), {
1573
1767
  timeZone: "Asia/Shanghai",
1574
1768
  year: "numeric",
1575
1769
  month: "numeric",
@@ -1627,13 +1821,13 @@ function Link(props) {
1627
1821
  }
1628
1822
  function ExportMenu(props) {
1629
1823
  const { t } = useI18n();
1630
- const [open, setOpen] = (0, import_react4.useState)(false);
1631
- const [done, setDone] = (0, import_react4.useState)(false);
1632
- const rootRef = (0, import_react4.useRef)(null);
1633
- const triggerRef = (0, import_react4.useRef)(null);
1634
- const menuRef = (0, import_react4.useRef)(null);
1635
- const doneTimerRef = (0, import_react4.useRef)(null);
1636
- (0, import_react4.useEffect)(() => {
1824
+ const [open, setOpen] = (0, import_react5.useState)(false);
1825
+ const [done, setDone] = (0, import_react5.useState)(false);
1826
+ const rootRef = (0, import_react5.useRef)(null);
1827
+ const triggerRef = (0, import_react5.useRef)(null);
1828
+ const menuRef = (0, import_react5.useRef)(null);
1829
+ const doneTimerRef = (0, import_react5.useRef)(null);
1830
+ (0, import_react5.useEffect)(() => {
1637
1831
  if (!open) return;
1638
1832
  const onDown = (event) => {
1639
1833
  if (rootRef.current !== null && !rootRef.current.contains(event.target)) setOpen(false);
@@ -1651,7 +1845,7 @@ function ExportMenu(props) {
1651
1845
  document.removeEventListener("keydown", onKey);
1652
1846
  };
1653
1847
  }, [open]);
1654
- (0, import_react4.useEffect)(() => () => {
1848
+ (0, import_react5.useEffect)(() => () => {
1655
1849
  if (doneTimerRef.current !== null) window.clearTimeout(doneTimerRef.current);
1656
1850
  }, []);
1657
1851
  const finish = () => {
@@ -1711,7 +1905,7 @@ function SessionUsageCard(props) {
1711
1905
  const { t, locale } = useI18n();
1712
1906
  const { data } = props;
1713
1907
  if (data.calls === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("sessionUsage.empty") });
1714
- const rangeTime = new Intl.DateTimeFormat(locale === "en" ? "en-US" : "zh-CN", {
1908
+ const rangeTime = new Intl.DateTimeFormat(intlLocale(locale), {
1715
1909
  timeZone: "Asia/Shanghai",
1716
1910
  year: "numeric",
1717
1911
  month: "numeric",
@@ -1925,8 +2119,8 @@ function SessionDetail(props) {
1925
2119
  function SessionRanking(props) {
1926
2120
  const { t, locale } = useI18n();
1927
2121
  const { sessions, count, totalCost } = props;
1928
- const [details, setDetails] = (0, import_react4.useState)({});
1929
- const startedRef = (0, import_react4.useRef)(/* @__PURE__ */ new Set());
2122
+ const [details, setDetails] = (0, import_react5.useState)({});
2123
+ const startedRef = (0, import_react5.useRef)(/* @__PURE__ */ new Set());
1930
2124
  const loadDetail = (id) => {
1931
2125
  if (startedRef.current.has(id)) return;
1932
2126
  startedRef.current.add(id);
@@ -2027,11 +2221,11 @@ function ModelRanking(props) {
2027
2221
  function ModelPicker(props) {
2028
2222
  const { t, locale } = useI18n();
2029
2223
  const { models } = props;
2030
- const [open, setOpen] = (0, import_react4.useState)(false);
2031
- const rootRef = (0, import_react4.useRef)(null);
2032
- const triggerRef = (0, import_react4.useRef)(null);
2033
- const menuRef = (0, import_react4.useRef)(null);
2034
- (0, import_react4.useEffect)(() => {
2224
+ const [open, setOpen] = (0, import_react5.useState)(false);
2225
+ const rootRef = (0, import_react5.useRef)(null);
2226
+ const triggerRef = (0, import_react5.useRef)(null);
2227
+ const menuRef = (0, import_react5.useRef)(null);
2228
+ (0, import_react5.useEffect)(() => {
2035
2229
  if (!open) return;
2036
2230
  const onDown = (e) => {
2037
2231
  if (rootRef.current !== null && !rootRef.current.contains(e.target)) setOpen(false);
@@ -2133,45 +2327,55 @@ function BalanceDashboard(props) {
2133
2327
  const cachedBalance = getCachedBalance();
2134
2328
  const cachedUsage = getCachedUsage();
2135
2329
  const cachedUsageAt = getCachedUsageAt();
2136
- const [balance, setBalance] = (0, import_react4.useState)(cachedBalance?.data ?? null);
2137
- const [usage, setUsage] = (0, import_react4.useState)(cachedUsage?.data ?? null);
2138
- const [error, setError] = (0, import_react4.useState)(null);
2139
- const [notice, setNotice] = (0, import_react4.useState)(null);
2140
- const [loadingBalance, setLoadingBalance] = (0, import_react4.useState)(cachedBalance === null);
2141
- const [loadingUsage, setLoadingUsage] = (0, import_react4.useState)(cachedUsage === null);
2142
- const [refreshing, setRefreshing] = (0, import_react4.useState)(false);
2143
- const [usageUpdatedAt, setUsageUpdatedAt] = (0, import_react4.useState)(cachedUsageAt);
2144
- const [syncState, setSyncState] = (0, import_react4.useState)(cachedUsageAt === null ? "syncing" : "cached");
2145
- const [freshnessNow, setFreshnessNow] = (0, import_react4.useState)(Date.now());
2146
- const hadDataRef = (0, import_react4.useRef)(cachedBalance !== null || cachedUsage !== null);
2147
- const [widgetOn, setWidgetOn] = (0, import_react4.useState)(widgetVisibleStore.get());
2148
- const [widgetTabIds, setWidgetTabIds] = (0, import_react4.useState)(widgetTabIdsStore.get());
2149
- const [viewTabs, setViewTabs] = (0, import_react4.useState)(() => props.views.list());
2150
- (0, import_react4.useEffect)(() => props.views.subscribe(() => setViewTabs(props.views.list())), [props.views]);
2151
- const [lowBalance, setLowBalance] = (0, import_react4.useState)(lowBalanceStore.get());
2152
- const [monthlyBudget, setMonthlyBudget] = (0, import_react4.useState)(monthlyBudgetStore.get());
2153
- const [barMode, setBarMode] = (0, import_react4.useState)("daily");
2154
- const [selectedModels, setSelectedModels] = (0, import_react4.useState)([]);
2155
- const [windowDays, setWindowDays] = (0, import_react4.useState)(usageWindowStore.get());
2156
- const [chartMetric, setChartMetric] = (0, import_react4.useState)(chartMetricStore.get());
2157
- const [sidebarEntry, setSidebarEntry] = (0, import_react4.useState)(sidebarEntryStore.get());
2158
- (0, import_react4.useEffect)(() => sidebarEntryStore.subscribe(() => setSidebarEntry(sidebarEntryStore.get())), []);
2159
- const [tabDragging, setTabDragging] = (0, import_react4.useState)(false);
2160
- const [tabDropTarget, setTabDropTarget] = (0, import_react4.useState)(null);
2161
- const [retainedVision, setRetainedVision] = (0, import_react4.useState)(null);
2162
- const [runwayHovering, setRunwayHovering] = (0, import_react4.useState)(false);
2163
- const [runwayManuallyOpened, setRunwayManuallyOpened] = (0, import_react4.useState)(false);
2164
- const [remainingHovering, setRemainingHovering] = (0, import_react4.useState)(false);
2165
- const [remainingManuallyOpened, setRemainingManuallyOpened] = (0, import_react4.useState)(false);
2166
- const budgetInputRef = (0, import_react4.useRef)(null);
2167
- const rootRef = (0, import_react4.useRef)(null);
2168
- const [composerHeight, setComposerHeight] = (0, import_react4.useState)(null);
2169
- const [composerWidth, setComposerWidth] = (0, import_react4.useState)(null);
2170
- const [sessionUsage, setSessionUsage] = (0, import_react4.useState)(null);
2171
- const [loadingSessionUsage, setLoadingSessionUsage] = (0, import_react4.useState)(props.sessionId !== void 0 && props.sessionId !== "");
2172
- const [sessionUsageError, setSessionUsageError] = (0, import_react4.useState)(null);
2173
- const sessionUsageIdRef = (0, import_react4.useRef)(null);
2174
- (0, import_react4.useLayoutEffect)(() => {
2330
+ const [balance, setBalance] = (0, import_react5.useState)(cachedBalance?.data ?? null);
2331
+ const [usage, setUsage] = (0, import_react5.useState)(cachedUsage?.data ?? null);
2332
+ const [error, setError] = (0, import_react5.useState)(null);
2333
+ const [notice, setNotice] = (0, import_react5.useState)(null);
2334
+ const [loadingBalance, setLoadingBalance] = (0, import_react5.useState)(cachedBalance === null);
2335
+ const [loadingUsage, setLoadingUsage] = (0, import_react5.useState)(cachedUsage === null);
2336
+ const [refreshing, setRefreshing] = (0, import_react5.useState)(false);
2337
+ const [usageUpdatedAt, setUsageUpdatedAt] = (0, import_react5.useState)(cachedUsageAt);
2338
+ const [syncState, setSyncState] = (0, import_react5.useState)(cachedUsageAt === null ? "syncing" : "cached");
2339
+ const [freshnessNow, setFreshnessNow] = (0, import_react5.useState)(Date.now());
2340
+ const hadDataRef = (0, import_react5.useRef)(cachedBalance !== null || cachedUsage !== null);
2341
+ const [widgetOn, setWidgetOn] = (0, import_react5.useState)(widgetVisibleStore.get());
2342
+ const [widgetTabIds, setWidgetTabIds] = (0, import_react5.useState)(widgetTabIdsStore.get());
2343
+ const [viewTabs, setViewTabs] = (0, import_react5.useState)(() => props.views.list());
2344
+ (0, import_react5.useEffect)(() => props.views.subscribe(() => setViewTabs(props.views.list())), [props.views]);
2345
+ const [lowBalance, setLowBalance] = (0, import_react5.useState)(lowBalanceStore.get());
2346
+ const [monthlyBudget, setMonthlyBudget] = (0, import_react5.useState)(monthlyBudgetStore.get());
2347
+ const breakerData = useBreakerData();
2348
+ const [breakerLoadError, setBreakerLoadError] = (0, import_react5.useState)(null);
2349
+ const [breakerActionError, setBreakerActionError] = (0, import_react5.useState)(null);
2350
+ const [breakerReleasing, setBreakerReleasing] = (0, import_react5.useState)(false);
2351
+ const [breakerSavingId, setBreakerSavingId] = (0, import_react5.useState)(null);
2352
+ const [breakerSavingMaster, setBreakerSavingMaster] = (0, import_react5.useState)(false);
2353
+ const [breakerTitles, setBreakerTitles] = (0, import_react5.useState)({});
2354
+ const breakerTitleRequests = (0, import_react5.useRef)(/* @__PURE__ */ new Set());
2355
+ const breakerMutationSeq = (0, import_react5.useRef)(0);
2356
+ const breakerMounted = (0, import_react5.useRef)(true);
2357
+ const [barMode, setBarMode] = (0, import_react5.useState)("daily");
2358
+ const [selectedModels, setSelectedModels] = (0, import_react5.useState)([]);
2359
+ const [windowDays, setWindowDays] = (0, import_react5.useState)(usageWindowStore.get());
2360
+ const [chartMetric, setChartMetric] = (0, import_react5.useState)(chartMetricStore.get());
2361
+ const [sidebarEntry, setSidebarEntry] = (0, import_react5.useState)(sidebarEntryStore.get());
2362
+ (0, import_react5.useEffect)(() => sidebarEntryStore.subscribe(() => setSidebarEntry(sidebarEntryStore.get())), []);
2363
+ const [tabDragging, setTabDragging] = (0, import_react5.useState)(false);
2364
+ const [tabDropTarget, setTabDropTarget] = (0, import_react5.useState)(null);
2365
+ const [retainedVision, setRetainedVision] = (0, import_react5.useState)(null);
2366
+ const [runwayHovering, setRunwayHovering] = (0, import_react5.useState)(false);
2367
+ const [runwayManuallyOpened, setRunwayManuallyOpened] = (0, import_react5.useState)(false);
2368
+ const [remainingHovering, setRemainingHovering] = (0, import_react5.useState)(false);
2369
+ const [remainingManuallyOpened, setRemainingManuallyOpened] = (0, import_react5.useState)(false);
2370
+ const budgetInputRef = (0, import_react5.useRef)(null);
2371
+ const rootRef = (0, import_react5.useRef)(null);
2372
+ const [composerHeight, setComposerHeight] = (0, import_react5.useState)(null);
2373
+ const [composerWidth, setComposerWidth] = (0, import_react5.useState)(null);
2374
+ const [sessionUsage, setSessionUsage] = (0, import_react5.useState)(null);
2375
+ const [loadingSessionUsage, setLoadingSessionUsage] = (0, import_react5.useState)(props.sessionId !== void 0 && props.sessionId !== "");
2376
+ const [sessionUsageError, setSessionUsageError] = (0, import_react5.useState)(null);
2377
+ const sessionUsageIdRef = (0, import_react5.useRef)(null);
2378
+ (0, import_react5.useLayoutEffect)(() => {
2175
2379
  const node = rootRef.current;
2176
2380
  if (node === null) return;
2177
2381
  const measure = () => {
@@ -2202,13 +2406,13 @@ function BalanceDashboard(props) {
2202
2406
  window.removeEventListener("resize", measure);
2203
2407
  };
2204
2408
  }, []);
2205
- (0, import_react4.useLayoutEffect)(() => {
2409
+ (0, import_react5.useLayoutEffect)(() => {
2206
2410
  const node = rootRef.current;
2207
2411
  if (node === null) return;
2208
2412
  const scrollport = node.closest("[data-conversation-scroll]");
2209
2413
  if (scrollport instanceof HTMLElement) scrollport.scrollTop = 0;
2210
2414
  }, [props.sessionId]);
2211
- (0, import_react4.useLayoutEffect)(() => {
2415
+ (0, import_react5.useLayoutEffect)(() => {
2212
2416
  quotaViewActiveStore.set(true);
2213
2417
  return () => quotaViewActiveStore.set(false);
2214
2418
  }, []);
@@ -2282,10 +2486,10 @@ function BalanceDashboard(props) {
2282
2486
  if (sessionUsageIdRef.current === id) setLoadingSessionUsage(false);
2283
2487
  }
2284
2488
  };
2285
- (0, import_react4.useEffect)(() => {
2489
+ (0, import_react5.useEffect)(() => {
2286
2490
  void load(false);
2287
2491
  }, []);
2288
- (0, import_react4.useEffect)(() => {
2492
+ (0, import_react5.useEffect)(() => {
2289
2493
  const id = props.sessionId;
2290
2494
  sessionUsageIdRef.current = id ?? null;
2291
2495
  if (id === void 0 || id === "") {
@@ -2299,7 +2503,57 @@ function BalanceDashboard(props) {
2299
2503
  setLoadingSessionUsage(true);
2300
2504
  void loadSessionUsage(id, false);
2301
2505
  }, [props.sessionId]);
2302
- (0, import_react4.useEffect)(() => {
2506
+ (0, import_react5.useEffect)(() => {
2507
+ let cancelled = false;
2508
+ let timer;
2509
+ let fast = false;
2510
+ const pull = async () => {
2511
+ const seqAtStart = breakerMutationSeq.current;
2512
+ const res = await fetchBreaker();
2513
+ if (cancelled) return;
2514
+ if (!breakerIsUsable(res)) {
2515
+ setBreakerLoadError(res.error ?? "");
2516
+ return;
2517
+ }
2518
+ fast = res.data.enabled && res.data.peak && res.data.sessions.length > 0;
2519
+ setBreakerLoadError(null);
2520
+ if (seqAtStart !== breakerMutationSeq.current) return;
2521
+ publishBreakerData(res.data);
2522
+ };
2523
+ const loop = async () => {
2524
+ await pull();
2525
+ if (cancelled) return;
2526
+ timer = window.setTimeout(() => {
2527
+ void loop();
2528
+ }, fast ? 5e3 : 3e4);
2529
+ };
2530
+ void loop();
2531
+ return () => {
2532
+ cancelled = true;
2533
+ if (timer !== void 0) window.clearTimeout(timer);
2534
+ };
2535
+ }, []);
2536
+ (0, import_react5.useEffect)(() => {
2537
+ breakerMounted.current = true;
2538
+ return () => {
2539
+ breakerMounted.current = false;
2540
+ };
2541
+ }, []);
2542
+ (0, import_react5.useEffect)(() => {
2543
+ if (breakerData === null) return;
2544
+ const known = usageTitleMap(usage);
2545
+ for (const sessionId of breakerData.sessions) {
2546
+ if (known.has(sessionId) || breakerTitles[sessionId] !== void 0) continue;
2547
+ if (breakerTitleRequests.current.has(sessionId)) continue;
2548
+ breakerTitleRequests.current.add(sessionId);
2549
+ void fetchSessionUsage(sessionId).then((res) => {
2550
+ if (!breakerMounted.current) return;
2551
+ const title = res.ok === true ? res.data?.title?.trim() ?? "" : "";
2552
+ setBreakerTitles((current2) => ({ ...current2, [sessionId]: title }));
2553
+ });
2554
+ }
2555
+ }, [breakerData, usage, breakerTitles]);
2556
+ (0, import_react5.useEffect)(() => {
2303
2557
  const timer = window.setInterval(() => setFreshnessNow(Date.now()), 3e4);
2304
2558
  return () => window.clearInterval(timer);
2305
2559
  }, []);
@@ -2323,6 +2577,66 @@ function BalanceDashboard(props) {
2323
2577
  setMonthlyBudget(value);
2324
2578
  monthlyBudgetStore.set(value);
2325
2579
  };
2580
+ const disableBreakerSession = async (sessionId) => {
2581
+ if (breakerSavingId !== null || breakerReleasing || breakerSavingMaster) return;
2582
+ const previous = breakerData;
2583
+ publishBreakerData(withSessionToggled(previous, sessionId, false));
2584
+ setBreakerActionError(null);
2585
+ setBreakerSavingId(sessionId);
2586
+ const seq = ++breakerMutationSeq.current;
2587
+ try {
2588
+ const res = await updateBreaker({ sessionId, enabled: false });
2589
+ if (seq !== breakerMutationSeq.current) return;
2590
+ if (breakerIsUsable(res)) {
2591
+ publishBreakerData(res.data);
2592
+ setBreakerLoadError(null);
2593
+ } else {
2594
+ publishBreakerData(previous);
2595
+ setBreakerActionError(res.error ?? "");
2596
+ }
2597
+ } finally {
2598
+ setBreakerSavingId(null);
2599
+ }
2600
+ };
2601
+ const toggleBreakerMaster = async (enabled) => {
2602
+ if (breakerSavingMaster || breakerSavingId !== null || breakerReleasing) return;
2603
+ const previous = breakerData;
2604
+ publishBreakerData(withMasterToggled(previous, enabled));
2605
+ setBreakerActionError(null);
2606
+ setBreakerSavingMaster(true);
2607
+ const seq = ++breakerMutationSeq.current;
2608
+ try {
2609
+ const res = await updateBreaker({ enabled });
2610
+ if (seq !== breakerMutationSeq.current) return;
2611
+ if (breakerIsUsable(res)) {
2612
+ publishBreakerData(res.data);
2613
+ setBreakerLoadError(null);
2614
+ } else {
2615
+ publishBreakerData(previous);
2616
+ setBreakerActionError(res.error ?? "");
2617
+ }
2618
+ } finally {
2619
+ setBreakerSavingMaster(false);
2620
+ }
2621
+ };
2622
+ const releaseBreakerNow = async () => {
2623
+ if (breakerReleasing || breakerSavingId !== null || breakerSavingMaster) return;
2624
+ setBreakerActionError(null);
2625
+ setBreakerReleasing(true);
2626
+ const seq = ++breakerMutationSeq.current;
2627
+ try {
2628
+ const res = await releaseBreaker();
2629
+ if (seq !== breakerMutationSeq.current) return;
2630
+ if (breakerIsUsable(res)) {
2631
+ publishBreakerData(res.data);
2632
+ setBreakerLoadError(null);
2633
+ } else {
2634
+ setBreakerActionError(res.error ?? "");
2635
+ }
2636
+ } finally {
2637
+ setBreakerReleasing(false);
2638
+ }
2639
+ };
2326
2640
  const changeWindowDays = (value) => {
2327
2641
  setWindowDays(value);
2328
2642
  usageWindowStore.set(value);
@@ -2401,6 +2715,7 @@ function BalanceDashboard(props) {
2401
2715
  const avgDailyCost = recentDays.length > 0 ? recentDays.reduce((sum, d) => sum + d.cost, 0) / recentDays.length : 0;
2402
2716
  const balanceValue = primary !== null ? Number(primary.total) : Number.NaN;
2403
2717
  const balanceLow = lowBalance > 0 && Number.isFinite(balanceValue) && balanceValue < lowBalance;
2718
+ const topUpPrompt = needsTopUp(balanceValue, lowBalance);
2404
2719
  const daysLeft = avgDailyCost > 0 && Number.isFinite(balanceValue) ? balanceValue / avgDailyCost : null;
2405
2720
  const daysLeftText = daysLeft === null ? "\u2014" : daysLeft >= 365 ? t("balance.overYear") : t("balance.days", { count: daysLeft < 10 ? daysLeft.toFixed(1) : Math.round(daysLeft) });
2406
2721
  const runwayTitle = daysLeft === null ? t("balance.noRunway", { days: recentDays.length || 7 }) : t("balance.runwayTitle", { days: recentDays.length, cost: fmt(avgDailyCost) });
@@ -2417,7 +2732,7 @@ function BalanceDashboard(props) {
2417
2732
  costTotal: window2.totals.cost
2418
2733
  });
2419
2734
  const liveVision = visionOpen && activeWindow !== null && visionStats !== void 0 ? visionPayload(activeWindow, visionStats, activeWindowName) : null;
2420
- (0, import_react4.useEffect)(() => {
2735
+ (0, import_react5.useEffect)(() => {
2421
2736
  if (visionOpen && activeWindow !== null && visionStats !== void 0) {
2422
2737
  setRetainedVision(visionPayload(activeWindow, visionStats, activeWindowName));
2423
2738
  return;
@@ -2501,6 +2816,15 @@ function BalanceDashboard(props) {
2501
2816
  { error: localizeApiError(notice.error, t, "error.query") }
2502
2817
  );
2503
2818
  const errorText = error === null ? null : localizeApiError(error, t, "error.query");
2819
+ const breakerPhaseNow = breakerLoadError !== null ? "unavailable" : breakerPhase(breakerData);
2820
+ const breakerResumeClock = beijingClock(breakerData?.resumeAt ?? null);
2821
+ const breakerHeldCount = breakerData?.holding.length ?? 0;
2822
+ const breakerStatusText = breakerPhaseNow === "unavailable" ? t("breaker.statusUnavailable") : breakerPhaseNow === "masterOff" ? t("breaker.statusMasterOff") : breakerPhaseNow === "peakHolding" ? breakerResumeClock === null ? t("breaker.statusHoldingNoClock", { count: breakerHeldCount }) : t("breaker.statusHolding", { count: breakerHeldCount, clock: breakerResumeClock }) : breakerPhaseNow === "peak" ? t("breaker.statusPeak") : t("breaker.statusOffPeak");
2823
+ const breakerUsageTitles = usageTitleMap(usage);
2824
+ const breakerTitleFor = (sessionId) => {
2825
+ const title = breakerUsageTitles.get(sessionId) ?? breakerTitles[sessionId];
2826
+ return title !== void 0 && title !== "" ? title : shortSessionId(sessionId);
2827
+ };
2504
2828
  const composerSafeAreaStyle = {
2505
2829
  "--dq-composer-h": `${composerHeight ?? COMPOSER_FALLBACK_PX}px`,
2506
2830
  // Only set once measured; .dq-balance's max-width falls back to 860px
@@ -2517,7 +2841,7 @@ function BalanceDashboard(props) {
2517
2841
  className: `dq-sync dq-sync--${syncState}`,
2518
2842
  role: "status",
2519
2843
  "aria-live": "polite",
2520
- title: usageUpdatedAt === null ? t("status.neverSynced") : t("status.usageTime", { time: new Date(usageUpdatedAt).toLocaleString(locale === "en" ? "en-US" : "zh-CN", { timeZone: "Asia/Shanghai" }) }),
2844
+ title: usageUpdatedAt === null ? t("status.neverSynced") : t("status.usageTime", { time: new Date(usageUpdatedAt).toLocaleString(intlLocale(locale), { timeZone: "Asia/Shanghai" }) }),
2521
2845
  children: [
2522
2846
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-sync-dot", "aria-hidden": "true" }),
2523
2847
  syncStatusText(syncState, usageUpdatedAt, freshnessNow, t)
@@ -2551,7 +2875,7 @@ function BalanceDashboard(props) {
2551
2875
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("strong", { children: t("alert.lowTitle", { balance: fmt(balanceValue), currency: primary?.currency, threshold: fmt(lowBalance) }) }),
2552
2876
  daysLeft !== null && t("alert.runway", { days: daysLeftText }),
2553
2877
  t("common.period"),
2554
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("a", { className: "dq-alert-link", href: "https://platform.deepseek.com/top_up", target: "_blank", rel: "noreferrer", children: t("alert.topUp") })
2878
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("a", { className: "dq-alert-link", href: TOP_UP_URL, target: "_blank", rel: "noreferrer", children: t("alert.topUp") })
2555
2879
  ] })
2556
2880
  ] }),
2557
2881
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card dq-card--hero", children: [
@@ -2592,29 +2916,35 @@ function BalanceDashboard(props) {
2592
2916
  ] }),
2593
2917
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-stat", children: [
2594
2918
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-label", children: t("balance.runway") }),
2595
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2596
- "details",
2597
- {
2598
- className: `dq-stat-value dq-runway${daysLeft !== null && daysLeft < 3 ? " dq-stat-value--bad" : ""}`,
2599
- open: runwayHovering || runwayManuallyOpened,
2600
- onPointerEnter: () => setRunwayHovering(true),
2601
- onPointerLeave: () => setRunwayHovering(false),
2602
- children: [
2603
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2604
- "summary",
2605
- {
2606
- title: t("balance.runwayBreakdownTitle"),
2607
- onClick: (e) => {
2608
- e.preventDefault();
2609
- setRunwayManuallyOpened((v) => !v);
2610
- },
2611
- children: daysLeftText
2612
- }
2613
- ),
2614
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-runway-detail", children: runwayTitle })
2615
- ]
2616
- }
2617
- )
2919
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-runway-line", children: [
2920
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2921
+ "details",
2922
+ {
2923
+ className: `dq-stat-value dq-runway${daysLeft !== null && daysLeft < 3 ? " dq-stat-value--bad" : ""}`,
2924
+ open: runwayHovering || runwayManuallyOpened,
2925
+ onPointerEnter: () => setRunwayHovering(true),
2926
+ onPointerLeave: () => setRunwayHovering(false),
2927
+ children: [
2928
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2929
+ "summary",
2930
+ {
2931
+ title: t("balance.runwayBreakdownTitle"),
2932
+ onClick: (e) => {
2933
+ e.preventDefault();
2934
+ setRunwayManuallyOpened((v) => !v);
2935
+ },
2936
+ children: daysLeftText
2937
+ }
2938
+ ),
2939
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-runway-detail", children: runwayTitle })
2940
+ ]
2941
+ }
2942
+ ),
2943
+ topUpPrompt && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("a", { className: "dq-topup", href: TOP_UP_URL, target: "_blank", rel: "noreferrer", title: t("balance.topUpTitle"), children: [
2944
+ t("alert.topUp"),
2945
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-topup-arrow", "aria-hidden": "true", children: "\u2197" })
2946
+ ] })
2947
+ ] })
2618
2948
  ] }),
2619
2949
  balance?.isAvailable === false && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Stat, { label: t("common.status"), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-stat-value dq-stat-value--bad", children: t("common.unavailable") }) })
2620
2950
  ] }) : loadingBalance ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(BalanceSkeleton, {}) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("balance.empty") })
@@ -2773,6 +3103,57 @@ function BalanceDashboard(props) {
2773
3103
  ] }, i)) }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("cache.empty") })
2774
3104
  ] })
2775
3105
  ] }),
3106
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card", children: [
3107
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CardTitle, { children: t("breaker.title") }),
3108
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-setting-hint dq-breaker-desc", children: t("breaker.desc") }),
3109
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-breaker-status", children: [
3110
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3111
+ "span",
3112
+ {
3113
+ role: "status",
3114
+ className: breakerPhaseNow === "peakHolding" ? "dq-warn" : void 0,
3115
+ title: breakerLoadError !== null && breakerLoadError !== "" ? breakerLoadError : void 0,
3116
+ children: breakerStatusText
3117
+ }
3118
+ ),
3119
+ breakerPhaseNow === "peakHolding" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3120
+ "button",
3121
+ {
3122
+ type: "button",
3123
+ className: "dq-refresh-btn",
3124
+ disabled: breakerReleasing || breakerSavingId !== null,
3125
+ "aria-busy": breakerReleasing,
3126
+ onClick: () => {
3127
+ void releaseBreakerNow();
3128
+ },
3129
+ children: breakerReleasing ? t("breaker.releaseBusy") : t("breaker.release")
3130
+ }
3131
+ )
3132
+ ] }),
3133
+ breakerData === null ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: breakerLoadError === null ? t("breaker.loading") : t("breaker.statusUnavailable") }) : breakerData.sessions.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-empty", children: t("breaker.empty") }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("ul", { className: `dq-breaker-list${breakerPhaseNow === "masterOff" ? " dq-breaker-list--muted" : ""}`, children: breakerData.sessions.map((sessionId) => {
3134
+ const title = breakerTitleFor(sessionId);
3135
+ const holding = breakerData.holding.includes(sessionId);
3136
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("li", { className: "dq-breaker-row", children: [
3137
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "dq-breaker-row-main", children: [
3138
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-breaker-row-title", title, children: title }),
3139
+ holding && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-breaker-row-state", children: t("breaker.holding", { clock: breakerResumeClock ?? "18:00" }) })
3140
+ ] }),
3141
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "dq-breaker-switch", title: t("breaker.rowDisableAria", { title }), children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3142
+ "input",
3143
+ {
3144
+ type: "checkbox",
3145
+ checked: true,
3146
+ disabled: breakerSavingId !== null || breakerReleasing || breakerSavingMaster,
3147
+ "aria-label": t("breaker.rowDisableAria", { title }),
3148
+ onChange: () => {
3149
+ void disableBreakerSession(sessionId);
3150
+ }
3151
+ }
3152
+ ) })
3153
+ ] }, sessionId);
3154
+ }) }),
3155
+ breakerActionError !== null && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-breaker-error dq-error", role: "alert", children: t("breaker.actionError", { error: localizeApiError(breakerActionError, t, "error.query") }) })
3156
+ ] }),
2776
3157
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-card", children: [
2777
3158
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CardTitle, { children: t("settings.title") }),
2778
3159
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "dq-links-title", children: t("links.title") }),
@@ -2803,6 +3184,22 @@ function BalanceDashboard(props) {
2803
3184
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "dq-setting-hint", children: widgetTabs.length === 0 ? t("settings.widgetTabsEmpty") : t("settings.widgetTabsHint") })
2804
3185
  ] })
2805
3186
  ] }),
3187
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "dq-toggle", children: [
3188
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
3189
+ "input",
3190
+ {
3191
+ type: "checkbox",
3192
+ checked: breakerData === null ? true : breakerData.enabled,
3193
+ disabled: breakerData === null || breakerSavingMaster,
3194
+ "aria-describedby": "dq-peak-pause-hint",
3195
+ onChange: (e) => {
3196
+ void toggleBreakerMaster(e.target.checked);
3197
+ }
3198
+ }
3199
+ ),
3200
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: t("settings.peakPause") })
3201
+ ] }),
3202
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { id: "dq-peak-pause-hint", className: "dq-toggle-hint", children: breakerData === null ? breakerLoadError === null ? t("settings.peakPauseLoading") : t("breaker.statusUnavailable") : t("settings.peakPauseHint") }),
2806
3203
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting", children: [
2807
3204
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("label", { className: "dq-setting-label", htmlFor: "dq-low-balance", children: t("settings.lowBalance") }),
2808
3205
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "dq-setting-control", children: [
@@ -2874,21 +3271,21 @@ function BalanceDashboard(props) {
2874
3271
  onDragOver: onOrderDragOver,
2875
3272
  onDrop: onOrderDrop,
2876
3273
  children: viewTabs.map((tab, index) => {
2877
- const current = tab.id === "balance";
2878
- const dropTarget = tabDragging && !current && tabDropTarget === index;
3274
+ const current2 = tab.id === "balance";
3275
+ const dropTarget = tabDragging && !current2 && tabDropTarget === index;
2879
3276
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2880
3277
  "span",
2881
3278
  {
2882
- className: `dq-tab-chip${current ? " dq-tab-chip--current" : ""}${current && tabDragging ? " dq-tab-chip--dragging" : ""}${dropTarget ? " dq-tab-chip--drop-target" : ""}`,
2883
- draggable: current,
2884
- tabIndex: current ? 0 : void 0,
2885
- role: current ? "button" : void 0,
2886
- "aria-label": current ? t("settings.tabDragAria") : void 0,
2887
- onDragStart: current ? onQuotaTabDragStart : void 0,
2888
- onDragEnd: current ? endTabDrag : void 0,
3279
+ className: `dq-tab-chip${current2 ? " dq-tab-chip--current" : ""}${current2 && tabDragging ? " dq-tab-chip--dragging" : ""}${dropTarget ? " dq-tab-chip--drop-target" : ""}`,
3280
+ draggable: current2,
3281
+ tabIndex: current2 ? 0 : void 0,
3282
+ role: current2 ? "button" : void 0,
3283
+ "aria-label": current2 ? t("settings.tabDragAria") : void 0,
3284
+ onDragStart: current2 ? onQuotaTabDragStart : void 0,
3285
+ onDragEnd: current2 ? endTabDrag : void 0,
2889
3286
  onDragOver: (event) => onChipDragOver(event, index),
2890
3287
  onDrop: (event) => onChipDrop(event, index),
2891
- onKeyDown: current ? onQuotaTabKeyDown : void 0,
3288
+ onKeyDown: current2 ? onQuotaTabKeyDown : void 0,
2892
3289
  children: tab.label
2893
3290
  },
2894
3291
  tab.id
@@ -2900,13 +3297,145 @@ function BalanceDashboard(props) {
2900
3297
  ] })
2901
3298
  ] })
2902
3299
  ] }),
2903
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-footer", children: "@cassius0924/dsh-usage-dashboard v0.8.1" })
3300
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "dq-footer", children: "@cassius0924/dsh-usage-dashboard v0.8.3" })
2904
3301
  ] });
2905
3302
  }
2906
3303
 
2907
- // src/client/message-cost.tsx
2908
- var import_react5 = require("react");
3304
+ // src/client/breaker-pill.tsx
3305
+ var import_react6 = require("react");
2909
3306
  var import_jsx_runtime5 = require("react/jsx-runtime");
3307
+ var POLL_MS = 1e4;
3308
+ function BreakerIcon({ state }) {
3309
+ if (state === "off") {
3310
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
3311
+ "svg",
3312
+ {
3313
+ width: "14",
3314
+ height: "14",
3315
+ viewBox: "0 0 16 16",
3316
+ fill: "none",
3317
+ stroke: "currentColor",
3318
+ strokeWidth: "1.2",
3319
+ strokeLinecap: "round",
3320
+ strokeLinejoin: "round",
3321
+ "aria-hidden": "true",
3322
+ focusable: "false",
3323
+ children: [
3324
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("circle", { cx: "8", cy: "8", r: "5.9" }),
3325
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M6.5 5.9v4.2" }),
3326
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M9.5 5.9v4.2" })
3327
+ ]
3328
+ }
3329
+ );
3330
+ }
3331
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 16 16", fill: "currentColor", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
3332
+ "path",
3333
+ {
3334
+ fillRule: "evenodd",
3335
+ clipRule: "evenodd",
3336
+ d: "M8 1.6a6.4 6.4 0 1 0 0 12.8A6.4 6.4 0 0 0 8 1.6Zm-1.85 3.5h1.3v5.8h-1.3V5.1Zm2.4 0h1.3v5.8h-1.3V5.1Z"
3337
+ }
3338
+ ) });
3339
+ }
3340
+ function BreakerPill({ sessionId }) {
3341
+ const { t } = useI18n();
3342
+ const data = useBreakerData();
3343
+ const [loadError, setLoadError] = (0, import_react6.useState)(null);
3344
+ const [actionError, setActionError] = (0, import_react6.useState)(null);
3345
+ const [saving, setSaving] = (0, import_react6.useState)(false);
3346
+ const savingRef = (0, import_react6.useRef)(false);
3347
+ const mutationSeq = (0, import_react6.useRef)(0);
3348
+ const sessionIdRef = (0, import_react6.useRef)(sessionId);
3349
+ (0, import_react6.useEffect)(() => {
3350
+ sessionIdRef.current = sessionId;
3351
+ }, [sessionId]);
3352
+ (0, import_react6.useEffect)(() => {
3353
+ if (sessionId === void 0 || sessionId === "") return;
3354
+ let cancelled = false;
3355
+ setLoadError(null);
3356
+ setActionError(null);
3357
+ const pull = async () => {
3358
+ const seqAtStart = mutationSeq.current;
3359
+ const res = await fetchBreaker();
3360
+ if (cancelled) return;
3361
+ if (!breakerIsUsable(res)) {
3362
+ setLoadError(res.error ?? "");
3363
+ return;
3364
+ }
3365
+ setLoadError(null);
3366
+ if (seqAtStart !== mutationSeq.current || savingRef.current) return;
3367
+ publishBreakerData(res.data);
3368
+ };
3369
+ void pull();
3370
+ const timer = window.setInterval(() => {
3371
+ void pull();
3372
+ }, POLL_MS);
3373
+ return () => {
3374
+ cancelled = true;
3375
+ window.clearInterval(timer);
3376
+ };
3377
+ }, [sessionId]);
3378
+ if (sessionId === void 0 || sessionId === "") return null;
3379
+ const state = sessionBreakerState(data, sessionId);
3380
+ const on = state !== "off";
3381
+ const masterOff = data !== null && !data.enabled;
3382
+ const optedIn = isSessionOptedIn(data, sessionId);
3383
+ const resumeClock = beijingClock(data?.resumeAt ?? null) ?? "18:00";
3384
+ const errorSource = actionError ?? loadError;
3385
+ const errorText = errorSource === null ? null : t("breaker.actionError", { error: localizeApiError(errorSource, t, "error.query") });
3386
+ const label = state === "holding" ? t("breaker.pillHolding", { clock: resumeClock }) : state === "on" ? t("breaker.pillOn") : t("breaker.pillOff");
3387
+ const baseTitle = masterOff ? t("breaker.pillTitleMasterOff") : state === "holding" ? t("breaker.pillTitleHolding", { clock: resumeClock }) : state === "on" ? t("breaker.pillTitleOn", { clock: resumeClock }) : t("breaker.pillTitleOff");
3388
+ const title = errorText === null ? baseTitle : `${baseTitle} \xB7 ${errorText}`;
3389
+ const baseAria = masterOff ? t("breaker.pillAriaMasterOff") : state === "holding" ? t("breaker.pillAriaHolding", { clock: resumeClock }) : state === "on" ? t("breaker.pillAriaOn", { clock: resumeClock }) : t("breaker.pillAriaOff");
3390
+ const ariaLabel = errorText === null ? baseAria : `${baseAria} \xB7 ${errorText}`;
3391
+ const toggle = async () => {
3392
+ if (savingRef.current) return;
3393
+ const next = !optedIn;
3394
+ const previous = data;
3395
+ publishBreakerData(withSessionToggled(previous, sessionId, next));
3396
+ setActionError(null);
3397
+ savingRef.current = true;
3398
+ setSaving(true);
3399
+ const seq = ++mutationSeq.current;
3400
+ try {
3401
+ const res = await updateBreaker({ sessionId, enabled: next });
3402
+ if (seq !== mutationSeq.current || sessionIdRef.current !== sessionId) return;
3403
+ if (breakerIsUsable(res)) {
3404
+ publishBreakerData(res.data);
3405
+ setLoadError(null);
3406
+ return;
3407
+ }
3408
+ publishBreakerData(previous);
3409
+ setActionError(res.error ?? "");
3410
+ } finally {
3411
+ savingRef.current = false;
3412
+ setSaving(false);
3413
+ }
3414
+ };
3415
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dq-cost-anchor", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
3416
+ "button",
3417
+ {
3418
+ type: "button",
3419
+ className: `dq-cost-pill dq-breaker-pill dq-breaker-pill--${state}`,
3420
+ "aria-pressed": on,
3421
+ "aria-busy": saving,
3422
+ "aria-label": ariaLabel,
3423
+ title,
3424
+ disabled: saving,
3425
+ onClick: () => {
3426
+ void toggle();
3427
+ },
3428
+ children: [
3429
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(BreakerIcon, { state }),
3430
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dq-cost-pill-label", children: label })
3431
+ ]
3432
+ }
3433
+ ) });
3434
+ }
3435
+
3436
+ // src/client/message-cost.tsx
3437
+ var import_react7 = require("react");
3438
+ var import_jsx_runtime6 = require("react/jsx-runtime");
2910
3439
  var fmtTurnCost = (cost) => cost.toLocaleString(void 0, {
2911
3440
  minimumFractionDigits: cost > 0 && cost < 0.01 ? 4 : 2,
2912
3441
  maximumFractionDigits: cost > 0 && cost < 0.01 ? 6 : 4
@@ -2917,8 +3446,8 @@ var cachedData = (sessionId) => {
2917
3446
  };
2918
3447
  function MessageCost({ sessionId, messageId }) {
2919
3448
  const { t } = useI18n();
2920
- const [data, setData] = (0, import_react5.useState)(() => cachedData(sessionId));
2921
- (0, import_react5.useEffect)(() => {
3449
+ const [data, setData] = (0, import_react7.useState)(() => cachedData(sessionId));
3450
+ (0, import_react7.useEffect)(() => {
2922
3451
  setData(cachedData(sessionId));
2923
3452
  const unsubscribe = subscribeSessionUsage(sessionId, () => setData(cachedData(sessionId)));
2924
3453
  const hit = cachedData(sessionId)?.turns.some((turn2) => turn2.messageId === messageId) ?? false;
@@ -2929,7 +3458,7 @@ function MessageCost({ sessionId, messageId }) {
2929
3458
  if (turn === void 0) return null;
2930
3459
  const amount = `\xA5${fmtTurnCost(turn.cost)}`;
2931
3460
  const tooltip = t("messageCost.tooltip");
2932
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
3461
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2933
3462
  "span",
2934
3463
  {
2935
3464
  className: "dq-message-cost",
@@ -2937,15 +3466,15 @@ function MessageCost({ sessionId, messageId }) {
2937
3466
  "data-dq-tooltip": tooltip,
2938
3467
  "aria-label": t("messageCost.aria", { amount }),
2939
3468
  children: [
2940
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dq-message-cost-separator", "aria-hidden": true, children: "\xB7" }),
2941
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "dq-message-cost-amount", children: amount })
3469
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dq-message-cost-separator", "aria-hidden": true, children: "\xB7" }),
3470
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dq-message-cost-amount", children: amount })
2942
3471
  ]
2943
3472
  }
2944
3473
  );
2945
3474
  }
2946
3475
 
2947
3476
  // src/client/session-cost-pill.tsx
2948
- var import_react6 = require("react");
3477
+ var import_react8 = require("react");
2949
3478
  var import_react_dom = require("react-dom");
2950
3479
 
2951
3480
  // src/pricing.ts
@@ -2953,7 +3482,7 @@ var PEAK_PRICING_FROM_MS = Date.UTC(2026, 7, 16, 16, 0, 0);
2953
3482
  var V41_FLASH_PRICING_FROM_MS = Date.UTC(2026, 8, 10, 4, 0, 0);
2954
3483
  var PRO_ROUTED_TO_FLASH_FROM_MS = Date.UTC(2026, 8, 14, 4, 0, 0);
2955
3484
  var PEAK_WINDOWS = [[9, 12], [14, 18]];
2956
- var BEIJING_OFFSET_MS3 = 8 * 36e5;
3485
+ var BEIJING_OFFSET_MS4 = 8 * 36e5;
2957
3486
  var FLAT_RATES = {
2958
3487
  pro: { cacheHit: 0.025, input: 3, output: 6 },
2959
3488
  flash: { cacheHit: 0.02, input: 1, output: 2 },
@@ -2998,12 +3527,12 @@ function billedTierOf(timeMs, model) {
2998
3527
  return tier === "pro" && timeMs >= PRO_ROUTED_TO_FLASH_FROM_MS ? "flash" : tier;
2999
3528
  }
3000
3529
  function isBeijingWeekday(timeMs) {
3001
- const day = new Date(timeMs + BEIJING_OFFSET_MS3).getUTCDay();
3530
+ const day = new Date(timeMs + BEIJING_OFFSET_MS4).getUTCDay();
3002
3531
  return day >= 1 && day <= 5;
3003
3532
  }
3004
3533
  function isPeak(timeMs) {
3005
3534
  if (eraAt(timeMs).weekdaysOnly && !isBeijingWeekday(timeMs)) return false;
3006
- const hour = Math.floor(((timeMs + BEIJING_OFFSET_MS3) % 864e5 + 864e5) % 864e5 / 36e5);
3535
+ const hour = Math.floor(((timeMs + BEIJING_OFFSET_MS4) % 864e5 + 864e5) % 864e5 / 36e5);
3007
3536
  return PEAK_WINDOWS.some(([from, to]) => hour >= from && hour < to);
3008
3537
  }
3009
3538
  function ratesAt(timeMs, model) {
@@ -3013,7 +3542,7 @@ function ratesAt(timeMs, model) {
3013
3542
  }
3014
3543
 
3015
3544
  // src/client/session-cost-pill.tsx
3016
- var import_jsx_runtime6 = require("react/jsx-runtime");
3545
+ var import_jsx_runtime7 = require("react/jsx-runtime");
3017
3546
  var PANEL_MARGIN = 12;
3018
3547
  var PANEL_GAP = 8;
3019
3548
  var cachedData2 = (sessionId) => {
@@ -3021,7 +3550,7 @@ var cachedData2 = (sessionId) => {
3021
3550
  return response?.ok === true ? response.data ?? null : null;
3022
3551
  };
3023
3552
  function CostIcon() {
3024
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3553
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3025
3554
  "svg",
3026
3555
  {
3027
3556
  width: "14",
@@ -3035,32 +3564,32 @@ function CostIcon() {
3035
3564
  "aria-hidden": "true",
3036
3565
  focusable: "false",
3037
3566
  children: [
3038
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("circle", { cx: "8", cy: "8", r: "6" }),
3039
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M5.5 5.3 8 8.4l2.5-3.1" }),
3040
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M8 8.4v2.9" }),
3041
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M5.8 9.5h4.4" }),
3042
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M5.8 10.9h4.4" })
3567
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("circle", { cx: "8", cy: "8", r: "6" }),
3568
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M5.5 5.3 8 8.4l2.5-3.1" }),
3569
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M8 8.4v2.9" }),
3570
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M5.8 9.5h4.4" }),
3571
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("path", { d: "M5.8 10.9h4.4" })
3043
3572
  ]
3044
3573
  }
3045
3574
  );
3046
3575
  }
3047
3576
  function SessionCostPill({ sessionId }) {
3048
3577
  const { t, locale } = useI18n();
3049
- const [data, setData] = (0, import_react6.useState)(
3578
+ const [data, setData] = (0, import_react8.useState)(
3050
3579
  () => sessionId === void 0 || sessionId === "" ? null : cachedData2(sessionId)
3051
3580
  );
3052
- const [open, setOpen] = (0, import_react6.useState)(false);
3053
- const anchorRef = (0, import_react6.useRef)(null);
3054
- const panelRef = (0, import_react6.useRef)(null);
3055
- const [pos, setPos] = (0, import_react6.useState)(null);
3056
- (0, import_react6.useEffect)(() => {
3581
+ const [open, setOpen] = (0, import_react8.useState)(false);
3582
+ const anchorRef = (0, import_react8.useRef)(null);
3583
+ const panelRef = (0, import_react8.useRef)(null);
3584
+ const [pos, setPos] = (0, import_react8.useState)(null);
3585
+ (0, import_react8.useEffect)(() => {
3057
3586
  if (sessionId === void 0 || sessionId === "") return;
3058
3587
  setData(cachedData2(sessionId));
3059
3588
  const unsubscribe = subscribeSessionUsage(sessionId, () => setData(cachedData2(sessionId)));
3060
3589
  if (getCachedSessionUsage(sessionId) === null) void fetchSessionUsage(sessionId);
3061
3590
  return unsubscribe;
3062
3591
  }, [sessionId]);
3063
- (0, import_react6.useLayoutEffect)(() => {
3592
+ (0, import_react8.useLayoutEffect)(() => {
3064
3593
  if (!open) {
3065
3594
  setPos(null);
3066
3595
  return;
@@ -3075,7 +3604,7 @@ function SessionCostPill({ sessionId }) {
3075
3604
  bottom: window.innerHeight - pill.top + PANEL_GAP
3076
3605
  });
3077
3606
  }, [open, data]);
3078
- (0, import_react6.useEffect)(() => {
3607
+ (0, import_react8.useEffect)(() => {
3079
3608
  if (!open) return;
3080
3609
  const close = () => setOpen(false);
3081
3610
  const onPointerDown = (event) => {
@@ -3127,8 +3656,8 @@ function SessionCostPill({ sessionId }) {
3127
3656
  hour12: false
3128
3657
  });
3129
3658
  const range = data.firstActive !== null && data.lastActive !== null ? `${rangeTime.format(data.firstActive)} \u2013 ${rangeTime.format(data.lastActive)}` : null;
3130
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
3131
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { ref: anchorRef, className: "dq-cost-anchor", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3659
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
3660
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { ref: anchorRef, className: "dq-cost-anchor", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3132
3661
  "button",
3133
3662
  {
3134
3663
  type: "button",
@@ -3138,13 +3667,13 @@ function SessionCostPill({ sessionId }) {
3138
3667
  "aria-label": t("sessionCost.aria", { amount }),
3139
3668
  onClick: () => setOpen((value) => !value),
3140
3669
  children: [
3141
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CostIcon, {}),
3142
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dq-cost-pill-label", children: amount })
3670
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CostIcon, {}),
3671
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "dq-cost-pill-label", children: amount })
3143
3672
  ]
3144
3673
  }
3145
3674
  ) }),
3146
3675
  open && (0, import_react_dom.createPortal)(
3147
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
3676
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
3148
3677
  "div",
3149
3678
  {
3150
3679
  ref: panelRef,
@@ -3153,35 +3682,35 @@ function SessionCostPill({ sessionId }) {
3153
3682
  "aria-label": t("sessionCost.title"),
3154
3683
  style: pos === null ? { visibility: "hidden", left: 0, top: 0 } : { visibility: "visible", left: pos.left, bottom: pos.bottom, top: "auto" },
3155
3684
  children: [
3156
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "dq-cost-panel-title", children: [
3157
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { className: "dq-cost-panel-title-label", children: [
3158
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CostIcon, {}),
3685
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "dq-cost-panel-title", children: [
3686
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "dq-cost-panel-title-label", children: [
3687
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CostIcon, {}),
3159
3688
  t("sessionCost.title")
3160
3689
  ] }),
3161
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "dq-cost-panel-value", children: amount })
3690
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "dq-cost-panel-value", children: amount })
3162
3691
  ] }),
3163
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "dq-cost-panel-rule", "aria-hidden": true }),
3164
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("dl", { className: "dq-cost-details", children: [
3165
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("dt", { children: t("common.input") }),
3166
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("dd", { children: [
3692
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "dq-cost-panel-rule", "aria-hidden": true }),
3693
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("dl", { className: "dq-cost-details", children: [
3694
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("dt", { children: t("common.input") }),
3695
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("dd", { children: [
3167
3696
  "\xA5",
3168
3697
  fmtTurnCost(split.input)
3169
3698
  ] }),
3170
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("dt", { children: t("common.cacheHit") }),
3171
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("dd", { children: [
3699
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("dt", { children: t("common.cacheHit") }),
3700
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("dd", { children: [
3172
3701
  "\xA5",
3173
3702
  fmtTurnCost(split.cache)
3174
3703
  ] }),
3175
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("dt", { children: t("common.output") }),
3176
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("dd", { children: [
3704
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("dt", { children: t("common.output") }),
3705
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("dd", { children: [
3177
3706
  "\xA5",
3178
3707
  fmtTurnCost(split.output)
3179
3708
  ] }),
3180
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("dt", { children: t("sessionUsage.calls") }),
3181
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("dd", { children: fmtInt(data.calls) }),
3182
- range !== null && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
3183
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("dt", { children: t("sessionCost.active") }),
3184
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("dd", { children: range })
3709
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("dt", { children: t("sessionUsage.calls") }),
3710
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("dd", { children: fmtInt(data.calls) }),
3711
+ range !== null && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
3712
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("dt", { children: t("sessionCost.active") }),
3713
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("dd", { children: range })
3185
3714
  ] })
3186
3715
  ] })
3187
3716
  ]
@@ -3244,6 +3773,18 @@ var css = `
3244
3773
  .dq-cost-pill:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
3245
3774
  .dq-cost-pill svg{flex:none}
3246
3775
  .dq-cost-pill-label{min-width:0;overflow:hidden;text-overflow:ellipsis}
3776
+ /* \u9AD8\u5CF0\u6682\u505C pill: same dock/skin as the cost pill. The three states never rely
3777
+ on colour alone \u2014 copy changes and the glyph switches outline\u2192solid \u2014 but the
3778
+ tone reinforces them: off stays neutral tertiary, on uses the primary colour,
3779
+ holding uses the warn tone. Nothing here may widen the dock row. */
3780
+ .dq-breaker-pill{font-variant-numeric:normal}
3781
+ .dq-breaker-pill--off{color:var(--dsw-alias-label-tertiary,#59636e)}
3782
+ .dq-breaker-pill--off:hover{color:var(--dsw-alias-label-secondary,#59636e)}
3783
+ .dq-breaker-pill--on{color:var(--dsw-alias-state-business-primary,#0969da)}
3784
+ .dq-breaker-pill--on:hover{color:var(--dsw-alias-state-business-primary,#0969da)}
3785
+ .dq-breaker-pill--holding{color:var(--dq-text-warn,#9a6700)}
3786
+ .dq-breaker-pill--holding:hover{color:var(--dq-text-warn,#9a6700)}
3787
+ .dq-breaker-pill:disabled{cursor:default;opacity:.7}
3247
3788
  /* Click-open breakdown: the host stat-dialog skin (menu surface, prominent
3248
3789
  elevation, 12/18 rows), portaled to the body and placed above the pill. */
3249
3790
  .dq-cost-panel{position:fixed;z-index:1100;box-sizing:border-box;width:max-content;min-width:min(300px,calc(100vw - 24px));max-width:min(440px,calc(100vw - 24px));padding:16px;border:0;border-radius:var(--dsw-radius-lg,16px);background:var(--dsw-specific-menu,var(--dsw-alias-bg-overlay,#fff));-webkit-backdrop-filter:var(--dsw-menu-backdrop-filter,blur(40px) saturate(150%));backdrop-filter:var(--dsw-menu-backdrop-filter,blur(40px) saturate(150%));box-shadow:var(--dsw-elevation-prominent,0 12px 32px rgba(0,0,0,.24));font-family:var(--dsw-font-family,-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei","Helvetica Neue",Helvetica,Arial,sans-serif);font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#59636e);cursor:default}
@@ -3350,6 +3891,17 @@ var css = `
3350
3891
  .dq-remaining-breakdown{display:none;position:absolute;top:calc(100% + 6px);left:0;z-index:20;flex-direction:column;gap:4px;padding:8px 10px;background:var(--dsw-alias-bg-overlay,#ffffff);border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.12));border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.15);white-space:nowrap;font-size:12px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary,#1f2328)}
3351
3892
  .dq-remaining[open] .dq-remaining-breakdown{display:flex}
3352
3893
  .dq-remaining-granted{color:var(--dsw-alias-label-secondary,#59636e);font-weight:400;font-size:12px}
3894
+ /* Low-balance top-up shortcut beside the runway value. It is an <a>, not a
3895
+ <button>, because it leaves the app; the \u2197 glyph states that. Colours come
3896
+ from the host's primary-button tokens (the same pair DSH's own filled
3897
+ buttons use) so the page keeps exactly one filled call to action instead of
3898
+ reading as another filter chip. Height stays on the runway text line so the
3899
+ hero row does not grow. */
3900
+ .dq-runway-line{display:flex;align-items:center;flex-wrap:wrap;gap:4px 10px}
3901
+ .dq-topup{display:inline-flex;align-items:center;gap:2px;height:26px;padding:0 10px;border-radius:var(--dsw-radius-sm,8px);background:var(--dsw-alias-button-primary-fill,#0969da);color:var(--dsw-alias-label-primary-foreground,#fff);font-size:12px;font-weight:600;line-height:1;text-decoration:none;white-space:nowrap;transition:background .15s ease}
3902
+ .dq-topup:hover{background:var(--dsw-alias-button-primary-hover,#0757ba)}
3903
+ .dq-topup:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
3904
+ .dq-topup-arrow{font-size:10px;line-height:1;transform:translateY(-1px)}
3353
3905
  /* narrow screens: .dq-balance-grid wraps its 3 stats onto separate lines, so the absolutely-positioned
3354
3906
  .dq-remaining-breakdown/.dq-runway-detail popovers can float down over whichever stat now sits in the
3355
3907
  next row (e.g. "\u4F59\u989D\u660E\u7EC6" open covering "\u9884\u8BA1\u53EF\u7528"'s clickable summary) \u2014 force each stat onto its own
@@ -3506,6 +4058,22 @@ var css = `
3506
4058
  package.json (see build.mjs) so it can never drift from the release. */
3507
4059
  .dq-footer{margin:16px 0 0;font-size:12px;text-align:center;color:var(--dsw-alias-label-tertiary,#59636e)}
3508
4060
  .dq-number{width:82px;padding:5px 8px;font-size:13px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.16));background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-primary,#1f2328);font-variant-numeric:tabular-nums}
4061
+ /* \u9AD8\u5CF0\u6682\u505C\u5361\uFF1A\u8BF4\u660E\u3001\u72B6\u6001\u884C\u4E0E\u4F1A\u8BDD\u5217\u8868\u3002320px \u4E0B\u6807\u9898\u7701\u7565\u3001\u72B6\u6001\u4E0E\u5F00\u5173\u6362\u884C\uFF0C
4062
+ \u5217\u8868\u884C\u4E0D\u5141\u8BB8\u6491\u51FA\u5361\u7247\u5BBD\u5EA6\u3002\u603B\u5F00\u5173\u5173\u95ED\u65F6\u5217\u8868\u6574\u4F53\u5F31\u5316\uFF0C\u4F46\u6BCF\u4F1A\u8BDD\u5F00\u5173\u4ECD\u53EF\u64CD\u4F5C\u3002 */
4063
+ .dq-breaker-desc{margin:-6px 0 0}
4064
+ .dq-breaker-status{display:flex;align-items:center;flex-wrap:wrap;gap:8px;min-width:0;margin-top:10px;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary,#59636e)}
4065
+ .dq-breaker-list{display:flex;flex-direction:column;margin:12px 0 0;padding:0;list-style:none;min-width:0}
4066
+ .dq-breaker-list--muted{opacity:.62}
4067
+ .dq-breaker-row{display:flex;align-items:center;gap:12px;min-width:0;padding:9px 0;border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.10))}
4068
+ .dq-breaker-row:first-child{border-top:0;padding-top:0}
4069
+ .dq-breaker-row-main{display:flex;align-items:center;flex-wrap:wrap;gap:4px 8px;flex:1 1 auto;min-width:0}
4070
+ .dq-breaker-row-title{min-width:0;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;color:var(--dsw-alias-label-primary,#1f2328)}
4071
+ .dq-breaker-row-state{font-size:12px;line-height:1.5;white-space:nowrap;color:var(--dq-text-warn)}
4072
+ .dq-breaker-switch{display:inline-flex;align-items:center;justify-content:center;flex:none;cursor:pointer}
4073
+ .dq-breaker-switch input{width:16px;height:16px;margin:0;accent-color:var(--dsw-alias-state-business-primary,#0969da);cursor:pointer}
4074
+ .dq-breaker-switch input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:2px}
4075
+ .dq-breaker-switch input:disabled{cursor:default;opacity:.6}
4076
+ .dq-breaker-error{margin-top:8px;font-size:12px;line-height:1.5}
3509
4077
  .dq-number:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#0969da);outline-offset:1px;border-color:transparent}
3510
4078
  .dq-links-title{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary,#1f2328);margin:0 0 12px}
3511
4079
  .dq-links{display:flex;flex-wrap:wrap;gap:8px}
@@ -3722,7 +4290,7 @@ html[data-dq-quota-view] .dq-balance{padding-bottom:48px}
3722
4290
  `;
3723
4291
 
3724
4292
  // src/client/widget.tsx
3725
- var import_react7 = require("react");
4293
+ var import_react9 = require("react");
3726
4294
 
3727
4295
  // src/client/sessions.ts
3728
4296
  function currentSessionIdOf(state) {
@@ -3731,7 +4299,7 @@ function currentSessionIdOf(state) {
3731
4299
  }
3732
4300
 
3733
4301
  // src/client/widget.tsx
3734
- var import_jsx_runtime7 = require("react/jsx-runtime");
4302
+ var import_jsx_runtime8 = require("react/jsx-runtime");
3735
4303
  var MARGIN = 16;
3736
4304
  var CORNER_KEY = "widget.corner";
3737
4305
  var COLLAPSED_KEY = "widget.collapsed";
@@ -3782,47 +4350,47 @@ function cornerPos(corner, node, bounds) {
3782
4350
  function QuotaWidget(props) {
3783
4351
  const { t } = useI18n();
3784
4352
  const sessionId = props.useSessions(currentSessionIdOf);
3785
- const [visible, setVisible] = (0, import_react7.useState)(widgetVisibleStore.get());
3786
- (0, import_react7.useEffect)(() => widgetVisibleStore.subscribe(() => setVisible(widgetVisibleStore.get())), []);
3787
- const [quotaViewActive2, setQuotaViewActive] = (0, import_react7.useState)(quotaViewActiveStore.get());
3788
- (0, import_react7.useEffect)(() => quotaViewActiveStore.subscribe(() => setQuotaViewActive(quotaViewActiveStore.get())), []);
3789
- const [lowBalance, setLowBalance] = (0, import_react7.useState)(lowBalanceStore.get());
3790
- (0, import_react7.useEffect)(() => lowBalanceStore.subscribe(() => setLowBalance(lowBalanceStore.get())), []);
3791
- const [selectedTabIds, setSelectedTabIds] = (0, import_react7.useState)(widgetTabIdsStore.get());
3792
- (0, import_react7.useEffect)(() => widgetTabIdsStore.subscribe(() => setSelectedTabIds(widgetTabIdsStore.get())), []);
3793
- const [viewTabs, setViewTabs] = (0, import_react7.useState)(() => props.views.list());
3794
- (0, import_react7.useEffect)(() => props.views.subscribe(() => setViewTabs(props.views.list())), [props.views]);
3795
- const [activeViewId, setActiveViewId] = (0, import_react7.useState)(null);
4353
+ const [visible, setVisible] = (0, import_react9.useState)(widgetVisibleStore.get());
4354
+ (0, import_react9.useEffect)(() => widgetVisibleStore.subscribe(() => setVisible(widgetVisibleStore.get())), []);
4355
+ const [quotaViewActive2, setQuotaViewActive] = (0, import_react9.useState)(quotaViewActiveStore.get());
4356
+ (0, import_react9.useEffect)(() => quotaViewActiveStore.subscribe(() => setQuotaViewActive(quotaViewActiveStore.get())), []);
4357
+ const [lowBalance, setLowBalance] = (0, import_react9.useState)(lowBalanceStore.get());
4358
+ (0, import_react9.useEffect)(() => lowBalanceStore.subscribe(() => setLowBalance(lowBalanceStore.get())), []);
4359
+ const [selectedTabIds, setSelectedTabIds] = (0, import_react9.useState)(widgetTabIdsStore.get());
4360
+ (0, import_react9.useEffect)(() => widgetTabIdsStore.subscribe(() => setSelectedTabIds(widgetTabIdsStore.get())), []);
4361
+ const [viewTabs, setViewTabs] = (0, import_react9.useState)(() => props.views.list());
4362
+ (0, import_react9.useEffect)(() => props.views.subscribe(() => setViewTabs(props.views.list())), [props.views]);
4363
+ const [activeViewId, setActiveViewId] = (0, import_react9.useState)(null);
3796
4364
  const cachedBalance = getCachedBalance();
3797
- const [data, setData] = (0, import_react7.useState)(cachedBalance?.data ?? null);
3798
- const [error, setError] = (0, import_react7.useState)(null);
3799
- const [loading, setLoading] = (0, import_react7.useState)(cachedBalance === null);
3800
- const [collapsed, setCollapsed] = (0, import_react7.useState)(() => loadPref(COLLAPSED_KEY, isBoolean, false));
3801
- const [pos, setPos] = (0, import_react7.useState)(null);
3802
- const [corner, setCorner] = (0, import_react7.useState)(() => loadPref(CORNER_KEY, isCorner, "br"));
3803
- const [dragging, setDragging] = (0, import_react7.useState)(false);
3804
- const rootRef = (0, import_react7.useRef)(null);
3805
- const dragRef = (0, import_react7.useRef)(null);
4365
+ const [data, setData] = (0, import_react9.useState)(cachedBalance?.data ?? null);
4366
+ const [error, setError] = (0, import_react9.useState)(null);
4367
+ const [loading, setLoading] = (0, import_react9.useState)(cachedBalance === null);
4368
+ const [collapsed, setCollapsed] = (0, import_react9.useState)(() => loadPref(COLLAPSED_KEY, isBoolean, false));
4369
+ const [pos, setPos] = (0, import_react9.useState)(null);
4370
+ const [corner, setCorner] = (0, import_react9.useState)(() => loadPref(CORNER_KEY, isCorner, "br"));
4371
+ const [dragging, setDragging] = (0, import_react9.useState)(false);
4372
+ const rootRef = (0, import_react9.useRef)(null);
4373
+ const dragRef = (0, import_react9.useRef)(null);
3806
4374
  const readUsage = () => {
3807
4375
  const cached = getCachedUsage();
3808
4376
  return { data: cached?.ok === true ? cached.data ?? null : null, at: getCachedUsageAt() };
3809
4377
  };
3810
- const [usage, setUsage] = (0, import_react7.useState)(readUsage);
3811
- (0, import_react7.useEffect)(() => subscribeUsage(() => setUsage(readUsage())), []);
4378
+ const [usage, setUsage] = (0, import_react9.useState)(readUsage);
4379
+ (0, import_react9.useEffect)(() => subscribeUsage(() => setUsage(readUsage())), []);
3812
4380
  const readSessionUsage = (id) => {
3813
4381
  if (id === void 0) return null;
3814
4382
  const response = getCachedSessionUsage(id);
3815
4383
  return response?.ok === true ? response.data ?? null : null;
3816
4384
  };
3817
- const [sessionUsage, setSessionUsage] = (0, import_react7.useState)(() => readSessionUsage(sessionId));
3818
- (0, import_react7.useEffect)(() => {
4385
+ const [sessionUsage, setSessionUsage] = (0, import_react9.useState)(() => readSessionUsage(sessionId));
4386
+ (0, import_react9.useEffect)(() => {
3819
4387
  setSessionUsage(readSessionUsage(sessionId));
3820
4388
  if (sessionId === void 0) return;
3821
4389
  const unsubscribe = subscribeSessionUsage(sessionId, () => setSessionUsage(readSessionUsage(sessionId)));
3822
4390
  void fetchSessionUsage(sessionId);
3823
4391
  return unsubscribe;
3824
4392
  }, [sessionId]);
3825
- (0, import_react7.useLayoutEffect)(() => {
4393
+ (0, import_react9.useLayoutEffect)(() => {
3826
4394
  const viewIds = viewTabs.map((tab) => tab.id);
3827
4395
  const read = () => {
3828
4396
  const next = getActiveConversationViewId(getShellFrame(rootRef.current), viewIds);
@@ -3859,14 +4427,14 @@ function QuotaWidget(props) {
3859
4427
  setLoading(false);
3860
4428
  }
3861
4429
  };
3862
- (0, import_react7.useEffect)(() => {
4430
+ (0, import_react9.useEffect)(() => {
3863
4431
  void load(true);
3864
4432
  const id = setInterval(() => {
3865
4433
  void load(false);
3866
4434
  }, 6e4);
3867
4435
  return () => clearInterval(id);
3868
4436
  }, []);
3869
- (0, import_react7.useLayoutEffect)(() => {
4437
+ (0, import_react9.useLayoutEffect)(() => {
3870
4438
  if (!visible || quotaViewActive2) return;
3871
4439
  const node = rootRef.current;
3872
4440
  if (node === null) return;
@@ -3994,9 +4562,9 @@ function QuotaWidget(props) {
3994
4562
  const dotClass = primary === null ? "dsh-quota-dot dsh-quota-dot--idle" : data?.isAvailable === false ? "dsh-quota-dot dsh-quota-dot--error" : low ? "dsh-quota-dot dsh-quota-dot--warn" : "dsh-quota-dot";
3995
4563
  const ageMinutes = usage.at === null ? null : Math.floor((Date.now() - usage.at) / 6e4);
3996
4564
  const usageAgeText = ageMinutes === null || ageMinutes < 1 ? t("widget.updatedNow") : t("widget.stale", { minutes: ageMinutes });
3997
- const sessionCostRow = sessionUsage === null ? null : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "dsh-quota-row", title: t("widget.sessionCostTitle"), children: [
3998
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "dsh-quota-label", children: t("widget.sessionCost") }),
3999
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "dsh-quota-value", children: [
4565
+ const sessionCostRow = sessionUsage === null ? null : /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "dsh-quota-row", title: t("widget.sessionCostTitle"), children: [
4566
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "dsh-quota-label", children: t("widget.sessionCost") }),
4567
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "dsh-quota-value", children: [
4000
4568
  "\xA5",
4001
4569
  fmtTurnCost(sessionUsage.cost)
4002
4570
  ] })
@@ -4004,50 +4572,50 @@ function QuotaWidget(props) {
4004
4572
  let body = null;
4005
4573
  if (!collapsed) {
4006
4574
  if (loading && data === null && error === null) {
4007
- body = /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "dsh-quota-body", children: [
4008
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { children: t("widget.loading") }),
4575
+ body = /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "dsh-quota-body", children: [
4576
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { children: t("widget.loading") }),
4009
4577
  sessionCostRow
4010
4578
  ] });
4011
4579
  } else if (error !== null && data === null) {
4012
- body = /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "dsh-quota-body", children: [
4013
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "dsh-quota-error", children: localizeApiError(error, t, "error.query") }),
4580
+ body = /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "dsh-quota-body", children: [
4581
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "dsh-quota-error", children: localizeApiError(error, t, "error.query") }),
4014
4582
  sessionCostRow
4015
4583
  ] });
4016
4584
  } else if (primary !== null) {
4017
- body = /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "dsh-quota-body", children: [
4018
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "dsh-quota-remaining-label", children: t("balance.remaining") }),
4019
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { title: low ? t("widget.lowTitle", { amount: fmt(lowBalance) }) : void 0, children: [
4020
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: `dsh-quota-total${low ? " dsh-quota-total--low" : ""}`, children: fmt(primary.total) }),
4021
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "dsh-quota-currency", children: primary.currency })
4585
+ body = /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "dsh-quota-body", children: [
4586
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "dsh-quota-remaining-label", children: t("balance.remaining") }),
4587
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { title: low ? t("widget.lowTitle", { amount: fmt(lowBalance) }) : void 0, children: [
4588
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: `dsh-quota-total${low ? " dsh-quota-total--low" : ""}`, children: fmt(primary.total) }),
4589
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "dsh-quota-currency", children: primary.currency })
4022
4590
  ] }),
4023
- usage.data !== null && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "dsh-quota-row", title: t("widget.todayCostTitle", { age: usageAgeText }), children: [
4024
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "dsh-quota-label", children: t("widget.todayCost") }),
4025
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "dsh-quota-value", children: [
4591
+ usage.data !== null && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "dsh-quota-row", title: t("widget.todayCostTitle", { age: usageAgeText }), children: [
4592
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "dsh-quota-label", children: t("widget.todayCost") }),
4593
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "dsh-quota-value", children: [
4026
4594
  "\xA5",
4027
4595
  fmt(usage.data.summary.today.cost)
4028
4596
  ] })
4029
4597
  ] }),
4030
4598
  sessionCostRow,
4031
- data?.isAvailable === false && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "dsh-quota-row", children: [
4032
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "dsh-quota-label", children: t("common.status") }),
4033
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "dsh-quota-value dsh-quota-error", children: t("common.unavailable") })
4599
+ data?.isAvailable === false && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "dsh-quota-row", children: [
4600
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "dsh-quota-label", children: t("common.status") }),
4601
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "dsh-quota-value dsh-quota-error", children: t("common.unavailable") })
4034
4602
  ] })
4035
4603
  ] });
4036
4604
  } else {
4037
- body = /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "dsh-quota-body", children: [
4038
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "dsh-quota-error", children: t("widget.noBalance") }),
4605
+ body = /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "dsh-quota-body", children: [
4606
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "dsh-quota-error", children: t("widget.noBalance") }),
4039
4607
  sessionCostRow
4040
4608
  ] });
4041
4609
  }
4042
4610
  }
4043
4611
  const style = pos !== null ? { left: `${pos.x}px`, top: `${pos.y}px`, right: "auto", bottom: "auto" } : void 0;
4044
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
4612
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4045
4613
  "div",
4046
4614
  {
4047
4615
  className: `dsh-quota-root${dragging ? " dsh-quota-dragging" : ""}${hidden ? " dsh-quota-root--hidden" : ""}`,
4048
4616
  style,
4049
4617
  "aria-hidden": hidden,
4050
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
4618
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
4051
4619
  "div",
4052
4620
  {
4053
4621
  ref: rootRef,
@@ -4059,14 +4627,14 @@ function QuotaWidget(props) {
4059
4627
  onPointerCancel: () => endDrag(false),
4060
4628
  onLostPointerCapture: () => endDrag(false),
4061
4629
  children: [
4062
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "dsh-quota-header", children: [
4063
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "dsh-quota-title", children: [
4064
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "dsh-quota-grip", title: t("widget.dragGrip"), children: "\u2807" }),
4065
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: dotClass }),
4066
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "dsh-quota-name", children: t("widget.title") })
4630
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "dsh-quota-header", children: [
4631
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "dsh-quota-title", children: [
4632
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "dsh-quota-grip", title: t("widget.dragGrip"), children: "\u2807" }),
4633
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: dotClass }),
4634
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "dsh-quota-name", children: t("widget.title") })
4067
4635
  ] }),
4068
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "dsh-quota-actions", children: [
4069
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
4636
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "dsh-quota-actions", children: [
4637
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4070
4638
  "button",
4071
4639
  {
4072
4640
  className: "dsh-quota-btn",
@@ -4082,7 +4650,7 @@ function QuotaWidget(props) {
4082
4650
  children: "\u21BB"
4083
4651
  }
4084
4652
  ),
4085
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
4653
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
4086
4654
  "button",
4087
4655
  {
4088
4656
  className: "dsh-quota-btn",
@@ -4100,7 +4668,7 @@ function QuotaWidget(props) {
4100
4668
  )
4101
4669
  ] })
4102
4670
  ] }),
4103
- collapsed && primary !== null && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "dsh-quota-collapsed-total-row", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: `dsh-quota-collapsed-total${low ? " dsh-quota-total--low" : ""}`, children: [
4671
+ collapsed && primary !== null && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "dsh-quota-collapsed-total-row", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: `dsh-quota-collapsed-total${low ? " dsh-quota-total--low" : ""}`, children: [
4104
4672
  fmt(primary.total),
4105
4673
  " ",
4106
4674
  primary.currency
@@ -4131,7 +4699,7 @@ function conversationViewsSource(slots) {
4131
4699
  }
4132
4700
 
4133
4701
  // src/client/index.tsx
4134
- var import_jsx_runtime8 = require("react/jsx-runtime");
4702
+ var import_jsx_runtime9 = require("react/jsx-runtime");
4135
4703
  var name = "dsh-usage-dashboard";
4136
4704
  var inject = ["slots", "locale", "layout"];
4137
4705
  var openQuotaTab = null;
@@ -4152,11 +4720,15 @@ function apply(ctx) {
4152
4720
  });
4153
4721
  ctx.slots.inject("shell.overlay", () => ctx.slots.register(
4154
4722
  { name: "shell.overlay", id: "deepseek-quota", order: 1e3, locale: NS, label: () => t("widget.title") },
4155
- ({ t: slotT, useSessions }) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ErrorBoundary, { t: slotT, silent: true, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(QuotaWidget, { useSessions, views }) }) })
4723
+ ({ t: slotT, useSessions }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ErrorBoundary, { t: slotT, silent: true, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(QuotaWidget, { useSessions, views }) }) })
4156
4724
  ));
4157
4725
  ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register(
4158
4726
  { name: "conversation.composer.dock", id: "quota-cost", order: 2, locale: NS },
4159
- ({ t: slotT, sessionId }) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ErrorBoundary, { t: slotT, silent: true, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(SessionCostPill, { sessionId }) }) })
4727
+ ({ t: slotT, sessionId }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ErrorBoundary, { t: slotT, silent: true, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(SessionCostPill, { sessionId }) }) })
4728
+ ));
4729
+ ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register(
4730
+ { name: "conversation.composer.dock", id: "peak-breaker", order: 3, locale: NS },
4731
+ ({ t: slotT, sessionId }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ErrorBoundary, { t: slotT, silent: true, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(BreakerPill, { sessionId }) }) })
4160
4732
  ));
4161
4733
  ctx.slots.inject("conversation.chat.assistant-actions", () => ctx.slots.register(
4162
4734
  {
@@ -4166,13 +4738,13 @@ function apply(ctx) {
4166
4738
  locale: NS,
4167
4739
  label: () => t("messageCost.label")
4168
4740
  },
4169
- ({ t: slotT, sessionId, messageId }) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ErrorBoundary, { t: slotT, silent: true, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(MessageCost, { sessionId, messageId }) }) })
4741
+ ({ t: slotT, sessionId, messageId }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ErrorBoundary, { t: slotT, silent: true, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(MessageCost, { sessionId, messageId }) }) })
4170
4742
  ));
4171
4743
  const QuotaView = ({ t: slotT, sessionId, openView }) => {
4172
- (0, import_react8.useEffect)(() => {
4744
+ (0, import_react10.useEffect)(() => {
4173
4745
  if (openView !== void 0) openQuotaTab = openView;
4174
4746
  }, [openView]);
4175
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ErrorBoundary, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(BalanceDashboard, { sessionId, views }) }) });
4747
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ErrorBoundary, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(BalanceDashboard, { sessionId, views }) }) });
4176
4748
  };
4177
4749
  ctx.effect(() => {
4178
4750
  let disposeView = null;
@@ -4208,14 +4780,14 @@ function apply(ctx) {
4208
4780
  disposeView?.();
4209
4781
  };
4210
4782
  }, "dsh-usage-dashboard: quota tab registration");
4211
- const QuotaPanel = ({ t: slotT }) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "dq-panel", children: [
4212
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "dq-panel-head", "data-window-drag": true, children: [
4213
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("h1", { className: "dq-panel-title", children: slotT("nav.quota") }),
4214
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "dq-panel-caption", children: slotT("panel.caption") })
4783
+ const QuotaPanel = ({ t: slotT }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dq-panel", children: [
4784
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "dq-panel-head", "data-window-drag": true, children: [
4785
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("h1", { className: "dq-panel-title", children: slotT("nav.quota") }),
4786
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dq-panel-caption", children: slotT("panel.caption") })
4215
4787
  ] }),
4216
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ErrorBoundary, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(BalanceDashboard, { views }) }) })
4788
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(LocaleProvider, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ErrorBoundary, { t: slotT, children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(BalanceDashboard, { views }) }) })
4217
4789
  ] });
4218
- const QuotaGlyph = ({ size, active }) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
4790
+ const QuotaGlyph = ({ size, active }) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
4219
4791
  "svg",
4220
4792
  {
4221
4793
  className: "dq-side-icon",
@@ -4231,23 +4803,23 @@ function apply(ctx) {
4231
4803
  "aria-hidden": "true",
4232
4804
  focusable: "false",
4233
4805
  children: [
4234
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M3.5 3.3 8 8.7l4.5-5.4" }),
4235
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M8 8.7V13.2" }),
4236
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M3.5 10.2h9" }),
4237
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("path", { d: "M3.5 12.1h9" })
4806
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M3.5 3.3 8 8.7l4.5-5.4" }),
4807
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M8 8.7V13.2" }),
4808
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M3.5 10.2h9" }),
4809
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("path", { d: "M3.5 12.1h9" })
4238
4810
  ]
4239
4811
  }
4240
4812
  );
4241
4813
  const QuotaFooterButton = ({ wide, t: slotT }) => {
4242
- const [active, setActive] = (0, import_react8.useState)(() => ctx.layout.panelInfo.getSnapshot().activePanelId === QUOTA_PANEL_ID);
4243
- (0, import_react8.useEffect)(() => {
4814
+ const [active, setActive] = (0, import_react10.useState)(() => ctx.layout.panelInfo.getSnapshot().activePanelId === QUOTA_PANEL_ID);
4815
+ (0, import_react10.useEffect)(() => {
4244
4816
  const sync = () => {
4245
4817
  setActive(ctx.layout.panelInfo.getSnapshot().activePanelId === QUOTA_PANEL_ID);
4246
4818
  };
4247
4819
  sync();
4248
4820
  return ctx.layout.panelInfo.subscribe(sync);
4249
4821
  }, []);
4250
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
4822
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
4251
4823
  "button",
4252
4824
  {
4253
4825
  type: "button",
@@ -4259,8 +4831,8 @@ function apply(ctx) {
4259
4831
  ctx.layout.selectPanel(active ? null : QUOTA_PANEL_ID);
4260
4832
  },
4261
4833
  children: [
4262
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "dq-side-entry-icon", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(QuotaGlyph, { size: 16 }) }),
4263
- wide && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "dq-side-entry-label", children: slotT("nav.quota") })
4834
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dq-side-entry-icon", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(QuotaGlyph, { size: 16 }) }),
4835
+ wide && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "dq-side-entry-label", children: slotT("nav.quota") })
4264
4836
  ]
4265
4837
  }
4266
4838
  );