dsh-qoder-connect 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -61,242 +61,691 @@ window.__ModuleLoader__.load({
61
61
  return status === "error" && typeof wrapped["message"] === "string";
62
62
  }
63
63
  //#endregion
64
- //#region src/client/QoderPluginCard.tsx
65
- /** Qoder status card contributed to Harness Plugin configuration. */
66
- /** China Qoder; the plugin's primary card and default. */
67
- const QODER_CN_CARD = {
68
- id: "qoder",
69
- titleKey: "title",
70
- introKey: "intro",
71
- signedOutKey: "signedOutHint",
72
- patGuideKey: "patGuide",
73
- patPlaceholderKey: "patPlaceholder",
74
- statusPath: QODER_STATUS_PATH,
75
- probePath: QODER_PROBE_PATH,
76
- authPath: QODER_AUTH_PATH
64
+ //#region src/client/quota-settings-store.ts
65
+ let pollIntervalMs = 3e5;
66
+ const toggles = {
67
+ cn: false,
68
+ global: false
77
69
  };
78
- /** Both cards, in display order (China first). */
79
- const QODER_CARD_VARIANTS = [QODER_CN_CARD, {
80
- id: "qoder-global",
81
- titleKey: "titleAI",
82
- introKey: "introAI",
83
- signedOutKey: "signedOutHintAI",
84
- patGuideKey: "patGuideAI",
85
- patPlaceholderKey: "patPlaceholderAI",
86
- statusPath: QODER_GLOBAL_STATUS_PATH,
87
- probePath: QODER_GLOBAL_PROBE_PATH,
88
- authPath: QODER_GLOBAL_AUTH_PATH
89
- }];
90
- const POLL_INTERVAL_MS = 6e4;
91
- const cardStyle$1 = {
92
- listStyle: "none",
93
- borderWidth: "0.5px",
94
- borderStyle: "solid",
95
- borderColor: "var(--dsw-alias-border-l4)",
96
- borderRadius: 16,
97
- background: "var(--dsw-alias-bg-layer-3)",
98
- transition: "border-color .16s, background .16s"
70
+ let togglesSnapshot = {
71
+ cn: false,
72
+ global: false
99
73
  };
100
- /** Hover, matching the built-in card's `:hover`. Inline styles cannot express a pseudo-class. */
101
- const cardHoverStyle$1 = { borderColor: "var(--dsw-alias-label-dimmed)" };
102
- /** Expanded, matching the built-in card's open state. */
103
- const cardOpenStyle$1 = {
104
- background: "var(--dsw-alias-bg-layer-2)",
105
- borderColor: "var(--dsw-alias-label-dimmed)"
74
+ const signIn = {
75
+ cn: false,
76
+ global: false
106
77
  };
107
- const headerStyle = {
108
- boxSizing: "border-box",
109
- width: "100%",
110
- display: "flex",
111
- alignItems: "center",
112
- gap: 12,
113
- border: 0,
114
- borderRadius: 12,
115
- padding: "14px 16px",
116
- background: "transparent",
117
- color: "inherit",
118
- font: "inherit",
119
- textAlign: "left",
120
- cursor: "pointer",
121
- appearance: "none"
78
+ let signInSnapshot = {
79
+ cn: false,
80
+ global: false
122
81
  };
82
+ let revision = 0;
83
+ const listeners = /* @__PURE__ */ new Set();
84
+ function bump() {
85
+ revision += 1;
86
+ for (const listener of listeners) listener();
87
+ }
88
+ /** Update the shared poll interval (from the settings document). */
89
+ function setQuotaPollMs(ms) {
90
+ if (Number.isFinite(ms) && ms >= 6e4 && pollIntervalMs !== ms) {
91
+ pollIntervalMs = ms;
92
+ bump();
93
+ }
94
+ }
95
+ /** Read the configured poll interval. */
96
+ function quotaPollMs() {
97
+ return pollIntervalMs;
98
+ }
99
+ /** Update both sidebar toggles (from the settings document). */
100
+ function setQuotaToggles(cn, global) {
101
+ if (toggles.cn !== cn || toggles.global !== global) {
102
+ toggles.cn = cn;
103
+ toggles.global = global;
104
+ togglesSnapshot = { ...toggles };
105
+ bump();
106
+ }
107
+ }
108
+ /** Read the current toggles. */
109
+ function quotaToggles() {
110
+ return togglesSnapshot;
111
+ }
112
+ /** Record a variant's sign-in state from any successful status poll. */
113
+ function noteQuotaSignIn(variantId, signedIn) {
114
+ if (variantId === "qoder" && signIn.cn !== signedIn) {
115
+ signIn.cn = signedIn;
116
+ signInSnapshot = { ...signIn };
117
+ bump();
118
+ } else if (variantId === "qoder-global" && signIn.global !== signedIn) {
119
+ signIn.global = signedIn;
120
+ signInSnapshot = { ...signIn };
121
+ bump();
122
+ }
123
+ }
124
+ /** Read the cached sign-in state. */
125
+ function quotaSignInState() {
126
+ return signInSnapshot;
127
+ }
128
+ /** Subscribe to any flag change; returns the disposer. */
129
+ function onQuotaSettingsChange(listener) {
130
+ listeners.add(listener);
131
+ return () => {
132
+ listeners.delete(listener);
133
+ };
134
+ }
135
+ /** The current revision — the useSyncExternalStore snapshot value. */
136
+ function quotaSettingsRevision() {
137
+ return revision;
138
+ }
139
+ /** Which variant a status route belongs to, from the route path. */
140
+ function variantOfStatusPath(statusPath) {
141
+ return statusPath.includes("/global/") ? "qoder-global" : "qoder";
142
+ }
123
143
  /**
124
- * The built-in header's keyboard focus ring.
144
+ * The last status document per variant, shared by EVERY quota surface.
125
145
  *
126
- * `:focus-visible` is what makes the ring appear for keyboard navigation but not
127
- * for a mouse click, and an inline style cannot express a pseudo-class — so the
128
- * component tracks it and applies this instead. Without it the header falls back
129
- * to the browser's own outline, which is the black box that used to appear on
130
- * focus where the built-in card shows a brand-coloured ring.
146
+ * The sidebar cards and the dashboard each used to fetch independently, so a
147
+ * dashboard refresh updated the panel while the sidebar card kept showing its
148
+ * previous read until its own next tick — two different numbers for one
149
+ * account on one screen. One store, one write path
150
+ * ({@link noteQuotaStatus}), and every subscriber re-renders through the
151
+ * same revision: whatever surface refreshed last, all of them show it.
152
+ *
153
+ * Documents are kept by identity (never mutated), so reference comparisons
154
+ * in useSyncExternalStore selectors stay cheap and stable.
131
155
  */
132
- const headerFocusStyle$1 = {
133
- outline: "2px solid var(--dsw-alias-brand-primary)",
134
- outlineOffset: -2
135
- };
136
- const headTextStyle$1 = {
137
- display: "flex",
138
- flex: 1,
139
- minWidth: 0,
140
- flexDirection: "column",
141
- gap: 4
142
- };
143
- const nameStyle = {
144
- fontSize: 15,
145
- lineHeight: 1.4,
146
- fontWeight: 600,
147
- color: "var(--dsw-alias-label-primary)"
156
+ const statusDocuments = {
157
+ cn: void 0,
158
+ global: void 0
148
159
  };
149
- const descriptionStyle = {
150
- fontSize: 13,
151
- lineHeight: 1.5,
152
- color: "var(--dsw-alias-label-tertiary)"
160
+ /**
161
+ * Publish one variant's freshly fetched status document. Downstream
162
+ * subscribers (both sidebar cards and the dashboard, through whichever
163
+ * observable wraps this store) re-render on the revision bump.
164
+ */
165
+ function noteQuotaStatus(variantId, status) {
166
+ if (variantId === "qoder" && statusDocuments.cn !== status) {
167
+ statusDocuments.cn = status;
168
+ statusFetchedAt.cn = Date.now();
169
+ bump();
170
+ } else if (variantId === "qoder-global" && statusDocuments.global !== status) {
171
+ statusDocuments.global = status;
172
+ statusFetchedAt.global = Date.now();
173
+ bump();
174
+ }
175
+ noteQuotaSignIn(variantId, status.status === "signed-in");
176
+ }
177
+ /** Read one variant's latest status document. */
178
+ function quotaStatus(variantId) {
179
+ return variantId === "qoder" ? statusDocuments.cn : statusDocuments.global;
180
+ }
181
+ /** When each variant's document was last fetched (per publish, not per read). */
182
+ const statusFetchedAt = {
183
+ cn: void 0,
184
+ global: void 0
153
185
  };
186
+ /** Read the time a variant's current document was fetched, if any. */
187
+ function quotaStatusFetchedAt(variantId) {
188
+ return variantId === "qoder" ? statusFetchedAt.cn : statusFetchedAt.global;
189
+ }
154
190
  /**
155
- * The disclosure chevron, drawn to match the Settings panel's own card.
191
+ * Whether ONE variant's shared document is fresh enough to skip a fetch:
192
+ * the user's rule — a click/mount/interval tick within the configured
193
+ * interval of the last successful read reuses the cached document, and only
194
+ * a variant with NO result yet (or a failed read that never landed one)
195
+ * forces the upstream call. The interval is a cache lifetime, not a metronome.
156
196
  *
157
- * The built-in card renders `IconChevronDownOutline14` from the client's shared
158
- * icon catalog, which the shell seeds into the module table. This plugin does
159
- * not request that catalog, so the same outline is drawn here from the same path
160
- * data: the text `⌄` glyph this replaces had a different shape, weight, and
161
- * baseline from the icon the cards beside it use.
197
+ * A `maxAgeMs` of the poll interval comes from the settings document; a
198
+ * failed last read is NOT tracked here (callers gate failures themselves),
199
+ * because "the last read failed" still means "no usable result".
200
+ *
201
+ * @param variantId - which variant's freshness to test.
202
+ * @param maxAgeMs - the configured poll interval (cache lifetime).
162
203
  */
163
- function ChevronDownIcon$1() {
164
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
165
- width: 14,
166
- height: 14,
167
- viewBox: "0 0 14 14",
168
- fill: "none",
169
- xmlns: "http://www.w3.org/2000/svg",
170
- "aria-hidden": "true",
171
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
172
- d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z",
173
- fill: "currentColor"
174
- })
175
- });
204
+ function quotaStatusIsFresh(variantId, maxAgeMs) {
205
+ const fetchedAt = variantId === "qoder" ? statusFetchedAt.cn : statusFetchedAt.global;
206
+ const document = variantId === "qoder" ? statusDocuments.cn : statusDocuments.global;
207
+ if (fetchedAt === void 0 || document === void 0) return false;
208
+ return Date.now() - fetchedAt < maxAgeMs;
176
209
  }
177
- /** The built-in card's chevron rule: tertiary color, and only the rotation animates. */
178
- const chevronStyle$1 = {
179
- flex: "none",
180
- display: "flex",
181
- color: "var(--dsw-alias-label-tertiary)",
182
- transition: "transform .16s"
183
- };
184
- const cardBodyStyle$1 = {
185
- borderTop: ".5px solid var(--dsw-alias-border-l2)",
186
- margin: "0 16px",
187
- padding: "12px 0 8px"
188
- };
189
- const bodyStyle = {
190
- margin: 0,
191
- fontSize: 13,
192
- lineHeight: 1.5,
193
- color: "var(--dsw-alias-label-tertiary)"
194
- };
195
- const rowStyle$1 = {
196
- display: "flex",
197
- alignItems: "center",
198
- justifyContent: "space-between",
199
- flexWrap: "wrap",
200
- gap: 12
201
- };
202
- const statusStyle = {
203
- display: "flex",
204
- alignItems: "center",
205
- gap: 8,
206
- fontSize: 13,
207
- fontWeight: 500,
208
- lineHeight: 1.5,
209
- color: "var(--dsw-alias-label-primary)"
210
- };
211
- /** The built-in secondary button: transparent, hairline border, 8px radius. */
212
- const buttonStyle$1 = {
213
- boxSizing: "border-box",
214
- padding: "5px 14px",
215
- border: "1px solid var(--dsw-alias-border-l2)",
216
- borderRadius: 8,
217
- background: "transparent",
218
- color: "var(--dsw-alias-label-secondary)",
219
- font: "inherit",
220
- fontSize: 13,
221
- lineHeight: 1.5,
222
- cursor: "pointer"
223
- };
224
- const errorStyle = {
225
- ...bodyStyle,
226
- color: "var(--dsw-alias-state-error-primary)"
210
+ //#endregion
211
+ //#region src/client/QuotaSettingsCard.tsx
212
+ /**
213
+ * The shared quota-settings card: one card above the two variant cards that
214
+ * configures both sidebar quota widgets.
215
+ *
216
+ * Like the built-in plugin cards, it registers into `settings.plugin.item`
217
+ * keyed by its own settings namespace (`qoder-quota`), binds that
218
+ * namespace through the client settings scope, and writes through the scope's
219
+ * revision-fenced `set` — the same durable-write path every preference row
220
+ * uses. A toggle commits on click: each click is one explicit user choice,
221
+ * and the scope's ordering makes the last one win, so no staged-draft form is
222
+ * needed for two booleans and a number.
223
+ *
224
+ * The two toggles gate the CN and international sidebar cards respectively;
225
+ * the interval is one shared poll period. Toggles are disabled while their
226
+ * variant is signed out: a quota card for an account nobody is signed into
227
+ * would render an error forever, so the setting waits for a session.
228
+ */
229
+ /** The default poll interval shown before a value is stored. */
230
+ const POLL_DEFAULT_MS = 3e5;
231
+ /** Floor the schema also enforces; mirrored here for immediate UI feedback. */
232
+ const POLL_MIN_MS = 6e4;
233
+ /** Read the section values out of a scope snapshot (defaults when absent). */
234
+ function project(scope) {
235
+ if (scope === void 0) return {
236
+ status: "unavailable",
237
+ writable: false,
238
+ values: {
239
+ sidebarQuotaCN: false,
240
+ sidebarQuotaGlobal: false,
241
+ quotaPollMs: POLL_DEFAULT_MS
242
+ }
243
+ };
244
+ const snapshot = scope.getSnapshot();
245
+ const value = snapshot.value ?? {};
246
+ return {
247
+ status: snapshot.status,
248
+ writable: snapshot.writable,
249
+ values: {
250
+ sidebarQuotaCN: value.sidebarQuotaCN === true,
251
+ sidebarQuotaGlobal: value.sidebarQuotaGlobal === true,
252
+ quotaPollMs: typeof value.quotaPollMs === "number" ? value.quotaPollMs : POLL_DEFAULT_MS
253
+ }
254
+ };
255
+ }
256
+ /**
257
+ * Stable-reference projection cache.
258
+ *
259
+ * React's useSyncExternalStore requires getSnapshot() to return THE SAME
260
+ * reference between renders unless the store actually changed. project()
261
+ * builds a fresh object every call, which re-renders forever and crashes the
262
+ * card with React error #185 ("maximum update depth exceeded") — exactly the
263
+ * crash the slot ledger reported. The cache below returns the last built
264
+ * projection until the underlying scope snapshot (or scope identity) changes,
265
+ * which is the only thing the projection actually derives from.
266
+ */
267
+ let cachedScope;
268
+ let cachedProjection;
269
+ const UNAVAILABLE = {
270
+ status: "unavailable",
271
+ writable: false,
272
+ values: {
273
+ sidebarQuotaCN: false,
274
+ sidebarQuotaGlobal: false,
275
+ quotaPollMs: POLL_DEFAULT_MS
276
+ }
227
277
  };
228
- const quotaListStyle = {
278
+ function stableProject(scope) {
279
+ if (scope === void 0) return UNAVAILABLE;
280
+ const next = project(scope);
281
+ if (cachedProjection === void 0 || cachedScope !== scope || cachedProjection.status !== next.status || cachedProjection.writable !== next.writable || cachedProjection.values.sidebarQuotaCN !== next.values.sidebarQuotaCN || cachedProjection.values.sidebarQuotaGlobal !== next.values.sidebarQuotaGlobal || cachedProjection.values.quotaPollMs !== next.values.quotaPollMs) {
282
+ cachedScope = scope;
283
+ cachedProjection = next;
284
+ }
285
+ return cachedProjection;
286
+ }
287
+ /** One toggle row: label, hint, and a switch drawn to the shell's proportions. */
288
+ function ToggleRow({ label, hint, checked, disabled, disabledHint, onToggle }) {
289
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
290
+ style: rowStyle$1,
291
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
292
+ style: rowTextStyle,
293
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
294
+ style: labelStyle$1,
295
+ children: label
296
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
297
+ style: hintStyle,
298
+ children: disabled === true && disabledHint !== void 0 ? disabledHint : hint
299
+ })]
300
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
301
+ type: "button",
302
+ role: "switch",
303
+ "aria-checked": checked,
304
+ disabled,
305
+ "aria-label": label,
306
+ onClick: () => {
307
+ if (disabled) return;
308
+ onToggle(!checked);
309
+ },
310
+ style: {
311
+ ...switchStyle,
312
+ background: checked ? "var(--dsw-alias-brand-primary)" : "var(--dsw-alias-bg-layer-3, rgba(127,127,127,0.2))",
313
+ justifyContent: checked ? "flex-end" : "flex-start",
314
+ opacity: disabled === true ? .45 : 1,
315
+ cursor: disabled === true ? "not-allowed" : "pointer"
316
+ },
317
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: knobStyle })
318
+ })]
319
+ });
320
+ }
321
+ /** The inner controls for sidebar quota settings. */
322
+ function QuotaSettingsContent({ t = (key) => key, scope, signedIn }) {
323
+ const subscribe = (0, react.useCallback)((onStoreChange) => {
324
+ return scope?.subscribe(onStoreChange) ?? (() => {});
325
+ }, [scope]);
326
+ const projection = (0, react.useSyncExternalStore)(subscribe, () => stableProject(scope));
327
+ const liveSignIn = (0, react.useSyncExternalStore)(onQuotaSettingsChange, quotaSignInState);
328
+ const [probe, setProbe] = (0, react.useState)();
329
+ (0, react.useEffect)(() => {
330
+ let disposed = false;
331
+ const probeOne = async (path) => {
332
+ try {
333
+ const body = await (await fetch(path, { headers: { accept: "application/json" } })).json();
334
+ if (disposed || !isQoderWebStatus(body)) return void 0;
335
+ noteQuotaSignIn(variantOfStatusPath(path), body.status === "signed-in");
336
+ return body.status === "signed-in";
337
+ } catch {
338
+ return;
339
+ }
340
+ };
341
+ (async () => {
342
+ const [cn, global] = await Promise.all([probeOne(QODER_STATUS_PATH), probeOne(QODER_GLOBAL_STATUS_PATH)]);
343
+ if (!disposed) setProbe({
344
+ cn: cn === true,
345
+ global: global === true
346
+ });
347
+ })();
348
+ return () => {
349
+ disposed = true;
350
+ };
351
+ }, []);
352
+ if (projection.status === "unavailable") return null;
353
+ const reported = signedIn?.();
354
+ const deriveSigned = (variant, variantId) => {
355
+ if (reported !== void 0) return Boolean(reported[variant]);
356
+ const currentStatus = quotaStatus(variantId);
357
+ if (currentStatus?.status === "signed-out") return false;
358
+ const live = liveSignIn[variant];
359
+ if (probe !== void 0) {
360
+ if (!probe[variant]) return Boolean(live && currentStatus?.status === "signed-in");
361
+ return Boolean(live);
362
+ }
363
+ return Boolean(live && currentStatus?.status === "signed-in");
364
+ };
365
+ const signed = {
366
+ cn: deriveSigned("cn", "qoder"),
367
+ global: deriveSigned("global", "qoder-global")
368
+ };
369
+ const write = (field, value) => {
370
+ if (field === "sidebarQuotaCN" && value === true && !signed.cn) return;
371
+ if (field === "sidebarQuotaGlobal" && value === true && !signed.global) return;
372
+ scope?.set(field, value);
373
+ };
374
+ const minutes = Math.max(POLL_MIN_MS / 6e4, Math.round(projection.values.quotaPollMs / 6e4));
375
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
376
+ style: {
377
+ display: "flex",
378
+ flexDirection: "column",
379
+ gap: 4
380
+ },
381
+ children: [
382
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
383
+ label: t("quotaToggleCN"),
384
+ hint: t("quotaToggleHint"),
385
+ checked: projection.values.sidebarQuotaCN,
386
+ disabled: !signed.cn,
387
+ disabledHint: t("quotaSignInRequired"),
388
+ onToggle: (next) => write("sidebarQuotaCN", next)
389
+ }),
390
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
391
+ label: t("quotaToggleGlobal"),
392
+ hint: t("quotaToggleHint"),
393
+ checked: projection.values.sidebarQuotaGlobal,
394
+ disabled: !signed.global,
395
+ disabledHint: t("quotaSignInRequired"),
396
+ onToggle: (next) => write("sidebarQuotaGlobal", next)
397
+ }),
398
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
399
+ style: {
400
+ ...rowStyle$1,
401
+ borderBottom: "none",
402
+ paddingBottom: 0
403
+ },
404
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
405
+ style: rowTextStyle,
406
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
407
+ style: labelStyle$1,
408
+ children: t("quotaPollLabel")
409
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
410
+ style: hintStyle,
411
+ children: t("quotaPollHint")
412
+ })]
413
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
414
+ style: pollFieldStyle,
415
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
416
+ type: "number",
417
+ min: POLL_MIN_MS / 6e4,
418
+ step: 1,
419
+ value: minutes,
420
+ "aria-label": t("quotaPollLabel"),
421
+ onChange: (event) => {
422
+ const mins = Number.parseInt(event.target.value, 10);
423
+ if (Number.isFinite(mins) && mins > 0) write("quotaPollMs", Math.max(POLL_MIN_MS, mins * 6e4));
424
+ },
425
+ style: inputStyle
426
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
427
+ style: hintStyle,
428
+ children: t("quotaPollUnit")
429
+ })]
430
+ })]
431
+ }),
432
+ projection.writable === false ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
433
+ style: hintStyle,
434
+ children: t("quotaSettingsSaveFailed")
435
+ }) : null
436
+ ]
437
+ });
438
+ }
439
+ /**
440
+ * One settings row: NO box of its own (the bordered rows read as nested
441
+ * cards, which the user ruled against) — rows are separated by a hairline
442
+ * bottom rule like the settings shell's own preference lists.
443
+ */
444
+ const rowStyle$1 = {
229
445
  display: "flex",
230
- flexDirection: "column",
231
- gap: 18,
232
- paddingTop: 2
446
+ alignItems: "center",
447
+ gap: 12,
448
+ borderBottom: ".5px solid var(--dsw-alias-border-l2)",
449
+ paddingBottom: 10
233
450
  };
