dsh-agy 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -31,35 +31,70 @@ window.__ModuleLoader__.load({
31
31
  * host has no primitive for.
32
32
  */
33
33
  const STYLE_ID = "dsh-agy-styles";
34
- const CSS = `
34
+ /**
35
+ * Single source of truth for `--dsw-alias-*` fallbacks: the colour an alias
36
+ * renders with when the host theme has not defined it. The host owns the real
37
+ * values; this table only pins the degraded-mode answer, and every usage must
38
+ * go through `aliasVar()` so one alias cannot carry two answers — without the
39
+ * table the file grew three values for `border-l2` and a renamed-forever
40
+ * `--dsw-alias-brand-primary-new-colorprimary-new-color` that no host defines.
41
+ * Normalisation picks (degraded-mode only, enforced by the alias scans in
42
+ * tests/client.test.ts): `state-success` took `#10b981` over the duplicated
43
+ * `#22c55e` because the 60%-alpha glow variant derives from the same base, and
44
+ * `state-business` took the brand blue over the popover pill's emerald —
45
+ * active/cooling/disabled reads as blue/amber/grey, not success-green.
46
+ */
47
+ const ALIAS_FALLBACKS = {
48
+ "bg-layer-1": "#fff",
49
+ "bg-layer-2": "#f4f5f7",
50
+ "bg-layer-3": "#fff",
51
+ "bg-overlay": "#fff",
52
+ "border-l1": "rgba(0,0,0,.04)",
53
+ "border-l2": "#e5e6eb",
54
+ "border-l3": "rgba(0,0,0,.15)",
55
+ "brand-primary": "#4176e6",
56
+ "interactive-bg-hover": "#f4f5f7",
57
+ "label-primary": "#1f2329",
58
+ "label-secondary": "#61666b",
59
+ "label-tertiary": "#8f959e",
60
+ "state-business-primary": "#4176e6",
61
+ "state-error-primary": "#ec1313",
62
+ "state-error-secondary": "#f59e0b",
63
+ "state-success-primary": "#10b981",
64
+ "state-warn-primary": "#f59e0b"
65
+ };
66
+ function aliasVar(alias) {
67
+ return `var(--dsw-alias-${alias}, ${ALIAS_FALLBACKS[alias]})`;
68
+ }
69
+ const AGY_STYLES_CSS = `
35
70
  .agy-root { display: flex; flex-direction: column; gap: 12px; font: var(--dsw-font-xs-13); }
36
71
  .agy-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
37
- .agy-title { font: var(--dsw-font-base-strong-16); color: var(--dsw-alias-label-primary, #1f2329); }
38
- .agy-sub { margin-top: 3px; font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-tertiary, #8f959e); }
72
+ .agy-title { font: var(--dsw-font-base-strong-16); color: ${aliasVar("label-primary")}; }
73
+ .agy-sub { margin-top: 3px; font: var(--dsw-font-xxs-12); color: ${aliasVar("label-tertiary")}; }
39
74
 
40
75
  .agy-tabs {
41
76
  display: flex; align-items: flex-end; gap: 22px; margin-top: 2px;
42
- border-bottom: 0.5px solid var(--dsw-alias-border-l2, #eef0f3);
77
+ border-bottom: 0.5px solid ${aliasVar("border-l2")};
43
78
  }
44
79
  .agy-tab {
45
80
  position: relative; border: 0; padding: 7px 1px 9px; background: transparent;
46
- color: var(--dsw-alias-label-tertiary, #8f959e);
81
+ color: ${aliasVar("label-tertiary")};
47
82
  font: var(--dsw-font-xs-13); cursor: pointer;
48
83
  }
49
- .agy-tab:hover, .agy-tab[data-active="true"] { color: var(--dsw-alias-label-primary, #1f2329); }
84
+ .agy-tab:hover, .agy-tab[data-active="true"] { color: ${aliasVar("label-primary")}; }
50
85
  /* Active tab is an underline rule, matching the Plugins settings section's own
51
86
  tab bar rather than introducing a second, boxed tab idiom. */
52
87
  .agy-tab[data-active="true"]::after {
53
88
  position: absolute; right: 0; bottom: -1px; left: 0; height: 2px;
54
- border-radius: 2px 2px 0 0; background: var(--dsw-alias-label-primary, #1f2329);
89
+ border-radius: 2px 2px 0 0; background: ${aliasVar("label-primary")};
55
90
  content: '';
56
91
  }
57
92
  .agy-tab:focus-visible {
58
- outline: 2px solid var(--dsw-alias-state-business-primary, #4176e6);
93
+ outline: 2px solid ${aliasVar("state-business-primary")};
59
94
  outline-offset: 2px;
60
95
  border-radius: 4px;
61
96
  }
62
- .agy-tab .agy-count { margin-left: 5px; font: var(--dsw-font-xxxs-11); color: var(--dsw-alias-label-tertiary, #8f959e); }
97
+ .agy-tab .agy-count { margin-left: 5px; font: var(--dsw-font-xxxs-11); color: ${aliasVar("label-tertiary")}; }
63
98
 
64
99
  /* A failed action states WHAT failed and then WHY, on two lines. The default
65
100
  white-space (normal) folds that newline into a space and runs the verdict
@@ -67,35 +102,35 @@ window.__ModuleLoader__.load({
67
102
  .agy-notice already does for its own multi-line form. */
68
103
  .agy-error { padding: 9px 12px; border-radius: 8px; font: var(--dsw-font-xxs-12);
69
104
  white-space: pre-wrap; overflow-wrap: anywhere;
70
- color: var(--dsw-alias-state-error-primary, #ec1313);
71
- background: color-mix(in srgb, var(--dsw-alias-state-error-primary, #ec1313) 10%, transparent); }
105
+ color: ${aliasVar("state-error-primary")};
106
+ background: color-mix(in srgb, ${aliasVar("state-error-primary")} 10%, transparent); }
72
107
  /* A non-fatal outcome (a partial import). Neutral, not alarming, and it keeps
73
108
  newlines so a list of per-source failures stays readable. */
74
109
  .agy-notice { padding: 9px 12px; border-radius: 8px; font: var(--dsw-font-xxs-12);
75
110
  white-space: pre-wrap; overflow-wrap: anywhere;
76
- color: var(--dsw-alias-label-secondary, #61666b);
77
- background: var(--dsw-alias-bg-layer-2, #f4f5f7); }
78
- .agy-empty { padding: 24px 12px; text-align: center; font: var(--dsw-font-xs-13); color: var(--dsw-alias-label-tertiary, #8f959e); }
79
- .agy-hint { margin: 0; font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-tertiary, #8f959e); }
80
- .agy-aside { font: var(--dsw-font-xxxs-11); color: var(--dsw-alias-label-tertiary, #8f959e); }
111
+ color: ${aliasVar("label-secondary")};
112
+ background: ${aliasVar("bg-layer-2")}; }
113
+ .agy-empty { padding: 24px 12px; text-align: center; font: var(--dsw-font-xs-13); color: ${aliasVar("label-tertiary")}; }
114
+ .agy-hint { margin: 0; font: var(--dsw-font-xxs-12); color: ${aliasVar("label-tertiary")}; }
115
+ .agy-aside { font: var(--dsw-font-xxxs-11); color: ${aliasVar("label-tertiary")}; }
81
116
  .agy-grow { flex: 1; }
82
117
 
83
118
  /* ── Grouping surface ───────────────────────────────────────────────────────
84
119
  DSH groups with spacing first and a light container second. A settings page
85
120
  built only from tables reads as a spreadsheet, so each block gets a card. */
86
121
  .agy-card {
87
- border: 0.5px solid var(--dsw-alias-border-l2, #eef0f3);
122
+ border: 0.5px solid ${aliasVar("border-l2")};
88
123
  border-radius: 12px;
89
- background: var(--dsw-alias-bg-layer-3, #fff);
124
+ background: ${aliasVar("bg-layer-3")};
90
125
  overflow: hidden;
91
126
  }
92
127
  .agy-card-head {
93
128
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
94
129
  padding: 10px 12px;
95
- border-bottom: 0.5px solid var(--dsw-alias-border-l1, rgba(0,0,0,.04));
96
- background: var(--dsw-alias-bg-layer-2, #f9fafb);
130
+ border-bottom: 0.5px solid ${aliasVar("border-l1")};
131
+ background: ${aliasVar("bg-layer-2")};
97
132
  }
98
- .agy-card-title { font: var(--dsw-font-xs-strong-13); color: var(--dsw-alias-label-primary, #1f2329); }
133
+ .agy-card-title { font: var(--dsw-font-xs-strong-13); color: ${aliasVar("label-primary")}; }
99
134
  .agy-card-body { padding: 6px 12px 8px; }
100
135
 
101
136
  /* ── Rows inside a card ────────────────────────────────────────────────────
@@ -111,8 +146,8 @@ window.__ModuleLoader__.load({
111
146
  .agy-live {
112
147
  display: flex; align-items: center; gap: 7px;
113
148
  margin: 2px 2px 6px; padding: 7px 8px; border-radius: 10px;
114
- font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-secondary, #61666b);
115
- background: var(--dsw-alias-bg-layer-2, #f4f5f7);
149
+ font: var(--dsw-font-xxs-12); color: ${aliasVar("label-secondary")};
150
+ background: ${aliasVar("bg-layer-2")};
116
151
  }
117
152
  /* Master/detail: the account list beside the selected account's detail, so a
118
153
  * row and the panel it opens stay in view together — but only when the
@@ -157,23 +192,23 @@ window.__ModuleLoader__.load({
157
192
  }
158
193
  .agy-rowitem[data-clickable="true"] { cursor: pointer; }
159
194
  .agy-rowitem[data-clickable="true"]:hover {
160
- background: var(--dsw-alias-interactive-bg-hover, #f4f5f7);
195
+ background: ${aliasVar("interactive-bg-hover")};
161
196
  }
162
197
  .agy-rowitem[data-selected="true"] {
163
- background: var(--dsw-alias-interactive-bg-hover, #f4f5f7);
198
+ background: ${aliasVar("interactive-bg-hover")};
164
199
  }
165
200
  .agy-rowitem:focus-visible {
166
- outline: 2px solid var(--dsw-alias-label-primary, #1f2329);
201
+ outline: 2px solid ${aliasVar("label-primary")};
167
202
  outline-offset: -2px;
168
203
  }
169
204
  .agy-rowmain { min-width: 0; flex: 1 1 160px; display: flex; flex-direction: column; gap: 3px; }
170
205
  .agy-rowtitle { display: flex; align-items: center; gap: 7px; min-width: 0; }
171
206
  .agy-rowname {
172
- font: var(--dsw-font-xs-strong-13); color: var(--dsw-alias-label-primary, #1f2329);
207
+ font: var(--dsw-font-xs-strong-13); color: ${aliasVar("label-primary")};
173
208
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
174
209
  }
175
210
  .agy-rowmeta {
176
- font: var(--dsw-font-xxxs-11); color: var(--dsw-alias-label-tertiary, #8f959e);
211
+ font: var(--dsw-font-xxxs-11); color: ${aliasVar("label-tertiary")};
177
212
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
178
213
  }
179
214
  /* margin-left: auto right-aligns the cluster while it shares a line with the
@@ -191,15 +226,15 @@ window.__ModuleLoader__.load({
191
226
  the gap and their own left alignment, and the dividers turned a metric strip
192
227
  into a spreadsheet grid. */
193
228
  .agy-metric { padding: 12px 0; }
194
- .agy-metric-k { font: var(--dsw-font-xxxs-11); color: var(--dsw-alias-label-tertiary, #8f959e); }
229
+ .agy-metric-k { font: var(--dsw-font-xxxs-11); color: ${aliasVar("label-tertiary")}; }
195
230
  /* The theme's own role carries family + size + line-height + weight; the
196
231
  metric only tightens the tracking. */
197
232
  .agy-metric-v { margin-top: 4px; font: var(--dsw-font-base-strong-16); letter-spacing: -.015em;
198
- font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-primary, #1f2329); }
233
+ font-variant-numeric: tabular-nums; color: ${aliasVar("label-primary")}; }
199
234
  .agy-metric-v small { margin-left: 2px; font: var(--dsw-font-xxs-strong-12);
200
- color: var(--dsw-alias-label-tertiary, #8f959e); }
235
+ color: ${aliasVar("label-tertiary")}; }
201
236
  .agy-metric-d { margin-top: 3px; font: var(--dsw-font-xxxs-11);
202
- color: var(--dsw-alias-label-tertiary, #8f959e); }
237
+ color: ${aliasVar("label-tertiary")}; }
203
238
 
204
239
  /* ── Token composition ─────────────────────────────────────────────────────
205
240
  A share bar per bucket, under the headline metrics. This exists to answer
@@ -212,16 +247,16 @@ window.__ModuleLoader__.load({
212
247
  display: grid; grid-template-columns: 64px minmax(0,1fr) 56px 44px;
213
248
  align-items: center; gap: 10px;
214
249
  }
215
- .agy-compose-k { font: var(--dsw-font-xxxs-11); color: var(--dsw-alias-label-tertiary, #8f959e); }
250
+ .agy-compose-k { font: var(--dsw-font-xxxs-11); color: ${aliasVar("label-tertiary")}; }
216
251
  .agy-compose-track {
217
252
  height: 6px; border-radius: 3px; overflow: hidden;
218
- background: var(--dsw-alias-border-l2, rgba(0,0,0,.12));
253
+ background: ${aliasVar("border-l2")};
219
254
  }
220
255
  .agy-compose-track i { display: block; height: 100%; border-radius: 3px; }
221
256
  .agy-compose-v { text-align: right; font: var(--dsw-font-xxs-12);
222
- font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-secondary, #61666b); }
257
+ font-variant-numeric: tabular-nums; color: ${aliasVar("label-secondary")}; }
223
258
  .agy-compose-p { text-align: right; font: var(--dsw-font-xxxs-11);
224
- font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-tertiary, #8f959e); }
259
+ font-variant-numeric: tabular-nums; color: ${aliasVar("label-tertiary")}; }
225
260
  /* A table's own footnote: states a column's scope that its header cannot. */
226
261
  .agy-table-note { padding: 6px 8px 2px; }
227
262
  /* The 65535 row is a CONFIGURATION of High, not a sibling tier: indenting it
@@ -231,25 +266,25 @@ window.__ModuleLoader__.load({
231
266
  /* ── Definition rows (label / value pairs) ───────────────────────────────── */
232
267
  .agy-defs { display: grid; grid-template-columns: 92px minmax(0,1fr); margin: 0; }
233
268
  .agy-defs dt {
234
- padding: 7px 0; font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-tertiary, #8f959e);
235
- border-bottom: 0.5px solid var(--dsw-alias-border-l1, rgba(0,0,0,.04));
269
+ padding: 7px 0; font: var(--dsw-font-xxs-12); color: ${aliasVar("label-tertiary")};
270
+ border-bottom: 0.5px solid ${aliasVar("border-l1")};
236
271
  }
237
272
  .agy-defs dd {
238
- margin: 0; padding: 7px 0; font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-secondary, #61666b);
239
- border-bottom: 0.5px solid var(--dsw-alias-border-l1, rgba(0,0,0,.04));
273
+ margin: 0; padding: 7px 0; font: var(--dsw-font-xxs-12); color: ${aliasVar("label-secondary")};
274
+ border-bottom: 0.5px solid ${aliasVar("border-l1")};
240
275
  overflow-wrap: anywhere;
241
276
  }
242
277
  .agy-defs dt:last-of-type, .agy-defs dd:last-of-type { border-bottom: 0; }
243
278
 
244
279
  /* ── Disclosure (the collapsible model-quota block) ──────────────────────── */
245
- .agy-disclosure { border-top: 0.5px solid var(--dsw-alias-border-l1, rgba(0,0,0,.04)); }
280
+ .agy-disclosure { border-top: 0.5px solid ${aliasVar("border-l1")}; }
246
281
  .agy-disclosure-toggle {
247
282
  display: flex; align-items: center; gap: 7px; width: 100%;
248
283
  padding: 9px 0; border: 0; cursor: pointer;
249
284
  font: var(--dsw-font-xxs-strong-12); text-align: left;
250
- color: var(--dsw-alias-label-secondary, #61666b); background: none;
285
+ color: ${aliasVar("label-secondary")}; background: none;
251
286
  }
252
- .agy-disclosure-toggle:hover { color: var(--dsw-alias-label-primary, #1f2329); }
287
+ .agy-disclosure-toggle:hover { color: ${aliasVar("label-primary")}; }
253
288
  .agy-caret {
254
289
  flex: none; width: 0; height: 0; border-left: 4px solid currentColor;
255
290
  border-top: 3.5px solid transparent; border-bottom: 3.5px solid transparent;
@@ -267,7 +302,7 @@ window.__ModuleLoader__.load({
267
302
  .agy-thinking-group { display: flex; flex-direction: column; }
268
303
  .agy-thinking-group + .agy-thinking-group { margin-top: 16px; }
269
304
  .agy-thinking-group-name {
270
- font: var(--dsw-font-xxs-strong-12); color: var(--dsw-alias-label-secondary, #61666b);
305
+ font: var(--dsw-font-xxs-strong-12); color: ${aliasVar("label-secondary")};
271
306
  margin-bottom: 2px;
272
307
  }
273
308
  .agy-thinking-group .agy-hint { margin: 0 0 8px; }
@@ -278,38 +313,38 @@ window.__ModuleLoader__.load({
278
313
  last one is why that group has no reference table. */
279
314
  .agy-thinking-notes { margin: 0 0 8px; padding-left: 18px; }
280
315
  .agy-thinking-notes li {
281
- font: var(--dsw-font-xxxs-11); color: var(--dsw-alias-label-tertiary, #8f959e);
316
+ font: var(--dsw-font-xxxs-11); color: ${aliasVar("label-tertiary")};
282
317
  line-height: 1.6;
283
318
  }
284
319
  .agy-thinking-row {
285
320
  display: grid; grid-template-columns: 72px minmax(0, 180px) auto;
286
321
  align-items: center; gap: 14px; padding: 8px 0;
287
322
  }
288
- .agy-thinking-k { font: var(--dsw-font-xxs-12); color: var(--dsw-alias-label-secondary, #61666b); }
323
+ .agy-thinking-k { font: var(--dsw-font-xxs-12); color: ${aliasVar("label-secondary")}; }
289
324
  /* Shortcut chips, not a second control: they fill the field beside them. */
290
325
  .agy-thinking-chips { display: flex; gap: 6px; }
291
326
  .agy-thinking-chip {
292
- border: 0.5px solid var(--dsw-alias-border-l3, rgba(0,0,0,.15));
327
+ border: 0.5px solid ${aliasVar("border-l3")};
293
328
  background: transparent; cursor: pointer; border-radius: 10px;
294
329
  padding: 2px 8px; font: var(--dsw-font-xxxs-11);
295
- color: var(--dsw-alias-label-secondary, #61666b);
330
+ color: ${aliasVar("label-secondary")};
296
331
  }
297
- .agy-thinking-chip:hover { background: var(--dsw-alias-interactive-bg-hover); }
332
+ .agy-thinking-chip:hover { background: ${aliasVar("interactive-bg-hover")}; }
298
333
  .agy-thinking-chip:focus-visible {
299
- outline: 2px solid var(--dsw-alias-brand-primary, #4176e6); outline-offset: 1px;
334
+ outline: 2px solid ${aliasVar("brand-primary")}; outline-offset: 1px;
300
335
  }
301
336
  .agy-disclosure-meta { margin-left: auto; font: var(--dsw-font-xxxs-11);
302
- color: var(--dsw-alias-label-tertiary, #8f959e); font-variant-numeric: tabular-nums; }
337
+ color: ${aliasVar("label-tertiary")}; font-variant-numeric: tabular-nums; }
303
338
 
304
339
  /* ── 5h / weekly limits ────────────────────────────────────────────────────
305
340
  One group per upstream group (Gemini, Claude+GPT), each with its windows.
306
341
  The rows are a fixed 4-column grid so the bars and the percentages line up
307
342
  across groups: label / bar / percentage / reset countdown. */
308
343
  .agy-limits { display: flex; flex-direction: column; gap: 10px; padding: 4px 0; }
309
- .agy-limit-age { font: var(--dsw-font-xxxs-11); color: var(--dsw-alias-label-tertiary, #8f959e); }
344
+ .agy-limit-age { font: var(--dsw-font-xxxs-11); color: ${aliasVar("label-tertiary")}; }
310
345
  .agy-limit-group { display: flex; flex-direction: column; gap: 2px; }
311
346
  .agy-limit-group-name {
312
- font: var(--dsw-font-xxs-strong-12); color: var(--dsw-alias-label-secondary, #61666b);
347
+ font: var(--dsw-font-xxs-strong-12); color: ${aliasVar("label-secondary")};
313
348
  padding-bottom: 2px;
314
349
  }
315
350
  .agy-limit-row {
@@ -317,33 +352,33 @@ window.__ModuleLoader__.load({
317
352
  align-items: center; gap: 10px; padding: 4px 0;
318
353
  font: var(--dsw-font-xxs-12);
319
354
  }
320
- .agy-limit-k { color: var(--dsw-alias-label-secondary, #61666b); }
355
+ .agy-limit-k { color: ${aliasVar("label-secondary")}; }
321
356
  .agy-limit-track { height: 6px; border-radius: 3px; overflow: hidden;
322
- background: var(--dsw-alias-border-l2, rgba(0,0,0,.12)); }
357
+ background: ${aliasVar("border-l2")}; }
323
358
  .agy-limit-track i { display: block; height: 100%; border-radius: 3px; }
324
359
  .agy-limit-p { text-align: right; font-variant-numeric: tabular-nums;
325
- color: var(--dsw-alias-label-primary, #1f2329); }
360
+ color: ${aliasVar("label-primary")}; }
326
361
  .agy-limit-reset { text-align: right; font: var(--dsw-font-xxxs-11);
327
- color: var(--dsw-alias-label-tertiary, #8f959e); }
362
+ color: ${aliasVar("label-tertiary")}; }
328
363
  /* The burn projection: indented to align with the bar (58px label + 10px gap),
329
364
  warn-tinted because "this window runs dry before it resets" is the one
330
365
  projection that asks the reader to act. */
331
366
  .agy-limit-burn { padding: 0 0 4px 68px;
332
367
  font: var(--dsw-font-xxxs-11);
333
- color: var(--dsw-alias-state-warn-primary, #f59e0b); }
368
+ color: ${aliasVar("state-warn-primary")}; }
334
369
 
335
370
  /* ── Dense breakdown tables (Usage tab only) ─────────────────────────────── */
336
371
  .agy-table-wrap { padding: 6px 0 2px; }
337
372
  .agy-table { width: 100%; border-collapse: collapse; font: var(--dsw-font-xs-13); table-layout: fixed; }
338
373
  .agy-table th {
339
374
  text-align: left; padding: 0 8px 7px; font: var(--dsw-font-xxxs-strong-11);
340
- color: var(--dsw-alias-label-tertiary, #8f959e);
341
- border-bottom: 0.5px solid var(--dsw-alias-border-l2, #eef0f3); white-space: nowrap;
375
+ color: ${aliasVar("label-tertiary")};
376
+ border-bottom: 0.5px solid ${aliasVar("border-l2")}; white-space: nowrap;
342
377
  }
343
- .agy-table td { padding: 8px; vertical-align: middle; color: var(--dsw-alias-label-secondary, #61666b);
344
- border-bottom: 0.5px solid var(--dsw-alias-border-l1, rgba(0,0,0,.04)); }
378
+ .agy-table td { padding: 8px; vertical-align: middle; color: ${aliasVar("label-secondary")};
379
+ border-bottom: 0.5px solid ${aliasVar("border-l1")}; }
345
380
  .agy-table tr:last-child td { border-bottom: 0; }
346
- .agy-table tbody tr:hover td { background: var(--dsw-alias-bg-layer-2, #f4f5f7); }
381
+ .agy-table tbody tr:hover td { background: ${aliasVar("bg-layer-2")}; }
347
382
  .agy-num { text-align: right; font-variant-numeric: tabular-nums; }
348
383
  /* Numeric HEADERS must right-align too, and .agy-num alone cannot do it: the
349
384
  .agy-table th rule above is specificity 0-1-1 and outranks the bare .agy-num
@@ -354,20 +389,20 @@ window.__ModuleLoader__.load({
354
389
  escalating with !important. */
355
390
  .agy-table th.agy-num { text-align: right; }
356
391
  /* Inline emphasis on a table cell: a strong role, not a heavier size. */
357
- .agy-strong { color: var(--dsw-alias-label-primary, #1f2329); font-weight: 500; }
392
+ .agy-strong { color: ${aliasVar("label-primary")}; font-weight: 500; }
358
393
  .agy-mail { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
359
394
  .agy-mono { font-family: var(--ds-font-family-code); }
360
395
  /* The one external link in the section (a verification appeal URL). Colored and
361
396
  underlined with theme tokens rather than left to the browser default, which
362
397
  ignores both the light/dark theme and the host's brand color. */
363
- .agy-link { color: var(--dsw-alias-brand-primary-new-colorprimary-new-color, #4176e6); text-decoration: underline; }
398
+ .agy-link { color: ${aliasVar("brand-primary")}; text-decoration: underline; }
364
399
  .agy-link:hover { opacity: 0.8; }
365
400
 
366
401
  .agy-bar { display: inline-flex; align-items: center; gap: 8px; justify-content: flex-end; }
367
402
  .agy-bar .agy-track { width: 56px; height: 4px; border-radius: 2px; overflow: hidden;
368
- background: var(--dsw-alias-border-l2, rgba(0,0,0,.12)); }
403
+ background: ${aliasVar("border-l2")}; }
369
404
  .agy-bar .agy-track i { display: block; height: 100%; border-radius: 2px;
370
- background: var(--dsw-alias-brand-primary-new-colorprimary-new-color, #4176e6); }
405
+ background: ${aliasVar("brand-primary")}; }
371
406
 
372
407
  /* ── Range picker container ────────────────────────────────────────────────
373
408
  The pills themselves are the host Pill primitive (its own fill pair and
@@ -375,16 +410,16 @@ window.__ModuleLoader__.load({
375
410
  .agy-chips { display: flex; gap: 6px; }
376
411
 
377
412
  /* Danger has no primitive variant; keep the ghost skin and tint the label. */
378
- .agy-btn-danger { color: var(--dsw-alias-state-error-primary, #ec1313) !important; }
413
+ .agy-btn-danger { color: ${aliasVar("state-error-primary")} !important; }
379
414
 
380
415
  /* ── Recent activity ring ──────────────────────────────────────────────────
381
416
  The "what just happened" list. Only the result cell carries color — ok
382
417
  inherits the table's neutral, and a wall of tinted rows would read as an
383
418
  alarm rather than a log. */
384
419
  .agy-recent-state { font: var(--dsw-font-xxs-12); }
385
- .agy-recent-state[data-kind="fail"] { color: var(--dsw-alias-state-error-primary, #ec1313); }
386
- .agy-recent-state[data-kind="limited"] { color: var(--dsw-alias-state-warn-primary, #f59e0b); }
387
- .agy-recent-state[data-kind="rotation"] { color: var(--dsw-alias-brand-primary-new-colorprimary-new-color, #4176e6); }
420
+ .agy-recent-state[data-kind="fail"] { color: ${aliasVar("state-error-primary")}; }
421
+ .agy-recent-state[data-kind="limited"] { color: ${aliasVar("state-warn-primary")}; }
422
+ .agy-recent-state[data-kind="rotation"] { color: ${aliasVar("brand-primary")}; }
388
423
  /* The recent list is a standalone disclosure on the tab root, not one block
389
424
  inside a card body — the separator border-top the disclosure idiom uses
390
425
  between sibling blocks would draw a stray line across nothing here. */
@@ -393,9 +428,471 @@ window.__ModuleLoader__.load({
393
428
  .agy-toolbar { display: flex; align-items: center; gap: 8px; }
394
429
  .agy-textarea { width: 100%; min-height: 88px; resize: vertical; outline: none;
395
430
  padding: 9px 10px; font: var(--dsw-font-xxs-12); font-family: var(--ds-font-family-code);
396
- color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-bg-layer-1, #fff);
397
- border: 0.5px solid var(--dsw-alias-border-l2, #eef0f3); border-radius: 8px; }
398
- .agy-textarea:focus { border-color: var(--dsw-alias-brand-primary-new-colorprimary-new-color, #4176e6); }
431
+ color: ${aliasVar("label-primary")}; background: ${aliasVar("bg-layer-1")};
432
+ border: 0.5px solid ${aliasVar("border-l2")}; border-radius: 8px; }
433
+ .agy-textarea:focus { border-color: ${aliasVar("brand-primary")}; }
434
+
435
+ /* ── Preferences card ───────────────────────────────────────────────────── */
436
+ .agy-pref-row {
437
+ display: flex;
438
+ align-items: center;
439
+ justify-content: space-between;
440
+ gap: 16px;
441
+ padding: 4px 0;
442
+ }
443
+ .agy-pref-name {
444
+ font: var(--dsw-font-xs-strong-13);
445
+ color: ${aliasVar("label-primary")};
446
+ }
447
+ .agy-pref-desc {
448
+ font: var(--dsw-font-xxs-12);
449
+ color: ${aliasVar("label-tertiary")};
450
+ margin-top: 2px;
451
+ }
452
+
453
+ /* ── Conversation-header quota badge & popover ─────────────────────────── */
454
+ .agy-ui-badge {
455
+ display: inline-flex;
456
+ align-items: center;
457
+ gap: 6px;
458
+ background: ${aliasVar("bg-layer-2")};
459
+ border: 0.5px solid ${aliasVar("border-l2")};
460
+ border-radius: 9999px;
461
+ padding: 3px 10px;
462
+ font: var(--dsw-font-xxs-12);
463
+ color: ${aliasVar("label-primary")};
464
+ cursor: pointer;
465
+ user-select: none;
466
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
467
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
468
+ font-family: inherit;
469
+ height: auto;
470
+ }
471
+
472
+ .agy-ui-badge:hover,
473
+ .agy-ui-badge.pinned {
474
+ background: ${aliasVar("bg-layer-3")};
475
+ border-color: ${aliasVar("border-l1")};
476
+ transform: translateY(-1px);
477
+ box-shadow: 0 3px 10px rgba(0, 0, 0, 0.12);
478
+ }
479
+
480
+ .agy-ui-badge.pinned {
481
+ border-color: ${aliasVar("brand-primary")};
482
+ box-shadow: 0 0 0 1px ${aliasVar("brand-primary")}, 0 3px 10px rgba(0, 0, 0, 0.12);
483
+ }
484
+
485
+ .agy-ui-dot {
486
+ display: inline-block;
487
+ width: 7px;
488
+ height: 7px;
489
+ border-radius: 50%;
490
+ flex-shrink: 0;
491
+ transition: background-color 0.3s;
492
+ }
493
+
494
+ .agy-ui-dot[data-state="done"],
495
+ .agy-ui-dot.active {
496
+ background-color: ${aliasVar("state-success-primary")};
497
+ box-shadow: 0 0 6px ${aliasVar("state-success-primary")};
498
+ }
499
+
500
+ .agy-ui-dot[data-state="warning"],
501
+ .agy-ui-dot.cooling {
502
+ background-color: ${aliasVar("state-warn-primary")};
503
+ box-shadow: 0 0 6px ${aliasVar("state-warn-primary")};
504
+ }
505
+
506
+ .agy-ui-dot[data-state="idle"],
507
+ .agy-ui-dot.disabled {
508
+ background-color: ${aliasVar("label-tertiary")};
509
+ }
510
+
511
+ .agy-ui-dot.updating {
512
+ animation: agy-ui-pulse 1.2s ease-in-out infinite;
513
+ }
514
+
515
+ .agy-ui-dot.active.updating,
516
+ .agy-ui-dot[data-state="done"].updating {
517
+ box-shadow: 0 0 10px ${aliasVar("state-success-primary")};
518
+ }
519
+
520
+ .agy-ui-dot.cooling.updating,
521
+ .agy-ui-dot[data-state="warning"].updating {
522
+ box-shadow: 0 0 10px ${aliasVar("state-warn-primary")};
523
+ }
524
+
525
+ @keyframes agy-ui-pulse {
526
+ 0%, 100% {
527
+ opacity: 1;
528
+ transform: scale(1);
529
+ }
530
+ 50% {
531
+ opacity: 0.35;
532
+ transform: scale(0.7);
533
+ }
534
+ }
535
+
536
+ .agy-ui-sparkle {
537
+ color: ${aliasVar("brand-primary")};
538
+ font: var(--dsw-font-xs-13);
539
+ }
540
+
541
+ .agy-ui-popover-container.desktop {
542
+ position: static;
543
+ }
544
+
545
+ .agy-ui-popover-container.mobile {
546
+ position: fixed;
547
+ inset: 0;
548
+ z-index: 1000;
549
+ background: rgba(0, 0, 0, 0.4);
550
+ backdrop-filter: blur(6px);
551
+ -webkit-backdrop-filter: blur(6px);
552
+ display: flex;
553
+ align-items: flex-end;
554
+ justify-content: center;
555
+ animation: agy-ui-fade-in 0.2s ease-out;
556
+ }
557
+
558
+ .agy-ui-popover {
559
+ background: ${aliasVar("bg-overlay")};
560
+ backdrop-filter: blur(20px);
561
+ -webkit-backdrop-filter: blur(20px);
562
+ border: 0.5px solid ${aliasVar("border-l2")};
563
+ box-shadow: 0 20px 45px rgba(0, 0, 0, 0.15), 0 0 0 0.5px ${aliasVar("border-l1")};
564
+ color: ${aliasVar("label-primary")};
565
+ overflow: hidden;
566
+ display: flex;
567
+ flex-direction: column;
568
+ box-sizing: border-box;
569
+ z-index: 1000;
570
+ }
571
+
572
+ .agy-ui-popover.desktop {
573
+ width: 380px;
574
+ max-width: calc(100vw - 24px);
575
+ border-radius: 14px;
576
+ animation: agy-ui-popover-in 0.18s cubic-bezier(0.16, 1, 0.3, 1);
577
+ }
578
+
579
+ .agy-ui-popover.mobile {
580
+ width: 100%;
581
+ max-height: 82vh;
582
+ border-radius: 20px 20px 0 0;
583
+ border-bottom: none;
584
+ animation: agy-ui-bottom-sheet-in 0.25s cubic-bezier(0.16, 1, 0.3, 1);
585
+ }
586
+
587
+ .agy-ui-mobile-handle {
588
+ width: 36px;
589
+ height: 4px;
590
+ background: ${aliasVar("border-l2")};
591
+ border-radius: 9999px;
592
+ margin: 8px auto 2px auto;
593
+ }
594
+
595
+ @keyframes agy-ui-popover-in {
596
+ from {
597
+ opacity: 0;
598
+ transform: translateY(4px) scale(0.98);
599
+ }
600
+ to {
601
+ opacity: 1;
602
+ transform: translateY(0) scale(1);
603
+ }
604
+ }
605
+
606
+ @keyframes agy-ui-bottom-sheet-in {
607
+ from {
608
+ transform: translateY(100%);
609
+ }
610
+ to {
611
+ transform: translateY(0);
612
+ }
613
+ }
614
+
615
+ @keyframes agy-ui-fade-in {
616
+ from { opacity: 0; }
617
+ to { opacity: 1; }
618
+ }
619
+
620
+ .agy-ui-modal-header {
621
+ padding: 10px 14px;
622
+ border-bottom: 0.5px solid ${aliasVar("border-l2")};
623
+ display: flex;
624
+ align-items: center;
625
+ justify-content: space-between;
626
+ background: ${aliasVar("bg-layer-1")};
627
+ }
628
+
629
+ .agy-ui-modal-title {
630
+ display: flex;
631
+ align-items: center;
632
+ gap: 7px;
633
+ font: var(--dsw-font-xs-strong-13);
634
+ color: ${aliasVar("label-primary")};
635
+ }
636
+
637
+ .agy-ui-pinned-tag {
638
+ font: var(--dsw-font-xxxs-11);
639
+ margin-left: 2px;
640
+ }
641
+
642
+ .agy-ui-header-actions {
643
+ display: flex;
644
+ align-items: center;
645
+ gap: 4px;
646
+ }
647
+
648
+ .agy-ui-icon-btn {
649
+ color: ${aliasVar("label-secondary")};
650
+ padding: 4px;
651
+ min-width: 28px;
652
+ height: 28px;
653
+ display: flex;
654
+ align-items: center;
655
+ justify-content: center;
656
+ background: transparent;
657
+ border: none;
658
+ cursor: pointer;
659
+ border-radius: 6px;
660
+ transition: all 0.15s;
661
+ }
662
+
663
+ .agy-ui-icon-btn:hover {
664
+ color: ${aliasVar("label-primary")};
665
+ background: ${aliasVar("bg-layer-2")};
666
+ }
667
+
668
+ .agy-ui-icon-btn.active {
669
+ color: ${aliasVar("brand-primary")};
670
+ background: color-mix(in srgb, ${aliasVar("brand-primary")} 18%, transparent);
671
+ }
672
+
673
+ .agy-ui-icon-btn:disabled {
674
+ opacity: 0.5;
675
+ cursor: not-allowed;
676
+ }
677
+
678
+ .agy-ui-spinning {
679
+ animation: agy-ui-spin 1s linear infinite;
680
+ }
681
+
682
+ @keyframes agy-ui-spin {
683
+ from { transform: rotate(0deg); }
684
+ to { transform: rotate(360deg); }
685
+ }
686
+
687
+ .agy-ui-modal-body {
688
+ padding: 14px 16px;
689
+ overflow-y: auto;
690
+ display: flex;
691
+ flex-direction: column;
692
+ gap: 12px;
693
+ max-height: 480px;
694
+ }
695
+
696
+ .agy-ui-account-card {
697
+ background: ${aliasVar("bg-layer-2")};
698
+ border: 0.5px solid ${aliasVar("border-l1")};
699
+ border-radius: 9px;
700
+ padding: 8px 12px;
701
+ display: flex;
702
+ align-items: center;
703
+ justify-content: space-between;
704
+ gap: 8px;
705
+ }
706
+
707
+ .agy-ui-account-email {
708
+ font: var(--dsw-font-xxs-12);
709
+ color: ${aliasVar("label-primary")};
710
+ }
711
+
712
+ .agy-ui-account-project {
713
+ font: var(--dsw-font-xxxs-11);
714
+ color: ${aliasVar("label-tertiary")};
715
+ margin-top: 2px;
716
+ }
717
+
718
+ .agy-ui-state-pill {
719
+ font: var(--dsw-font-xxxs-11);
720
+ padding: 2px 7px;
721
+ border-radius: 9999px;
722
+ text-transform: capitalize;
723
+ }
724
+
725
+ .agy-ui-state-pill.active {
726
+ background: color-mix(in srgb, ${aliasVar("state-business-primary")} 15%, transparent);
727
+ color: ${aliasVar("state-business-primary")};
728
+ border: 0.5px solid color-mix(in srgb, ${aliasVar("state-business-primary")} 30%, transparent);
729
+ }
730
+
731
+ .agy-ui-state-pill.cooling {
732
+ background: color-mix(in srgb, ${aliasVar("state-error-secondary")} 15%, transparent);
733
+ color: ${aliasVar("state-error-secondary")};
734
+ border: 0.5px solid color-mix(in srgb, ${aliasVar("state-error-secondary")} 30%, transparent);
735
+ }
736
+
737
+ .agy-ui-state-pill.verification-required {
738
+ background: color-mix(in srgb, ${aliasVar("brand-primary")} 15%, transparent);
739
+ color: ${aliasVar("brand-primary")};
740
+ border: 0.5px solid color-mix(in srgb, ${aliasVar("brand-primary")} 30%, transparent);
741
+ }
742
+
743
+ .agy-ui-state-pill.disabled {
744
+ background: color-mix(in srgb, ${aliasVar("label-tertiary")} 15%, transparent);
745
+ color: ${aliasVar("label-secondary")};
746
+ border: 0.5px solid color-mix(in srgb, ${aliasVar("label-tertiary")} 30%, transparent);
747
+ }
748
+
749
+ .agy-ui-section-label {
750
+ font: var(--dsw-font-xxxs-11);
751
+ color: ${aliasVar("label-tertiary")};
752
+ text-transform: uppercase;
753
+ letter-spacing: 0.05em;
754
+ }
755
+
756
+ .agy-ui-quota-card {
757
+ background: ${aliasVar("bg-layer-2")};
758
+ border: 0.5px solid ${aliasVar("border-l1")};
759
+ border-radius: 10px;
760
+ padding: 10px 12px;
761
+ display: flex;
762
+ flex-direction: column;
763
+ gap: 6px;
764
+ }
765
+
766
+ .agy-ui-quota-header {
767
+ display: flex;
768
+ align-items: center;
769
+ justify-content: space-between;
770
+ margin-bottom: 2px;
771
+ }
772
+
773
+ .agy-ui-model-name {
774
+ font: var(--dsw-font-xxs-12);
775
+ color: ${aliasVar("label-primary")};
776
+ }
777
+
778
+ .agy-ui-limit-row {
779
+ display: flex;
780
+ flex-direction: column;
781
+ gap: 3px;
782
+ }
783
+
784
+ .agy-ui-limit-row + .agy-ui-limit-row {
785
+ margin-top: 5px;
786
+ }
787
+
788
+ .agy-ui-limit-header {
789
+ display: flex;
790
+ align-items: center;
791
+ justify-content: space-between;
792
+ font: var(--dsw-font-xxxs-11);
793
+ }
794
+
795
+ .agy-ui-limit-title {
796
+ color: ${aliasVar("label-secondary")};
797
+ font: var(--dsw-font-xxxs-11);
798
+ }
799
+
800
+ .agy-ui-limit-percent {
801
+ font: var(--dsw-font-xxs-12);
802
+ font-variant-numeric: tabular-nums;
803
+ }
804
+
805
+ .agy-ui-progress-track {
806
+ width: 100%;
807
+ height: 5px;
808
+ background: color-mix(in srgb, ${aliasVar("label-primary")} 8%, transparent);
809
+ border-radius: 9999px;
810
+ overflow: hidden;
811
+ }
812
+
813
+ .agy-ui-progress-fill {
814
+ height: 100%;
815
+ border-radius: 9999px;
816
+ transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
817
+ }
818
+
819
+ .agy-ui-quota-footer {
820
+ display: flex;
821
+ justify-content: flex-end;
822
+ font: var(--dsw-font-xxxs-11);
823
+ color: ${aliasVar("label-tertiary")};
824
+ font-variant-numeric: tabular-nums;
825
+ }
826
+
827
+ .agy-ui-modal-footer {
828
+ padding: 8px 14px;
829
+ border-top: 0.5px solid ${aliasVar("border-l2")};
830
+ background: ${aliasVar("bg-layer-1")};
831
+ display: flex;
832
+ align-items: center;
833
+ justify-content: space-between;
834
+ gap: 8px;
835
+ flex-wrap: wrap;
836
+ font: var(--dsw-font-xxxs-11);
837
+ color: ${aliasVar("label-tertiary")};
838
+ }
839
+
840
+ .agy-ui-window-note {
841
+ font: var(--dsw-font-xxxs-11);
842
+ color: ${aliasVar("label-tertiary")};
843
+ margin-top: 3px;
844
+ padding: 8px 0;
845
+ }
846
+
847
+ .agy-ui-limit-age {
848
+ font: var(--dsw-font-xxxs-11);
849
+ color: ${aliasVar("label-tertiary")};
850
+ text-align: right;
851
+ font-variant-numeric: tabular-nums;
852
+ }
853
+
854
+ .agy-ui-verify-note {
855
+ display: flex;
856
+ align-items: center;
857
+ justify-content: space-between;
858
+ gap: 8px;
859
+ font: var(--dsw-font-xxs-12);
860
+ color: ${aliasVar("brand-primary")};
861
+ background: color-mix(in srgb, ${aliasVar("brand-primary")} 10%, transparent);
862
+ border: 0.5px solid color-mix(in srgb, ${aliasVar("brand-primary")} 30%, transparent);
863
+ border-radius: 9999px;
864
+ padding: 6px 12px;
865
+ }
866
+
867
+ .agy-ui-link-btn {
868
+ color: ${aliasVar("brand-primary")};
869
+ text-decoration: none;
870
+ font: var(--dsw-font-xxs-12);
871
+ display: inline-flex;
872
+ align-items: center;
873
+ gap: 4px;
874
+ cursor: pointer;
875
+ transition: color 0.15s;
876
+ background: none;
877
+ border: none;
878
+ padding: 0;
879
+ }
880
+
881
+ .agy-ui-link-btn:hover {
882
+ text-decoration: underline;
883
+ }
884
+
885
+ @media (max-width: 640px) {
886
+ .agy-ui-badge {
887
+ padding: 2px 7px;
888
+ font: var(--dsw-font-xxxs-11);
889
+ gap: 4px;
890
+ }
891
+ .agy-ui-modal-body {
892
+ padding: 12px 14px;
893
+ gap: 10px;
894
+ }
895
+ }
399
896
  `;
400
897
  /** Install the stylesheet once (idempotent across plugin reloads): a second
401
898
  * install never appends a duplicate, and one whose content has gone stale
@@ -407,12 +904,12 @@ window.__ModuleLoader__.load({
407
904
  if (typeof document === "undefined") return () => {};
408
905
  const existing = document.getElementById(STYLE_ID);
409
906
  if (existing !== null) {
410
- if (existing.textContent !== CSS) existing.textContent = CSS;
907
+ if (existing.textContent !== AGY_STYLES_CSS) existing.textContent = AGY_STYLES_CSS;
411
908
  return () => {};
412
909
  }
413
910
  const style = document.createElement("style");
414
911
  style.id = STYLE_ID;
415
- style.textContent = CSS;
912
+ style.textContent = AGY_STYLES_CSS;
416
913
  document.head.appendChild(style);
417
914
  return () => {};
418
915
  }
@@ -476,6 +973,37 @@ window.__ModuleLoader__.load({
476
973
  limitBurnWarn: "按此速度{value}后耗尽",
477
974
  quotaWindow5h: "5 小时",
478
975
  quotaWindowWeekly: "每周",
976
+ quotaWindowDaily: "每日",
977
+ quotaWindowMonthly: "每月",
978
+ quotaResetPassed: "已重置",
979
+ quotaStaleNote: "该窗口已重置,等待下次刷新",
980
+ quotaUnmeasuredNote: "上游未返回该窗口的剩余额度",
981
+ quotaGroupFallback: "配额分组",
982
+ badgeTitle: "Antigravity 配额状态",
983
+ badgeAria: "Antigravity 配额",
984
+ badgeHint: "固定弹窗",
985
+ badgeUnpinHint: "取消固定",
986
+ badgeRefreshHint: "刷新配额状态",
987
+ badgePinned: "已固定",
988
+ badgeClose: "关闭",
989
+ badgeReading: "{window}剩余 {percent}%",
990
+ badgeUnmeasured: "尚未测量",
991
+ badgeNoAccount: "未检测到活跃账号,请前往设置页登录",
992
+ badgeSectionMonitor: "额度监控 (5小时 / 日 / 周 / 月)",
993
+ badgeVerifyRequired: "该账号被要求验证身份",
994
+ badgeAppealLink: "打开申诉链接 ↗",
995
+ badgeNoQuotaData: "暂无额度数据,点击右上角刷新重新测量",
996
+ badgeMeasuredAt: "额度测量于 {time}",
997
+ badgeTooltip: "Antigravity: {count} 个账号 · {reading} · 悬停或点击查看配额详情",
998
+ quotaSourceCaption: "数据来源: dsh-agy 分组额度",
999
+ quotaManageHint: "管理入口: 设置 → Antigravity",
1000
+ preferencesTitle: "偏好设置",
1001
+ prefConversationBadge: "会话顶栏配额胶囊",
1002
+ prefConversationBadgeDesc: "在聊天会话顶栏显示当前账号配额与水位状态浮层",
1003
+ prefSaveFailed: "保存失败:{message}",
1004
+ badgeQuotaFormat: "AGY · {percent}%",
1005
+ badgeWindowQuotaFormat: "AGY · {window}{percent}%",
1006
+ badgeCountFormat: "AGY ✦ {count}",
479
1007
  fieldProject: "项目",
480
1008
  fieldProxy: "代理",
481
1009
  fieldFingerprint: "指纹",
@@ -658,6 +1186,37 @@ window.__ModuleLoader__.load({
658
1186
  limitBurnWarn: "at this rate, empty in {value}",
659
1187
  quotaWindow5h: "5 hours",
660
1188
  quotaWindowWeekly: "Weekly",
1189
+ quotaWindowDaily: "Daily",
1190
+ quotaWindowMonthly: "Monthly",
1191
+ quotaResetPassed: "already reset",
1192
+ quotaStaleNote: "This window has already reset — waiting for the next refresh",
1193
+ quotaUnmeasuredNote: "Upstream reported no remaining fraction for this window",
1194
+ quotaGroupFallback: "Quota group",
1195
+ badgeTitle: "Antigravity quota status",
1196
+ badgeAria: "Antigravity quota",
1197
+ badgeHint: "Pin modal",
1198
+ badgeUnpinHint: "Unpin modal",
1199
+ badgeRefreshHint: "Refresh quota status",
1200
+ badgePinned: "Pinned",
1201
+ badgeClose: "Close",
1202
+ badgeReading: "{window} {percent}% left",
1203
+ badgeUnmeasured: "not measured yet",
1204
+ badgeNoAccount: "No active account, please login in Settings",
1205
+ badgeSectionMonitor: "Quota monitor (5h / Daily / Weekly / Monthly)",
1206
+ badgeVerifyRequired: "Account verification required",
1207
+ badgeAppealLink: "Open appeal link ↗",
1208
+ badgeNoQuotaData: "No quota data yet, click refresh in top right to measure",
1209
+ badgeMeasuredAt: "Limits measured {time}",
1210
+ badgeTooltip: "Antigravity: {count} accounts · {reading} · Hover or click to view quota details",
1211
+ quotaSourceCaption: "Source: dsh-agy grouped limits",
1212
+ quotaManageHint: "Manage: Settings → Antigravity",
1213
+ preferencesTitle: "Preferences",
1214
+ prefConversationBadge: "Conversation header quota badge",
1215
+ prefConversationBadgeDesc: "Display active account quota badge and popover in conversation session header",
1216
+ prefSaveFailed: "Save failed: {message}",
1217
+ badgeQuotaFormat: "AGY · {percent}%",
1218
+ badgeWindowQuotaFormat: "AGY · {window}{percent}%",
1219
+ badgeCountFormat: "AGY ✦ {count}",
661
1220
  fieldProject: "Project",
662
1221
  fieldProxy: "Proxy",
663
1222
  fieldFingerprint: "Fingerprint",
@@ -791,6 +1350,732 @@ window.__ModuleLoader__.load({
791
1350
  exportAll: "Export all"
792
1351
  };
793
1352
  //#endregion
1353
+ //#region src/client/element.ts
1354
+ /**
1355
+ * The element shorthand the browser half is written in.
1356
+ *
1357
+ * `h(tag, props, ...children)` keeps an element tree flat and readable where
1358
+ * nested `createElement` calls become a parenthesis maze — and this bundle has
1359
+ * no JSX transform, so there is no third option.
1360
+ *
1361
+ * A shared module rather than a copy per file: the Settings section and the
1362
+ * conversation-header badge build their trees the same way.
1363
+ */
1364
+ function h(tag, props, ...children) {
1365
+ return (0, react.createElement)(tag, props ?? null, ...children);
1366
+ }
1367
+ //#endregion
1368
+ //#region src/types.ts
1369
+ /**
1370
+ * The upstream `window` tokens we understand: their ORDER by duration, and which
1371
+ * of them `CachedQuota` has a field for.
1372
+ *
1373
+ * ONE table for a vocabulary that used to live in two places giving two different
1374
+ * answers: `adapter/quota-summary.ts` sorted by its own token table while
1375
+ * `runtime/quota.ts` classified windows with `includes()` checks. The
1376
+ * disagreement was not academic — `daily` existed in one and not the other, so a
1377
+ * daily group parsed and display-sorted correctly, then contributed an EMPTY
1378
+ * record that replaced a real measurement.
1379
+ *
1380
+ * Keeping both facts in one row is deliberate: a token cannot be rankable in one
1381
+ * consumer and unknown to the other, because there is only one place to add it.
1382
+ * A missing `kind` means the cache has nowhere to put that reading, which is what
1383
+ * stops the empty-record overwrite.
1384
+ *
1385
+ * Declared in this leaf rather than in either consumer because BOTH need it:
1386
+ * `runtime/` must not reach into `adapter/`, and the persisted `QuotaWindow` is
1387
+ * already defined here.
1388
+ *
1389
+ * `rank` is explicit rather than inferred from token length so it states the
1390
+ * intended duration ordering: `daily` (5 chars) would otherwise sort before
1391
+ * `weekly` (6) for the wrong reason.
1392
+ */
1393
+ const QUOTA_WINDOWS = {
1394
+ "5h": {
1395
+ rank: 0,
1396
+ kind: "rolling"
1397
+ },
1398
+ daily: { rank: 1 },
1399
+ weekly: {
1400
+ rank: 2,
1401
+ kind: "weekly"
1402
+ },
1403
+ monthly: { rank: 3 }
1404
+ };
1405
+ //#endregion
1406
+ //#region src/runtime/rotation.ts
1407
+ /** Below this remaining fraction the account is treated as soft-quota-exhausted. */
1408
+ const SOFT_QUOTA_THRESHOLD = .15;
1409
+ /**
1410
+ * The WEEKLY window's exhaustion threshold, and deliberately NOT
1411
+ * `SOFT_QUOTA_THRESHOLD`.
1412
+ *
1413
+ * The two numbers answer the same question — "how much work is left in this
1414
+ * window?" — about windows of very different lengths, so they only look
1415
+ * inconsistent when read side by side:
1416
+ *
1417
+ * 15% of a 5-hour window is ~45 minutes of runway.
1418
+ * 1% of a 7-day window is ~1.7 hours of runway.
1419
+ *
1420
+ * The weekly bar is the stricter one where it matters, because being wrong is
1421
+ * asymmetric: a drained 5-hour window refills within five hours, while a drained
1422
+ * weekly window parks the account for DAYS. Treating an account as drained a
1423
+ * little early costs one rotation; treating it as usable too late costs the user
1424
+ * a failed request with no healthy fallback.
1425
+ *
1426
+ * Do not "unify" these two constants without re-deriving both runways above.
1427
+ */
1428
+ const WEEKLY_QUOTA_THRESHOLD = .01;
1429
+ /** Absolute server-reported reset in ms when it lies in the future, else undefined. */
1430
+ function parseFutureResetMs(resetTime, now = Date.now()) {
1431
+ if (!resetTime) return void 0;
1432
+ const reset = Date.parse(resetTime);
1433
+ if (Number.isNaN(reset) || reset <= now) return void 0;
1434
+ return reset;
1435
+ }
1436
+ //#endregion
1437
+ //#region src/client/quota-view.ts
1438
+ /**
1439
+ * Pure quota view layer, shared by the conversation-header badge
1440
+ * (`quota-badge.ts`) and the Settings section's own limits card.
1441
+ *
1442
+ * No React, no fetch, no DOM: every figure and every word is computed here from
1443
+ * data handed in, with `now` always injected, so `tests/quota-view.test.ts` can
1444
+ * pin the badge's arithmetic without a browser.
1445
+ *
1446
+ * Nothing in this module restates an upstream fact:
1447
+ *
1448
+ * - the window vocabulary and its order come from `QUOTA_WINDOWS`
1449
+ * (`../types.ts`), the one table the adapter and the scheduler also read;
1450
+ * - the drain thresholds are IMPORTED from `../runtime/rotation.ts`, so the
1451
+ * badge ranks windows by the same numbers the pool rotates on;
1452
+ * - "the reset has passed" is `parseFutureResetMs`, the predicate the pool
1453
+ * itself applies (`isFamilyDrained`, `rankPoolCandidates`);
1454
+ * - every visible word comes from the `agy` dictionary (`locales.ts`).
1455
+ *
1456
+ * A window with no reported fraction is "unknown" and renders as an em dash —
1457
+ * never as 0%, because unknown headroom and no headroom are opposite facts.
1458
+ */
1459
+ const MINUTE_MS = 6e4;
1460
+ const HOUR_MS = 60 * MINUTE_MS;
1461
+ const DAY_MS = 24 * HOUR_MS;
1462
+ /**
1463
+ * Localized label for an upstream quota window token.
1464
+ *
1465
+ * The tokens are mapped rather than printed so the surface follows the UI
1466
+ * language, and an UNRECOGNIZED token falls back to its raw value: upstream may
1467
+ * add a window, and showing `30d` is better than a blank or a wrong label.
1468
+ */
1469
+ function windowLabel(window, t) {
1470
+ switch (window) {
1471
+ case "5h": return t("quotaWindow5h");
1472
+ case "daily": return t("quotaWindowDaily");
1473
+ case "weekly": return t("quotaWindowWeekly");
1474
+ case "monthly": return t("quotaWindowMonthly");
1475
+ default: return window;
1476
+ }
1477
+ }
1478
+ /** Localized account-state label. */
1479
+ function stateLabel(state, t) {
1480
+ switch (state) {
1481
+ case "active": return t("stateActive");
1482
+ case "cooling": return t("stateCooling");
1483
+ case "verification-required": return t("stateVerificationRequired");
1484
+ case "disabled": return t("stateDisabled");
1485
+ }
1486
+ }
1487
+ /**
1488
+ * Time until a future moment, as localized copy.
1489
+ *
1490
+ * A quota reset wall is often more than 24h out, so a bare `HH:mm` cannot say
1491
+ * whether it means today or tomorrow. Bucket boundaries mirror the host's
1492
+ * `relativeTime`; the words stay in this plugin's own dictionary, which is
1493
+ * exactly the split that API intends.
1494
+ */
1495
+ function untilText(iso, t, now) {
1496
+ if (iso === null) return "—";
1497
+ const at = new Date(iso).getTime();
1498
+ if (Number.isNaN(at)) return "—";
1499
+ const diff = at - now;
1500
+ if (diff <= 0) return t("relNow");
1501
+ return t("quotaResetIn", { value: diff < 6e4 ? t("relNow") : diff < 36e5 ? t("relMinutes", { n: Math.floor(diff / MINUTE_MS) }) : diff < 864e5 ? t("relHours", { n: Math.floor(diff / HOUR_MS) }) : diff < 2592e6 ? t("relDays", { n: Math.floor(diff / DAY_MS) }) : diff < 31536e6 ? t("relMonths", { n: Math.floor(diff / (30 * DAY_MS)) }) : t("relYears", { n: Math.floor(diff / (365 * DAY_MS)) }) });
1502
+ }
1503
+ /**
1504
+ * How long ago a past moment was (the mirror of `untilText`).
1505
+ *
1506
+ * Reuses the same `rel*` magnitudes so the two read consistently, but adds a
1507
+ * direction suffix: a bare magnitude beside a cooldown could equally mean when
1508
+ * it started or when it ends.
1509
+ */
1510
+ function agoText(iso, t, now) {
1511
+ if (iso === null) return "—";
1512
+ const at = new Date(iso).getTime();
1513
+ if (Number.isNaN(at)) return "—";
1514
+ const diff = now - at;
1515
+ if (diff < 6e4) return t("relJustNow");
1516
+ return t("relAgo", { value: diff < 36e5 ? t("relMinutes", { n: Math.floor(diff / MINUTE_MS) }) : diff < 864e5 ? t("relHours", { n: Math.floor(diff / HOUR_MS) }) : diff < 2592e6 ? t("relDays", { n: Math.floor(diff / DAY_MS) }) : diff < 31536e6 ? t("relMonths", { n: Math.floor(diff / (30 * DAY_MS)) }) : t("relYears", { n: Math.floor(diff / (365 * DAY_MS)) }) });
1517
+ }
1518
+ /**
1519
+ * The reset moment of one window.
1520
+ *
1521
+ * A wall that has already passed is its own state rather than a zero-distance
1522
+ * countdown: `untilText` answers "shortly" for `diff <= 0`, which is precisely
1523
+ * the promise the old badge made about a window that had already refilled.
1524
+ */
1525
+ function resetText(resetTime, t, now) {
1526
+ if (resetTime === null) return null;
1527
+ const at = Date.parse(resetTime);
1528
+ if (Number.isNaN(at)) return null;
1529
+ if (at <= now) return t("quotaResetPassed");
1530
+ return untilText(resetTime, t, now);
1531
+ }
1532
+ /** Sort rank for a window token; an unknown token ranks last. */
1533
+ function windowRank(window) {
1534
+ return QUOTA_WINDOWS[window]?.rank ?? Number.MAX_SAFE_INTEGER;
1535
+ }
1536
+ /** Order windows shortest-first; an unknown token last, tie-broken alphabetically. */
1537
+ function sortWindows(windows) {
1538
+ return [...windows].sort((a, b) => windowRank(a.window) - windowRank(b.window) || a.window.localeCompare(b.window));
1539
+ }
1540
+ /** 0..100 for a usable fraction, or null when it is absent or not finite. */
1541
+ function toPercent(fraction) {
1542
+ if (typeof fraction !== "number" || !Number.isFinite(fraction)) return null;
1543
+ return Math.round(Math.max(0, Math.min(1, fraction)) * 100);
1544
+ }
1545
+ /**
1546
+ * Quota tint by remaining fraction: healthy / low / critical.
1547
+ *
1548
+ * The thresholds are the Settings section's own, so the badge's bars read the
1549
+ * same colour as the limits card's beside them. Built from `--dsw-alias-*`
1550
+ * tokens (with the literal as a pre-theme fallback) rather than fixed hex: a
1551
+ * private palette does not follow the host's light/dark switch.
1552
+ */
1553
+ function quotaColor(fraction) {
1554
+ if (fraction > .7) return aliasVar("state-success-primary");
1555
+ if (fraction >= .3) return aliasVar("state-warn-primary");
1556
+ return aliasVar("state-error-primary");
1557
+ }
1558
+ /**
1559
+ * The drain threshold that governs one window token, or undefined when the pool
1560
+ * does not block on that window.
1561
+ *
1562
+ * The two numbers are IMPORTED from `runtime/rotation.ts` because they answer
1563
+ * the same question there ("how much work is left" over windows of different
1564
+ * lengths), which is why they are deliberately asymmetric. `daily`/`monthly`
1565
+ * report but never block, so they have no threshold and never drive the badge.
1566
+ */
1567
+ function drainThresholdFor(window) {
1568
+ if (window === "5h") return SOFT_QUOTA_THRESHOLD;
1569
+ if (window === "weekly") return WEEKLY_QUOTA_THRESHOLD;
1570
+ }
1571
+ /** The windows the pool actually blocks on. */
1572
+ function rotationWindows(group) {
1573
+ return group.windows.filter((window) => drainThresholdFor(window.window) !== void 0);
1574
+ }
1575
+ /**
1576
+ * Whether a window's reset moment has already passed.
1577
+ *
1578
+ * The pool treats such a window as unmeasured (`isFamilyDrained`,
1579
+ * `rankPoolCandidates`): the cached fraction describes a period that has ENDED,
1580
+ * so it is not evidence about the one we are in. This view layer reads the same
1581
+ * records, so it has to apply the same rule — otherwise a spent five-hour window
1582
+ * that has already rolled over keeps the badge red long after the pool resumed
1583
+ * serving that account.
1584
+ *
1585
+ * A reset moment that cannot be parsed counts as passed: it cannot be shown to
1586
+ * be in the future, and the reading it dates is not trustworthy either way.
1587
+ * A window that reported no reset moment at all is NOT stale — the pool keeps
1588
+ * using its fraction, and so does this.
1589
+ */
1590
+ function resetInPast(resetTime, now) {
1591
+ if (resetTime === null) return false;
1592
+ return parseFutureResetMs(resetTime, now) === void 0;
1593
+ }
1594
+ /**
1595
+ * Whether a window's fraction is a leftover from a period that has ended.
1596
+ *
1597
+ * Only a window that DID report a fraction can be stale: one that never reported
1598
+ * stays "unknown", which the row already renders as its own case.
1599
+ */
1600
+ function windowIsStale(window, now) {
1601
+ return window.remainingFraction !== null && resetInPast(window.resetTime, now);
1602
+ }
1603
+ /**
1604
+ * How close a window is to its own drain point, as a threshold multiple.
1605
+ *
1606
+ * Comparing raw `remainingFraction` values would rank a comfortable 44% week
1607
+ * above a comfortable 82% five-hour window and hand the badge to the week, which
1608
+ * is not what stops a request. Dividing by each window's own drain threshold asks
1609
+ * the question the pool itself asks — how much runway is left before this window
1610
+ * stops serving — so the five-hour window stays in charge until the week is
1611
+ * genuinely nearly spent.
1612
+ */
1613
+ function windowPressure(window, now) {
1614
+ if (window.remainingFraction === null) return null;
1615
+ if (resetInPast(window.resetTime, now)) return null;
1616
+ const threshold = drainThresholdFor(window.window);
1617
+ if (threshold === void 0 || threshold <= 0) return null;
1618
+ return Math.max(window.remainingFraction, 0) / threshold;
1619
+ }
1620
+ /**
1621
+ * Pick the window that constrains the account first, for the header badge.
1622
+ *
1623
+ * Groups are read in upstream's own order — it lists the Gemini budget first —
1624
+ * and the FIRST group reporting a blocking window decides on its own: mixing
1625
+ * groups would blend one budget with another. Within that group the window with
1626
+ * the smallest threshold multiple wins.
1627
+ *
1628
+ * Deliberately no family inference here (the badge used to match group names
1629
+ * against `/gemini/`): upstream's grouping is a payload detail, and the pool's
1630
+ * own family mapping lives in `runtime/quota.ts`, which the browser bundle does
1631
+ * not carry.
1632
+ */
1633
+ function pickBadgeWindow(limits, now) {
1634
+ if (!limits || limits.length === 0) return null;
1635
+ for (const group of limits) {
1636
+ const candidates = rotationWindows(group);
1637
+ if (candidates.length === 0) continue;
1638
+ let best = null;
1639
+ let bestPressure = Number.POSITIVE_INFINITY;
1640
+ for (const window of candidates) {
1641
+ const pressure = windowPressure(window, now);
1642
+ if (pressure === null) continue;
1643
+ if (pressure < bestPressure) {
1644
+ best = window;
1645
+ bestPressure = pressure;
1646
+ }
1647
+ }
1648
+ if (best !== null) return best;
1649
+ return null;
1650
+ }
1651
+ for (const group of limits) {
1652
+ const first = sortWindows(group.windows).filter((window) => !windowIsStale(window, now))[0];
1653
+ if (first !== void 0) return first;
1654
+ }
1655
+ return null;
1656
+ }
1657
+ /**
1658
+ * The badge's reading: the most constrained tracked window of the ACTIVE account.
1659
+ *
1660
+ * Reported `daily`/`monthly` windows never drive the badge: the pool does not
1661
+ * block on them, so a low monthly figure is not a reason to alarm the header.
1662
+ * Returns null when nothing usable is known — the caller renders a dash instead
1663
+ * of inventing 0%.
1664
+ */
1665
+ function pickBadgeQuota(accounts, now = Date.now()) {
1666
+ const window = pickBadgeWindow((accounts.find((account) => account.active) ?? accounts[0])?.limits, now);
1667
+ if (window === null) return null;
1668
+ const percent = toPercent(window.remainingFraction);
1669
+ if (percent === null) return null;
1670
+ return {
1671
+ percent,
1672
+ window: window.window
1673
+ };
1674
+ }
1675
+ /** Build the window rows of one group card. */
1676
+ function windowRows(windows, t, now) {
1677
+ return sortWindows(windows).map((window) => {
1678
+ const stale = windowIsStale(window, now);
1679
+ return {
1680
+ bucketId: window.bucketId,
1681
+ label: windowLabel(window.window, t),
1682
+ percent: stale ? null : toPercent(window.remainingFraction),
1683
+ reset: resetText(window.resetTime, t, now),
1684
+ stale
1685
+ };
1686
+ });
1687
+ }
1688
+ /**
1689
+ * Build the quota cards for one account.
1690
+ *
1691
+ * One card per upstream group, exactly as the group arrived: the grouped
1692
+ * windows are the only quota channel this plugin has now, and upstream's own
1693
+ * split ("Gemini Models" / "Claude and GPT models") cannot be re-derived from
1694
+ * model-id prefixes — `3p-*` covers Claude AND GPT.
1695
+ *
1696
+ * A group whose windows all turned out to be unusable would render as an empty
1697
+ * card that pushes a real one off screen, so those are dropped.
1698
+ */
1699
+ function buildQuotaCards(account, t, now) {
1700
+ const groups = account?.limits;
1701
+ if (!groups || groups.length === 0) return [];
1702
+ const cards = [];
1703
+ for (const group of groups) {
1704
+ const rows = windowRows(group.windows, t, now);
1705
+ if (rows.length === 0) continue;
1706
+ cards.push({
1707
+ key: group.name || `group-${cards.length}`,
1708
+ title: group.name || t("quotaGroupFallback"),
1709
+ windows: rows
1710
+ });
1711
+ }
1712
+ return cards;
1713
+ }
1714
+ /**
1715
+ * Which StateDot semantic the badge's health light shows.
1716
+ *
1717
+ * The pool is usable when some account is serving; anything else (a cooling
1718
+ * account, one parked behind verification, a disabled one) is user attention
1719
+ * rather than a hard failure, and an empty pool is idle rather than broken.
1720
+ */
1721
+ function dotStateFor(accounts) {
1722
+ if (accounts.length === 0) return "idle";
1723
+ if (!accounts.some((account) => account.active || account.state === "active")) return "warning";
1724
+ return accounts.some((account) => account.state !== "active") ? "warning" : "done";
1725
+ }
1726
+ /**
1727
+ * Mask an account email for the header surface.
1728
+ *
1729
+ * Deliberately NOT what the Settings section does: that panel shows the address
1730
+ * in full, on a page the user opened on purpose. The conversation header sits on
1731
+ * screen for the whole session — including during a share or a recording — so the
1732
+ * local part is shortened and the domain kept, which is enough to tell two
1733
+ * accounts apart without publishing the address.
1734
+ */
1735
+ function desensitizeEmail(email) {
1736
+ if (!email || !email.includes("@")) return email || "—";
1737
+ const [name, domain] = email.split("@");
1738
+ if (name === void 0 || domain === void 0) return email;
1739
+ if (name.length <= 3) return `${name.slice(0, 1)}***@${domain}`;
1740
+ if (name.length <= 6) return `${name.slice(0, 2)}***@${domain}`;
1741
+ return `${name.slice(0, 5)}***@${domain}`;
1742
+ }
1743
+ /**
1744
+ * Which probe a refresh should run.
1745
+ *
1746
+ * `force` re-probes inside the host's window (the user asked); `off` leaves the
1747
+ * window refresh out entirely for this tick; `auto` lets the host's TTL decide.
1748
+ * Ported from the standalone badge, which learned that a failed probe run has to
1749
+ * back itself off.
1750
+ */
1751
+ function shouldProbeLimits(input) {
1752
+ if (input.force) return "force";
1753
+ if (input.failedAt !== null && input.now - input.failedAt < 6e5) return "off";
1754
+ return "auto";
1755
+ }
1756
+ //#endregion
1757
+ //#region src/client/quota-badge.ts
1758
+ /**
1759
+ * Conversation-header quota badge & popover.
1760
+ *
1761
+ * Shows the tightest of the active account's tracked windows, and opens the
1762
+ * window breakdown floating card on hover or click.
1763
+ *
1764
+ * Built with DSH native UI primitives (Button, Tag, StateDot, useAnchoredPosition,
1765
+ * useDismissOnOutsidePointer) and semantic design tokens (--dsw-*).
1766
+ */
1767
+ /**
1768
+ * Render-tick cadence for the countdown and staleness figures. Pure state
1769
+ * recompute from already-fetched data — deliberately ZERO RPC: the fetch paths
1770
+ * are mount, window wake (throttled above) and the popover's refresh button,
1771
+ * and without this tick the countdown text would freeze between fetches.
1772
+ */
1773
+ const RENDER_TICK_MS = 6e4;
1774
+ /**
1775
+ * Grace period before hover close (250ms).
1776
+ */
1777
+ const HOVER_CLOSE_DELAY_MS = 250;
1778
+ /**
1779
+ * Quota bar and percentage color for a window.
1780
+ * Strictly unified with upstream Settings panel via quotaColor(percent / 100).
1781
+ * Thresholds: >70% success green, 30%-70% warning amber, <30% error red.
1782
+ */
1783
+ function quotaColorForWindow(percent) {
1784
+ if (percent === null) return aliasVar("label-tertiary");
1785
+ return quotaColor(percent / 100);
1786
+ }
1787
+ /** Render one window row of a quota card. */
1788
+ function renderWindow(window, index, t) {
1789
+ return h("div", {
1790
+ key: window.bucketId,
1791
+ className: "agy-ui-limit-row"
1792
+ }, h("div", { className: "agy-ui-limit-header" }, h("span", { className: "agy-ui-limit-title" }, window.label), h("span", {
1793
+ className: "agy-ui-limit-percent",
1794
+ style: { color: window.color }
1795
+ }, window.percent === null ? "—" : `${window.percent}%`)), h("div", { className: "agy-ui-progress-track" }, h("div", {
1796
+ className: "agy-ui-progress-fill",
1797
+ style: {
1798
+ width: `${window.percent ?? 0}%`,
1799
+ backgroundColor: window.color
1800
+ }
1801
+ })), window.reset ? h("div", { className: "agy-ui-quota-footer" }, h("span", null, window.reset)) : null, window.percent === null ? h("div", { className: "agy-ui-window-note" }, window.stale ? t("quotaStaleNote") : t("quotaUnmeasuredNote")) : null);
1802
+ }
1803
+ /**
1804
+ * The badge and its popover modal.
1805
+ */
1806
+ function AgyQuotaBadge({ rpc, t }) {
1807
+ const [accounts, setAccounts] = (0, react.useState)([]);
1808
+ const [loading, setLoading] = (0, react.useState)(false);
1809
+ const [isUpdating, setIsUpdating] = (0, react.useState)(false);
1810
+ const [isHovered, setIsHovered] = (0, react.useState)(false);
1811
+ const [isPinned, setIsPinned] = (0, react.useState)(false);
1812
+ const [isMobile, setIsMobile] = (0, react.useState)(false);
1813
+ const [now, setNow] = (0, react.useState)(() => Date.now());
1814
+ const [error, setError] = (0, react.useState)(null);
1815
+ const rootRef = (0, react.useRef)(null);
1816
+ const panelRef = (0, react.useRef)(null);
1817
+ const leaveTimerRef = (0, react.useRef)(null);
1818
+ const lastFetchTimeRef = (0, react.useRef)(0);
1819
+ const failedAtRef = (0, react.useRef)(null);
1820
+ (0, react.useEffect)(() => {
1821
+ installAgyStyles();
1822
+ const checkMobile = () => {
1823
+ setIsMobile(typeof window !== "undefined" && window.innerWidth <= 640);
1824
+ };
1825
+ checkMobile();
1826
+ window.addEventListener("resize", checkMobile);
1827
+ return () => window.removeEventListener("resize", checkMobile);
1828
+ }, []);
1829
+ const load = (0, react.useCallback)(async (isManual = false) => {
1830
+ const currentNow = Date.now();
1831
+ if (!isManual && lastFetchTimeRef.current > 0 && currentNow - lastFetchTimeRef.current < 119e3) return;
1832
+ lastFetchTimeRef.current = currentNow;
1833
+ setIsUpdating(true);
1834
+ setLoading(true);
1835
+ try {
1836
+ const listed = await rpc.call("account.list", {});
1837
+ setAccounts(listed.accounts);
1838
+ setError(null);
1839
+ const probe = shouldProbeLimits({
1840
+ force: isManual,
1841
+ failedAt: failedAtRef.current,
1842
+ now: Date.now()
1843
+ });
1844
+ if (probe !== "off") {
1845
+ const limits = await rpc.call("account.limits", probe === "force" ? { force: true } : {});
1846
+ const byIndex = new Map(limits.limits.map((entry) => [entry.index, entry]));
1847
+ setAccounts((current) => current.map((account) => {
1848
+ const entry = byIndex.get(account.index);
1849
+ return entry === void 0 ? account : {
1850
+ ...account,
1851
+ limits: entry.groups,
1852
+ limitsUpdatedAt: entry.updatedAt
1853
+ };
1854
+ }));
1855
+ failedAtRef.current = limits.measured === 0 && limits.failed > 0 ? Date.now() : null;
1856
+ }
1857
+ } catch (err) {
1858
+ setError(err instanceof Error ? err.message : String(err));
1859
+ } finally {
1860
+ setLoading(false);
1861
+ setNow(Date.now());
1862
+ setTimeout(() => {
1863
+ setIsUpdating(false);
1864
+ }, 1200);
1865
+ }
1866
+ }, [rpc]);
1867
+ (0, react.useEffect)(() => {
1868
+ load(false);
1869
+ const tick = window.setInterval(() => setNow(Date.now()), RENDER_TICK_MS);
1870
+ const onWake = () => {
1871
+ if (document.visibilityState !== "hidden") load(false);
1872
+ };
1873
+ window.addEventListener("focus", onWake);
1874
+ document.addEventListener("visibilitychange", onWake);
1875
+ return () => {
1876
+ window.clearInterval(tick);
1877
+ window.removeEventListener("focus", onWake);
1878
+ document.removeEventListener("visibilitychange", onWake);
1879
+ if (leaveTimerRef.current !== null) window.clearTimeout(leaveTimerRef.current);
1880
+ };
1881
+ }, [load]);
1882
+ const isOpen = isPinned || isHovered;
1883
+ (0, _deepseek_ai_dsh_client_ui_primitives.useDismissOnOutsidePointer)(rootRef, isPinned, setIsPinned, panelRef);
1884
+ const anchored = (0, _deepseek_ai_dsh_client_ui_primitives.useAnchoredPosition)({
1885
+ open: isOpen && !isMobile,
1886
+ anchorRef: rootRef,
1887
+ panelRef,
1888
+ side: "bottom",
1889
+ gap: 6,
1890
+ margin: 12
1891
+ });
1892
+ const handleMouseEnterBadge = () => {
1893
+ if (leaveTimerRef.current !== null) {
1894
+ window.clearTimeout(leaveTimerRef.current);
1895
+ leaveTimerRef.current = null;
1896
+ }
1897
+ setIsHovered(true);
1898
+ };
1899
+ const handleMouseLeaveBadge = () => {
1900
+ if (leaveTimerRef.current !== null) window.clearTimeout(leaveTimerRef.current);
1901
+ leaveTimerRef.current = window.setTimeout(() => {
1902
+ setIsHovered(false);
1903
+ }, HOVER_CLOSE_DELAY_MS);
1904
+ };
1905
+ const handleMouseEnterPopover = () => {
1906
+ if (leaveTimerRef.current !== null) {
1907
+ window.clearTimeout(leaveTimerRef.current);
1908
+ leaveTimerRef.current = null;
1909
+ }
1910
+ };
1911
+ const handleMouseLeavePopover = () => {
1912
+ if (leaveTimerRef.current !== null) window.clearTimeout(leaveTimerRef.current);
1913
+ leaveTimerRef.current = window.setTimeout(() => {
1914
+ setIsHovered(false);
1915
+ }, HOVER_CLOSE_DELAY_MS);
1916
+ };
1917
+ const handleTogglePin = (e) => {
1918
+ e?.stopPropagation();
1919
+ setIsPinned((prev) => !prev);
1920
+ setIsHovered(true);
1921
+ };
1922
+ const handleClose = () => {
1923
+ if (leaveTimerRef.current !== null) {
1924
+ window.clearTimeout(leaveTimerRef.current);
1925
+ leaveTimerRef.current = null;
1926
+ }
1927
+ setIsPinned(false);
1928
+ setIsHovered(false);
1929
+ };
1930
+ const handleRefreshClick = async (e) => {
1931
+ e.stopPropagation();
1932
+ if (loading) return;
1933
+ await load(true);
1934
+ };
1935
+ const activeAccount = accounts.find((a) => a.active) ?? accounts[0];
1936
+ const accountCount = accounts.length;
1937
+ const rawDot = dotStateFor(accounts);
1938
+ const dotState = rawDot === "done" ? "active" : rawDot === "warning" ? "cooling" : "disabled";
1939
+ const badgeQuota = pickBadgeQuota(accounts, now);
1940
+ const badgeWindow = badgeQuota?.window && badgeQuota.window !== "5h" ? `${windowLabel(badgeQuota.window, t)} ` : "";
1941
+ const displayText = badgeQuota !== null ? badgeWindow !== "" ? t("badgeWindowQuotaFormat", {
1942
+ window: badgeWindow,
1943
+ percent: badgeQuota.percent
1944
+ }) : t("badgeQuotaFormat", { percent: badgeQuota.percent }) : t("badgeCountFormat", { count: accountCount });
1945
+ const cards = buildQuotaCards(activeAccount, t, now).map((rc) => ({
1946
+ key: rc.key,
1947
+ title: rc.title,
1948
+ windows: rc.windows.map((w) => ({
1949
+ bucketId: w.bucketId,
1950
+ label: w.label,
1951
+ percent: w.percent,
1952
+ color: quotaColorForWindow(w.percent),
1953
+ reset: w.reset,
1954
+ stale: w.stale
1955
+ }))
1956
+ }));
1957
+ const limitsAge = activeAccount?.limitsUpdatedAt ? agoText(new Date(activeAccount.limitsUpdatedAt).toISOString(), t, now) : null;
1958
+ const badgeTooltip = t("badgeTooltip", {
1959
+ count: accountCount,
1960
+ reading: badgeQuota !== null ? t("badgeReading", {
1961
+ window: badgeWindow,
1962
+ percent: badgeQuota.percent
1963
+ }) : t("badgeUnmeasured")
1964
+ });
1965
+ const popoverContent = h("div", {
1966
+ ref: panelRef,
1967
+ className: `agy-ui-popover ${isMobile ? "mobile" : "desktop"}`,
1968
+ style: !isMobile ? anchored === null ? { visibility: "hidden" } : {
1969
+ position: "fixed",
1970
+ top: `${anchored.top}px`,
1971
+ left: `${anchored.left}px`,
1972
+ visibility: "visible"
1973
+ } : void 0,
1974
+ onMouseEnter: handleMouseEnterPopover,
1975
+ onMouseLeave: handleMouseLeavePopover,
1976
+ onClick: (e) => {
1977
+ e.stopPropagation();
1978
+ }
1979
+ }, isMobile ? h("div", { className: "agy-ui-mobile-handle" }) : null, h("div", { className: "agy-ui-modal-header" }, h("div", { className: "agy-ui-modal-title" }, h("span", { className: "agy-ui-sparkle" }, "✦"), h("span", null, t("badgeTitle")), isPinned && !isMobile ? h(_deepseek_ai_dsh_client_ui_primitives.Tag, {
1980
+ tone: "solid",
1981
+ className: "agy-ui-pinned-tag"
1982
+ }, t("badgePinned")) : null), h("div", { className: "agy-ui-header-actions" }, !isMobile ? h(_deepseek_ai_dsh_client_ui_primitives.Button, {
1983
+ variant: "ghost",
1984
+ size: "sm",
1985
+ className: `agy-ui-icon-btn ${isPinned ? "active" : ""}`,
1986
+ title: isPinned ? t("badgeUnpinHint") : t("badgeHint"),
1987
+ onClick: handleTogglePin
1988
+ }, h("svg", {
1989
+ width: "13",
1990
+ height: "13",
1991
+ viewBox: "0 0 24 24",
1992
+ fill: isPinned ? "currentColor" : "none",
1993
+ stroke: "currentColor",
1994
+ strokeWidth: "2"
1995
+ }, h("path", { d: "M12 2v8m0 0l3-3m-3 3L9 7M5 10h14a2 2 0 0 1 2 2v1a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-1a2 2 0 0 1 2-2zM12 15v7" }))) : null, h(_deepseek_ai_dsh_client_ui_primitives.Button, {
1996
+ variant: "ghost",
1997
+ size: "sm",
1998
+ className: "agy-ui-icon-btn",
1999
+ title: t("badgeRefreshHint"),
2000
+ onClick: handleRefreshClick,
2001
+ disabled: loading
2002
+ }, h("svg", {
2003
+ className: loading || isUpdating ? "agy-ui-spinning" : "",
2004
+ width: "13",
2005
+ height: "13",
2006
+ viewBox: "0 0 24 24",
2007
+ fill: "none",
2008
+ stroke: "currentColor",
2009
+ strokeWidth: "2",
2010
+ strokeLinecap: "round",
2011
+ strokeLinejoin: "round"
2012
+ }, h("path", { d: "M21.5 2v6h-6M2.5 22v-6h6M2.5 11.5a10 10 0 0 1 17.5-4.5l1.5 2M21.5 12.5a10 10 0 0 1-17.5 4.5l-1.5-2" }))), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
2013
+ variant: "ghost",
2014
+ size: "sm",
2015
+ className: "agy-ui-icon-btn",
2016
+ title: t("badgeClose"),
2017
+ onClick: handleClose
2018
+ }, h("svg", {
2019
+ width: "13",
2020
+ height: "13",
2021
+ viewBox: "0 0 24 24",
2022
+ fill: "none",
2023
+ stroke: "currentColor",
2024
+ strokeWidth: "2",
2025
+ strokeLinecap: "round",
2026
+ strokeLinejoin: "round"
2027
+ }, h("line", {
2028
+ x1: "18",
2029
+ y1: "6",
2030
+ x2: "6",
2031
+ y2: "18"
2032
+ }), h("line", {
2033
+ x1: "6",
2034
+ y1: "6",
2035
+ x2: "18",
2036
+ y2: "18"
2037
+ }))))), h("div", { className: "agy-ui-modal-body" }, activeAccount ? h("div", { className: "agy-ui-account-card" }, h("div", null, h("div", { className: "agy-ui-account-email" }, desensitizeEmail(activeAccount.email)), h("div", { className: "agy-ui-account-project" }, `${t("fieldProject")}: ${activeAccount.projectId || t("thinkingDefaultAll")}`)), h(_deepseek_ai_dsh_client_ui_primitives.Tag, {
2038
+ tone: activeAccount.state === "active" ? "success" : activeAccount.state === "cooling" ? "warning" : "danger",
2039
+ className: `agy-ui-state-pill ${activeAccount.state || "active"}`
2040
+ }, stateLabel(activeAccount.state || "active", t))) : h("div", { className: "agy-ui-account-card" }, h("div", {
2041
+ className: "agy-ui-account-email",
2042
+ style: { color: aliasVar("label-tertiary") }
2043
+ }, error ?? t("badgeNoAccount"))), activeAccount?.verificationRequired && activeAccount?.verificationUrl ? h("div", { className: "agy-ui-verify-note" }, h("span", null, t("badgeVerifyRequired")), h("a", {
2044
+ className: "agy-ui-link-btn",
2045
+ href: activeAccount.verificationUrl,
2046
+ target: "_blank",
2047
+ rel: "noopener noreferrer"
2048
+ }, t("badgeAppealLink"))) : null, h("div", { className: "agy-ui-section-label" }, t("badgeSectionMonitor")), ...cards.map((card) => h("div", {
2049
+ key: card.key,
2050
+ className: "agy-ui-quota-card"
2051
+ }, h("div", { className: "agy-ui-quota-header" }, h("span", { className: "agy-ui-model-name" }, card.title)), ...card.windows.map((w, idx) => renderWindow(w, idx, t)))), cards.length === 0 ? h("div", { className: "agy-ui-window-note" }, t("badgeNoQuotaData")) : null, limitsAge ? h("div", { className: "agy-ui-limit-age" }, t("badgeMeasuredAt", { time: limitsAge })) : null), h("div", { className: "agy-ui-modal-footer" }, h("span", null, t("quotaSourceCaption")), h("span", null, t("quotaManageHint"))));
2052
+ const popoverNode = isOpen ? isMobile ? h("div", {
2053
+ className: "agy-ui-popover-container mobile",
2054
+ onClick: handleClose
2055
+ }, popoverContent) : popoverContent : null;
2056
+ return h("span", {
2057
+ ref: rootRef,
2058
+ style: {
2059
+ position: "relative",
2060
+ display: "inline-flex",
2061
+ alignItems: "center"
2062
+ }
2063
+ }, h(_deepseek_ai_dsh_client_ui_primitives.Button, {
2064
+ variant: "ghost",
2065
+ size: "sm",
2066
+ className: `agy-ui-badge ${isPinned ? "pinned" : ""}`,
2067
+ title: badgeTooltip,
2068
+ "aria-label": t("badgeAria"),
2069
+ onClick: handleTogglePin,
2070
+ onMouseEnter: handleMouseEnterBadge,
2071
+ onMouseLeave: handleMouseLeaveBadge
2072
+ }, h(_deepseek_ai_dsh_client_ui_primitives.StateDot, {
2073
+ state: rawDot,
2074
+ size: 7,
2075
+ className: `agy-ui-dot ${dotState}${isUpdating ? " updating" : ""}`
2076
+ }), h("span", null, displayText)), popoverNode);
2077
+ }
2078
+ //#endregion
794
2079
  //#region src/thinking-types.ts
