dsh-grok-subscription 0.1.3 → 0.1.13

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.
Files changed (4) hide show
  1. package/README.md +79 -3
  2. package/lib/client.js +613 -49
  3. package/lib/index.js +914 -309
  4. package/package.json +21 -2
package/lib/client.js CHANGED
@@ -21,18 +21,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var client_exports = {};
22
22
  __export(client_exports, {
23
23
  GrokSubscriptionSection: () => GrokSubscriptionSection,
24
+ RPC_CALL_TIMEOUT_MS: () => RPC_CALL_TIMEOUT_MS,
24
25
  apply: () => apply,
26
+ callRpc: () => callRpc,
25
27
  inject: () => inject,
26
28
  pickCopy: () => pickCopy
27
29
  });
28
30
  module.exports = __toCommonJS(client_exports);
29
- var import_react = require("react");
31
+ var import_react2 = require("react");
30
32
 
31
33
  // src/locales.js
32
34
  var zh = {
33
35
  nav: "Grok \u8BA2\u9605",
34
36
  title: "Grok \u8BA2\u9605",
35
- subtitle: "\u7528 SuperGrok / X Premium\uFF08Grok Build\uFF09\u4F1A\u8BDD\uFF0C\u800C\u4E0D\u662F XAI_API_KEY\u3002",
37
+ subtitle: "\u7528 SuperGrok / X Premium\uFF08Grok Build\uFF09\u4F1A\u8BDD\uFF0C\u800C\u4E0D\u662F XAI_API_KEY\u3002\xB7 0.1.13",
36
38
  signedIn: "\u5DF2\u767B\u5F55",
37
39
  signedOut: "\u672A\u767B\u5F55",
38
40
  account: "\u8D26\u6237",
@@ -51,17 +53,35 @@ var zh = {
51
53
  deviceHint: "\u8BBE\u5907\u7801\u63D0\u793A\u4F1A\u51FA\u73B0\u5728 DSH \u7EC8\u7AEF\u3002\u4E5F\u53EF\u624B\u52A8\u8FD0\u884C grok login --device-auth\uFF0C\u5B8C\u6210\u540E\u70B9\u201C\u4ECE Grok CLI \u62C9\u53D6\u201D\u3002",
52
54
  pullHint: "\u5B89\u5168\u8BFB\u53D6 ~/.grok/auth.json\uFF08\u62D2\u7EDD\u7B26\u53F7\u94FE\u63A5\u4E0E\u7EC4/\u5176\u4ED6\u4EBA\u53EF\u8BFB\uFF09\uFF0C\u53EA\u628A\u77ED\u671F access token \u5199\u5165 DSH\u3002",
53
55
  busy: "\u5904\u7406\u4E2D\u2026",
56
+ busyCatalog: "\u6B63\u5728\u5237\u65B0\u6A21\u578B\u76EE\u5F55\u2026",
57
+ busyUsage: "\u6B63\u5728\u5237\u65B0\u7528\u91CF\u2026",
54
58
  error: "\u64CD\u4F5C\u5931\u8D25",
55
59
  pullOk: "\u5DF2\u4ECE Grok CLI \u62C9\u53D6\u6210\u529F",
56
60
  loginOk: "\u767B\u5F55\u5B8C\u6210\uFF0C\u4F1A\u8BDD\u5DF2\u540C\u6B65",
57
61
  logoutOk: "\u5DF2\u9000\u51FA\u767B\u5F55",
58
62
  cliMissing: "\u672A\u53D1\u73B0 grok CLI\u3002\u8BF7\u5148\u5B89\u88C5\u5B98\u65B9 CLI \u5E76\u8FD0\u884C grok login\uFF0C\u7136\u540E\u62C9\u53D6\u3002",
59
- caveats: "\u793E\u533A\u63D2\u4EF6\uFF0C\u4E0D\u662F\u5B98\u65B9\u4EA7\u54C1\u3002\u8BA2\u9605\u7528\u4E8E\u975E\u5B98\u65B9\u5BA2\u6237\u7AEF\u53EF\u80FD\u5904\u4E8E\u4F9B\u5E94\u5546\u6761\u6B3E\u7070\u8272\u5730\u5E26\uFF1B\u53EA\u4F7F\u7528\u4F60\u81EA\u5DF1\u7684\u8D26\u53F7\u3002\u534F\u8BAE\u53EF\u80FD\u53D8\u5316\uFF0C\u56E0\u4E3A\u5B98\u65B9\u6587\u6863\u672A\u516C\u5F00\u5B8C\u6574 HTTP wire protocol\u3002"
63
+ caveats: "\u793E\u533A\u63D2\u4EF6\uFF0C\u4E0D\u662F\u5B98\u65B9\u4EA7\u54C1\u3002\u8BA2\u9605\u7528\u4E8E\u975E\u5B98\u65B9\u5BA2\u6237\u7AEF\u53EF\u80FD\u5904\u4E8E\u4F9B\u5E94\u5546\u6761\u6B3E\u7070\u8272\u5730\u5E26\uFF1B\u53EA\u4F7F\u7528\u4F60\u81EA\u5DF1\u7684\u8D26\u53F7\u3002\u534F\u8BAE\u53EF\u80FD\u53D8\u5316\uFF0C\u56E0\u4E3A\u5B98\u65B9\u6587\u6863\u672A\u516C\u5F00\u5B8C\u6574 HTTP wire protocol\u3002",
64
+ usageTitle: "\u7528\u91CF\uFF08\u5B9E\u9A8C\u6027\uFF09",
65
+ usageSubtitle: "\u6765\u81EA\u672A\u6587\u6863\u5316\u7684\u8BA2\u9605 billing API\uFF08/v1/billing?format=credits\uFF09\uFF0C\u4EC5\u4F9B\u53C2\u8003\uFF0C\u53EF\u80FD\u968F\u65F6\u5931\u6548\u3002",
66
+ usageUsed: "\u5DF2\u7528",
67
+ usageRemaining: "\u5269\u4F59\u7EA6",
68
+ usageReset: "\u5468\u671F\u7ED3\u675F / \u91CD\u7F6E",
69
+ usageUnavailable: "\u4E0D\u53EF\u7528",
70
+ usageRefresh: "\u5237\u65B0\u7528\u91CF",
71
+ usageOpenGrok: "\u5728 grok.com \u67E5\u770B\u7528\u91CF",
72
+ usageProduct: "\u4EA7\u54C1\u660E\u7EC6",
73
+ usageFetchedAt: "\u6700\u8FD1\u5237\u65B0",
74
+ usageRefreshOk: "\u7528\u91CF\u5DF2\u5237\u65B0",
75
+ catalogRefreshOk: "\u6A21\u578B\u76EE\u5F55\u5DF2\u5237\u65B0",
76
+ composerQuotaDetails: "\u6BCF\u5468\u989D\u5EA6",
77
+ composerQuotaRemaining: "\u6BCF\u5468\u989D\u5EA6 \u5269\u4F59 {remaining}%",
78
+ composerQuotaResets: "\u91CD\u7F6E\u4E8E {reset}",
79
+ composerQuotaResetUnknown: "\u91CD\u7F6E\u65F6\u95F4\u672A\u77E5"
60
80
  };
61
81
  var en = {
62
82
  nav: "Grok Subscription",
63
83
  title: "Grok Subscription",
64
- subtitle: "Use a SuperGrok / X Premium (Grok Build) session, not XAI_API_KEY.",
84
+ subtitle: "Use a SuperGrok / X Premium (Grok Build) session, not XAI_API_KEY. \xB7 0.1.13",
65
85
  signedIn: "Signed in",
66
86
  signedOut: "Signed out",
67
87
  account: "Account",
@@ -80,12 +100,30 @@ var en = {
80
100
  deviceHint: "The device-code prompt appears in the DSH terminal. You can also run grok login --device-auth yourself, then Pull from Grok CLI.",
81
101
  pullHint: "Reads ~/.grok/auth.json securely (refuses symlinks and group/other-readable files) and stores only the short-lived access token in DSH.",
82
102
  busy: "Working\u2026",
103
+ busyCatalog: "Refreshing model catalog\u2026",
104
+ busyUsage: "Refreshing usage\u2026",
83
105
  error: "Request failed",
84
106
  pullOk: "Pulled from Grok CLI successfully",
85
107
  loginOk: "Signed in and session synced",
86
108
  logoutOk: "Signed out",
87
109
  cliMissing: "The grok CLI was not found. Install it, run grok login, then pull.",
88
- caveats: "Community plugin, not an official product. Using a subscription from an unofficial client may be a vendor-ToS gray area; use your own account only. The protocol can change because official docs do not publish the full HTTP wire protocol."
110
+ caveats: "Community plugin, not an official product. Using a subscription from an unofficial client may be a vendor-ToS gray area; use your own account only. The protocol can change because official docs do not publish the full HTTP wire protocol.",
111
+ usageTitle: "Usage (experimental)",
112
+ usageSubtitle: "From the undocumented subscription billing API (/v1/billing?format=credits). Informational only; the endpoint may change or disappear.",
113
+ usageUsed: "Used",
114
+ usageRemaining: "Remaining \u2248",
115
+ usageReset: "Period end / reset",
116
+ usageUnavailable: "Unavailable",
117
+ usageRefresh: "Refresh usage",
118
+ usageOpenGrok: "View usage on grok.com",
119
+ usageProduct: "Product breakdown",
120
+ usageFetchedAt: "Last refreshed",
121
+ usageRefreshOk: "Usage refreshed",
122
+ catalogRefreshOk: "Model catalog refreshed",
123
+ composerQuotaDetails: "Weekly quota",
124
+ composerQuotaRemaining: "Weekly quota \xB7 {remaining}% left",
125
+ composerQuotaResets: "Resets {reset}",
126
+ composerQuotaResetUnknown: "Reset time unknown"
89
127
  };
90
128
 
91
129
  // src/rpc-contract.js
@@ -96,7 +134,9 @@ var RPC_ENDPOINTS = Object.freeze([
96
134
  "login/device",
97
135
  "pull",
98
136
  "logout",
99
- "catalog/refresh"
137
+ "catalog/refresh",
138
+ "usage",
139
+ "usage/refresh"
100
140
  ]);
101
141
  function createRpcClient(transport) {
102
142
  return Object.freeze({
@@ -114,7 +154,9 @@ function unwrap(response) {
114
154
  }
115
155
 
116
156
  // src/constants.js
157
+ var PROVIDER_ID = "grok-build";
117
158
  var LOCALE_NS = "settings.grokSubscription";
159
+ var USAGE_PAGE_URL = "https://grok.com/?_s=usage";
118
160
  var SESSION_AUTH_MODES = Object.freeze(["oidc", "external", "web_login", "grok"]);
119
161
  var API_KEY_AUTH_MODES = Object.freeze(["api_key"]);
120
162
  var STREAM_IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
@@ -123,9 +165,340 @@ var REQUEST_IMAGE_PIXEL_BUDGET = 2048 * 2048;
123
165
  var REQUEST_IMAGE_MAX_BYTES = 1024 * 1024;
124
166
  var FALLBACK_MODEL_IDS = Object.freeze(["grok-4.7", "grok-4.6", "grok-4.5"]);
125
167
 
126
- // src/client.jsx
168
+ // src/client-composer-quota.jsx
169
+ var import_react = require("react");
170
+ var import_react_dom = require("react-dom");
171
+
172
+ // src/client-composer-quota-shared.js
173
+ var QUICK_QUOTA_REFRESH_EVENT = "dsh-grok-subscription:refresh-quick-quota";
174
+ var QUICK_QUOTA_REFRESH_MS = 6e4;
175
+ var COMPOSER_QUOTA_STYLE = `
176
+ .grokComposerQuota:focus-visible{outline:1px solid var(--dsw-alias-border-l3);outline-offset:2px}
177
+ .grokComposerQuota{display:inline-flex;align-items:center;gap:9px;flex:0 0 auto;height:28px;box-sizing:border-box;padding:0 5px;border:0;border-radius:6px;background:transparent;cursor:pointer;color:var(--dsw-alias-label-secondary);font-family:inherit;font-size:12px;line-height:20px;font-weight:500;font-variant-numeric:tabular-nums;white-space:nowrap}
178
+ .grokComposerQuota:hover,.grokComposerQuota[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover)}
179
+ .grokQuotaPopover{position:fixed;z-index:1000;width:max-content;max-width:calc(100vw - 24px);max-height:calc(100vh - 24px);overflow:auto;box-sizing:border-box;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 4px 16px #0002;font-size:12px;line-height:20px;outline:none}
180
+ .grokQuotaDetail>div{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
181
+ .grokQuotaDetail strong{font-weight:500}
182
+ .grokQuotaReset{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px}
183
+ `;
184
+ function fillTemplate(text, values) {
185
+ return Object.entries(values).reduce(
186
+ (next, [key, value]) => next.replaceAll(`{${key}}`, String(value)),
187
+ String(text ?? "")
188
+ );
189
+ }
190
+ function formatRemainingPercent(value) {
191
+ if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
192
+ const rounded = Math.round(value * 10) / 10;
193
+ return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
194
+ }
195
+ function formatShortReset(usage) {
196
+ if (!usage || typeof usage !== "object") return void 0;
197
+ const iso = typeof usage.periodEnd === "string" ? usage.periodEnd : void 0;
198
+ if (iso) {
199
+ const ms = Date.parse(iso);
200
+ if (Number.isFinite(ms)) {
201
+ return new Date(ms).toLocaleString(void 0, {
202
+ month: "numeric",
203
+ day: "numeric",
204
+ hour: "2-digit",
205
+ minute: "2-digit"
206
+ });
207
+ }
208
+ }
209
+ const local = typeof usage.periodEndLocal === "string" ? usage.periodEndLocal.trim() : "";
210
+ if (!local) return void 0;
211
+ const match = local.match(/(\d{4})[/-](\d{1,2})[/-](\d{1,2})\s+(\d{1,2}):(\d{2})/);
212
+ if (match) {
213
+ return `${Number(match[2])}/${Number(match[3])} ${match[4]}:${match[5]}`;
214
+ }
215
+ return local;
216
+ }
217
+ function isComposerQuotaEnabled(provider, usage) {
218
+ if (provider !== PROVIDER_ID) return false;
219
+ if (!usage || usage.status !== "ok") return false;
220
+ return typeof usage.remainingPercent === "number" && Number.isFinite(usage.remainingPercent);
221
+ }
222
+ function notifyQuickQuota() {
223
+ try {
224
+ if (typeof window !== "undefined") {
225
+ window.dispatchEvent(new Event(QUICK_QUOTA_REFRESH_EVENT));
226
+ }
227
+ } catch {
228
+ }
229
+ }
230
+
231
+ // src/client-composer-quota.jsx
127
232
  var import_jsx_runtime = require("react/jsx-runtime");
128
- var inject = ["slots", "locale", "connection", "settingsScope"];
233
+ var QUICK_RPC_TIMEOUT_MS = 12e3;
234
+ async function quickCall(rpc, endpoint) {
235
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
236
+ let timer;
237
+ try {
238
+ const call = rpc.call(CHANNEL, endpoint, {}, controller?.signal);
239
+ const timeout = new Promise((_, reject) => {
240
+ timer = setTimeout(() => {
241
+ try {
242
+ controller?.abort();
243
+ } catch {
244
+ }
245
+ reject(new Error(`Request timed out after ${QUICK_RPC_TIMEOUT_MS}ms`));
246
+ }, QUICK_RPC_TIMEOUT_MS);
247
+ });
248
+ return unwrap(await Promise.race([call, timeout]));
249
+ } finally {
250
+ if (timer) clearTimeout(timer);
251
+ }
252
+ }
253
+ function useAnchoredPositionFallback({ open, anchorRef }) {
254
+ const [position, setPosition] = (0, import_react.useState)(null);
255
+ (0, import_react.useEffect)(() => {
256
+ if (!open || !anchorRef?.current) {
257
+ setPosition(null);
258
+ return void 0;
259
+ }
260
+ const place = () => {
261
+ const rect = anchorRef.current.getBoundingClientRect();
262
+ setPosition({
263
+ position: "fixed",
264
+ left: Math.max(12, Math.min(rect.left, window.innerWidth - 12)),
265
+ top: Math.max(12, rect.top - 8),
266
+ transform: "translateY(-100%)"
267
+ });
268
+ };
269
+ place();
270
+ window.addEventListener("resize", place);
271
+ window.addEventListener("scroll", place, true);
272
+ return () => {
273
+ window.removeEventListener("resize", place);
274
+ window.removeEventListener("scroll", place, true);
275
+ };
276
+ }, [open, anchorRef]);
277
+ return position;
278
+ }
279
+ function useDismissOnOutsidePointerFallback(triggerRef, open, dismiss, panelRef) {
280
+ (0, import_react.useEffect)(() => {
281
+ if (!open) return void 0;
282
+ const onPointer = (event) => {
283
+ const target = event.target;
284
+ if (triggerRef.current?.contains?.(target) || panelRef.current?.contains?.(target)) return;
285
+ dismiss();
286
+ };
287
+ document.addEventListener("pointerdown", onPointer, true);
288
+ return () => document.removeEventListener("pointerdown", onPointer, true);
289
+ }, [open, triggerRef, panelRef, dismiss]);
290
+ }
291
+ function loadAnchoringHooks() {
292
+ try {
293
+ const req = typeof require === "function" ? require : void 0;
294
+ const mod = req?.("@deepseek-ai/dsh-client-ui-primitives");
295
+ if (mod?.useAnchoredPosition && mod?.useDismissOnOutsidePointer) {
296
+ return {
297
+ useAnchoredPosition: mod.useAnchoredPosition,
298
+ useDismissOnOutsidePointer: mod.useDismissOnOutsidePointer
299
+ };
300
+ }
301
+ } catch {
302
+ }
303
+ return {
304
+ useAnchoredPosition: useAnchoredPositionFallback,
305
+ useDismissOnOutsidePointer: useDismissOnOutsidePointerFallback
306
+ };
307
+ }
308
+ var anchoring = loadAnchoringHooks();
309
+ function useGrokQuickQuota(rpc, enabled) {
310
+ const [usage, setUsage] = (0, import_react.useState)(void 0);
311
+ (0, import_react.useEffect)(() => {
312
+ if (!enabled) {
313
+ setUsage(void 0);
314
+ return void 0;
315
+ }
316
+ let live = true;
317
+ let loading = false;
318
+ const accept = (next) => {
319
+ if (!live) return;
320
+ if (next && next.status === "ok" && typeof next.remainingPercent === "number" && Number.isFinite(next.remainingPercent)) {
321
+ setUsage(next);
322
+ } else {
323
+ setUsage(void 0);
324
+ }
325
+ };
326
+ const softRefresh = () => {
327
+ void quickCall(rpc, "usage/refresh").then((value) => {
328
+ if (value?.usage) accept(value.usage);
329
+ }).catch(() => {
330
+ });
331
+ };
332
+ const load = async () => {
333
+ if (loading) return;
334
+ loading = true;
335
+ try {
336
+ let status;
337
+ try {
338
+ status = await quickCall(rpc, "status");
339
+ } catch {
340
+ status = void 0;
341
+ }
342
+ if (!live) return;
343
+ if (status?.account?.signedIn !== true) {
344
+ try {
345
+ const value = await quickCall(rpc, "usage");
346
+ accept(value?.usage);
347
+ } catch {
348
+ accept(void 0);
349
+ }
350
+ return;
351
+ }
352
+ let next = status?.usage;
353
+ if (!next || next.status !== "ok") {
354
+ try {
355
+ const value = await quickCall(rpc, "usage");
356
+ next = value?.usage ?? next;
357
+ } catch {
358
+ }
359
+ }
360
+ if (!live) return;
361
+ accept(next);
362
+ softRefresh();
363
+ } catch {
364
+ if (live) setUsage(void 0);
365
+ } finally {
366
+ loading = false;
367
+ }
368
+ };
369
+ const refresh = () => {
370
+ void load();
371
+ };
372
+ void load();
373
+ const timer = window.setInterval(refresh, QUICK_QUOTA_REFRESH_MS);
374
+ window.addEventListener(QUICK_QUOTA_REFRESH_EVENT, refresh);
375
+ return () => {
376
+ live = false;
377
+ window.clearInterval(timer);
378
+ window.removeEventListener(QUICK_QUOTA_REFRESH_EVENT, refresh);
379
+ };
380
+ }, [rpc, enabled]);
381
+ return usage;
382
+ }
383
+ function GrokComposerQuota({ rpc, t, directory }) {
384
+ const modelState = (0, import_react.useSyncExternalStore)(
385
+ (listener) => directory.subscribe(listener),
386
+ () => directory.getSnapshot()
387
+ );
388
+ const current = modelState?.current;
389
+ const provider = current?.provider;
390
+ const providerEnabled = provider === PROVIDER_ID;
391
+ const usage = useGrokQuickQuota(rpc, providerEnabled);
392
+ const enabled = isComposerQuotaEnabled(provider, usage);
393
+ const remaining = enabled ? formatRemainingPercent(usage.remainingPercent) : void 0;
394
+ const [open, setOpen] = (0, import_react.useState)(false);
395
+ const trigger = (0, import_react.useRef)(null);
396
+ const panel = (0, import_react.useRef)(null);
397
+ const pinned = (0, import_react.useRef)(false);
398
+ const dismissTimer = (0, import_react.useRef)(null);
399
+ const enter = () => {
400
+ clearTimeout(dismissTimer.current);
401
+ setOpen(true);
402
+ };
403
+ const leave = () => {
404
+ if (!pinned.current) dismissTimer.current = setTimeout(() => setOpen(false), 150);
405
+ };
406
+ const dismiss = () => {
407
+ pinned.current = false;
408
+ setOpen(false);
409
+ };
410
+ (0, import_react.useEffect)(() => () => clearTimeout(dismissTimer.current), []);
411
+ const id = (0, import_react.useId)();
412
+ const visible = open && enabled && remaining !== void 0;
413
+ const position = anchoring.useAnchoredPosition({
414
+ open: visible,
415
+ anchorRef: trigger,
416
+ panelRef: panel,
417
+ side: "top",
418
+ gap: 8,
419
+ margin: 12
420
+ });
421
+ anchoring.useDismissOnOutsidePointer(trigger, visible, dismiss, panel);
422
+ (0, import_react.useEffect)(() => {
423
+ setOpen(false);
424
+ }, [current?.model, enabled]);
425
+ (0, import_react.useEffect)(() => {
426
+ if (!visible) return void 0;
427
+ const escape = (event) => {
428
+ if (event.key !== "Escape") return;
429
+ event.preventDefault();
430
+ dismiss();
431
+ trigger.current?.focus?.();
432
+ };
433
+ document.addEventListener("keydown", escape);
434
+ return () => document.removeEventListener("keydown", escape);
435
+ }, [visible]);
436
+ if (!enabled || remaining === void 0) return null;
437
+ const reset = formatShortReset(usage);
438
+ const remainingLine = fillTemplate(t("composerQuotaRemaining"), { remaining });
439
+ const resetLine = reset ? fillTemplate(t("composerQuotaResets"), { reset }) : t("composerQuotaResetUnknown");
440
+ const detailsLabel = `${remainingLine} ${resetLine}`;
441
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
442
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
443
+ "button",
444
+ {
445
+ ref: trigger,
446
+ type: "button",
447
+ className: "grokComposerQuota",
448
+ title: detailsLabel,
449
+ "aria-label": `${t("composerQuotaDetails")}: ${detailsLabel}`,
450
+ "aria-haspopup": "dialog",
451
+ "aria-expanded": visible,
452
+ "aria-controls": visible ? id : void 0,
453
+ onMouseEnter: enter,
454
+ onMouseLeave: leave,
455
+ onClick: () => {
456
+ if (pinned.current) dismiss();
457
+ else {
458
+ pinned.current = true;
459
+ setOpen(true);
460
+ requestAnimationFrame(() => panel.current?.focus?.());
461
+ }
462
+ },
463
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: `${remaining}%` })
464
+ }
465
+ ),
466
+ visible ? (0, import_react_dom.createPortal)(
467
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
468
+ "section",
469
+ {
470
+ ref: panel,
471
+ id,
472
+ role: "dialog",
473
+ "aria-label": t("composerQuotaDetails"),
474
+ className: "grokQuotaPopover",
475
+ tabIndex: -1,
476
+ onMouseEnter: enter,
477
+ onMouseLeave: leave,
478
+ style: { ...position || {}, visibility: position ? "visible" : "hidden" },
479
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "grokQuotaDetail", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
480
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: remainingLine }),
481
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "grokQuotaReset", children: resetLine })
482
+ ] }) })
483
+ }
484
+ ),
485
+ document.body
486
+ ) : null
487
+ ] });
488
+ }
489
+
490
+ // src/client.jsx
491
+ var import_jsx_runtime2 = require("react/jsx-runtime");
492
+ var inject = [
493
+ "slots",
494
+ "locale",
495
+ "connection",
496
+ "settingsScope",
497
+ "modelDirectories",
498
+ "sessions",
499
+ "remote"
500
+ ];
501
+ var RPC_CALL_TIMEOUT_MS = 12e3;
129
502
  function pickCopy(locale) {
130
503
  const language = typeof locale === "string" ? locale : locale?.language ?? locale?.lang;
131
504
  const resolved = language ?? (typeof navigator !== "undefined" ? navigator.language : "zh-CN");
@@ -136,30 +509,148 @@ function sourceLabel(source, t) {
136
509
  if (source === "fallback") return t("catalogSourceFallback");
137
510
  return t("catalogSourceSignedOut");
138
511
  }
512
+ function formatPercent(value) {
513
+ if (typeof value !== "number" || !Number.isFinite(value)) return void 0;
514
+ const rounded = Math.round(value * 10) / 10;
515
+ return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
516
+ }
517
+ async function callRpc(rpc, endpoint, payload = {}) {
518
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
519
+ let timer;
520
+ try {
521
+ const call = rpc.call(CHANNEL, endpoint, payload, controller?.signal);
522
+ const timeout = new Promise((_, reject) => {
523
+ timer = setTimeout(() => {
524
+ try {
525
+ controller?.abort();
526
+ } catch {
527
+ }
528
+ reject(new Error(`Request timed out after ${RPC_CALL_TIMEOUT_MS}ms`));
529
+ }, RPC_CALL_TIMEOUT_MS);
530
+ });
531
+ return unwrap(await Promise.race([call, timeout]));
532
+ } finally {
533
+ if (timer) clearTimeout(timer);
534
+ }
535
+ }
536
+ function UsagePanel({ usage, t, usageBusy, onRefresh, signedIn }) {
537
+ const ok = usage?.status === "ok";
538
+ const used = ok ? formatPercent(usage.usedPercent) : void 0;
539
+ const remaining = ok ? formatPercent(usage.remainingPercent) : void 0;
540
+ const resetLabel = usage?.periodEndLocal || usage?.periodEnd;
541
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "usageBlock", children: [
542
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: t("usageTitle") }),
543
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "muted", children: t("usageSubtitle") }),
544
+ ok && used !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
545
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { children: [
546
+ t("usageUsed"),
547
+ ": ",
548
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("strong", { children: [
549
+ used,
550
+ "%"
551
+ ] }),
552
+ remaining !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
553
+ " \xB7 ",
554
+ t("usageRemaining"),
555
+ " ",
556
+ remaining,
557
+ "%"
558
+ ] }) : null
559
+ ] }),
560
+ resetLabel ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "muted", children: [
561
+ t("usageReset"),
562
+ ": ",
563
+ resetLabel,
564
+ usage.periodEnd && usage.periodEndLocal ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
565
+ " (",
566
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("code", { children: usage.periodEnd }),
567
+ ")"
568
+ ] }) : null
569
+ ] }) : null,
570
+ Array.isArray(usage.productUsage) && usage.productUsage.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
571
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "muted", children: t("usageProduct") }),
572
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { children: usage.productUsage.map((row, index) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { children: [
573
+ row.name ?? "\u2014",
574
+ typeof row.usedPercent === "number" ? ` \xB7 ${formatPercent(row.usedPercent)}%` : ""
575
+ ] }, `${row.name ?? "row"}-${index}`)) })
576
+ ] }) : null,
577
+ usage.fetchedAt ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "muted", children: [
578
+ t("usageFetchedAt"),
579
+ ": ",
580
+ usage.fetchedAt
581
+ ] }) : null
582
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { children: [
583
+ t("usageUnavailable"),
584
+ usage?.reason ? `: ${usage.reason}` : ""
585
+ ] }),
586
+ usageBusy ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "muted", children: t("busyUsage") }) : null,
587
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "row", children: [
588
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", disabled: Boolean(usageBusy) || !signedIn, onClick: onRefresh, children: t("usageRefresh") }),
589
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("a", { href: USAGE_PAGE_URL, target: "_blank", rel: "noreferrer", children: t("usageOpenGrok") })
590
+ ] })
591
+ ] });
592
+ }
139
593
  function GrokSubscriptionSection({ rpc, t }) {
140
- const [busy, setBusy] = (0, import_react.useState)("");
141
- const [error, setError] = (0, import_react.useState)("");
142
- const [notice, setNotice] = (0, import_react.useState)("");
143
- const [status, setStatus] = (0, import_react.useState)(void 0);
594
+ const [busy, setBusy] = (0, import_react2.useState)("");
595
+ const [usageBusy, setUsageBusy] = (0, import_react2.useState)(false);
596
+ const [error, setError] = (0, import_react2.useState)("");
597
+ const [notice, setNotice] = (0, import_react2.useState)("");
598
+ const [status, setStatus] = (0, import_react2.useState)(void 0);
599
+ const initialUsageKick = (0, import_react2.useRef)(false);
600
+ const applyPartial = (value) => {
601
+ if (value?.account || value?.catalog || value?.usage) {
602
+ setStatus((current) => ({ ...current, ...value }));
603
+ }
604
+ };
144
605
  const load = async () => {
145
- const value = unwrap(await rpc.call(CHANNEL, "status", {}));
606
+ const value = await callRpc(rpc, "status", {});
146
607
  setStatus(value);
147
608
  setError("");
609
+ return value;
148
610
  };
149
- (0, import_react.useEffect)(() => {
150
- void load().catch((item) => {
611
+ (0, import_react2.useEffect)(() => {
612
+ void load().then((value) => {
613
+ if (initialUsageKick.current) return;
614
+ if (value?.account?.signedIn !== true) return;
615
+ initialUsageKick.current = true;
616
+ void callRpc(rpc, "usage/refresh", {}).then((partial) => {
617
+ applyPartial(partial);
618
+ notifyQuickQuota();
619
+ }).catch(() => {
620
+ });
621
+ }).catch((item) => {
151
622
  setNotice("");
152
623
  setError(item instanceof Error ? item.message : String(item));
153
624
  });
154
625
  }, [rpc]);
626
+ const kickFollowUpRefresh = () => {
627
+ void callRpc(rpc, "catalog/refresh", {}).then((partial) => {
628
+ applyPartial(partial);
629
+ }).catch(() => {
630
+ });
631
+ void callRpc(rpc, "usage/refresh", {}).then((partial) => {
632
+ applyPartial(partial);
633
+ notifyQuickQuota();
634
+ }).catch(() => {
635
+ });
636
+ };
155
637
  const run = async (endpoint) => {
156
- setBusy(endpoint);
638
+ const isUsageRefresh = endpoint === "usage/refresh";
639
+ if (isUsageRefresh) {
640
+ setUsageBusy(true);
641
+ } else {
642
+ setBusy(endpoint);
643
+ }
157
644
  setError("");
158
645
  setNotice("");
646
+ let value;
159
647
  try {
160
- const value = unwrap(await rpc.call(CHANNEL, endpoint, {}));
161
- if (value?.account || value?.catalog) setStatus((current) => ({ ...current, ...value }));
162
- else await load();
648
+ value = await callRpc(rpc, endpoint, {});
649
+ if (value?.account || value?.catalog || value?.usage) {
650
+ applyPartial(value);
651
+ } else {
652
+ await load();
653
+ }
163
654
  if (value?.ok === false && value.error) {
164
655
  setNotice("");
165
656
  setError(value.error);
@@ -168,81 +659,113 @@ function GrokSubscriptionSection({ rpc, t }) {
168
659
  pull: "pullOk",
169
660
  "login/cli": "loginOk",
170
661
  "login/device": "loginOk",
171
- logout: "logoutOk"
662
+ logout: "logoutOk",
663
+ "usage/refresh": "usageRefreshOk"
172
664
  }[endpoint];
173
665
  if (successKey) setNotice(t(successKey));
666
+ if (endpoint === "usage/refresh" && value?.ok !== false && !value?.error) {
667
+ notifyQuickQuota();
668
+ }
174
669
  }
175
670
  } catch (item) {
176
671
  setNotice("");
177
672
  setError(item instanceof Error ? item.message : String(item));
178
673
  } finally {
179
- setBusy("");
674
+ if (isUsageRefresh) setUsageBusy(false);
675
+ else setBusy("");
180
676
  }
677
+ const shouldFollowUp = (endpoint === "pull" || endpoint === "login/cli" || endpoint === "login/device") && value?.ok !== false && !value?.error;
678
+ if (shouldFollowUp) kickFollowUpRefresh();
181
679
  };
182
680
  const account = status?.account;
183
681
  const catalog = status?.catalog;
184
682
  const models = catalog?.models ?? [];
185
683
  const signedIn = account?.signedIn === true;
186
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "grokSubscription", children: [
187
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `
684
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("section", { className: "grokSubscription", children: [
685
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: `
188
686
  .grokSubscription { display: grid; gap: 12px; max-width: 42rem; }
189
687
  .grokSubscription h2 { margin: 0 0 4px; font-size: 1.15rem; }
190
688
  .grokSubscription p, .grokSubscription li { line-height: 1.5; }
191
689
  .grokSubscription .muted { opacity: 0.78; font-size: 0.92rem; }
192
- .grokSubscription .row { display: flex; flex-wrap: wrap; gap: 8px; }
690
+ .grokSubscription .row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
193
691
  .grokSubscription button { cursor: pointer; }
194
692
  .grokSubscription .error { color: #b42318; }
195
693
  .grokSubscription .notice { color: #067647; }
196
694
  .grokSubscription ul { margin: 0; padding-left: 1.2rem; }
695
+ .grokSubscription .usageBlock { display: grid; gap: 8px; padding: 10px 0; border-top: 1px solid color-mix(in srgb, currentColor 18%, transparent); }
696
+ .grokSubscription a { color: inherit; }
197
697
  ` }),
198
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
199
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { children: t("title") }),
200
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "muted", children: t("subtitle") })
698
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
699
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("h2", { children: t("title") }),
700
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "muted", children: t("subtitle") })
201
701
  ] }),
202
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
203
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
702
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
703
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("strong", { children: [
204
704
  t("account"),
205
705
  ": "
206
706
  ] }),
207
707
  signedIn ? `${t("signedIn")} \xB7 ${account.maskedAccount ?? t("unknownAccount")}` : t("signedOut")
208
708
  ] }),
209
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "row", children: [
210
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: Boolean(busy), onClick: () => void run("login/cli"), children: t("loginCli") }),
211
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: Boolean(busy), onClick: () => void run("login/device"), children: t("loginDevice") }),
212
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: Boolean(busy), onClick: () => void run("pull"), children: t("pull") }),
213
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: Boolean(busy) || !signedIn, onClick: () => void run("logout"), children: t("logout") })
709
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "row", children: [
710
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", disabled: Boolean(busy), onClick: () => void run("login/cli"), children: t("loginCli") }),
711
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", disabled: Boolean(busy), onClick: () => void run("login/device"), children: t("loginDevice") }),
712
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", disabled: Boolean(busy), onClick: () => void run("pull"), children: t("pull") }),
713
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", disabled: Boolean(busy) || !signedIn, onClick: () => void run("logout"), children: t("logout") })
214
714
  ] }),
215
- busy ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "muted", children: t("busy") }) : null,
216
- notice ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "notice", children: notice }) : null,
217
- error ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "error", children: [
715
+ busy ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "muted", children: t("busy") }) : null,
716
+ notice ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "notice", children: notice }) : null,
717
+ error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "error", children: [
218
718
  t("error"),
219
719
  ": ",
220
720
  error
221
721
  ] }) : null,
222
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "muted", children: t("loginHint") }),
223
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "muted", children: t("deviceHint") }),
224
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "muted", children: t("pullHint") }),
225
- status?.cliAvailable === false ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "muted", children: t("cliMissing") }) : null,
226
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
227
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: t("models") }),
228
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "muted", children: sourceLabel(catalog?.source, t) }),
229
- models.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: t("noModels") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { children: models.map((model) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { children: [
722
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "muted", children: t("loginHint") }),
723
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "muted", children: t("deviceHint") }),
724
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "muted", children: t("pullHint") }),
725
+ status?.cliAvailable === false ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "muted", children: t("cliMissing") }) : null,
726
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
727
+ UsagePanel,
728
+ {
729
+ usage: status?.usage,
730
+ t,
731
+ usageBusy,
732
+ signedIn,
733
+ onRefresh: () => void run("usage/refresh")
734
+ }
735
+ ),
736
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
737
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("strong", { children: t("models") }),
738
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "muted", children: sourceLabel(catalog?.source, t) }),
739
+ models.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { children: t("noModels") }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { children: models.map((model) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { children: [
230
740
  model.name,
231
741
  " ",
232
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: model.id })
742
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("code", { children: model.id })
233
743
  ] }, model.id)) }),
234
- catalog?.error ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "error", children: [
744
+ catalog?.error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("p", { className: "error", children: [
235
745
  t("catalogError"),
236
746
  ": ",
237
747
  catalog.error
238
748
  ] }) : null
239
749
  ] }),
240
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "muted", children: t("caveats") })
750
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "muted", children: t("caveats") })
241
751
  ] });
242
752
  }
243
753
  function apply(ctx) {
244
754
  ctx.effect?.(() => ctx.locale?.register?.(LOCALE_NS, { zh, en }), "grok-subscription: copy");
245
- const connection = ctx.get?.("connection") ?? ctx.connection;
755
+ let connection;
756
+ try {
757
+ connection = typeof ctx.get === "function" ? ctx.get("connection") : void 0;
758
+ } catch {
759
+ connection = void 0;
760
+ }
761
+ if (!connection) connection = ctx.connection;
762
+ if (!connection?.rpc) {
763
+ try {
764
+ ctx.logger?.warn?.("Grok subscription Settings section skipped: connection unavailable");
765
+ } catch {
766
+ }
767
+ return;
768
+ }
246
769
  const rpc = createRpcClient(connection.rpc);
247
770
  const bound = ctx.locale?.bind?.(LOCALE_NS);
248
771
  const t = (key) => {
@@ -253,6 +776,14 @@ function apply(ctx) {
253
776
  }
254
777
  return pickCopy(ctx.locale)[key] ?? key;
255
778
  };
779
+ ctx.effect?.(() => {
780
+ if (typeof document === "undefined") return void 0;
781
+ const tag = document.createElement("style");
782
+ tag.dataset.plugin = "dsh-grok-subscription";
783
+ tag.textContent = COMPOSER_QUOTA_STYLE;
784
+ document.head.append(tag);
785
+ return () => tag.remove();
786
+ }, "grok-subscription: composer-quota-style");
256
787
  ctx.slots.inject("settings.section", () => ctx.slots.register({
257
788
  name: "settings.section",
258
789
  id: "grok-subscription",
@@ -261,5 +792,38 @@ function apply(ctx) {
261
792
  locale: LOCALE_NS,
262
793
  inject: () => ({ rpc, t })
263
794
  }, GrokSubscriptionSection));
795
+ const installDirectorySlots = (scope) => {
796
+ let modelDirectories;
797
+ try {
798
+ modelDirectories = typeof scope.get === "function" ? scope.get("modelDirectories") : void 0;
799
+ } catch {
800
+ modelDirectories = void 0;
801
+ }
802
+ if (!modelDirectories?.directoryFor) {
803
+ try {
804
+ ctx.logger?.warn?.("Grok composer quota skipped: modelDirectories unavailable");
805
+ } catch {
806
+ }
807
+ return;
808
+ }
809
+ scope.slots.inject("conversation.input.right", () => scope.slots.register({
810
+ name: "conversation.input.right",
811
+ id: "grok-subscription-quota",
812
+ order: 16,
813
+ locale: LOCALE_NS,
814
+ inject: (sessionId) => ({
815
+ rpc,
816
+ t,
817
+ directory: modelDirectories.directoryFor(sessionId).store
818
+ })
819
+ }, GrokComposerQuota));
820
+ };
821
+ try {
822
+ const remoteSession = typeof ctx.get === "function" ? ctx.get("remote.session") : void 0;
823
+ if (remoteSession === void 0) installDirectorySlots(ctx);
824
+ else ctx.inject(["remote.session"], installDirectorySlots);
825
+ } catch {
826
+ installDirectorySlots(ctx);
827
+ }
264
828
  }
265
829
  return module.exports; } });