234
- const quotaGroupStyle = {
451
+ const rowTextStyle = {
235
452
  display: "flex",
453
+ flex: 1,
454
+ minWidth: 0,
236
455
  flexDirection: "column",
237
- gap: 10
456
+ gap: 2
238
457
  };
239
- const quotaTitleStyle = {
240
- margin: 0,
458
+ const labelStyle$1 = {
241
459
  fontSize: 13,
460
+ fontWeight: 500,
242
461
  lineHeight: 1.5,
243
- fontWeight: 600,
244
462
  color: "var(--dsw-alias-label-primary)"
245
463
  };
246
- const quotaLabelStyle = {
247
- display: "flex",
248
- justifyContent: "space-between",
249
- gap: 12,
250
- fontSize: 13,
251
- lineHeight: 1.5,
252
- color: "var(--dsw-alias-label-secondary)"
253
- };
254
- const modelRateStyle = {
464
+ const hintStyle = {
255
465
  fontSize: 12,
256
466
  lineHeight: 1.5,
257
467
  color: "var(--dsw-alias-label-tertiary)"
258
468
  };
259
- const contextPreferenceStyle = {
260
- display: "flex",
261
- alignItems: "flex-start",
262
- gap: 9,
263
- padding: "10px 12px",
264
- border: ".5px solid var(--dsw-alias-border-l4)",
265
- borderRadius: 8,
266
- background: "var(--dsw-alias-bg-layer-3)",
267
- color: "var(--dsw-alias-label-primary)",
268
- fontSize: 13,
269
- lineHeight: 1.5
270
- };
271
- const contextPreferenceCopyStyle = {
272
- display: "flex",
273
- flexDirection: "column",
274
- gap: 2
275
- };
276
- /** The right-hand cell of one model row: value and its note on one line. */
277
- const contextPickerRowStyle = {
469
+ const switchStyle = {
470
+ flex: "none",
278
471
  display: "flex",
472
+ width: 36,
473
+ height: 20,
474
+ borderRadius: 10,
475
+ borderWidth: "1px",
476
+ borderStyle: "solid",
477
+ borderColor: "var(--dsw-alias-border-l2)",
478
+ padding: 1,
479
+ cursor: "pointer",
279
480
  alignItems: "center",
280
- justifyContent: "flex-end",
281
- gap: 8,
282
- flexWrap: "wrap"
481
+ transition: "background .16s"
283
482
  };
284
- const progressTrackStyle = {
285
- height: 8,
286
- overflow: "hidden",
287
- borderRadius: 999,
288
- background: "var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.08))"
483
+ const knobStyle = {
484
+ display: "block",
485
+ width: 16,
486
+ height: 16,
487
+ borderRadius: "50%",
488
+ background: "var(--dsw-alias-bg-layer-1, #fff)",
489
+ boxShadow: "0 1px 2px rgba(0,0,0,0.2)"
289
490
  };
290
- /**
291
- * The PAT entry row: the password field takes the row's flexible width, the
292
- * Save button keeps its own, and the whole block sits inside the card body
293
- * without a nested box (the same rule the settings rows follow).
294
- */
295
- const patRowStyle = {
491
+ const pollFieldStyle = {
296
492
  display: "flex",
297
493
  alignItems: "center",
298
- gap: 8,
299
- flexWrap: "wrap"
494
+ gap: 6,
495
+ flex: "none"
496
+ };
497
+ const inputStyle = {
498
+ boxSizing: "border-box",
499
+ width: 55,
500
+ padding: "5px 8px",
501
+ borderWidth: "1px",
502
+ borderStyle: "solid",
503
+ borderColor: "var(--dsw-alias-border-l2)",
504
+ borderRadius: 8,
505
+ background: "var(--dsw-alias-bg-layer-2)",
506
+ color: "var(--dsw-alias-label-primary)",
507
+ font: "inherit",
508
+ fontSize: 13,
509
+ textAlign: "right"
510
+ };
511
+ //#endregion
512
+ //#region src/client/QoderPluginCard.tsx
513
+ /** Qoder status card contributed to Harness Plugin configuration. */
514
+ /** China Qoder; the plugin's primary card and default. */
515
+ const QODER_CN_CARD = {
516
+ id: "qoder",
517
+ titleKey: "title",
518
+ introKey: "intro",
519
+ signedOutKey: "signedOutHint",
520
+ patGuideKey: "patGuide",
521
+ patPlaceholderKey: "patPlaceholder",
522
+ statusPath: QODER_STATUS_PATH,
523
+ probePath: QODER_PROBE_PATH,
524
+ authPath: QODER_AUTH_PATH
525
+ };
526
+ /** International Qoder Global. */
527
+ const QODER_GLOBAL_CARD = {
528
+ id: "qoder-global",
529
+ titleKey: "titleAI",
530
+ introKey: "introAI",
531
+ signedOutKey: "signedOutHintAI",
532
+ patGuideKey: "patGuideAI",
533
+ patPlaceholderKey: "patPlaceholderAI",
534
+ statusPath: QODER_GLOBAL_STATUS_PATH,
535
+ probePath: QODER_GLOBAL_PROBE_PATH,
536
+ authPath: QODER_GLOBAL_AUTH_PATH
537
+ };
538
+ /** Both cards, in display order (China first). */
539
+ const QODER_CARD_VARIANTS = [QODER_CN_CARD, QODER_GLOBAL_CARD];
540
+ const POLL_INTERVAL_MS = 6e4;
541
+ const cardStyle = {
542
+ listStyle: "none",
543
+ borderWidth: "0.5px",
544
+ borderStyle: "solid",
545
+ borderColor: "var(--dsw-alias-border-l4)",
546
+ borderRadius: 16,
547
+ background: "var(--dsw-alias-bg-layer-3)",
548
+ transition: "border-color .16s, background .16s"
549
+ };
550
+ /** Hover, matching the built-in card's `:hover`. Inline styles cannot express a pseudo-class. */
551
+ const cardHoverStyle = { borderColor: "var(--dsw-alias-label-dimmed)" };
552
+ /** Expanded, matching the built-in card's open state. */
553
+ const cardOpenStyle = {
554
+ background: "var(--dsw-alias-bg-layer-2)",
555
+ borderColor: "var(--dsw-alias-label-dimmed)"
556
+ };
557
+ const headerStyle = {
558
+ boxSizing: "border-box",
559
+ width: "100%",
560
+ display: "flex",
561
+ alignItems: "center",
562
+ gap: 12,
563
+ border: 0,
564
+ borderRadius: 12,
565
+ padding: "14px 16px",
566
+ background: "transparent",
567
+ color: "inherit",
568
+ font: "inherit",
569
+ textAlign: "left",
570
+ cursor: "pointer",
571
+ appearance: "none"
572
+ };
573
+ /**
574
+ * The built-in header's keyboard focus ring.
575
+ *
576
+ * `:focus-visible` is what makes the ring appear for keyboard navigation but not
577
+ * for a mouse click, and an inline style cannot express a pseudo-class — so the
578
+ * component tracks it and applies this instead. Without it the header falls back
579
+ * to the browser's own outline, which is the black box that used to appear on
580
+ * focus where the built-in card shows a brand-coloured ring.
581
+ */
582
+ const headerFocusStyle = {
583
+ outline: "2px solid var(--dsw-alias-brand-primary)",
584
+ outlineOffset: -2
585
+ };
586
+ const headTextStyle = {
587
+ display: "flex",
588
+ flex: 1,
589
+ minWidth: 0,
590
+ flexDirection: "column",
591
+ gap: 4
592
+ };
593
+ const nameStyle = {
594
+ fontSize: 15,
595
+ lineHeight: 1.4,
596
+ fontWeight: 600,
597
+ color: "var(--dsw-alias-label-primary)"
598
+ };
599
+ const descriptionStyle = {
600
+ fontSize: 13,
601
+ lineHeight: 1.5,
602
+ color: "var(--dsw-alias-label-tertiary)"
603
+ };
604
+ /**
605
+ * The disclosure chevron, drawn to match the Settings panel's own card.
606
+ *
607
+ * The built-in card renders `IconChevronDownOutline14` from the client's shared
608
+ * icon catalog, which the shell seeds into the module table. This plugin does
609
+ * not request that catalog, so the same outline is drawn here from the same path
610
+ * data: the text `⌄` glyph this replaces had a different shape, weight, and
611
+ * baseline from the icon the cards beside it use.
612
+ */
613
+ function ChevronDownIcon() {
614
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
615
+ width: 14,
616
+ height: 14,
617
+ viewBox: "0 0 14 14",
618
+ fill: "none",
619
+ xmlns: "http://www.w3.org/2000/svg",
620
+ "aria-hidden": "true",
621
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
622
+ d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z",
623
+ fill: "currentColor"
624
+ })
625
+ });
626
+ }
627
+ /** The built-in card's chevron rule: tertiary color, and only the rotation animates. */
628
+ const chevronStyle = {
629
+ flex: "none",
630
+ display: "flex",
631
+ color: "var(--dsw-alias-label-tertiary)",
632
+ transition: "transform .16s"
633
+ };
634
+ const cardBodyStyle = {
635
+ borderTop: ".5px solid var(--dsw-alias-border-l2)",
636
+ margin: "0 16px",
637
+ padding: "12px 0 8px"
638
+ };
639
+ const bodyStyle = {
640
+ margin: 0,
641
+ fontSize: 13,
642
+ lineHeight: 1.5,
643
+ color: "var(--dsw-alias-label-tertiary)"
644
+ };
645
+ const rowStyle = {
646
+ display: "flex",
647
+ alignItems: "center",
648
+ justifyContent: "space-between",
649
+ flexWrap: "wrap",
650
+ gap: 12
651
+ };
652
+ const statusStyle = {
653
+ display: "flex",
654
+ alignItems: "center",
655
+ gap: 8,
656
+ fontSize: 13,
657
+ fontWeight: 500,
658
+ lineHeight: 1.5,
659
+ color: "var(--dsw-alias-label-primary)"
660
+ };
661
+ /** The built-in secondary button: transparent, hairline border, 8px radius. */
662
+ const buttonStyle$1 = {
663
+ boxSizing: "border-box",
664
+ padding: "5px 14px",
665
+ border: "1px solid var(--dsw-alias-border-l2)",
666
+ borderRadius: 8,
667
+ background: "transparent",
668
+ color: "var(--dsw-alias-label-secondary)",
669
+ font: "inherit",
670
+ fontSize: 13,
671
+ lineHeight: 1.5,
672
+ cursor: "pointer"
673
+ };
674
+ const errorStyle = {
675
+ ...bodyStyle,
676
+ color: "var(--dsw-alias-state-error-primary)"
677
+ };
678
+ const quotaListStyle = {
679
+ display: "flex",
680
+ flexDirection: "column",
681
+ gap: 18,
682
+ paddingTop: 2
683
+ };
684
+ const quotaGroupStyle = {
685
+ display: "flex",
686
+ flexDirection: "column",
687
+ gap: 10
688
+ };
689
+ const quotaTitleStyle = {
690
+ margin: 0,
691
+ fontSize: 13,
692
+ lineHeight: 1.5,
693
+ fontWeight: 600,
694
+ color: "var(--dsw-alias-label-primary)"
695
+ };
696
+ const quotaLabelStyle = {
697
+ display: "flex",
698
+ justifyContent: "space-between",
699
+ gap: 12,
700
+ fontSize: 13,
701
+ lineHeight: 1.5,
702
+ color: "var(--dsw-alias-label-secondary)"
703
+ };
704
+ const modelRateStyle = {
705
+ fontSize: 12,
706
+ lineHeight: 1.5,
707
+ color: "var(--dsw-alias-label-tertiary)"
708
+ };
709
+ const contextPreferenceStyle = {
710
+ display: "flex",
711
+ alignItems: "flex-start",
712
+ gap: 9,
713
+ padding: "10px 12px",
714
+ border: ".5px solid var(--dsw-alias-border-l4)",
715
+ borderRadius: 8,
716
+ background: "var(--dsw-alias-bg-layer-3)",
717
+ color: "var(--dsw-alias-label-primary)",
718
+ fontSize: 13,
719
+ lineHeight: 1.5
720
+ };
721
+ const contextPreferenceCopyStyle = {
722
+ display: "flex",
723
+ flexDirection: "column",
724
+ gap: 2
725
+ };
726
+ const contextPickerRowStyle = {
727
+ display: "flex",
728
+ alignItems: "center",
729
+ justifyContent: "flex-end",
730
+ gap: 8,
731
+ flexWrap: "wrap"
732
+ };
733
+ const progressTrackStyle = {
734
+ height: 8,
735
+ overflow: "hidden",
736
+ borderRadius: 999,
737
+ background: "var(--dsw-alias-bg-layer-2, rgba(0, 0, 0, 0.08))"
738
+ };
739
+ /**
740
+ * The PAT entry row: the password field takes the row's flexible width, the
741
+ * Save button keeps its own, and the whole block sits inside the card body
742
+ * without a nested box (the same rule the settings rows follow).
743
+ */
744
+ const patRowStyle = {
745
+ display: "flex",
746
+ alignItems: "center",
747
+ gap: 8,
748
+ flexWrap: "wrap"
300
749
  };
301
750
  const patInputStyle = {
302
751
  boxSizing: "border-box",
@@ -353,11 +802,49 @@ window.__ModuleLoader__.load({
353
802
  */
354
803
  const primaryButtonStyle$1 = {
355
804
  ...buttonStyle$1,
356
- border: "1px solid var(--dsw-alias-button-primary-fill)",
805
+ borderWidth: "1px",
806
+ borderStyle: "solid",
807
+ borderColor: "var(--dsw-alias-button-primary-fill)",
357
808
  background: "var(--dsw-alias-button-primary-fill)",
358
809
  color: "var(--dsw-alias-label-primary-foreground)"
359
810
  };
360
- function progressFillStyle(percent) {
811
+ const segmentedContainerStyle = {
812
+ display: "flex",
813
+ alignItems: "center",
814
+ background: "var(--dsw-alias-bg-layer-1, rgba(20, 20, 20, 0.6))",
815
+ borderWidth: "1px",
816
+ borderStyle: "solid",
817
+ borderColor: "var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.08))",
818
+ borderRadius: 8,
819
+ padding: 3,
820
+ gap: 4,
821
+ marginTop: 14,
822
+ marginBottom: 16
823
+ };
824
+ function segmentedTabItemStyle(active) {
825
+ return {
826
+ flex: 1,
827
+ display: "flex",
828
+ alignItems: "center",
829
+ justifyContent: "center",
830
+ gap: 8,
831
+ padding: "6px 12px",
832
+ borderRadius: 6,
833
+ borderWidth: "1px",
834
+ borderStyle: "solid",
835
+ borderColor: active ? "var(--dsw-alias-border-l4, rgba(255, 255, 255, 0.18))" : "transparent",
836
+ background: active ? "var(--dsw-alias-bg-layer-3, rgba(255, 255, 255, 0.08))" : "transparent",
837
+ color: active ? "var(--dsw-alias-label-primary, #fff)" : "var(--dsw-alias-label-tertiary, #8c8c8c)",
838
+ fontWeight: active ? 500 : 400,
839
+ fontSize: 13,
840
+ lineHeight: "18px",
841
+ cursor: "pointer",
842
+ appearance: "none",
843
+ outline: "none",
844
+ transition: "all .16s ease"
845
+ };
846
+ }
847
+ function progressFillStyle(percent) {
361
848
  return {
362
849
  width: `${Math.max(0, Math.min(100, percent))}%`,
363
850
  height: "100%",
@@ -373,11 +860,11 @@ window.__ModuleLoader__.load({
373
860
  */
374
861
  function dotStyle(status) {
375
862
  return {
376
- width: 9,
377
- height: 9,
863
+ width: 8,
864
+ height: 8,
378
865
  borderRadius: "50%",
379
866
  flex: "0 0 auto",
380
- background: status === "signed-in" ? "var(--dsw-alias-state-success-primary, #22a06b)" : status === "error" ? "var(--dsw-alias-state-error-primary, #d92d20)" : "var(--dsw-alias-label-dimmed, #9aa0a6)"
867
+ background: status === "signed-in" ? "var(--dsw-alias-state-success-primary, #22a06b)" : status === "error" ? "var(--dsw-alias-state-error-primary, #d92d20)" : "var(--dsw-alias-label-dimmed, #8c8c8c)"
381
868
  };
382
869
  }
383
870
  function formatNumber(value) {
@@ -397,7 +884,6 @@ window.__ModuleLoader__.load({
397
884
  if (!Number.isNaN(parsed)) return formatTime(parsed);
398
885
  return time;
399
886
  }
400
- /** Localize where the credential in effect came from. */
401
887
  function patSourceText(source, t) {
402
888
  if (source === "env") return t("patSourceEnv");
403
889
  if (source === "cli") return t("patSourceCli");
@@ -549,72 +1035,35 @@ window.__ModuleLoader__.load({
549
1035
  ]
550
1036
  });
551
1037
  }
552
- /**
553
- * Compact token count for display: the catalog's own round numbers (`200000`,
554
- * `1000000`) read better as `200K` / `1M`, and no precision is lost because
555
- * these values are always whole thousands.
556
- */
557
1038
  function formatTokens(tokens) {
558
1039
  if (tokens >= 1e6 && tokens % 1e6 === 0) return `${tokens / 1e6}M`;
559
1040
  if (tokens >= 1e3 && tokens % 1e3 === 0) return `${tokens / 1e3}K`;
560
1041
  return String(tokens);
561
1042
  }
562
1043
  /** Render Qoder PAT state, quota, catalog, and context capacities as one expandable card. */
563
- function QoderPluginCard({ t, variant = QODER_CN_CARD }) {
1044
+ function QoderPluginCard(props) {
1045
+ const { t, scope, signedIn, variant, unified } = props;
564
1046
  if (t === void 0) throw new Error("Qoder plugin card requires its translation function");
1047
+ const isUnified = unified === true;
1048
+ const liveSignIn = (0, react.useSyncExternalStore)(onQuotaSettingsChange, quotaSignInState);
1049
+ const [activeVariantId, setActiveVariantId] = (0, react.useState)("qoder");
1050
+ const currentVariant = isUnified ? activeVariantId === "qoder" ? QODER_CN_CARD : QODER_GLOBAL_CARD : variant ?? QODER_CN_CARD;
565
1051
  const [open, setOpen] = (0, react.useState)(false);
566
- /** Whether the pointer is over the card; drives the same border tint the built-in card gets on hover. */
567
1052
  const [hovered, setHovered] = (0, react.useState)(false);
568
- /** Keyboard focus on the header, reproducing the built-in's `:focus-visible` ring. */
569
1053
  const [headerFocused, setHeaderFocused] = (0, react.useState)(false);
570
- /**
571
- * The document to render. `undefined` means *not read yet*, which is a
572
- * distinct state from "signed out": seeding this with a signed-out document
573
- * told an already-signed-in user they were signed out for the whole first
574
- * round trip (and forever, if the read never settled).
575
- */
576
1054
  const [status, setStatus] = (0, react.useState)();
577
- /**
578
- * Whether the last **successful** read found a usable credential.
579
- *
580
- * Kept apart from `status` because the poll's liveness must depend on what the
581
- * account actually is, not on what the card last displayed: a failed read
582
- * leaves this untouched, so a transient failure cannot disarm the interval,
583
- * while a genuine signed-out answer still stops it.
584
- *
585
- * `undefined` therefore means "no successful read yet", which is also the
586
- * condition that decides whether a failed read has anything to preserve.
587
- */
588
- const [signedIn, setSignedIn] = (0, react.useState)();
589
- /**
590
- * Why the most recent read failed, when it did. Rendered as a notice beside
591
- * whatever document is still on screen, rather than replacing it.
592
- */
1055
+ const [signedInState, setSignedInState] = (0, react.useState)();
593
1056
  const [readFailure, setReadFailure] = (0, react.useState)();
594
1057
  const [busy, setBusy] = (0, react.useState)(false);
595
- /** The PAT draft in the input box. Never persisted; lives only in component state. */
596
1058
  const [patDraft, setPatDraft] = (0, react.useState)("");
597
- /** Whether the signed-in card is showing the replace-PAT entry. */
598
1059
  const [replacing, setReplacing] = (0, react.useState)(false);
599
- /** Whether a PAT save or clear is in flight. Own flag, so the 60s busy label stays honest. */
600
1060
  const [patBusy, setPatBusy] = (0, react.useState)(false);
601
- /** Why the most recent PAT save/clear failed, when it did. */
602
1061
  const [patError, setPatError] = (0, react.useState)();
603
- /** The most recent PAT outcome the card should report beside the entry. */
604
1062
  const [patNotice, setPatNotice] = (0, react.useState)();
605
- /** The password input, so 「更换 PAT」 can focus and pre-clear it. */
606
1063
  const patInput = (0, react.useRef)(null);
607
1064
  const [tab, setTab] = (0, react.useState)("status");
608
1065
  const mounted = (0, react.useRef)(true);
609
- /**
610
- * Identity of the newest read that may write. Assigned when a read *starts*,
611
- * so a response is superseded by anything begun after it — "the response whose
612
- * request started last wins". Without this, a slow poll begun before a manual
613
- * action could settle after the action's own refresh and restore the older
614
- * document.
615
- */
616
1066
  const readSeq = (0, react.useRef)(0);
617
- /** Manual requests in flight, so unmount can abort them like the poll's. */
618
1067
  const manualControllers = (0, react.useRef)(/* @__PURE__ */ new Set());
619
1068
  (0, react.useEffect)(() => {
620
1069
  mounted.current = true;
@@ -624,39 +1073,17 @@ window.__ModuleLoader__.load({
624
1073
  manualControllers.current.clear();
625
1074
  };
626
1075
  }, []);
627
- /** Register a manual request's controller so unmount aborts it. */
628
1076
  const trackController = (0, react.useCallback)(() => {
629
1077
  const controller = new AbortController();
630
1078
  manualControllers.current.add(controller);
631
1079
  return controller;
632
1080
  }, []);
633
- /**
634
- * The in-process key authorizing this card's PAT writes, or undefined until a
635
- * document carrying one has been read.
636
- *
637
- * Derived once rather than read off each use site: the `error` arm carries no
638
- * key, and reaching for `status.authKey` in three places is three chances to
639
- * dereference a state that has none. The constant-time comparison happens on
640
- * the host; the card only passes the key through.
641
- */
642
1081
  const authKey = status === void 0 || status.status === "error" ? void 0 : status.authKey;
643
- /**
644
- * Read the status document and apply it under the two policies the card's
645
- * correctness rests on:
646
- *
647
- * - a non-document body (empty, `null`, a non-JSON page) is a failed read, not
648
- * something to store and then dereference in the render;
649
- * - a failed read never discards a document already on screen. It is recorded
650
- * and shown as a notice beside that document; only when nothing has been
651
- * read yet does the failure itself become the rendered state.
652
- *
653
- * Returns whether this read produced the current document.
654
- */
655
1082
  const refresh = (0, react.useCallback)(async (signal) => {
656
1083
  const seq = ++readSeq.current;
657
1084
  const current = () => mounted.current && signal?.aborted !== true && seq === readSeq.current;
658
1085
  try {
659
- const response = await fetch(variant.statusPath, {
1086
+ const response = await fetch(currentVariant.statusPath, {
660
1087
  headers: { accept: "application/json" },
661
1088
  credentials: "same-origin",
662
1089
  ...signal === void 0 ? {} : { signal }
@@ -666,8 +1093,13 @@ window.__ModuleLoader__.load({
666
1093
  if (!isQoderWebStatus(value)) throw new Error(t("statusResponseInvalid"));
667
1094
  if (!current()) return false;
668
1095
  setStatus(value);
669
- if (value.status === "signed-in") setSignedIn(true);
670
- else if (value.status === "signed-out") setSignedIn(false);
1096
+ if (value.status === "signed-in") {
1097
+ setSignedInState(true);
1098
+ noteQuotaStatus(currentVariant.id, value);
1099
+ } else if (value.status === "signed-out") {
1100
+ setSignedInState(false);
1101
+ noteQuotaStatus(currentVariant.id, value);
1102
+ }
671
1103
  setReadFailure(void 0);
672
1104
  return true;
673
1105
  } catch (error) {
@@ -681,17 +1113,28 @@ window.__ModuleLoader__.load({
681
1113
  }
682
1114
  return false;
683
1115
  }
684
- }, [t, variant.statusPath]);
1116
+ }, [currentVariant.statusPath, t]);
685
1117
  (0, react.useEffect)(() => {
686
1118
  if (!open) return;
1119
+ setStatus(void 0);
1120
+ setSignedInState(void 0);
1121
+ setReadFailure(void 0);
1122
+ setPatDraft("");
1123
+ setReplacing(false);
1124
+ setPatError(void 0);
1125
+ setPatNotice(void 0);
687
1126
  const controller = new AbortController();
688
1127
  refresh(controller.signal);
689
1128
  return () => {
690
1129
  controller.abort();
691
1130
  };
692
- }, [open, refresh]);
1131
+ }, [
1132
+ open,
1133
+ currentVariant.statusPath,
1134
+ refresh
1135
+ ]);
693
1136
  (0, react.useEffect)(() => {
694
- if (!open || signedIn === false) return;
1137
+ if (!open || signedInState === false) return;
695
1138
  const controller = new AbortController();
696
1139
  const timer = window.setInterval(() => {
697
1140
  refresh(controller.signal);
@@ -703,7 +1146,7 @@ window.__ModuleLoader__.load({
703
1146
  }, [
704
1147
  open,
705
1148
  refresh,
706
- signedIn
1149
+ signedInState
707
1150
  ]);
708
1151
  const manualRefresh = async () => {
709
1152
  setBusy(true);
@@ -729,7 +1172,7 @@ window.__ModuleLoader__.load({
729
1172
  setBusy(true);
730
1173
  const controller = trackController();
731
1174
  try {
732
- const response = await fetch(variant.probePath, {
1175
+ const response = await fetch(currentVariant.probePath, {
733
1176
  method: "POST",
734
1177
  headers: {
735
1178
  "Content-Type": "application/json",
@@ -753,11 +1196,11 @@ window.__ModuleLoader__.load({
753
1196
  manualControllers.current.delete(controller);
754
1197
  }
755
1198
  }, [
1199
+ currentVariant.probePath,
756
1200
  refresh,
757
1201
  status,
758
1202
  t,
759
- trackController,
760
- variant.probePath
1203
+ trackController
761
1204
  ]);
762
1205
  /**
763
1206
  * Run one probe-route control action and refresh the card's state afterwards.
@@ -772,7 +1215,7 @@ window.__ModuleLoader__.load({
772
1215
  setBusy(true);
773
1216
  const controller = trackController();
774
1217
  try {
775
- const response = await fetch(variant.probePath, {
1218
+ const response = await fetch(currentVariant.probePath, {
776
1219
  method: "POST",
777
1220
  headers: {
778
1221
  "Content-Type": "application/json",
@@ -799,11 +1242,11 @@ window.__ModuleLoader__.load({
799
1242
  if (mounted.current) setBusy(false);
800
1243
  }
801
1244
  }, [
1245
+ currentVariant.probePath,
802
1246
  refresh,
803
1247
  status,
804
1248
  t,
805
- trackController,
806
- variant.probePath
1249
+ trackController
807
1250
  ]);
808
1251
  /**
809
1252
  * Validate and store the pasted PAT (or overwrite the stored one).
@@ -823,7 +1266,7 @@ window.__ModuleLoader__.load({
823
1266
  setPatNotice(void 0);
824
1267
  const controller = trackController();
825
1268
  try {
826
- const response = await fetch(variant.authPath, {
1269
+ const response = await fetch(currentVariant.authPath, {
827
1270
  method: "POST",
828
1271
  headers: {
829
1272
  "Content-Type": "application/json",
@@ -850,6 +1293,7 @@ window.__ModuleLoader__.load({
850
1293
  setPatDraft("");
851
1294
  setReplacing(false);
852
1295
  setPatNotice(t("patSaved"));
1296
+ noteQuotaSignIn(currentVariant.id, true);
853
1297
  await refresh(controller.signal);
854
1298
  } catch (error) {
855
1299
  if (mounted.current && controller.signal.aborted !== true) setPatError(t("patSaveFailed", { message: error instanceof Error ? error.message : t("requestFailed") }));
@@ -859,12 +1303,12 @@ window.__ModuleLoader__.load({
859
1303
  }
860
1304
  }, [
861
1305
  authKey,
1306
+ currentVariant.authPath,
862
1307
  patBusy,
863
1308
  patDraft,
864
1309
  refresh,
865
1310
  t,
866
- trackController,
867
- variant.authPath
1311
+ trackController
868
1312
  ]);
869
1313
  /** Remove the stored PAT; the host answer is `{ ok: true }`, then the card re-reads. */
870
1314
  const clearPat = (0, react.useCallback)(async () => {
@@ -876,7 +1320,7 @@ window.__ModuleLoader__.load({
876
1320
  setReplacing(false);
877
1321
  const controller = trackController();
878
1322
  try {
879
- const response = await fetch(variant.authPath, {
1323
+ const response = await fetch(currentVariant.authPath, {
880
1324
  method: "POST",
881
1325
  headers: {
882
1326
  "Content-Type": "application/json",
@@ -894,6 +1338,14 @@ window.__ModuleLoader__.load({
894
1338
  return;
895
1339
  }
896
1340
  setPatDraft("");
1341
+ const signedOutDoc = {
1342
+ status: "signed-out",
1343
+ authKey: key
1344
+ };
1345
+ setStatus(signedOutDoc);
1346
+ setSignedInState(false);
1347
+ noteQuotaStatus(currentVariant.id, signedOutDoc);
1348
+ noteQuotaSignIn(currentVariant.id, false);
897
1349
  await refresh(controller.signal);
898
1350
  } catch (error) {
899
1351
  if (mounted.current && controller.signal.aborted !== true) setPatError(t("patSaveFailed", { message: error instanceof Error ? error.message : t("requestFailed") }));
@@ -903,11 +1355,11 @@ window.__ModuleLoader__.load({
903
1355
  }
904
1356
  }, [
905
1357
  authKey,
1358
+ currentVariant.authPath,
906
1359
  patBusy,
907
1360
  refresh,
908
1361
  t,
909
- trackController,
910
- variant.authPath
1362
+ trackController
911
1363
  ]);
912
1364
  /**
913
1365
  * 更换 PAT: open the entry over the signed-in document, pre-cleared and
@@ -925,7 +1377,6 @@ window.__ModuleLoader__.load({
925
1377
  patInput.current?.focus();
926
1378
  });
927
1379
  }, []);
928
- /** The PAT entry: guide line, password input, Save. Shared by both arms. */
929
1380
  const patEntry = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
930
1381
  style: {
931
1382
  display: "flex",
@@ -935,7 +1386,7 @@ window.__ModuleLoader__.load({
935
1386
  children: [
936
1387
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
937
1388
  style: bodyStyle,
938
- children: t(variant.patGuideKey)
1389
+ children: t(currentVariant.patGuideKey)
939
1390
  }),
940
1391
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
941
1392
  style: patRowStyle,
@@ -944,7 +1395,7 @@ window.__ModuleLoader__.load({
944
1395
  ref: patInput,
945
1396
  type: "password",
946
1397
  value: patDraft,
947
- placeholder: t(variant.patPlaceholderKey),
1398
+ placeholder: t(currentVariant.patPlaceholderKey),
948
1399
  "aria-label": t("patHeading"),
949
1400
  disabled: patBusy,
950
1401
  onChange: (event) => {
@@ -990,9 +1441,9 @@ window.__ModuleLoader__.load({
990
1441
  })
991
1442
  ]
992
1443
  });
993
- const title = t(variant.titleKey);
1444
+ const cardTitle = isUnified ? t("unifiedTitle") : t(currentVariant.titleKey);
1445
+ const cardIntro = isUnified ? t("unifiedIntro") : t(currentVariant.introKey);
994
1446
  const label = status === void 0 ? t("loading") : status.status === "signed-in" ? t("signedIn") : status.status === "error" ? t("requestFailed") : t("signedOut");
995
- /** One line summarizing the PAT in effect: source, saved-at, and the redacted tail. */
996
1447
  const patSummaryLine = (pat) => {
997
1448
  const parts = [
998
1449
  patSourceText(pat.source, t),
@@ -1004,11 +1455,16 @@ window.__ModuleLoader__.load({
1004
1455
  children: parts.join(" · ")
1005
1456
  });
1006
1457
  };
1458
+ const reported = signedIn?.();
1459
+ const cnSignedIn = reported !== void 0 ? reported.cn : liveSignIn.cn;
1460
+ const globalSignedIn = reported !== void 0 ? reported.global : liveSignIn.global;
1461
+ const cnDotStatus = isUnified && activeVariantId === "qoder" ? status === void 0 ? "loading" : status.status : cnSignedIn ? "signed-in" : "signed-out";
1462
+ const globalDotStatus = isUnified && activeVariantId === "qoder-global" ? status === void 0 ? "loading" : status.status : globalSignedIn ? "signed-in" : "signed-out";
1007
1463
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1008
1464
  style: {
1009
- ...cardStyle$1,
1010
- ...hovered ? cardHoverStyle$1 : {},
1011
- ...open ? cardOpenStyle$1 : {}
1465
+ ...cardStyle,
1466
+ ...hovered ? cardHoverStyle : {},
1467
+ ...open ? cardOpenStyle : {}
1012
1468
  },
1013
1469
  onMouseEnter: () => {
1014
1470
  setHovered(true);
@@ -1020,10 +1476,10 @@ window.__ModuleLoader__.load({
1020
1476
  type: "button",
1021
1477
  style: {
1022
1478
  ...headerStyle,
1023
- ...headerFocused ? headerFocusStyle$1 : {}
1479
+ ...headerFocused ? headerFocusStyle : {}
1024
1480
  },
1025
1481
  "aria-expanded": open,
1026
- "aria-label": `${t(open ? "collapse" : "expand")}: ${title}`,
1482
+ "aria-label": `${t(open ? "collapse" : "expand")}: ${cardTitle}`,
1027
1483
  onClick: () => {
1028
1484
  setOpen(!open);
1029
1485
  },
@@ -1040,30 +1496,60 @@ window.__ModuleLoader__.load({
1040
1496
  setHeaderFocused(false);
1041
1497
  },
1042
1498
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1043
- style: headTextStyle$1,
1499
+ style: headTextStyle,
1044
1500
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1045
1501
  style: nameStyle,
1046
- children: title
1502
+ children: cardTitle
1047
1503
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1048
1504
  style: descriptionStyle,
1049
- children: t(variant.introKey)
1505
+ children: cardIntro
1050
1506
  })]
1051
1507
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1052
1508
  style: {
1053
- ...chevronStyle$1,
1509
+ ...chevronStyle,
1054
1510
  transform: open ? "rotate(180deg)" : "none"
1055
1511
  },
1056
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDownIcon$1, {})
1512
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDownIcon, {})
1057
1513
  })]
1058
1514
  }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1059
- style: cardBodyStyle$1,
1515
+ style: cardBodyStyle,
1060
1516
  children: [
1517
+ isUnified ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuotaSettingsContent, {
1518
+ t,
1519
+ scope,
1520
+ signedIn
1521
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1522
+ style: segmentedContainerStyle,
1523
+ role: "tablist",
1524
+ "aria-label": "Qoder Version Selection",
1525
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1526
+ type: "button",
1527
+ role: "tab",
1528
+ "aria-selected": activeVariantId === "qoder",
1529
+ style: segmentedTabItemStyle(activeVariantId === "qoder"),
1530
+ onClick: () => setActiveVariantId("qoder"),
1531
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1532
+ style: dotStyle(cnDotStatus),
1533
+ "aria-hidden": "true"
1534
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("variantTabCN") })]
1535
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1536
+ type: "button",
1537
+ role: "tab",
1538
+ "aria-selected": activeVariantId === "qoder-global",
1539
+ style: segmentedTabItemStyle(activeVariantId === "qoder-global"),
1540
+ onClick: () => setActiveVariantId("qoder-global"),
1541
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1542
+ style: dotStyle(globalDotStatus),
1543
+ "aria-hidden": "true"
1544
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("variantTabGlobal") })]
1545
+ })]
1546
+ })] }) : null,
1061
1547
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1062
1548
  style: quotaTitleStyle,
1063
1549
  children: t("accountHeading")
1064
1550
  }),
1065
1551
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1066
- style: rowStyle$1,
1552
+ style: rowStyle,
1067
1553
  children: [
1068
1554
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1069
1555
  style: statusStyle,
@@ -1100,7 +1586,7 @@ window.__ModuleLoader__.load({
1100
1586
  })] })
1101
1587
  ]
1102
1588
  }),
1103
- readFailure === void 0 || signedIn === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1589
+ readFailure === void 0 || signedInState === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1104
1590
  style: errorStyle,
1105
1591
  children: t("statusRefreshFailed", { message: readFailure })
1106
1592
  }),
@@ -1112,7 +1598,7 @@ window.__ModuleLoader__.load({
1112
1598
  children: t("jobTokenRefreshed", { time: formatTime(status.jobTokenRefreshedAt) })
1113
1599
  }),
1114
1600
  status.catalog === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1115
- style: rowStyle$1,
1601
+ style: rowStyle,
1116
1602
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1117
1603
  style: bodyStyle,
1118
1604
  children: status.catalog.source === "live" && status.catalog.fetchedAt !== void 0 ? t("catalogLive", { time: formatTime(status.catalog.fetchedAt) }) : status.catalog.source === "saved" && status.catalog.fetchedAt !== void 0 ? t("catalogSaved", { time: formatTime(status.catalog.fetchedAt) }) : t("catalogFallback")
@@ -1156,7 +1642,7 @@ window.__ModuleLoader__.load({
1156
1642
  children: [status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1157
1643
  style: quotaListStyle,
1158
1644
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1159
- style: rowStyle$1,
1645
+ style: rowStyle,
1160
1646
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1161
1647
  style: quotaTitleStyle,
1162
1648
  children: t("creditsHeading")
@@ -1206,7 +1692,7 @@ window.__ModuleLoader__.load({
1206
1692
  ] }) : null,
1207
1693
  status?.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1208
1694
  style: status.reason === void 0 ? bodyStyle : errorStyle,
1209
- children: status.reason ?? t(variant.signedOutKey)
1695
+ children: status.reason ?? t(currentVariant.signedOutKey)
1210
1696
  }), authKey === void 0 ? null : patEntry()] }) : null,
1211
1697
  status?.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1212
1698
  style: errorStyle,
@@ -1278,7 +1764,7 @@ window.__ModuleLoader__.load({
1278
1764
  * it names the feature, so it should read as an annotation attached to the icon
1279
1765
  * rather than compete with the adjacent model selector.
1280
1766
  */
1281
- const labelStyle$1 = {
1767
+ const labelStyle = {
1282
1768
  fontSize: 11,
1283
1769
  lineHeight: "16px",
1284
1770
  color: "var(--dsw-alias-label-tertiary)"
@@ -1287,919 +1773,365 @@ window.__ModuleLoader__.load({
1287
1773
  const tooltipStyle = {
1288
1774
  position: "absolute",
1289
1775
  left: "50%",
1290
- bottom: "calc(100% + 8px)",
1291
- zIndex: 1e3,
1292
- transform: "translateX(-50%)",
1293
- padding: "4px 8px",
1294
- borderRadius: 6,
1295
- background: "var(--dsw-specific-tip, #1f2329)",
1296
- boxShadow: "var(--dsw-shadow-lv2)",
1297
- color: "var(--dsw-alias-label-primary, #fff)",
1298
- fontSize: 12,
1299
- lineHeight: "18px",
1300
- whiteSpace: "nowrap",
1301
- pointerEvents: "none"
1302
- };
1303
- /** Confirmation bubble: same anchor, but interactive and allowed to wrap. */
1304
- const confirmStyle = {
1305
- position: "absolute",
1306
- right: 0,
1307
- bottom: "calc(100% + 8px)",
1308
- zIndex: 1001,
1309
- display: "flex",
1310
- flexDirection: "column",
1311
- gap: 8,
1312
- width: 260,
1313
- padding: "10px 12px",
1314
- border: "1px solid var(--dsw-alias-border-l2)",
1315
- borderRadius: 8,
1316
- background: "var(--dsw-alias-bg-layer-1, #fff)",
1317
- boxShadow: "var(--dsw-shadow-lv2)",
1318
- color: "var(--dsw-alias-label-primary)",
1319
- fontSize: 12,
1320
- lineHeight: "18px"
1321
- };
1322
- const confirmRowStyle = {
1323
- display: "flex",
1324
- justifyContent: "flex-end",
1325
- gap: 8
1326
- };
1327
- const confirmButtonStyle = {
1328
- padding: "3px 10px",
1329
- border: "1px solid var(--dsw-alias-border-l2)",
1330
- borderRadius: 6,
1331
- background: "transparent",
1332
- color: "inherit",
1333
- font: "inherit",
1334
- fontSize: 12,
1335
- cursor: "pointer"
1336
- };
1337
- /**
1338
- * Primary action inside the confirmation bubble.
1339
- *
1340
- * The fill and its text colour must come as a pair: `brand-primary` resolves to
1341
- * a light accent in this theme, so hardcoding `color: #fff` on top of it renders
1342
- * white-on-white. `button-primary-fill` + `label-primary-foreground` is the
1343
- * theme's own pair for exactly this, and is what `dsh-codex-connect` uses for
1344
- * the same job.
1345
- */
1346
- const primaryButtonStyle = {
1347
- ...confirmButtonStyle,
1348
- border: "1px solid var(--dsw-alias-button-primary-fill)",
1349
- background: "var(--dsw-alias-button-primary-fill)",
1350
- color: "var(--dsw-alias-label-primary-foreground)"
1351
- };
1352
- /**
1353
- * Result note: a single line + a dismiss button, anchored to the control's
1354
- * right side. Smaller than the confirmation bubble because it carries an
1355
- * *outcome*, not a *decision* — the work is done, the user only has to read
1356
- * and dismiss.
1357
- */
1358
- const noteStyle = {
1359
- position: "absolute",
1360
- right: 0,
1361
- bottom: "calc(100% + 8px)",
1362
- zIndex: 1001,
1363
- display: "flex",
1364
- alignItems: "center",
1365
- gap: 12,
1366
- padding: "6px 10px",
1367
- border: "1px solid var(--dsw-alias-border-l2)",
1368
- borderRadius: 8,
1369
- background: "var(--dsw-alias-bg-layer-1)",
1370
- boxShadow: "var(--dsw-shadow-lv2)",
1371
- color: "var(--dsw-alias-label-primary)",
1372
- fontSize: 12,
1373
- lineHeight: "18px",
1374
- whiteSpace: "nowrap"
1375
- };
1376
- /**
1377
- * The note's dismiss action. Outlined rather than bare text: inside an already
1378
- * bordered bubble, an unbordered word does not read as something you can click.
1379
- * Matches the outlined pill convention the plugin's other secondary actions use.
1380
- */
1381
- const noteDismissStyle = {
1382
- padding: "2px 8px",
1383
- border: "1px solid var(--dsw-alias-border-l2)",
1384
- borderRadius: 6,
1385
- background: "transparent",
1386
- color: "var(--dsw-alias-label-secondary)",
1387
- font: "inherit",
1388
- fontSize: 12,
1389
- lineHeight: "18px",
1390
- cursor: "pointer"
1391
- };
1392
- /**
1393
- * The feature's static inline label. Deliberately not a state readout — see the
1394
- * module comment.
1395
- */
1396
- function useLabel(t) {
1397
- return t("probeLabel");
1398
- }
1399
- /** Pick the model's recorded observation out of the probe section. */
1400
- function resultFor(status, model) {
1401
- if (status.status !== "signed-in") return void 0;
1402
- return status.probe?.results.find((result) => result.id === model);
1403
- }
1404
- /**
1405
- * The one-line tooltip: current state first, then what a click does — the same
1406
- * two-part shape Fast Mode uses.
1407
- *
1408
- * A recorded result outranks a remembered failure. `failed` only means "the last
1409
- * run from this control did not complete"; the host can record a result for the
1410
- * same model at any time (a detection started from the settings card, another
1411
- * conversation, or a finished sweep), and the levels the user paid for are the
1412
- * more useful answer than the stale failure. Failure copy is what remains when
1413
- * there is no result to report.
1414
- */
1415
- function tooltipText(t, model, state) {
1416
- if (state.busy) return t("probeRunning", { model });
1417
- const result = state.result;
1418
- if (result !== void 0) {
1419
- if (result.validation === "validating" && result.efforts.length > 0) return t("probeTooltipVerified", { levels: result.efforts.join(" / ") });
1420
- if (result.validation === "non-validating") return t("probeTooltipNotValidating");
1421
- return t("probeTooltipRetry");
1422
- }
1423
- if (state.failed) return t("probeTooltipRetry");
1424
- return t("probeTooltipIdle", { model });
1425
- }
1426
- /** Model-independent shell: resolves the selection, then delegates per model. */
1427
- function QoderProbeControl({ directory, t }) {
1428
- const subscribe = (0, react.useCallback)((listener) => directory.subscribe(listener), [directory]);
1429
- const snapshot = (0, react.useCallback)(() => directory.getSnapshot(), [directory]);
1430
- const selection = (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot).current;
1431
- const card = selection == null ? void 0 : cardVariantFor(selection.provider);
1432
- const key = card === void 0 || selection == null ? void 0 : `${card.id}:${selection.model}`;
1433
- return card === void 0 || selection == null || key === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelProbe, {
1434
- model: selection.model,
1435
- card,
1436
- label: useLabel(t),
1437
- t
1438
- }, key);
1439
- }
1440
- function ModelProbe({ model, card, label, t }) {
1441
- const [status, setStatus] = (0, react.useState)();
1442
- const [busy, setBusy] = (0, react.useState)(false);
1443
- const [confirming, setConfirming] = (0, react.useState)(false);
1444
- const [tooltipVisible, setTooltipVisible] = (0, react.useState)(false);
1445
- const [failed, setFailed] = (0, react.useState)(false);
1446
- const [note, setNote] = (0, react.useState)();
1447
- const inFlight = (0, react.useRef)(false);
1448
- const mounted = (0, react.useRef)(false);
1449
- const readSeq = (0, react.useRef)(0);
1450
- const tooltipId = (0, react.useId)();
1451
- const refresh = (0, react.useCallback)(async (signal) => {
1452
- const seq = ++readSeq.current;
1453
- const response = await fetch(card.statusPath, {
1454
- credentials: "same-origin",
1455
- headers: { accept: "application/json" },
1456
- ...signal === void 0 ? {} : { signal }
1457
- });
1458
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
1459
- const value = await response.json().catch(() => void 0);
1460
- if (!isQoderWebStatus(value)) throw new Error(t("statusResponseInvalid"));
1461
- if (mounted.current && !signal?.aborted && seq === readSeq.current) setStatus(value);
1462
- }, [card.statusPath, t]);
1463
- (0, react.useEffect)(() => {
1464
- mounted.current = true;
1465
- const controller = new AbortController();
1466
- const load = () => {
1467
- refresh(controller.signal).catch(() => {});
1468
- };
1469
- load();
1470
- const timer = window.setInterval(load, RECONCILE_MS);
1471
- window.addEventListener("focus", load);
1472
- return () => {
1473
- mounted.current = false;
1474
- controller.abort();
1475
- window.clearInterval(timer);
1476
- window.removeEventListener("focus", load);
1477
- };
1478
- }, [refresh]);
1479
- const probe = status?.status === "signed-in" ? status.probe : void 0;
1480
- const key = status?.status === "signed-in" ? status.probeKey : void 0;
1481
- const result = status === void 0 ? void 0 : resultFor(status, model);
1482
- const visible = probe?.candidates.includes(model) === true || result !== void 0;
1483
- (0, react.useEffect)(() => {
1484
- if (result !== void 0) setFailed(false);
1485
- }, [result]);
1486
- (0, react.useEffect)(() => {
1487
- setConfirming(false);
1488
- setNote(void 0);
1489
- }, [model]);
1490
- const detect = async () => {
1491
- if (key === void 0 || inFlight.current || probe?.running === true) return;
1492
- inFlight.current = true;
1493
- setNote(void 0);
1494
- setConfirming(false);
1495
- setBusy(true);
1496
- setFailed(false);
1497
- try {
1498
- const response = await fetch(card.probePath, {
1499
- method: "POST",
1500
- credentials: "same-origin",
1501
- headers: {
1502
- "Content-Type": "application/json",
1503
- "X-Qoder-Probe-Key": key
1504
- },
1505
- body: JSON.stringify({
1506
- action: "probe",
1507
- model
1508
- })
1509
- });
1510
- const body = await response.json();
1511
- if (!response.ok || body.state !== "ok" || body.validation !== "validating" && body.validation !== "non-validating" || !Array.isArray(body.efforts) || !body.efforts.every((effort) => typeof effort === "string")) throw new Error("probe failed");
1512
- if (mounted.current) {
1513
- const completed = {
1514
- id: model,
1515
- name: model,
1516
- validation: body.validation,
1517
- efforts: body.efforts,
1518
- probedAt: Date.now()
1519
- };
1520
- setNote(completed);
1521
- }
1522
- refresh().catch(() => {});
1523
- } catch {
1524
- if (mounted.current) setFailed(true);
1525
- } finally {
1526
- inFlight.current = false;
1527
- if (mounted.current) setBusy(false);
1528
- }
1529
- };
1530
- if (!visible) return null;
1531
- const text = tooltipText(t, model, {
1532
- busy,
1533
- result,
1534
- failed
1535
- });
1536
- const disabled = busy || probe?.running === true || key === void 0;
1537
- const showTooltip = tooltipVisible && !confirming && note === void 0;
1538
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1539
- style: wrapperStyle,
1540
- onMouseEnter: () => {
1541
- setTooltipVisible(true);
1542
- },
1543
- onMouseLeave: () => {
1544
- setTooltipVisible(false);
1545
- },
1546
- children: [
1547
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1548
- type: "button",
1549
- "aria-label": text,
1550
- "aria-describedby": showTooltip ? tooltipId : void 0,
1551
- "aria-busy": busy,
1552
- "aria-expanded": confirming,
1553
- disabled,
1554
- onClick: () => {
1555
- setConfirming(true);
1556
- },
1557
- onFocus: () => {
1558
- setTooltipVisible(true);
1559
- },
1560
- onBlur: () => {
1561
- setTooltipVisible(false);
1562
- },
1563
- style: {
1564
- ...buttonStyle,
1565
- opacity: disabled && !confirming ? .6 : 1,
1566
- cursor: disabled ? "default" : "pointer"
1567
- },
1568
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1569
- width: "16",
1570
- height: "16",
1571
- viewBox: "0 0 24 24",
1572
- fill: "none",
1573
- stroke: "currentColor",
1574
- strokeWidth: "1.6",
1575
- "aria-hidden": "true",
1576
- focusable: "false",
1577
- children: [
1578
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1579
- cx: "12",
1580
- cy: "12",
1581
- r: "9"
1582
- }),
1583
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1584
- cx: "12",
1585
- cy: "12",
1586
- r: "4"
1587
- }),
1588
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 12 20 4" }),
1589
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1590
- cx: "12",
1591
- cy: "12",
1592
- r: "1"
1593
- })
1594
- ]
1595
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1596
- style: labelStyle$1,
1597
- children: label
1598
- })]
1599
- }),
1600
- showTooltip && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1601
- id: tooltipId,
1602
- role: "tooltip",
1603
- style: tooltipStyle,
1604
- children: text
1605
- }),
1606
- confirming && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1607
- style: confirmStyle,
1608
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("probeBubbleBody") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1609
- style: confirmRowStyle,
1610
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1611
- type: "button",
1612
- style: confirmButtonStyle,
1613
- onClick: () => {
1614
- setConfirming(false);
1615
- },
1616
- children: t("cancel")
1617
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1618
- type: "button",
1619
- style: primaryButtonStyle,
1620
- onClick: () => {
1621
- detect();
1622
- },
1623
- children: t("probeConfirmAction")
1624
- })]
1625
- })]
1626
- }),
1627
- note === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1628
- role: "status",
1629
- "aria-live": "polite",
1630
- style: noteStyle,
1631
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: noteText(t, note) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1632
- type: "button",
1633
- style: noteDismissStyle,
1634
- onClick: () => {
1635
- setNote(void 0);
1636
- },
1637
- children: t("probeNoteDismiss")
1638
- })]
1639
- })
1640
- ]
1641
- });
1642
- }
1643
- /** Compose the one-line outcome string the note bubble shows. */
1644
- function noteText(t, result) {
1645
- if (result.validation === "validating" && result.efforts.length > 0) return t("probeNoteVerified", { levels: result.efforts.join(" / ") });
1646
- if (result.validation === "non-validating") return t("probeNoteNotValidating");
1647
- return t("probeNoteUnknown");
1648
- }
1649
- //#endregion
1650
- //#region src/client/quota-settings-store.ts
1651
- let pollIntervalMs = 3e5;
1652
- const toggles = {
1653
- cn: false,
1654
- global: false
1776
+ bottom: "calc(100% + 8px)",
1777
+ zIndex: 1e3,
1778
+ transform: "translateX(-50%)",
1779
+ padding: "4px 8px",
1780
+ borderRadius: 6,
1781
+ background: "var(--dsw-specific-tip, #1f2329)",
1782
+ boxShadow: "var(--dsw-shadow-lv2)",
1783
+ color: "var(--dsw-alias-label-primary, #fff)",
1784
+ fontSize: 12,
1785
+ lineHeight: "18px",
1786
+ whiteSpace: "nowrap",
1787
+ pointerEvents: "none"
1655
1788
  };