795
2080
  /**
796
2081
  * Thinking-budget vocabulary shared by the host store, the RPC wire contract,
@@ -869,9 +2154,6 @@ window.__ModuleLoader__.load({
869
2154
  * without a visible request cost.
870
2155
  */
871
2156
  const POOL_POLL_INTERVAL_MS = 3e3;
872
- function h(tag, props, ...children) {
873
- return (0, react.createElement)(tag, props ?? null, ...children);
874
- }
875
2157
  /** Call one management method and unwrap the Connection RPC envelope. */
876
2158
  function createRpc(connection) {
877
2159
  return { async call(method, payload, signal) {
@@ -982,21 +2264,6 @@ window.__ModuleLoader__.load({
982
2264
  function promptTokens(counters) {
983
2265
  return counters.input + counters.cacheRead + counters.cacheWrite;
984
2266
  }
985
- /** Quota tint by remaining fraction: healthy / low / critical. */
986
- function quotaColor(fraction) {
987
- if (fraction > .7) return "var(--dsw-alias-state-success-primary, #22c55e)";
988
- if (fraction >= .3) return "var(--dsw-alias-state-warn-primary, #f59e0b)";
989
- return "var(--dsw-alias-state-error-primary, #ec1313)";
990
- }
991
- /** Localized account-state label. */
992
- function stateLabel(state, t) {
993
- switch (state) {
994
- case "active": return t("stateActive");
995
- case "cooling": return t("stateCooling");
996
- case "verification-required": return t("stateVerificationRequired");
997
- case "disabled": return t("stateDisabled");
998
- }
999
- }
1000
2267
  /**
1001
2268
  * Localized label for a reasoning level.
1002
2269
  *
@@ -1012,21 +2279,6 @@ window.__ModuleLoader__.load({
1012
2279
  }
1013
2280
  }
1014
2281
  /**
1015
- * Localized label for an upstream quota window token.
1016
- *
1017
- * Upstream sends `5h` / `weekly` today. The tokens are mapped rather than
1018
- * printed so the panel follows the UI language, and an UNRECOGNIZED token falls
1019
- * back to its raw value: upstream may add a window, and showing `30d` is better
1020
- * than a blank or a wrong localized label.
1021
- */
1022
- function windowLabel(window, t) {
1023
- switch (window) {
1024
- case "5h": return t("quotaWindow5h");
1025
- case "weekly": return t("quotaWindowWeekly");
1026
- default: return window;
1027
- }
1028
- }
1029
- /**
1030
2282
  * Humanize a burn horizon (hours until a window runs dry at the sampled rate).
1031
2283
  *
1032
2284
  * Reuses the shared `rel*` magnitudes so the phrasing matches every other
@@ -1079,27 +2331,6 @@ window.__ModuleLoader__.load({
1079
2331
  day: "numeric"
1080
2332
  });
1081
2333
  }
1082
- const MINUTE_MS = 6e4;
1083
- const HOUR_MS = 60 * MINUTE_MS;
1084
- const DAY_MS = 24 * HOUR_MS;
1085
- /**
1086
- * Time until a future moment, as localized copy.
1087
- *
1088
- * A quota reset wall is often more than 24h out, so a bare `HH:mm` cannot say
1089
- * whether it means today or tomorrow — the failure this replaces. Bucket
1090
- * boundaries mirror the host's `relativeTime` (which is defined for past-dated
1091
- * rows and would need its arguments reversed to serve a future one, so the
1092
- * comparison is written out here instead); the words stay in this plugin's own
1093
- * dictionary, which is exactly the split that API intends.
1094
- */
1095
- function untilText(iso, t, now) {
1096
- if (iso === null) return "—";
1097
- const at = new Date(iso).getTime();
1098
- if (Number.isNaN(at)) return "—";
1099
- const diff = at - now;
1100
- if (diff <= 0) return t("relNow");
1101
- return t("quotaResetIn", { value: diff < MINUTE_MS ? t("relNow") : diff < HOUR_MS ? t("relMinutes", { n: Math.floor(diff / MINUTE_MS) }) : diff < DAY_MS ? t("relHours", { n: Math.floor(diff / HOUR_MS) }) : diff < 30 * DAY_MS ? t("relDays", { n: Math.floor(diff / DAY_MS) }) : diff < 365 * DAY_MS ? t("relMonths", { n: Math.floor(diff / (30 * DAY_MS)) }) : t("relYears", { n: Math.floor(diff / (365 * DAY_MS)) }) });
1102
- }
1103
2334
  /**
1104
2335
  * Localized label for a cooldown reason.
1105
2336
  *
@@ -1116,22 +2347,6 @@ window.__ModuleLoader__.load({
1116
2347
  }
1117
2348
  }
1118
2349
  /**
1119
- * How long ago a past moment was (the mirror of `untilText`).
1120
- *
1121
- * Reuses the same `rel*` magnitudes so the two read consistently, but adds a
1122
- * direction suffix: a bare magnitude beside a cooldown could equally mean when
1123
- * it started or when it ends. The suffix and every magnitude come from the
1124
- * dictionary, so nothing here is language-specific.
1125
- */
1126
- function agoText(iso, t, now) {
1127
- if (iso === null) return "—";
1128
- const at = new Date(iso).getTime();
1129
- if (Number.isNaN(at)) return "—";
1130
- const diff = now - at;
1131
- if (diff < MINUTE_MS) return t("relJustNow");
1132
- return t("relAgo", { value: diff < HOUR_MS ? t("relMinutes", { n: Math.floor(diff / MINUTE_MS) }) : diff < DAY_MS ? t("relHours", { n: Math.floor(diff / HOUR_MS) }) : diff < 30 * DAY_MS ? t("relDays", { n: Math.floor(diff / DAY_MS) }) : diff < 365 * DAY_MS ? t("relMonths", { n: Math.floor(diff / (30 * DAY_MS)) }) : t("relYears", { n: Math.floor(diff / (365 * DAY_MS)) }) });
1133
- }
1134
- /**
1135
2350
  * Ago label with seconds resolution, for the recent list. `agoText` collapses
1136
2351
  * the whole first minute into "just now", which is too coarse when the reader
1137
2352
  * is watching live activity — here the seconds carry the information.
@@ -1139,7 +2354,7 @@ window.__ModuleLoader__.load({
1139
2354
  function recentAgo(at, now, t) {
1140
2355
  const diff = now - at;
1141
2356
  if (diff < 1e4) return t("relJustNow");
1142
- if (diff < MINUTE_MS) return t("relSeconds", { n: Math.floor(diff / 1e3) });
2357
+ if (diff < 6e4) return t("relSeconds", { n: Math.floor(diff / 1e3) });
1143
2358
  return agoText(new Date(at).toISOString(), t, now);
1144
2359
  }
1145
2360
  /**
@@ -1356,6 +2571,59 @@ window.__ModuleLoader__.load({
1356
2571
  const activePos = accounts.findIndex((a) => a.active);
1357
2572
  return activePos >= 0 ? activePos : 0;
1358
2573
  }
2574
+ function PreferencesCard(props) {
2575
+ const { rpc, t, onBadgePrefChange } = props;
2576
+ const [badgeEnabled, setBadgeEnabled] = (0, react.useState)(false);
2577
+ const [loading, setLoading] = (0, react.useState)(true);
2578
+ const [saving, setSaving] = (0, react.useState)(false);
2579
+ const [saveError, setSaveError] = (0, react.useState)(null);
2580
+ const errorTimer = (0, react.useRef)(void 0);
2581
+ (0, react.useEffect)(() => () => {
2582
+ if (errorTimer.current !== void 0) clearTimeout(errorTimer.current);
2583
+ }, []);
2584
+ (0, react.useEffect)(() => {
2585
+ let active = true;
2586
+ rpc.call("ui.prefs.get", {}).then((prefs) => {
2587
+ if (active) {
2588
+ setBadgeEnabled(prefs?.conversationBadge === true);
2589
+ setLoading(false);
2590
+ }
2591
+ }).catch(() => {
2592
+ if (active) setLoading(false);
2593
+ });
2594
+ return () => {
2595
+ active = false;
2596
+ };
2597
+ }, [rpc]);
2598
+ const toggleBadge = (0, react.useCallback)(async (checked) => {
2599
+ setSaving(true);
2600
+ setBadgeEnabled(checked);
2601
+ if (errorTimer.current !== void 0) clearTimeout(errorTimer.current);
2602
+ try {
2603
+ const res = await rpc.call("ui.prefs.set", { conversationBadge: checked });
2604
+ setBadgeEnabled(res.conversationBadge);
2605
+ setSaveError(null);
2606
+ onBadgePrefChange?.(res.conversationBadge);
2607
+ } catch (err) {
2608
+ setBadgeEnabled(!checked);
2609
+ setSaveError(t("prefSaveFailed", { message: err instanceof Error ? err.message : String(err) }));
2610
+ errorTimer.current = setTimeout(() => setSaveError(null), ACTION_MESSAGE_TTL_MS);
2611
+ } finally {
2612
+ setSaving(false);
2613
+ }
2614
+ }, [
2615
+ rpc,
2616
+ t,
2617
+ onBadgePrefChange
2618
+ ]);
2619
+ const row = h("div", { className: "agy-pref-row" }, h("div", { className: "agy-pref-info" }, h("div", { className: "agy-pref-name" }, t("prefConversationBadge")), h("div", { className: "agy-pref-desc" }, t("prefConversationBadgeDesc"))), h(_deepseek_ai_dsh_client_ui_primitives.Switch, {
2620
+ label: t("prefConversationBadge"),
2621
+ checked: badgeEnabled,
2622
+ disabled: loading || saving,
2623
+ onChange: (checked) => void toggleBadge(checked)
2624
+ }));
2625
+ return card(t("preferencesTitle"), saveError === null ? row : [row, h("div", { className: "agy-error" }, saveError)]);
2626
+ }
1359
2627
  function AccountsTab(props) {
1360
2628
  const { accounts, busy, handlers, t } = props;
1361
2629
  const [selected, setSelected] = (0, react.useState)(null);
@@ -1434,6 +2702,10 @@ window.__ModuleLoader__.load({
1434
2702
  })), h(RecentCard, {
1435
2703
  rpc: props.rpc,
1436
2704
  t
2705
+ }), h(PreferencesCard, {
2706
+ rpc: props.rpc,
2707
+ t,
2708
+ onBadgePrefChange: props.onBadgePrefChange
1437
2709
  })));
1438
2710
  }
1439
2711
  /**
@@ -1966,7 +3238,7 @@ window.__ModuleLoader__.load({
1966
3238
  id: "cacheRead",
1967
3239
  labelKey: "kpiCacheRead",
1968
3240
  value: counters.cacheRead,
1969
- tone: "var(--dsw-alias-brand-primary-new-colorprimary-new-color, #4176e6)"
3241
+ tone: aliasVar("brand-primary")
1970
3242
  },
1971
3243
  {
1972
3244
  id: "missed",
@@ -1978,7 +3250,7 @@ window.__ModuleLoader__.load({
1978
3250
  id: "output",
1979
3251
  labelKey: "kpiOutput",
1980
3252
  value: counters.output,
1981
- tone: "var(--dsw-alias-state-success-primary, #22c55e)"
3253
+ tone: aliasVar("state-success-primary")
1982
3254
  }
1983
3255
  ].map((row) => {
1984
3256
  const share = total > 0 ? row.value / total * 100 : 0;
@@ -2049,6 +3321,9 @@ window.__ModuleLoader__.load({
2049
3321
  /** The Settings section body. */
2050
3322
  function AgySettings(props) {
2051
3323
  const { rpc, t } = props;
3324
+ (0, react.useEffect)(() => {
3325
+ installAgyStyles();
3326
+ }, []);
2052
3327
  const [tab, setTab] = (0, react.useState)("accounts");
2053
3328
  const [accounts, setAccounts] = (0, react.useState)([]);
2054
3329
  const [models, setModels] = (0, react.useState)([]);
@@ -2421,7 +3696,8 @@ window.__ModuleLoader__.load({
2421
3696
  handlers,
2422
3697
  lang: props.lang,
2423
3698
  rpc,
2424
- t
3699
+ t,
3700
+ onBadgePrefChange: props.onBadgePrefChange
2425
3701
  }) : tab === "models" ? modelError === void 0 ? h(ModelsTab, {
2426
3702
  models,
2427
3703
  account: modelAccount,
@@ -2552,6 +3828,32 @@ window.__ModuleLoader__.load({
2552
3828
  const rpc = createRpc(connection);
2553
3829
  const t = ctx.locale.bind(NS);
2554
3830
  const lang = ctx.locale.getLocale?.().active;
3831
+ let badgeDisposer = null;
3832
+ const registerBadge = () => {
3833
+ if (badgeDisposer !== null) return;
3834
+ badgeDisposer = ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
3835
+ name: "conversation.session.header.actions",
3836
+ id: "agy-quota-badge",
3837
+ order: 8,
3838
+ label: () => t("title")
3839
+ }, () => h(AgyQuotaBadge, {
3840
+ rpc,
3841
+ t
3842
+ })));
3843
+ };
3844
+ const unregisterBadge = () => {
3845
+ if (badgeDisposer !== null) {
3846
+ badgeDisposer();
3847
+ badgeDisposer = null;
3848
+ }
3849
+ };
3850
+ rpc.call("ui.prefs.get", {}).then((prefs) => {
3851
+ if (prefs?.conversationBadge) registerBadge();
3852
+ }).catch(() => {});
3853
+ const onBadgePrefChange = (enabled) => {
3854
+ if (enabled) registerBadge();
3855
+ else unregisterBadge();
3856
+ };
2555
3857
  ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
2556
3858
  name: "settings.section",
2557
3859
  id: "agy",
@@ -2561,8 +3863,12 @@ window.__ModuleLoader__.load({
2561
3863
  }, () => h(AgySettings, {
2562
3864
  rpc,
2563
3865
  t,
2564
- lang
3866
+ lang,
3867
+ onBadgePrefChange
2565
3868
  }))), "dsh-agy: Settings section");
3869
+ ctx.effect(() => () => {
3870
+ unregisterBadge();
3871
+ }, "dsh-agy: quota badge cleanup");
2566
3872
  }
2567
3873
  //#endregion
2568
3874
  exports.AgySettings = AgySettings;