1656
- const signIn = {
1657
- cn: false,
1658
- global: false
1789
+ /** Confirmation bubble: same anchor, but interactive and allowed to wrap. */
1790
+ const confirmStyle = {
1791
+ position: "absolute",
1792
+ right: 0,
1793
+ bottom: "calc(100% + 8px)",
1794
+ zIndex: 1001,
1795
+ display: "flex",
1796
+ flexDirection: "column",
1797
+ gap: 8,
1798
+ width: 260,
1799
+ padding: "10px 12px",
1800
+ border: "1px solid var(--dsw-alias-border-l2)",
1801
+ borderRadius: 8,
1802
+ background: "var(--dsw-alias-bg-layer-1, #fff)",
1803
+ boxShadow: "var(--dsw-shadow-lv2)",
1804
+ color: "var(--dsw-alias-label-primary)",
1805
+ fontSize: 12,
1806
+ lineHeight: "18px"
1807
+ };
1808
+ const confirmRowStyle = {
1809
+ display: "flex",
1810
+ justifyContent: "flex-end",
1811
+ gap: 8
1812
+ };
1813
+ const confirmButtonStyle = {
1814
+ padding: "3px 10px",
1815
+ border: "1px solid var(--dsw-alias-border-l2)",
1816
+ borderRadius: 6,
1817
+ background: "transparent",
1818
+ color: "inherit",
1819
+ font: "inherit",
1820
+ fontSize: 12,
1821
+ cursor: "pointer"
1659
1822
  };
1660
- let revision = 0;
1661
- const listeners = /* @__PURE__ */ new Set();
1662
- function bump() {
1663
- revision += 1;
1664
- for (const listener of listeners) listener();
1665
- }
1666
- /** Update the shared poll interval (from the settings document). */
1667
- function setQuotaPollMs(ms) {
1668
- if (Number.isFinite(ms) && ms >= 6e4 && pollIntervalMs !== ms) {
1669
- pollIntervalMs = ms;
1670
- bump();
1671
- }
1672
- }
1673
- /** Read the configured poll interval. */
1674
- function quotaPollMs() {
1675
- return pollIntervalMs;
1676
- }
1677
- /** Update both sidebar toggles (from the settings document). */
1678
- function setQuotaToggles(cn, global) {
1679
- if (toggles.cn !== cn || toggles.global !== global) {
1680
- toggles.cn = cn;
1681
- toggles.global = global;
1682
- bump();
1683
- }
1684
- }
1685
- /** Read the current toggles. */
1686
- function quotaToggles() {
1687
- return { ...toggles };
1688
- }
1689
- /** Record a variant's sign-in state from any successful status poll. */
1690
- function noteQuotaSignIn(variantId, signedIn) {
1691
- if (variantId === "qoder" && signIn.cn !== signedIn) {
1692
- signIn.cn = signedIn;
1693
- bump();
1694
- } else if (variantId === "qoder-global" && signIn.global !== signedIn) {
1695
- signIn.global = signedIn;
1696
- bump();
1697
- }
1698
- }
1699
- /** Read the cached sign-in state. */
1700
- function quotaSignInState() {
1701
- return { ...signIn };
1702
- }
1703
- /** Subscribe to any flag change; returns the disposer. */
1704
- function onQuotaSettingsChange(listener) {
1705
- listeners.add(listener);
1706
- return () => {
1707
- listeners.delete(listener);
1708
- };
1709
- }
1710
- /** The current revision — the useSyncExternalStore snapshot value. */
1711
- function quotaSettingsRevision() {
1712
- return revision;
1713
- }
1714
- /** Which variant a status route belongs to, from the route path. */
1715
- function variantOfStatusPath(statusPath) {
1716
- return statusPath.includes("/global/") ? "qoder-global" : "qoder";
1717
- }
1718
1823
  /**
1719
- * The last status document per variant, shared by EVERY quota surface.
1720
- *
1721
- * The sidebar cards and the dashboard each used to fetch independently, so a
1722
- * dashboard refresh updated the panel while the sidebar card kept showing its
1723
- * previous read until its own next tick — two different numbers for one
1724
- * account on one screen. One store, one write path
1725
- * ({@link noteQuotaStatus}), and every subscriber re-renders through the
1726
- * same revision: whatever surface refreshed last, all of them show it.
1824
+ * Primary action inside the confirmation bubble.
1727
1825
  *
1728
- * Documents are kept by identity (never mutated), so reference comparisons
1729
- * in useSyncExternalStore selectors stay cheap and stable.
1826
+ * The fill and its text colour must come as a pair: `brand-primary` resolves to
1827
+ * a light accent in this theme, so hardcoding `color: #fff` on top of it renders
1828
+ * white-on-white. `button-primary-fill` + `label-primary-foreground` is the
1829
+ * theme's own pair for exactly this, and is what `dsh-codex-connect` uses for
1830
+ * the same job.
1730
1831
  */
1731
- const statusDocuments = {
1732
- cn: void 0,
1733
- global: void 0
1832
+ const primaryButtonStyle = {
1833
+ ...confirmButtonStyle,
1834
+ border: "1px solid var(--dsw-alias-button-primary-fill)",
1835
+ background: "var(--dsw-alias-button-primary-fill)",
1836
+ color: "var(--dsw-alias-label-primary-foreground)"
1734
1837
  };
1735
1838
  /**
1736
- * Publish one variant's freshly fetched status document. Downstream
1737
- * subscribers (both sidebar cards and the dashboard, through whichever
1738
- * observable wraps this store) re-render on the revision bump.
1839
+ * Result note: a single line + a dismiss button, anchored to the control's
1840
+ * right side. Smaller than the confirmation bubble because it carries an
1841
+ * *outcome*, not a *decision* — the work is done, the user only has to read
1842
+ * and dismiss.
1739
1843
  */
1740
- function noteQuotaStatus(variantId, status) {
1741
- if (variantId === "qoder" && statusDocuments.cn !== status) {
1742
- statusDocuments.cn = status;
1743
- statusFetchedAt.cn = Date.now();
1744
- bump();
1745
- } else if (variantId === "qoder-global" && statusDocuments.global !== status) {
1746
- statusDocuments.global = status;
1747
- statusFetchedAt.global = Date.now();
1748
- bump();
1749
- }
1750
- noteQuotaSignIn(variantId, status.status === "signed-in");
1751
- }
1752
- /** Read one variant's latest status document. */
1753
- function quotaStatus(variantId) {
1754
- return variantId === "qoder" ? statusDocuments.cn : statusDocuments.global;
1755
- }
1756
- /** When each variant's document was last fetched (per publish, not per read). */
1757
- const statusFetchedAt = {
1758
- cn: void 0,
1759
- global: void 0
1844
+ const noteStyle = {
1845
+ position: "absolute",
1846
+ right: 0,
1847
+ bottom: "calc(100% + 8px)",
1848
+ zIndex: 1001,
1849
+ display: "flex",
1850
+ alignItems: "center",
1851
+ gap: 12,
1852
+ padding: "6px 10px",
1853
+ border: "1px solid var(--dsw-alias-border-l2)",
1854
+ borderRadius: 8,
1855
+ background: "var(--dsw-alias-bg-layer-1)",
1856
+ boxShadow: "var(--dsw-shadow-lv2)",
1857
+ color: "var(--dsw-alias-label-primary)",
1858
+ fontSize: 12,
1859
+ lineHeight: "18px",
1860
+ whiteSpace: "nowrap"
1760
1861
  };
1761
- /** Read the time a variant's current document was fetched, if any. */
1762
- function quotaStatusFetchedAt(variantId) {
1763
- return variantId === "qoder" ? statusFetchedAt.cn : statusFetchedAt.global;
1764
- }
1765
1862
  /**
1766
- * Whether ONE variant's shared document is fresh enough to skip a fetch:
1767
- * the user's rule — a click/mount/interval tick within the configured
1768
- * interval of the last successful read reuses the cached document, and only
1769
- * a variant with NO result yet (or a failed read that never landed one)
1770
- * forces the upstream call. The interval is a cache lifetime, not a metronome.
1771
- *
1772
- * A `maxAgeMs` of the poll interval comes from the settings document; a
1773
- * failed last read is NOT tracked here (callers gate failures themselves),
1774
- * because "the last read failed" still means "no usable result".
1775
- *
1776
- * @param variantId - which variant's freshness to test.
1777
- * @param maxAgeMs - the configured poll interval (cache lifetime).
1863
+ * The note's dismiss action. Outlined rather than bare text: inside an already
1864
+ * bordered bubble, an unbordered word does not read as something you can click.
1865
+ * Matches the outlined pill convention the plugin's other secondary actions use.
1778
1866
  */
1779
- function quotaStatusIsFresh(variantId, maxAgeMs) {
1780
- const fetchedAt = variantId === "qoder" ? statusFetchedAt.cn : statusFetchedAt.global;
1781
- const document = variantId === "qoder" ? statusDocuments.cn : statusDocuments.global;
1782
- if (fetchedAt === void 0 || document === void 0) return false;
1783
- return Date.now() - fetchedAt < maxAgeMs;
1784
- }
1785
- //#endregion
1786
- //#region src/client/QuotaSettingsCard.tsx
1867
+ const noteDismissStyle = {
1868
+ padding: "2px 8px",
1869
+ border: "1px solid var(--dsw-alias-border-l2)",
1870
+ borderRadius: 6,
1871
+ background: "transparent",
1872
+ color: "var(--dsw-alias-label-secondary)",
1873
+ font: "inherit",
1874
+ fontSize: 12,
1875
+ lineHeight: "18px",
1876
+ cursor: "pointer"
1877
+ };
1787
1878
  /**
1788
- * The shared quota-settings card: one card above the two variant cards that
1789
- * configures both sidebar quota widgets.
1790
- *
1791
- * Like the built-in plugin cards, it registers into `settings.plugin.item`
1792
- * keyed by its own settings namespace (`qoder-quota`), binds that
1793
- * namespace through the client settings scope, and writes through the scope's
1794
- * revision-fenced `set` — the same durable-write path every preference row
1795
- * uses. A toggle commits on click: each click is one explicit user choice,
1796
- * and the scope's ordering makes the last one win, so no staged-draft form is
1797
- * needed for two booleans and a number.
1798
- *
1799
- * The two toggles gate the CN and international sidebar cards respectively;
1800
- * the interval is one shared poll period. Toggles are disabled while their
1801
- * variant is signed out: a quota card for an account nobody is signed into
1802
- * would render an error forever, so the setting waits for a session.
1879
+ * The feature's static inline label. Deliberately not a state readout — see the
1880
+ * module comment.
1803
1881
  */
1804
- /** The default poll interval shown before a value is stored. */
1805
- const POLL_DEFAULT_MS = 3e5;
1806
- /** Floor the schema also enforces; mirrored here for immediate UI feedback. */
1807
- const POLL_MIN_MS = 6e4;
1808
- /** Read the section values out of a scope snapshot (defaults when absent). */
1809
- function project(scope) {
1810
- if (scope === void 0) return {
1811
- status: "unavailable",
1812
- writable: false,
1813
- values: {
1814
- sidebarQuotaCN: false,
1815
- sidebarQuotaGlobal: false,
1816
- quotaPollMs: POLL_DEFAULT_MS
1817
- }
1818
- };
1819
- const snapshot = scope.getSnapshot();
1820
- const value = snapshot.value ?? {};
1821
- return {
1822
- status: snapshot.status,
1823
- writable: snapshot.writable,
1824
- values: {
1825
- sidebarQuotaCN: value.sidebarQuotaCN === true,
1826
- sidebarQuotaGlobal: value.sidebarQuotaGlobal === true,
1827
- quotaPollMs: typeof value.quotaPollMs === "number" ? value.quotaPollMs : POLL_DEFAULT_MS
1828
- }
1829
- };
1882
+ function useLabel(t) {
1883
+ return t("probeLabel");
1884
+ }
1885
+ /** Pick the model's recorded observation out of the probe section. */
1886
+ function resultFor(status, model) {
1887
+ if (status.status !== "signed-in") return void 0;
1888
+ return status.probe?.results.find((result) => result.id === model);
1830
1889
  }
1831
1890
  /**
1832
- * Stable-reference projection cache.
1891
+ * The one-line tooltip: current state first, then what a click does — the same
1892
+ * two-part shape Fast Mode uses.
1833
1893
  *
1834
- * React's useSyncExternalStore requires getSnapshot() to return THE SAME
1835
- * reference between renders unless the store actually changed. project()
1836
- * builds a fresh object every call, which re-renders forever and crashes the
1837
- * card with React error #185 ("maximum update depth exceeded") — exactly the
1838
- * crash the slot ledger reported. The cache below returns the last built
1839
- * projection until the underlying scope snapshot (or scope identity) changes,
1840
- * which is the only thing the projection actually derives from.
1894
+ * A recorded result outranks a remembered failure. `failed` only means "the last
1895
+ * run from this control did not complete"; the host can record a result for the
1896
+ * same model at any time (a detection started from the settings card, another
1897
+ * conversation, or a finished sweep), and the levels the user paid for are the
1898
+ * more useful answer than the stale failure. Failure copy is what remains when
1899
+ * there is no result to report.
1841
1900
  */
1842
- let cachedScope;
1843
- let cachedSource;
1844
- let cachedProjection;
1845
- const UNAVAILABLE = {
1846
- status: "unavailable",
1847
- writable: false,
1848
- values: {
1849
- sidebarQuotaCN: false,
1850
- sidebarQuotaGlobal: false,
1851
- quotaPollMs: POLL_DEFAULT_MS
1852
- }
1853
- };
1854
- function stableProject(scope) {
1855
- if (scope === void 0) return UNAVAILABLE;
1856
- const source = scope.getSnapshot();
1857
- if (scope !== cachedScope || source !== cachedSource || cachedProjection === void 0) {
1858
- cachedScope = scope;
1859
- cachedSource = source;
1860
- cachedProjection = project(scope);
1901
+ function tooltipText(t, model, state) {
1902
+ if (state.busy) return t("probeRunning", { model });
1903
+ const result = state.result;
1904
+ if (result !== void 0) {
1905
+ if (result.validation === "validating" && result.efforts.length > 0) return t("probeTooltipVerified", { levels: result.efforts.join(" / ") });
1906
+ if (result.validation === "non-validating") return t("probeTooltipNotValidating");
1907
+ return t("probeTooltipRetry");
1861
1908
  }
1862
- return cachedProjection;
1909
+ if (state.failed) return t("probeTooltipRetry");
1910
+ return t("probeTooltipIdle", { model });
1863
1911
  }
1864
- /** One toggle row: label, hint, and a switch drawn to the shell's proportions. */
1865
- function ToggleRow({ label, hint, checked, disabled, disabledHint, onToggle }) {
1866
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1867
- style: rowStyle,
1868
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1869
- style: rowTextStyle,
1870
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1871
- style: labelStyle,
1872
- children: label
1873
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1874
- style: hintStyle,
1875
- children: disabled === true && disabledHint !== void 0 ? disabledHint : hint
1876
- })]
1877
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1878
- type: "button",
1879
- role: "switch",
1880
- "aria-checked": checked,
1881
- disabled,
1882
- "aria-label": label,
1883
- onClick: () => onToggle(!checked),
1884
- style: {
1885
- ...switchStyle,
1886
- background: checked ? "var(--dsw-alias-brand-primary)" : "var(--dsw-alias-bg-layer-3, rgba(127,127,127,0.2))",
1887
- justifyContent: checked ? "flex-end" : "flex-start",
1888
- opacity: disabled === true ? .45 : 1,
1889
- cursor: disabled === true ? "not-allowed" : "pointer"
1890
- },
1891
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: knobStyle })
1892
- })]
1893
- });
1912
+ /** Model-independent shell: resolves the selection, then delegates per model. */
1913
+ function QoderProbeControl({ directory, t }) {
1914
+ const subscribe = (0, react.useCallback)((listener) => directory.subscribe(listener), [directory]);
1915
+ const snapshot = (0, react.useCallback)(() => directory.getSnapshot(), [directory]);
1916
+ const selection = (0, react.useSyncExternalStore)(subscribe, snapshot, snapshot).current;
1917
+ const card = selection == null ? void 0 : cardVariantFor(selection.provider);
1918
+ const key = card === void 0 || selection == null ? void 0 : `${card.id}:${selection.model}`;
1919
+ return card === void 0 || selection == null || key === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModelProbe, {
1920
+ model: selection.model,
1921
+ card,
1922
+ label: useLabel(t),
1923
+ t
1924
+ }, key);
1894
1925
  }
1895
- /** The shared quota-settings card. */
1896
- function QuotaSettingsCard(props) {
1897
- const { t = (key) => key, scope, signedIn } = props;
1898
- const projection = (0, react.useSyncExternalStore)(scope?.subscribe.bind(scope) ?? ((listener) => listener), () => stableProject(scope));
1899
- const [open, setOpen] = (0, react.useState)(false);
1900
- const [hovered, setHovered] = (0, react.useState)(false);
1901
- const [headerFocused, setHeaderFocused] = (0, react.useState)(false);
1902
- const [probe, setProbe] = (0, react.useState)();
1926
+ function ModelProbe({ model, card, label, t }) {
1927
+ const [status, setStatus] = (0, react.useState)();
1928
+ const [busy, setBusy] = (0, react.useState)(false);
1929
+ const [confirming, setConfirming] = (0, react.useState)(false);
1930
+ const [tooltipVisible, setTooltipVisible] = (0, react.useState)(false);
1931
+ const [failed, setFailed] = (0, react.useState)(false);
1932
+ const [note, setNote] = (0, react.useState)();
1933
+ const inFlight = (0, react.useRef)(false);
1934
+ const mounted = (0, react.useRef)(false);
1935
+ const readSeq = (0, react.useRef)(0);
1936
+ const tooltipId = (0, react.useId)();
1937
+ const refresh = (0, react.useCallback)(async (signal) => {
1938
+ const seq = ++readSeq.current;
1939
+ const response = await fetch(card.statusPath, {
1940
+ credentials: "same-origin",
1941
+ headers: { accept: "application/json" },
1942
+ ...signal === void 0 ? {} : { signal }
1943
+ });
1944
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
1945
+ const value = await response.json().catch(() => void 0);
1946
+ if (!isQoderWebStatus(value)) throw new Error(t("statusResponseInvalid"));
1947
+ if (mounted.current && !signal?.aborted && seq === readSeq.current) setStatus(value);
1948
+ }, [card.statusPath, t]);
1903
1949
  (0, react.useEffect)(() => {
1904
- let disposed = false;
1905
- const probeOne = async (path) => {
1906
- try {
1907
- const body = await (await fetch(path, { headers: { accept: "application/json" } })).json();
1908
- if (disposed || !isQoderWebStatus(body)) return void 0;
1909
- noteQuotaSignIn(variantOfStatusPath(path), body.status === "signed-in");
1910
- return body.status === "signed-in";
1911
- } catch {
1912
- return;
1913
- }
1950
+ mounted.current = true;
1951
+ const controller = new AbortController();
1952
+ const load = () => {
1953
+ refresh(controller.signal).catch(() => {});
1914
1954
  };
1915
- (async () => {
1916
- const [cn, global] = await Promise.all([probeOne(QODER_STATUS_PATH), probeOne(QODER_GLOBAL_STATUS_PATH)]);
1917
- if (!disposed) setProbe({
1918
- cn: cn === true,
1919
- global: global === true
1920
- });
1921
- })();
1955
+ load();
1956
+ const timer = window.setInterval(load, RECONCILE_MS);
1957
+ window.addEventListener("focus", load);
1922
1958
  return () => {
1923
- disposed = true;
1959
+ mounted.current = false;
1960
+ controller.abort();
1961
+ window.clearInterval(timer);
1962
+ window.removeEventListener("focus", load);
1924
1963
  };
1925
- }, [open]);
1926
- if (projection.status === "unavailable") return null;
1927
- const reported = signedIn?.() ?? {
1928
- cn: false,
1929
- global: false
1930
- };
1931
- const signed = {
1932
- cn: probe?.cn ?? reported.cn,
1933
- global: probe?.global ?? reported.global
1934
- };
1935
- const write = (field, value) => {
1936
- scope?.set(field, value);
1964
+ }, [refresh]);
1965
+ const probe = status?.status === "signed-in" ? status.probe : void 0;
1966
+ const key = status?.status === "signed-in" ? status.probeKey : void 0;
1967
+ const result = status === void 0 ? void 0 : resultFor(status, model);
1968
+ const visible = probe?.candidates.includes(model) === true || result !== void 0;
1969
+ (0, react.useEffect)(() => {
1970
+ if (result !== void 0) setFailed(false);
1971
+ }, [result]);
1972
+ (0, react.useEffect)(() => {
1973
+ setConfirming(false);
1974
+ setNote(void 0);
1975
+ }, [model]);
1976
+ const detect = async () => {
1977
+ if (key === void 0 || inFlight.current || probe?.running === true) return;
1978
+ inFlight.current = true;
1979
+ setNote(void 0);
1980
+ setConfirming(false);
1981
+ setBusy(true);
1982
+ setFailed(false);
1983
+ try {
1984
+ const response = await fetch(card.probePath, {
1985
+ method: "POST",
1986
+ credentials: "same-origin",
1987
+ headers: {
1988
+ "Content-Type": "application/json",
1989
+ "X-Qoder-Probe-Key": key
1990
+ },
1991
+ body: JSON.stringify({
1992
+ action: "probe",
1993
+ model
1994
+ })
1995
+ });
1996
+ const body = await response.json();
1997
+ if (!response.ok || body.state !== "ok" || body.validation !== "validating" && body.validation !== "non-validating" || !Array.isArray(body.efforts) || !body.efforts.every((effort) => typeof effort === "string")) throw new Error("probe failed");
1998
+ if (mounted.current) {
1999
+ const completed = {
2000
+ id: model,
2001
+ name: model,
2002
+ validation: body.validation,
2003
+ efforts: body.efforts,
2004
+ probedAt: Date.now()
2005
+ };
2006
+ setNote(completed);
2007
+ }
2008
+ refresh().catch(() => {});
2009
+ } catch {
2010
+ if (mounted.current) setFailed(true);
2011
+ } finally {
2012
+ inFlight.current = false;
2013
+ if (mounted.current) setBusy(false);
2014
+ }
1937
2015
  };
1938
- const minutes = Math.max(POLL_MIN_MS / 6e4, Math.round(projection.values.quotaPollMs / 6e4));
1939
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
1940
- style: {
1941
- ...cardStyle,
1942
- ...hovered ? cardHoverStyle : {},
1943
- ...open ? cardOpenStyle : {}
1944
- },
2016
+ if (!visible) return null;
2017
+ const text = tooltipText(t, model, {
2018
+ busy,
2019
+ result,
2020
+ failed
2021
+ });
2022
+ const disabled = busy || probe?.running === true || key === void 0;
2023
+ const showTooltip = tooltipVisible && !confirming && note === void 0;
2024
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2025
+ style: wrapperStyle,
1945
2026
  onMouseEnter: () => {
1946
- setHovered(true);
2027
+ setTooltipVisible(true);
1947
2028
  },
1948
2029
  onMouseLeave: () => {
1949
- setHovered(false);
2030
+ setTooltipVisible(false);
1950
2031
  },
1951
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1952
- type: "button",
1953
- "aria-expanded": open,
1954
- "aria-label": `${t(open ? "collapse" : "expand")}: ${t("quotaSettingsTitle")}`,
1955
- onClick: () => setOpen((o) => !o),
1956
- onFocus: (event) => {
1957
- let keyboard = true;
1958
- try {
1959
- keyboard = event.currentTarget.matches(":focus-visible");
1960
- } catch {
1961
- keyboard = true;
1962
- }
1963
- if (keyboard) setHeaderFocused(true);
1964
- },
1965
- onBlur: () => {
1966
- setHeaderFocused(false);
1967
- },
1968
- style: {
1969
- ...headerButtonStyle,
1970
- ...headerFocused ? headerFocusStyle : {}
1971
- },
1972
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1973
- style: headTextStyle,
1974
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1975
- style: titleStyle,
1976
- children: t("quotaSettingsTitle")
1977
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1978
- style: introStyle,
1979
- children: t("quotaSettingsIntro")
1980
- })]
1981
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2032
+ children: [
2033
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2034
+ type: "button",
2035
+ "aria-label": text,
2036
+ "aria-describedby": showTooltip ? tooltipId : void 0,
2037
+ "aria-busy": busy,
2038
+ "aria-expanded": confirming,
2039
+ disabled,
2040
+ onClick: () => {
2041
+ setConfirming(true);
2042
+ },
2043
+ onFocus: () => {
2044
+ setTooltipVisible(true);
2045
+ },
2046
+ onBlur: () => {
2047
+ setTooltipVisible(false);
2048
+ },
1982
2049
  style: {
1983
- ...chevronStyle,
1984
- transform: open ? "rotate(180deg)" : "none"
2050
+ ...buttonStyle,
2051
+ opacity: disabled && !confirming ? .6 : 1,
2052
+ cursor: disabled ? "default" : "pointer"
1985
2053
  },
1986
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronDownIcon, {})
1987
- })]
1988
- }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1989
- style: cardBodyStyle,
1990
- children: [
1991
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
1992
- label: t("quotaToggleCN"),
1993
- hint: t("quotaToggleHint"),
1994
- checked: projection.values.sidebarQuotaCN,
1995
- disabled: !signed.cn,
1996
- disabledHint: t("quotaSignInRequired"),
1997
- onToggle: (next) => write("sidebarQuotaCN", next)
1998
- }),
1999
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
2000
- label: t("quotaToggleGlobal"),
2001
- hint: t("quotaToggleHint"),
2002
- checked: projection.values.sidebarQuotaGlobal,
2003
- disabled: !signed.global,
2004
- disabledHint: t("quotaSignInRequired"),
2005
- onToggle: (next) => write("sidebarQuotaGlobal", next)
2006
- }),
2007
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2008
- style: {
2009
- ...rowStyle,
2010
- borderBottom: "none",
2011
- paddingBottom: 0
2012
- },
2013
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2014
- style: rowTextStyle,
2015
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2016
- style: labelStyle,
2017
- children: t("quotaPollLabel")
2018
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2019
- style: hintStyle,
2020
- children: t("quotaPollHint")
2021
- })]
2022
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2023
- style: pollFieldStyle,
2024
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2025
- type: "number",
2026
- min: POLL_MIN_MS / 6e4,
2027
- step: 1,
2028
- value: minutes,
2029
- "aria-label": t("quotaPollLabel"),
2030
- onChange: (event) => {
2031
- const mins = Number.parseInt(event.target.value, 10);
2032
- if (Number.isFinite(mins) && mins > 0) write("quotaPollMs", Math.max(POLL_MIN_MS, mins * 6e4));
2033
- },
2034
- style: inputStyle
2035
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2036
- style: hintStyle,
2037
- children: t("quotaPollUnit")
2038
- })]
2054
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
2055
+ width: "16",
2056
+ height: "16",
2057
+ viewBox: "0 0 24 24",
2058
+ fill: "none",
2059
+ stroke: "currentColor",
2060
+ strokeWidth: "1.6",
2061
+ "aria-hidden": "true",
2062
+ focusable: "false",
2063
+ children: [
2064
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
2065
+ cx: "12",
2066
+ cy: "12",
2067
+ r: "9"
2068
+ }),
2069
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
2070
+ cx: "12",
2071
+ cy: "12",
2072
+ r: "4"
2073
+ }),
2074
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M12 12 20 4" }),
2075
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
2076
+ cx: "12",
2077
+ cy: "12",
2078
+ r: "1"
2079
+ })
2080
+ ]
2081
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2082
+ style: labelStyle,
2083
+ children: label
2084
+ })]
2085
+ }),
2086
+ showTooltip && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2087
+ id: tooltipId,
2088
+ role: "tooltip",
2089
+ style: tooltipStyle,
2090
+ children: text
2091
+ }),
2092
+ confirming && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2093
+ style: confirmStyle,
2094
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("probeBubbleBody") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2095
+ style: confirmRowStyle,
2096
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2097
+ type: "button",
2098
+ style: confirmButtonStyle,
2099
+ onClick: () => {
2100
+ setConfirming(false);
2101
+ },
2102
+ children: t("cancel")
2103
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2104
+ type: "button",
2105
+ style: primaryButtonStyle,
2106
+ onClick: () => {
2107
+ detect();
2108
+ },
2109
+ children: t("probeConfirmAction")
2039
2110
  })]
2040
- }),
2041
- projection.writable === false ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2042
- style: hintStyle,
2043
- children: t("quotaSettingsSaveFailed")
2044
- }) : null
2045
- ]
2046
- }) : null]
2111
+ })]
2112
+ }),
2113
+ note === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2114
+ role: "status",
2115
+ "aria-live": "polite",
2116
+ style: noteStyle,
2117
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: noteText(t, note) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2118
+ type: "button",
2119
+ style: noteDismissStyle,
2120
+ onClick: () => {
2121
+ setNote(void 0);
2122
+ },
2123
+ children: t("probeNoteDismiss")
2124
+ })]
2125
+ })
2126
+ ]
2047
2127
  });
2048
2128
  }
2049
- /** The disclosure chevron, drawn from the same path data as the variant cards'. */
2050
- function ChevronDownIcon() {
2051
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
2052
- width: 14,
2053
- height: 14,
2054
- viewBox: "0 0 14 14",
2055
- fill: "none",
2056
- xmlns: "http://www.w3.org/2000/svg",
2057
- "aria-hidden": "true",
2058
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
2059
- d: "M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z",
2060
- fill: "currentColor"
2061
- })
2062
- });
2129
+ /** Compose the one-line outcome string the note bubble shows. */
2130
+ function noteText(t, result) {
2131
+ if (result.validation === "validating" && result.efforts.length > 0) return t("probeNoteVerified", { levels: result.efforts.join(" / ") });
2132
+ if (result.validation === "non-validating") return t("probeNoteNotValidating");
2133
+ return t("probeNoteUnknown");
2063
2134
  }
2064
- const cardStyle = {
2065
- listStyle: "none",
2066
- borderWidth: "0.5px",
2067
- borderStyle: "solid",
2068
- borderColor: "var(--dsw-alias-border-l4)",
2069
- borderRadius: 16,
2070
- background: "var(--dsw-alias-bg-layer-3)",
2071
- transition: "border-color .16s, background .16s"
2072
- };
2073
- /** Hover, matching the built-in card's `:hover` (inline styles cannot express pseudo-classes). */
2074
- const cardHoverStyle = { borderColor: "var(--dsw-alias-label-dimmed)" };
2075
- /** Expanded, matching the built-in card's open state. */
2076
- const cardOpenStyle = {
2077
- background: "var(--dsw-alias-bg-layer-2)",
2078
- borderColor: "var(--dsw-alias-label-dimmed)"
2079
- };
2080
- const headerButtonStyle = {
2081
- boxSizing: "border-box",
2082
- width: "100%",
2083
- display: "flex",
2084
- alignItems: "center",
2085
- gap: 12,
2086
- border: 0,
2087
- borderRadius: 12,
2088
- padding: "14px 16px",
2089
- background: "transparent",
2090
- color: "inherit",
2091
- font: "inherit",
2092
- textAlign: "left",
2093
- cursor: "pointer",
2094
- appearance: "none"
2095
- };
2096
- const headerFocusStyle = {
2097
- outline: "2px solid var(--dsw-alias-brand-primary)",
2098
- outlineOffset: -2
2099
- };
2100
- const headTextStyle = {
2101
- display: "flex",
2102
- flex: 1,
2103
- minWidth: 0,
2104
- flexDirection: "column",
2105
- gap: 4
2106
- };
2107
- const titleStyle = {
2108
- fontSize: 15,
2109
- lineHeight: 1.4,
2110
- fontWeight: 600,
2111
- color: "var(--dsw-alias-label-primary)"
2112
- };
2113
- const introStyle = {
2114
- fontSize: 13,
2115
- lineHeight: 1.5,
2116
- color: "var(--dsw-alias-label-tertiary)"
2117
- };
2118
- /** The variant cards' chevron rule. */
2119
- const chevronStyle = {
2120
- flex: "none",
2121
- display: "flex",
2122
- color: "var(--dsw-alias-label-tertiary)",
2123
- transition: "transform .16s"
2124
- };
2125
- /** The variant cards' body: hairline top border, inset margins, no extra box. */
2126
- const cardBodyStyle = {
2127
- borderTop: ".5px solid var(--dsw-alias-border-l2)",
2128
- margin: "0 16px",
2129
- padding: "12px 0 8px"
2130
- };
2131
- /**
2132
- * One settings row: NO box of its own (the bordered rows read as nested
2133
- * cards, which the user ruled against) — rows are separated by a hairline
2134
- * bottom rule like the settings shell's own preference lists.
2135
- */
2136
- const rowStyle = {
2137
- display: "flex",
2138
- alignItems: "center",
2139
- gap: 12,
2140
- borderBottom: ".5px solid var(--dsw-alias-border-l2)",
2141
- paddingBottom: 10
2142
- };
2143
- const rowTextStyle = {
2144
- display: "flex",
2145
- flex: 1,
2146
- minWidth: 0,
2147
- flexDirection: "column",
2148
- gap: 2
2149
- };
2150
- const labelStyle = {
2151
- fontSize: 13,
2152
- fontWeight: 500,
2153
- lineHeight: 1.5,
2154
- color: "var(--dsw-alias-label-primary)"
2155
- };
2156
- const hintStyle = {
2157
- fontSize: 12,
2158
- lineHeight: 1.5,
2159
- color: "var(--dsw-alias-label-tertiary)"
2160
- };
2161
- const switchStyle = {
2162
- flex: "none",
2163
- display: "flex",
2164
- width: 36,
2165
- height: 20,
2166
- borderRadius: 10,
2167
- borderWidth: "1px",
2168
- borderStyle: "solid",
2169
- borderColor: "var(--dsw-alias-border-l2)",
2170
- padding: 1,
2171
- cursor: "pointer",
2172
- alignItems: "center",
2173
- transition: "background .16s"
2174
- };
2175
- const knobStyle = {
2176
- display: "block",
2177
- width: 16,
2178
- height: 16,
2179
- borderRadius: "50%",
2180
- background: "var(--dsw-alias-bg-layer-1, #fff)",
2181
- boxShadow: "0 1px 2px rgba(0,0,0,0.2)"
2182
- };
2183
- const pollFieldStyle = {
2184
- display: "flex",
2185
- alignItems: "center",
2186
- gap: 6,
2187
- flex: "none"
2188
- };
2189
- const inputStyle = {
2190
- boxSizing: "border-box",
2191
- width: 55,
2192
- padding: "5px 8px",
2193
- borderWidth: "1px",
2194
- borderStyle: "solid",
2195
- borderColor: "var(--dsw-alias-border-l2)",
2196
- borderRadius: 8,
2197
- background: "var(--dsw-alias-bg-layer-2)",
2198
- color: "var(--dsw-alias-label-primary)",
2199
- font: "inherit",
2200
- fontSize: 13,
2201
- textAlign: "right"
2202
- };
2203
2135
  //#endregion
2204
2136
  //#region src/client/quota-merge.ts
2205
2137
  /**
@@ -2441,6 +2373,7 @@ window.__ModuleLoader__.load({
2441
2373
  (0, react.useSyncExternalStore)(onQuotaSettingsChange, quotaSettingsRevision);
2442
2374
  const enabled = variantId === "qoder" ? quotaToggles().cn : quotaToggles().global;
2443
2375
  const status = quotaStatus(variantId);
2376
+ const signedIn = status?.status === "signed-in";
2444
2377
  (0, react.useEffect)(() => {
2445
2378
  if (statusPath === void 0 || !enabled) return void 0;
2446
2379
  let disposed = false;
@@ -2464,21 +2397,22 @@ window.__ModuleLoader__.load({
2464
2397
  if (!disposed) setFailed(true);
2465
2398
  }
2466
2399
  };
2400
+ const isHidden = () => typeof document !== "undefined" && document.hidden;
2467
2401
  const loop = () => {
2468
- if (document.hidden) return;
2402
+ if (isHidden()) return;
2469
2403
  refresh();
2470
2404
  };
2471
2405
  timer = window.setInterval(loop, Math.max(6e4, quotaPollMs()));
2472
2406
  refresh();
2473
2407
  const onVisible = () => {
2474
- if (!document.hidden) loop();
2408
+ if (!isHidden()) loop();
2475
2409
  };
2476
- document.addEventListener("visibilitychange", onVisible);
2410
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisible);
2477
2411
  return () => {
2478
2412
  disposed = true;
2479
2413
  controller.abort();
2480
2414
  if (timer !== void 0) window.clearInterval(timer);
2481
- document.removeEventListener("visibilitychange", onVisible);
2415
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisible);
2482
2416
  };
2483
2417
  }, [
2484
2418
  statusPath,
@@ -2507,7 +2441,11 @@ window.__ModuleLoader__.load({
2507
2441
  className: "qdp-railButton",
2508
2442
  "aria-label": title,
2509
2443
  title,
2510
- onClick: () => open?.(),
2444
+ disabled: !signedIn,
2445
+ onClick: () => {
2446
+ if (!signedIn) return;
2447
+ open?.();
2448
+ },
2511
2449
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Ring, {
2512
2450
  percent: ringPercent,
2513
2451
  warn: ringWarn,
@@ -2519,7 +2457,11 @@ window.__ModuleLoader__.load({
2519
2457
  className: "qdp-foot",
2520
2458
  "aria-label": title,
2521
2459
  title,
2522
- onClick: () => open?.(),
2460
+ disabled: !signedIn,
2461
+ onClick: () => {
2462
+ if (!signedIn) return;
2463
+ open?.();
2464
+ },
2523
2465
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2524
2466
  className: "qdp-footTop",
2525
2467
  children: [
@@ -2983,6 +2925,10 @@ window.__ModuleLoader__.load({
2983
2925
  intro: "Use the models from your Qoder (China) account directly in DSH — paste a Personal Access Token, no browser sign-in flow.",
2984
2926
  titleAI: "Qoder Global",
2985
2927
  introAI: "Use the models from your Qoder Global account directly in DSH — paste a Personal Access Token, no browser sign-in flow.",
2928
+ unifiedTitle: "Qoder",
2929
+ unifiedIntro: "Manage Qoder (China) and Qoder Global models, credentials, and sidebar quota displays.",
2930
+ variantTabCN: "China",
2931
+ variantTabGlobal: "Global",
2986
2932
  expand: "Expand",
2987
2933
  collapse: "Collapse",
2988
2934
  loading: "Loading account…",
@@ -3113,6 +3059,10 @@ window.__ModuleLoader__.load({
3113
3059
  intro: "保存个人访问令牌(PAT)后,即可在 DSH 中直接使用 Qoder 国内版的模型。",
3114
3060
  titleAI: "Qoder Global(国际版)",
3115
3061
  introAI: "保存个人访问令牌(PAT)后,即可在 DSH 中直接使用 Qoder Global 的模型。",
3062
+ unifiedTitle: "Qoder",
3063
+ unifiedIntro: "统一管理 Qoder(国内版)与 Qoder Global(国际版)模型、凭证及侧栏额度展示。",
3064
+ variantTabCN: "国内版",
3065
+ variantTabGlobal: "国际版",
3116
3066
  expand: "展开",
3117
3067
  collapse: "收起",
3118
3068
  loading: "正在读取账号…",
@@ -3299,10 +3249,6 @@ window.__ModuleLoader__.load({
3299
3249
  en
3300
3250
  }), "dsh-qoder-connect: settings copy");
3301
3251
  const t = ctx.locale.bind(namespace);
3302
- const quotaSettingsInjected = {
3303
- t,
3304
- signedIn: () => quotaSignInState()
3305
- };
3306
3252
  let quotaScope;
3307
3253
  try {
3308
3254
  const scope = ctx.settingsScope.bind({ namespace: "qoder-quota" });
@@ -3319,20 +3265,13 @@ window.__ModuleLoader__.load({
3319
3265
  }
3320
3266
  ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
3321
3267
  name: "settings.plugin.item",
3322
- key: "qoder-quota",
3268
+ key: "qoder",
3323
3269
  priority: 50,
3324
- inject: () => ({
3325
- ...quotaSettingsInjected,
3326
- scope: quotaScope
3327
- })
3328
- }, QuotaSettingsCard));
3329
- for (const [index, variant] of QODER_CARD_VARIANTS.entries()) ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
3330
- name: "settings.plugin.item",
3331
- key: variant.id,
3332
- priority: 60 + index * 10,
3333
3270
  inject: () => ({
3334
3271
  t,
3335
- variant
3272
+ scope: quotaScope,
3273
+ signedIn: () => quotaSignInState(),
3274
+ unified: true
3336
3275
  })
3337
3276
  }, QoderPluginCard));
3338
3277
  const QUOTA_PANEL_ID = "qoder-quota-panel";