dsh-plugin-subscriptions 0.4.2 → 0.5.1

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 (44) hide show
  1. package/README.md +19 -6
  2. package/README.zh.md +18 -6
  3. package/lib/auth/device-flow.d.ts +64 -0
  4. package/lib/auth/device-flow.js +176 -0
  5. package/lib/auth/oauth-flow.js +1 -1
  6. package/lib/auth/rpc.d.ts +21 -2
  7. package/lib/auth/rpc.js +23 -3
  8. package/lib/auth/store.d.ts +20 -2
  9. package/lib/auth/store.js +45 -9
  10. package/lib/client/ImageGallery.d.ts +54 -0
  11. package/lib/client/ImageGallery.js +112 -0
  12. package/lib/client/ImageGenerateToolview.d.ts +1 -1
  13. package/lib/client/ImageGenerateToolview.js +2 -2
  14. package/lib/client/SpeedSelect.d.ts +48 -0
  15. package/lib/client/SpeedSelect.js +173 -0
  16. package/lib/client/SubscriptionsSection.d.ts +10 -1
  17. package/lib/client/SubscriptionsSection.js +48 -5
  18. package/lib/client/index.d.ts +1 -0
  19. package/lib/client/index.js +42 -0
  20. package/lib/client/locales.d.ts +22 -0
  21. package/lib/client/locales.js +22 -0
  22. package/lib/client.js +679 -77
  23. package/lib/client.js.map +1 -1
  24. package/lib/index.d.ts +3 -2
  25. package/lib/index.js +1868 -183
  26. package/lib/providers/catalog-store.js +19 -0
  27. package/lib/providers/claude.d.ts +20 -1
  28. package/lib/providers/claude.js +58 -31
  29. package/lib/providers/codex.d.ts +27 -0
  30. package/lib/providers/codex.js +117 -27
  31. package/lib/providers/common.d.ts +34 -1
  32. package/lib/providers/common.js +48 -1
  33. package/lib/providers/copilot.d.ts +315 -0
  34. package/lib/providers/copilot.js +786 -0
  35. package/lib/providers/grok.d.ts +7 -2
  36. package/lib/providers/grok.js +46 -18
  37. package/lib/tools/image-generate.js +3 -10
  38. package/lib/translate/anthropic.d.ts +47 -6
  39. package/lib/translate/anthropic.js +135 -20
  40. package/lib/translate/chat-completions.d.ts +120 -0
  41. package/lib/translate/chat-completions.js +363 -0
  42. package/lib/translate/responses.d.ts +49 -5
  43. package/lib/translate/responses.js +40 -7
  44. package/package.json +6 -3
package/lib/client.js CHANGED
@@ -29,8 +29,6 @@ let react_jsx_runtime = require("react/jsx-runtime");
29
29
  react_jsx_runtime = __toESM(react_jsx_runtime);
30
30
  let __deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
31
31
  __deepseek_ai_dsh_client_ui_primitives = __toESM(__deepseek_ai_dsh_client_ui_primitives);
32
- let __deepseek_ai_dsh_client_ui_attachment = require("@deepseek-ai/dsh-client-ui-attachment");
33
- __deepseek_ai_dsh_client_ui_attachment = __toESM(__deepseek_ai_dsh_client_ui_attachment);
34
32
 
35
33
  //#region src/client/locales.ts
36
34
  /** Copy dictionaries for the Subscriptions settings section. */
@@ -54,6 +52,10 @@ const en = {
54
52
  manualPlaceholder: "Paste the callback URL or code",
55
53
  submit: "Submit",
56
54
  loginMissingUrl: "login answered without an authorizeUrl",
55
+ deviceCodePrompt: "Enter this code on the GitHub verification page:",
56
+ deviceCodeCopy: "Copy code",
57
+ deviceCodeCopied: "Copied",
58
+ deviceCodeOpenPage: "Open GitHub verification page",
57
59
  usageTitle: "Usage",
58
60
  usageRefresh: "Refresh",
59
61
  usageLoading: "Loading usage…",
@@ -74,7 +76,14 @@ const en = {
74
76
  imageClose: "Close",
75
77
  generatingVideo: "Generating video…",
76
78
  videoLoading: "Loading video…",
77
- videoLoadFailed: "Video failed to load: {message}"
79
+ videoLoadFailed: "Video failed to load: {message}",
80
+ speed: "Speed",
81
+ speedStandard: "Standard",
82
+ speedStandardDescription: "Default speed",
83
+ speedFast: "Fast",
84
+ speedFastDescription: "1.5x speed, more usage",
85
+ commandFast: "Switch the Codex speed tier (Standard/Fast)",
86
+ commandFastUnavailable: "The current model has no fast tier; /fast only works on Codex models whose catalog advertises one"
78
87
  };
79
88
  /** zh strings, one per {@link en} key. */
80
89
  const zh = {
@@ -96,6 +105,10 @@ const zh = {
96
105
  manualPlaceholder: "粘贴回调 URL 或授权码",
97
106
  submit: "提交",
98
107
  loginMissingUrl: "login 响应缺少 authorizeUrl",
108
+ deviceCodePrompt: "在 GitHub 验证页面输入此验证码:",
109
+ deviceCodeCopy: "复制验证码",
110
+ deviceCodeCopied: "已复制",
111
+ deviceCodeOpenPage: "打开 GitHub 验证页面",
99
112
  usageTitle: "用量",
100
113
  usageRefresh: "刷新",
101
114
  usageLoading: "用量加载中…",
@@ -116,7 +129,14 @@ const zh = {
116
129
  imageClose: "关闭",
117
130
  generatingVideo: "正在生成视频…",
118
131
  videoLoading: "视频加载中…",
119
- videoLoadFailed: "视频加载失败:{message}"
132
+ videoLoadFailed: "视频加载失败:{message}",
133
+ speed: "速度",
134
+ speedStandard: "标准",
135
+ speedStandardDescription: "默认速度",
136
+ speedFast: "快速",
137
+ speedFastDescription: "约 1.5 倍速度,消耗更多用量",
138
+ commandFast: "切换 Codex 速度档(标准/快速)",
139
+ commandFastUnavailable: "当前模型不支持快速档;/fast 仅对目录声明了 fast tier 的 Codex 模型可用"
120
140
  };
121
141
 
122
142
  //#endregion
@@ -124,7 +144,7 @@ const zh = {
124
144
  /** Logical RPC channel served by the node half of this plugin. */
125
145
  const SUBSCRIPTIONS_AUTH_CHANNEL$2 = "/subscriptions-auth";
126
146
  /** Poll cadence while a provider login attempt is busy. */
127
- const POLL_INTERVAL_MS = 2e3;
147
+ const POLL_INTERVAL_MS$1 = 2e3;
128
148
  /** Card display metadata, in page order (names are brand names, not translated). */
129
149
  const PROVIDERS = [
130
150
  {
@@ -138,18 +158,23 @@ const PROVIDERS = [
138
158
  {
139
159
  id: "grok",
140
160
  name: "Grok (X Premium)"
161
+ },
162
+ {
163
+ id: "copilot",
164
+ name: "GitHub Copilot"
141
165
  }
142
166
  ];
143
167
  /** Business error returned by the `/subscriptions-auth` channel (error branch message). */
144
168
  var SubscriptionsAuthError = class extends Error {};
145
169
  /**
146
170
  * Call one `/subscriptions-auth` endpoint and unwrap the business result.
171
+ * Shared by the settings section and the composer Speed toggle.
147
172
  * @param rpc - Connection RPC caller.
148
173
  * @param endpoint - channel-relative endpoint.
149
174
  * @param payload - channel-owned request payload.
150
175
  * @returns the success value, cast by the caller to the endpoint's shape.
151
176
  */
152
- async function callSubscriptionsAuth$1(rpc, endpoint, payload) {
177
+ async function callSubscriptionsAuth(rpc, endpoint, payload) {
153
178
  let result;
154
179
  try {
155
180
  result = await rpc.call(SUBSCRIPTIONS_AUTH_CHANNEL$2, endpoint, payload);
@@ -170,12 +195,12 @@ function messageOf(error) {
170
195
  * @param params - `{name}` template params.
171
196
  * @returns the template with params substituted.
172
197
  */
173
- function fallbackTranslate$2(key, params) {
198
+ function fallbackTranslate$3(key, params) {
174
199
  let text = en[key];
175
200
  for (const [name, value] of Object.entries(params ?? {})) text = text.replaceAll(`{${name}}`, String(value));
176
201
  return text;
177
202
  }
178
- const styles$2 = {
203
+ const styles$4 = {
179
204
  section: {
180
205
  display: "flex",
181
206
  flexDirection: "column",
@@ -337,6 +362,24 @@ const styles$2 = {
337
362
  lineHeight: "22px",
338
363
  background: "var(--dsw-alias-bg-layer-1)",
339
364
  color: "var(--dsw-alias-label-primary)"
365
+ },
366
+ deviceCode: {
367
+ marginTop: 4,
368
+ display: "flex",
369
+ flexDirection: "column",
370
+ gap: 6,
371
+ border: "1px solid var(--dsw-alias-border-l2)",
372
+ borderRadius: 8,
373
+ padding: "10px 12px",
374
+ background: "var(--dsw-alias-bg-layer-1)"
375
+ },
376
+ deviceCodeText: {
377
+ fontFamily: "monospace",
378
+ fontSize: 18,
379
+ lineHeight: "24px",
380
+ letterSpacing: 2,
381
+ color: "var(--dsw-alias-label-primary)",
382
+ userSelect: "all"
340
383
  }
341
384
  };
342
385
  /** Status dot color for one provider state. */
@@ -388,14 +431,18 @@ function usageBarColor(usedPercent) {
388
431
  */
389
432
  function SubscriptionsSection(props) {
390
433
  const { rpc } = props;
391
- const t = props.t ?? fallbackTranslate$2;
434
+ const t = props.t ?? fallbackTranslate$3;
392
435
  const [statuses, setStatuses] = (0, react.useState)({});
393
436
  const [errors, setErrors] = (0, react.useState)({});
394
437
  const [manualDrafts, setManualDrafts] = (0, react.useState)({
395
438
  codex: "",
396
439
  claude: "",
397
- grok: ""
440
+ grok: "",
441
+ copilot: ""
398
442
  });
443
+ /** Pending device-flow codes (copilot), shown while the attempt polls. */
444
+ const [deviceCodes, setDeviceCodes] = (0, react.useState)({});
445
+ const [copiedCode, setCopiedCode] = (0, react.useState)(void 0);
399
446
  const [usages, setUsages] = (0, react.useState)({});
400
447
  const [usageErrors, setUsageErrors] = (0, react.useState)({});
401
448
  const [usageLoading, setUsageLoading] = (0, react.useState)({});
@@ -424,7 +471,7 @@ function SubscriptionsSection(props) {
424
471
  if (rpc === void 0) return;
425
472
  let response;
426
473
  try {
427
- response = await callSubscriptionsAuth$1(rpc, "status", {});
474
+ response = await callSubscriptionsAuth(rpc, "status", {});
428
475
  } catch {
429
476
  return;
430
477
  }
@@ -432,14 +479,22 @@ function SubscriptionsSection(props) {
432
479
  setStatuses(response.providers);
433
480
  for (const { id } of PROVIDERS) {
434
481
  const status = response.providers[id];
435
- if (status.loggedIn || !status.busy) stopPolling(id);
482
+ if (status.loggedIn || !status.busy) {
483
+ stopPolling(id);
484
+ setDeviceCodes((prev) => {
485
+ if (prev[id] === void 0) return prev;
486
+ const next = { ...prev };
487
+ delete next[id];
488
+ return next;
489
+ });
490
+ }
436
491
  }
437
492
  }, [rpc, stopPolling]);
438
493
  const startPolling = (0, react.useCallback)((provider) => {
439
494
  if (pollersRef.current.has(provider)) return;
440
495
  pollersRef.current.set(provider, setInterval(() => {
441
496
  refresh();
442
- }, POLL_INTERVAL_MS));
497
+ }, POLL_INTERVAL_MS$1));
443
498
  }, [refresh]);
444
499
  (0, react.useEffect)(() => {
445
500
  mountedRef.current = true;
@@ -464,7 +519,7 @@ function SubscriptionsSection(props) {
464
519
  [provider]: true
465
520
  }));
466
521
  try {
467
- const usage = await callSubscriptionsAuth$1(rpc, "usage", { provider });
522
+ const usage = await callSubscriptionsAuth(rpc, "usage", { provider });
468
523
  if (!mountedRef.current) return;
469
524
  setUsages((prev) => ({
470
525
  ...prev,
@@ -517,13 +572,12 @@ function SubscriptionsSection(props) {
517
572
  if (rpc === void 0) return;
518
573
  setProviderError(provider, void 0);
519
574
  try {
520
- const response = await callSubscriptionsAuth$1(rpc, "login", { provider });
575
+ const response = await callSubscriptionsAuth(rpc, "login", { provider });
521
576
  if (typeof response.authorizeUrl === "string" && response.authorizeUrl === "") {
522
577
  await refresh();
523
578
  return;
524
579
  }
525
580
  if (typeof response.authorizeUrl !== "string") throw new SubscriptionsAuthError(t("loginMissingUrl"));
526
- window.open(response.authorizeUrl, "_blank", "noopener");
527
581
  if (!mountedRef.current) return;
528
582
  setStatuses((prev) => ({
529
583
  ...prev,
@@ -533,6 +587,14 @@ function SubscriptionsSection(props) {
533
587
  loggedIn: false
534
588
  }
535
589
  }));
590
+ if (typeof response.userCode === "string" && response.userCode.length > 0) setDeviceCodes((prev) => ({
591
+ ...prev,
592
+ [provider]: {
593
+ userCode: response.userCode,
594
+ verificationUrl: response.authorizeUrl
595
+ }
596
+ }));
597
+ else window.open(response.authorizeUrl, "_blank", "noopener");
536
598
  startPolling(provider);
537
599
  } catch (error) {
538
600
  setProviderError(provider, messageOf(error));
@@ -547,7 +609,7 @@ function SubscriptionsSection(props) {
547
609
  if (rpc === void 0) return;
548
610
  stopPolling(provider);
549
611
  try {
550
- await callSubscriptionsAuth$1(rpc, "cancel", { provider });
612
+ await callSubscriptionsAuth(rpc, "cancel", { provider });
551
613
  } catch (error) {
552
614
  setProviderError(provider, messageOf(error));
553
615
  }
@@ -564,7 +626,7 @@ function SubscriptionsSection(props) {
564
626
  if (input === "") return;
565
627
  setProviderError(provider, void 0);
566
628
  try {
567
- await callSubscriptionsAuth$1(rpc, "manual", {
629
+ await callSubscriptionsAuth(rpc, "manual", {
568
630
  provider,
569
631
  input
570
632
  });
@@ -587,7 +649,7 @@ function SubscriptionsSection(props) {
587
649
  if (!window.confirm(t("logoutConfirm", { provider: name }))) return;
588
650
  setProviderError(provider, void 0);
589
651
  try {
590
- await callSubscriptionsAuth$1(rpc, "logout", { provider });
652
+ await callSubscriptionsAuth(rpc, "logout", { provider });
591
653
  } catch (error) {
592
654
  setProviderError(provider, messageOf(error));
593
655
  }
@@ -598,52 +660,62 @@ function SubscriptionsSection(props) {
598
660
  setProviderError,
599
661
  refresh
600
662
  ]);
663
+ const copyDeviceCode = (0, react.useCallback)((provider, userCode) => {
664
+ navigator.clipboard?.writeText(userCode).then(() => {
665
+ if (!mountedRef.current) return;
666
+ setCopiedCode(provider);
667
+ setTimeout(() => {
668
+ if (mountedRef.current) setCopiedCode((current) => current === provider ? void 0 : current);
669
+ }, 1500);
670
+ }).catch(() => void 0);
671
+ }, []);
601
672
  if (rpc === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
602
- style: styles$2.intro,
673
+ style: styles$4.intro,
603
674
  children: t("unavailable")
604
675
  });
605
676
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
606
- style: styles$2.section,
677
+ style: styles$4.section,
607
678
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
608
- style: styles$2.intro,
679
+ style: styles$4.intro,
609
680
  children: t("intro")
610
681
  }), PROVIDERS.map(({ id, name }) => {
611
682
  const status = statuses[id];
612
683
  const busy = status?.busy === true;
684
+ const deviceCode = deviceCodes[id];
613
685
  const usage = usages[id];
614
686
  const usageError = usageErrors[id];
615
687
  const showUsage = status?.loggedIn === true && usage?.supported !== false && (usage !== void 0 || usageError !== void 0 || usageLoading[id] === true);
616
688
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
617
- style: styles$2.card,
689
+ style: styles$4.card,
618
690
  children: [
619
691
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
620
- style: styles$2.cardHeader,
692
+ style: styles$4.cardHeader,
621
693
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
622
- ...styles$2.dot,
694
+ ...styles$4.dot,
623
695
  background: dotColor(status)
624
696
  } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
625
- style: styles$2.name,
697
+ style: styles$4.name,
626
698
  children: name
627
699
  })]
628
700
  }),
629
701
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
630
- style: styles$2.statusLine,
702
+ style: styles$4.statusLine,
631
703
  children: statusText(t, status)
632
704
  }),
633
705
  status?.detail !== void 0 && status.detail !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
634
- style: styles$2.statusLine,
706
+ style: styles$4.statusLine,
635
707
  children: status.detail
636
708
  }),
637
709
  errors[id] !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
638
- style: styles$2.errorLine,
710
+ style: styles$4.errorLine,
639
711
  children: errors[id]
640
712
  }),
641
713
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
642
- style: styles$2.actions,
714
+ style: styles$4.actions,
643
715
  children: [
644
716
  !busy && status?.loggedIn !== true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
645
717
  type: "button",
646
- style: styles$2.button,
718
+ style: styles$4.button,
647
719
  onClick: () => {
648
720
  login(id);
649
721
  },
@@ -651,7 +723,7 @@ function SubscriptionsSection(props) {
651
723
  }),
652
724
  busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
653
725
  type: "button",
654
- style: styles$2.button,
726
+ style: styles$4.button,
655
727
  onClick: () => {
656
728
  cancel(id);
657
729
  },
@@ -659,7 +731,7 @@ function SubscriptionsSection(props) {
659
731
  }),
660
732
  status?.loggedIn === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
661
733
  type: "button",
662
- style: styles$2.button,
734
+ style: styles$4.button,
663
735
  onClick: () => {
664
736
  logout(id, name);
665
737
  },
@@ -668,23 +740,23 @@ function SubscriptionsSection(props) {
668
740
  ]
669
741
  }),
670
742
  showUsage && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
671
- style: styles$2.usage,
743
+ style: styles$4.usage,
672
744
  children: [
673
745
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
674
- style: styles$2.usageHeader,
746
+ style: styles$4.usageHeader,
675
747
  children: [
676
748
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
677
- style: styles$2.usageTitle,
749
+ style: styles$4.usageTitle,
678
750
  children: t("usageTitle")
679
751
  }),
680
752
  usage?.plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
681
- style: styles$2.usagePlan,
753
+ style: styles$4.usagePlan,
682
754
  children: t("usagePlan", { plan: usage.plan })
683
755
  }),
684
756
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
685
757
  type: "button",
686
758
  style: {
687
- ...styles$2.usageRefresh,
759
+ ...styles$4.usageRefresh,
688
760
  ...usageLoading[id] === true ? {
689
761
  opacity: .5,
690
762
  cursor: "default"
@@ -699,28 +771,28 @@ function SubscriptionsSection(props) {
699
771
  ]
700
772
  }),
701
773
  usage === void 0 && usageError === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
702
- style: styles$2.statusLine,
774
+ style: styles$4.statusLine,
703
775
  children: t("usageLoading")
704
776
  }),
705
777
  usageError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
706
- style: styles$2.errorLine,
778
+ style: styles$4.errorLine,
707
779
  children: t("usageError", { message: usageError })
708
780
  }),
709
781
  usage?.windows !== void 0 && usage.windows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
710
- style: styles$2.statusLine,
782
+ style: styles$4.statusLine,
711
783
  children: t("usageEmpty")
712
784
  }),
713
785
  (usage?.windows ?? []).map((window$1, index) => {
714
786
  const percent = Math.min(100, Math.max(0, window$1.usedPercent));
715
787
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
716
- style: styles$2.usageRow,
788
+ style: styles$4.usageRow,
717
789
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
718
- style: styles$2.usageMeta,
790
+ style: styles$4.usageMeta,
719
791
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: usageWindowLabel(t, window$1) }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [`${String(Math.round(percent))}%`, window$1.resetsAt !== void 0 && ` · ${t("usageResets", { date: new Date(window$1.resetsAt).toLocaleString() })}`] })]
720
792
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
721
- style: styles$2.usageTrack,
793
+ style: styles$4.usageTrack,
722
794
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
723
- ...styles$2.usageFill,
795
+ ...styles$4.usageFill,
724
796
  width: `${String(percent)}%`,
725
797
  background: usageBarColor(percent)
726
798
  } })
@@ -729,12 +801,43 @@ function SubscriptionsSection(props) {
729
801
  })
730
802
  ]
731
803
  }),
732
- busy && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
733
- style: styles$2.manual,
804
+ busy && deviceCode !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
805
+ style: styles$4.deviceCode,
806
+ children: [
807
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
808
+ style: styles$4.statusLine,
809
+ children: t("deviceCodePrompt")
810
+ }),
811
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
812
+ style: styles$4.deviceCodeText,
813
+ children: deviceCode.userCode
814
+ }),
815
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
816
+ style: styles$4.actions,
817
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
818
+ type: "button",
819
+ style: styles$4.button,
820
+ onClick: () => {
821
+ copyDeviceCode(id, deviceCode.userCode);
822
+ },
823
+ children: copiedCode === id ? t("deviceCodeCopied") : t("deviceCodeCopy")
824
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
825
+ type: "button",
826
+ style: styles$4.button,
827
+ onClick: () => {
828
+ window.open(deviceCode.verificationUrl, "_blank", "noopener");
829
+ },
830
+ children: t("deviceCodeOpenPage")
831
+ })]
832
+ })
833
+ ]
834
+ }),
835
+ busy && deviceCode === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
836
+ style: styles$4.manual,
734
837
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("manualSummary") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
735
- style: styles$2.manualRow,
838
+ style: styles$4.manualRow,
736
839
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
737
- style: styles$2.manualInput,
840
+ style: styles$4.manualInput,
738
841
  value: manualDrafts[id],
739
842
  placeholder: t("manualPlaceholder"),
740
843
  onChange: (event) => setManualDrafts((prev) => ({
@@ -743,7 +846,7 @@ function SubscriptionsSection(props) {
743
846
  }))
744
847
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
745
848
  type: "button",
746
- style: styles$2.button,
849
+ style: styles$4.button,
747
850
  onClick: () => {
748
851
  submitManual(id);
749
852
  },
@@ -757,6 +860,229 @@ function SubscriptionsSection(props) {
757
860
  });
758
861
  }
759
862
 
863
+ //#endregion
864
+ //#region src/client/ImageGallery.tsx
865
+ /** Display box for a lone image (platform rule): long edge 240px with the
866
+ * rendered aspect ratio clamped to [0.25, 4] — the overflow is cropped by
867
+ * `object-fit: cover` — and never upscaled past the image's natural size. The
868
+ * crop anchor keeps the top of very tall images and the left of very wide
869
+ * ones, where the informative content usually starts. */
870
+ function singleFit(attachment) {
871
+ const natural = attachment.width / attachment.height;
872
+ const ratio = Math.min(4, Math.max(.25, natural));
873
+ const box = ratio >= 1 ? {
874
+ width: 240,
875
+ height: 240 / ratio
876
+ } : {
877
+ width: 240 * ratio,
878
+ height: 240
879
+ };
880
+ const scale = Math.min(1, attachment.width / box.width, attachment.height / box.height);
881
+ return {
882
+ width: Math.max(1, Math.round(box.width * scale)),
883
+ height: Math.max(1, Math.round(box.height * scale)),
884
+ objectPosition: natural < .25 ? "center top" : natural > 4 ? "left center" : "center"
885
+ };
886
+ }
887
+ const styles$3 = {
888
+ gallery: {
889
+ display: "flex",
890
+ flexWrap: "wrap",
891
+ gap: 8,
892
+ justifyContent: "flex-start"
893
+ },
894
+ frame: {
895
+ display: "grid",
896
+ placeItems: "center",
897
+ overflow: "hidden",
898
+ padding: 0,
899
+ border: "1px solid var(--dsw-alias-border-l2-darkmode-thin)",
900
+ borderRadius: 8,
901
+ background: "var(--dsw-alias-interactive-bg-hover-solid)",
902
+ cursor: "zoom-in"
903
+ },
904
+ tile: {
905
+ width: 64,
906
+ height: 64
907
+ },
908
+ img: {
909
+ width: "100%",
910
+ height: "100%",
911
+ objectFit: "cover",
912
+ display: "block"
913
+ },
914
+ loading: {
915
+ fontSize: 12,
916
+ color: "var(--dsw-alias-label-tertiary)",
917
+ padding: "0 8px"
918
+ },
919
+ error: {
920
+ fontSize: 12,
921
+ color: "var(--dsw-alias-state-error-primary)",
922
+ cursor: "pointer",
923
+ border: "1px solid var(--dsw-alias-border-l2-darkmode-thin)",
924
+ borderRadius: 8,
925
+ background: "transparent",
926
+ padding: "6px 10px"
927
+ },
928
+ overlay: {
929
+ position: "fixed",
930
+ inset: 0,
931
+ zIndex: 1e3,
932
+ display: "grid",
933
+ placeItems: "center",
934
+ background: "rgba(0, 0, 0, 0.72)",
935
+ padding: 24
936
+ },
937
+ overlayImg: {
938
+ maxWidth: "92vw",
939
+ maxHeight: "92vh",
940
+ objectFit: "contain",
941
+ borderRadius: 4
942
+ },
943
+ close: {
944
+ position: "absolute",
945
+ top: 12,
946
+ right: 12,
947
+ width: 32,
948
+ height: 32,
949
+ display: "grid",
950
+ placeItems: "center",
951
+ border: "none",
952
+ borderRadius: "50%",
953
+ cursor: "pointer",
954
+ background: "rgba(255, 255, 255, 0.16)",
955
+ color: "#fff",
956
+ fontSize: 16,
957
+ lineHeight: 1
958
+ }
959
+ };
960
+ /**
961
+ * Full-viewport original-image preview: backdrop or close-control click and
962
+ * Escape all dismiss; the image itself is inert so a click on it does not
963
+ * fall through to the backdrop dismissal.
964
+ */
965
+ function ImageLightbox({ src, alt, labels, onClose }) {
966
+ (0, react.useEffect)(() => {
967
+ const onKey = (event) => {
968
+ if (event.key === "Escape") onClose();
969
+ };
970
+ window.addEventListener("keydown", onKey);
971
+ return () => {
972
+ window.removeEventListener("keydown", onKey);
973
+ };
974
+ }, [onClose]);
975
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
976
+ role: "dialog",
977
+ "aria-label": labels.dialog,
978
+ style: styles$3.overlay,
979
+ onClick: onClose,
980
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
981
+ src,
982
+ alt,
983
+ style: styles$3.overlayImg,
984
+ onClick: (event) => {
985
+ event.stopPropagation();
986
+ }
987
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
988
+ type: "button",
989
+ "aria-label": labels.close,
990
+ style: styles$3.close,
991
+ onClick: onClose,
992
+ children: "×"
993
+ })]
994
+ });
995
+ }
996
+ /**
997
+ * Compact history renderer with retryable loading and click-to-open original
998
+ * preview. A lone image renders at its `singleFit` size; an image among
999
+ * several renders as a fixed 64px square tile.
1000
+ */
1001
+ function MessageImage({ attachment, load, variant, labels }) {
1002
+ const [src, setSrc] = (0, react.useState)(null);
1003
+ const [error, setError] = (0, react.useState)(false);
1004
+ const [open, setOpen] = (0, react.useState)(false);
1005
+ const [attempt, setAttempt] = (0, react.useState)(0);
1006
+ const retry = (0, react.useCallback)(() => {
1007
+ setAttempt((a) => a + 1);
1008
+ }, []);
1009
+ const close = (0, react.useCallback)(() => {
1010
+ setOpen(false);
1011
+ }, []);
1012
+ const fit = (0, react.useMemo)(() => variant === "single" ? singleFit(attachment) : void 0, [attachment, variant]);
1013
+ (0, react.useEffect)(() => {
1014
+ let live = true;
1015
+ setError(false);
1016
+ setSrc(null);
1017
+ load(attachment).then((url) => {
1018
+ if (live) setSrc(url);
1019
+ }).catch(() => {
1020
+ if (live) setError(true);
1021
+ });
1022
+ return () => {
1023
+ live = false;
1024
+ };
1025
+ }, [
1026
+ attachment,
1027
+ load,
1028
+ attempt
1029
+ ]);
1030
+ const label = attachment.name ?? labels.image;
1031
+ if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1032
+ type: "button",
1033
+ style: styles$3.error,
1034
+ onClick: retry,
1035
+ children: labels.loadFailed
1036
+ });
1037
+ const box = fit === void 0 ? styles$3.tile : {
1038
+ width: fit.width,
1039
+ height: fit.height
1040
+ };
1041
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1042
+ type: "button",
1043
+ style: {
1044
+ ...styles$3.frame,
1045
+ ...box
1046
+ },
1047
+ title: labels.open,
1048
+ "aria-label": labels.openNamed(label),
1049
+ onClick: () => {
1050
+ if (src !== null) setOpen(true);
1051
+ },
1052
+ children: src === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1053
+ style: styles$3.loading,
1054
+ children: labels.loading
1055
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
1056
+ src,
1057
+ alt: label,
1058
+ style: {
1059
+ ...styles$3.img,
1060
+ objectPosition: fit?.objectPosition
1061
+ }
1062
+ })
1063
+ }), open && src !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageLightbox, {
1064
+ src,
1065
+ alt: label,
1066
+ labels: labels.lightbox,
1067
+ onClose: close
1068
+ })] });
1069
+ }
1070
+ /** Wrapping image group: a lone image renders large, several render as 64px
1071
+ * square tiles (same rule as the platform gallery). */
1072
+ function ImageGallery({ images, load, labels }) {
1073
+ if (images.length === 0) return null;
1074
+ const variant = images.length === 1 ? "single" : "tile";
1075
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1076
+ style: styles$3.gallery,
1077
+ children: images.map((image, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageImage, {
1078
+ attachment: image.attachment,
1079
+ load,
1080
+ variant,
1081
+ labels
1082
+ }, `${image.attachment.attachmentId}:${index}`))
1083
+ });
1084
+ }
1085
+
760
1086
  //#endregion
761
1087
  //#region src/client/ImageGenerateToolview.tsx
762
1088
  /** Logical RPC channel served by the node half of this plugin. */
@@ -770,7 +1096,7 @@ const PROMPT_MAX_LENGTH$1 = 60;
770
1096
  * @param payload - channel-owned request payload.
771
1097
  * @returns the success value, cast by the caller to the endpoint's shape.
772
1098
  */
773
- async function callSubscriptionsAuth(rpc, endpoint, payload) {
1099
+ async function callSubscriptionsAuth$1(rpc, endpoint, payload) {
774
1100
  const result = await rpc.call(SUBSCRIPTIONS_AUTH_CHANNEL$1, endpoint, payload);
775
1101
  if (!result.ok) throw new Error(result.error.message);
776
1102
  return result.value;
@@ -781,7 +1107,7 @@ async function callSubscriptionsAuth(rpc, endpoint, payload) {
781
1107
  * @returns loader resolving an attachment ref to a data URL.
782
1108
  */
783
1109
  function createImageLoader(rpc) {
784
- return (attachment) => callSubscriptionsAuth(rpc, "image", { ...attachment }).then((result) => `data:${result.mediaType};base64,${result.dataBase64}`);
1110
+ return (attachment) => callSubscriptionsAuth$1(rpc, "image", { ...attachment }).then((result) => `data:${result.mediaType};base64,${result.dataBase64}`);
785
1111
  }
786
1112
  /**
787
1113
  * English-dictionary fallback for a missing locale seat (standalone renders);
@@ -790,7 +1116,7 @@ function createImageLoader(rpc) {
790
1116
  * @param params - `{name}` template params.
791
1117
  * @returns the template with params substituted.
792
1118
  */
793
- function fallbackTranslate$1(key, params) {
1119
+ function fallbackTranslate$2(key, params) {
794
1120
  let text = en[key];
795
1121
  for (const [name, value] of Object.entries(params ?? {})) text = text.replaceAll(`{${name}}`, String(value));
796
1122
  return text;
@@ -830,7 +1156,7 @@ function resultImages(block) {
830
1156
  for (const part of block.content) if (part.type === "image") images.push({ attachment: part.attachment });
831
1157
  return images;
832
1158
  }
833
- const styles$1 = {
1159
+ const styles$2 = {
834
1160
  container: {
835
1161
  display: "flex",
836
1162
  flexDirection: "column",
@@ -884,7 +1210,7 @@ const styles$1 = {
884
1210
  */
885
1211
  function ImageGenerateToolview(props) {
886
1212
  const { block, load } = props;
887
- const t = props.t ?? fallbackTranslate$1;
1213
+ const t = props.t ?? fallbackTranslate$2;
888
1214
  if (block === void 0) return null;
889
1215
  const settled = "kind" in block;
890
1216
  const title = `image_generate: ${derivePrompt$1((settled ? block.call?.argsRaw : block.argsRaw) ?? "")}`;
@@ -902,34 +1228,33 @@ function ImageGenerateToolview(props) {
902
1228
  }
903
1229
  };
904
1230
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
905
- style: styles$1.container,
1231
+ style: styles$2.container,
906
1232
  children: [
907
1233
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
908
- style: styles$1.row,
1234
+ style: styles$2.row,
909
1235
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
910
- style: styles$1.icon,
1236
+ style: styles$2.icon,
911
1237
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconSparkle16, { size: 14 })
912
1238
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
913
- style: styles$1.title,
1239
+ style: styles$2.title,
914
1240
  children: title
915
1241
  })]
916
1242
  }),
917
1243
  !settled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
918
- style: styles$1.subtle,
1244
+ style: styles$2.subtle,
919
1245
  children: t("generating")
920
1246
  }),
921
1247
  settled && block.isError && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
922
- style: styles$1.error,
1248
+ style: styles$2.error,
923
1249
  children: text.split("\n", 1)[0]
924
1250
  }),
925
- settled && !block.isError && images.length > 0 && load !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_attachment.ImageGallery, {
1251
+ settled && !block.isError && images.length > 0 && load !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageGallery, {
926
1252
  images,
927
1253
  load,
928
- align: "start",
929
1254
  labels
930
1255
  }),
931
1256
  settled && !block.isError && images.length === 0 && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
932
- style: styles$1.output,
1257
+ style: styles$2.output,
933
1258
  children: text
934
1259
  })
935
1260
  ]
@@ -958,7 +1283,7 @@ function createVideoLoader(rpc) {
958
1283
  * English-dictionary fallback for a missing locale seat (standalone renders);
959
1284
  * the framework always supplies the namespace-bound one.
960
1285
  */
961
- function fallbackTranslate(key, params) {
1286
+ function fallbackTranslate$1(key, params) {
962
1287
  let text = en[key];
963
1288
  for (const [name, value] of Object.entries(params ?? {})) text = text.replaceAll(`{${name}}`, String(value));
964
1289
  return text;
@@ -1015,7 +1340,7 @@ function base64Bytes(dataBase64) {
1015
1340
  for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
1016
1341
  return bytes;
1017
1342
  }
1018
- const styles = {
1343
+ const styles$1 = {
1019
1344
  container: {
1020
1345
  display: "flex",
1021
1346
  flexDirection: "column",
@@ -1076,7 +1401,7 @@ const styles = {
1076
1401
  */
1077
1402
  function VideoGenerateToolview(props) {
1078
1403
  const { block, loadVideo } = props;
1079
- const t = props.t ?? fallbackTranslate;
1404
+ const t = props.t ?? fallbackTranslate$1;
1080
1405
  const settled = block !== void 0 && "kind" in block;
1081
1406
  const isError = settled && block.isError;
1082
1407
  const fileName = block !== void 0 && settled && !isError ? resolveFileName(block) : void 0;
@@ -1109,48 +1434,285 @@ function VideoGenerateToolview(props) {
1109
1434
  const title = `video_generate: ${derivePrompt((settled ? block.call?.argsRaw : block.argsRaw) ?? "")}`;
1110
1435
  const text = settled ? resultText(block) : "";
1111
1436
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1112
- style: styles.container,
1437
+ style: styles$1.container,
1113
1438
  children: [
1114
1439
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1115
- style: styles.row,
1440
+ style: styles$1.row,
1116
1441
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1117
- style: styles.icon,
1442
+ style: styles$1.icon,
1118
1443
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconSparkle16, { size: 14 })
1119
1444
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1120
- style: styles.title,
1445
+ style: styles$1.title,
1121
1446
  children: title
1122
1447
  })]
1123
1448
  }),
1124
1449
  !settled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1125
- style: styles.subtle,
1450
+ style: styles$1.subtle,
1126
1451
  children: t("generatingVideo")
1127
1452
  }),
1128
1453
  settled && isError && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1129
- style: styles.error,
1454
+ style: styles$1.error,
1130
1455
  children: text.split("\n", 1)[0]
1131
1456
  }),
1132
1457
  settled && !isError && fileName !== void 0 && load.phase === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1133
- style: styles.subtle,
1458
+ style: styles$1.subtle,
1134
1459
  children: t("videoLoading")
1135
1460
  }),
1136
1461
  settled && !isError && fileName !== void 0 && load.phase === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1137
- style: styles.error,
1462
+ style: styles$1.error,
1138
1463
  children: t("videoLoadFailed", { message: load.message })
1139
1464
  }),
1140
1465
  settled && !isError && fileName !== void 0 && load.phase === "ready" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("video", {
1141
- style: styles.video,
1466
+ style: styles$1.video,
1142
1467
  src: load.url,
1143
1468
  controls: true,
1144
1469
  preload: "metadata"
1145
1470
  }),
1146
1471
  settled && !isError && fileName === void 0 && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1147
- style: styles.output,
1472
+ style: styles$1.output,
1148
1473
  children: text
1149
1474
  })
1150
1475
  ]
1151
1476
  });
1152
1477
  }
1153
1478
 
1479
+ //#endregion
1480
+ //#region src/client/SpeedSelect.tsx
1481
+ /**
1482
+ * The `loadSpeed` half of the inject face: the plugin's own speed state plus
1483
+ * the host's current model selection (the visibility gate). A model-RPC
1484
+ * failure throws rather than answering "hidden" — the caller keeps its last
1485
+ * known state, so a transient failure never locks the toggle away.
1486
+ *
1487
+ * `sessionId` is a plain string: slot and command contexts brand it through
1488
+ * different dsh-session copies, and only the API-client boundary needs one.
1489
+ */
1490
+ function createSpeedLoader(connection, sessionId) {
1491
+ return async () => {
1492
+ const state = await callSubscriptionsAuth(connection.rpc, "speed", { sessionId });
1493
+ const { result } = await connection.api.sessions.models({ sessionId });
1494
+ if (!result.ok) throw new Error(`session.models failed: ${result.error.code}: ${result.error.message}`);
1495
+ const current = result.value.current;
1496
+ return {
1497
+ visible: current !== null && current.provider === "codex" && state.fastModels.includes(current.model),
1498
+ tier: state.tier
1499
+ };
1500
+ };
1501
+ }
1502
+ /** The `setSpeed` half of the inject face: boolean outcome for the component's busy state. */
1503
+ function createSpeedSetter(connection, sessionId) {
1504
+ return (tier) => callSubscriptionsAuth(connection.rpc, "setSpeed", {
1505
+ sessionId,
1506
+ tier
1507
+ }).then(() => true, () => false);
1508
+ }
1509
+ /** English-dictionary fallback for a missing inject `t` (standalone renders). */
1510
+ function fallbackTranslate(key) {
1511
+ return en[key];
1512
+ }
1513
+ const TIERS = ["standard", "fast"];
1514
+ /**
1515
+ * The composer Speed control: a trigger reading `速度 · 快速`/`速度 · 标准`
1516
+ * that opens a two-row menu (standard/fast with descriptions, check mark on
1517
+ * the current tier). Mount and every open reload the host state so a model
1518
+ * switch made since the last open self-corrects.
1519
+ */
1520
+ /** How often the control re-reads the host state (model switches arrive only by asking). */
1521
+ const POLL_INTERVAL_MS = 3e3;
1522
+ /**
1523
+ * The composer Speed control: a trigger reading `速度 · 快速`/`速度 · 标准`
1524
+ * that opens a two-row menu (standard/fast with descriptions, check mark on
1525
+ * the current tier). The host pushes nothing on a model switch, so the
1526
+ * control re-reads on a slow poll with a single-flight guard; a failed read
1527
+ * keeps the last known state, so a transient RPC failure can never lock the
1528
+ * toggle away (the earlier mount-only load had no recovery path).
1529
+ */
1530
+ function SpeedSelect({ loadSpeed, setSpeed, t }) {
1531
+ const translate = t ?? fallbackTranslate;
1532
+ const [state, setState] = (0, react.useState)(null);
1533
+ const [open, setOpen] = (0, react.useState)(false);
1534
+ const [busy, setBusy] = (0, react.useState)(false);
1535
+ const rootRef = (0, react.useRef)(null);
1536
+ const loadRef = (0, react.useRef)(loadSpeed);
1537
+ loadRef.current = loadSpeed;
1538
+ (0, react.useEffect)(() => {
1539
+ if (loadRef.current === void 0) return;
1540
+ let cancelled = false;
1541
+ let inflight = false;
1542
+ const reload = () => {
1543
+ const load = loadRef.current;
1544
+ if (load === void 0 || inflight) return;
1545
+ inflight = true;
1546
+ load().then((loaded) => {
1547
+ if (!cancelled) setState(loaded);
1548
+ }, () => {}).finally(() => {
1549
+ inflight = false;
1550
+ });
1551
+ };
1552
+ reload();
1553
+ const timer = setInterval(reload, POLL_INTERVAL_MS);
1554
+ return () => {
1555
+ cancelled = true;
1556
+ clearInterval(timer);
1557
+ };
1558
+ }, []);
1559
+ (0, react.useEffect)(() => {
1560
+ if (!open) return;
1561
+ const closeOutside = (event) => {
1562
+ if (!rootRef.current?.contains(event.target)) setOpen(false);
1563
+ };
1564
+ document.addEventListener("mousedown", closeOutside);
1565
+ return () => {
1566
+ document.removeEventListener("mousedown", closeOutside);
1567
+ };
1568
+ }, [open]);
1569
+ if (loadSpeed === void 0 || setSpeed === void 0 || state === null || !state.visible) return null;
1570
+ const choose = (tier) => {
1571
+ if (busy) return;
1572
+ if (tier === state.tier) {
1573
+ setOpen(false);
1574
+ return;
1575
+ }
1576
+ setBusy(true);
1577
+ setSpeed(tier).then((ok) => {
1578
+ setBusy(false);
1579
+ if (ok) {
1580
+ setState({
1581
+ visible: true,
1582
+ tier
1583
+ });
1584
+ setOpen(false);
1585
+ }
1586
+ });
1587
+ };
1588
+ const show = () => {
1589
+ setOpen(true);
1590
+ const load = loadRef.current;
1591
+ if (load === void 0) return;
1592
+ load().then(setState, () => {});
1593
+ };
1594
+ const tierName = (tier) => translate(tier === "fast" ? "speedFast" : "speedStandard");
1595
+ const tierDescription = (tier) => translate(tier === "fast" ? "speedFastDescription" : "speedStandardDescription");
1596
+ const triggerLabel = `${translate("speed")} · ${tierName(state.tier)}`;
1597
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1598
+ ref: rootRef,
1599
+ style: styles.root,
1600
+ onKeyDown: (event) => {
1601
+ if (event.key === "Escape" && open) {
1602
+ event.preventDefault();
1603
+ setOpen(false);
1604
+ }
1605
+ },
1606
+ children: [open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1607
+ style: styles.menu,
1608
+ role: "menu",
1609
+ "aria-label": translate("speed"),
1610
+ children: TIERS.map((tier) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1611
+ type: "button",
1612
+ role: "menuitemradio",
1613
+ "aria-checked": tier === state.tier,
1614
+ style: styles.item,
1615
+ disabled: busy,
1616
+ onClick: () => {
1617
+ choose(tier);
1618
+ },
1619
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1620
+ style: styles.itemCheck,
1621
+ children: tier === state.tier ? "✓" : ""
1622
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1623
+ style: styles.itemText,
1624
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1625
+ style: styles.itemName,
1626
+ children: tierName(tier)
1627
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1628
+ style: styles.itemDescription,
1629
+ children: tierDescription(tier)
1630
+ })]
1631
+ })]
1632
+ }, tier))
1633
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1634
+ type: "button",
1635
+ style: styles.trigger,
1636
+ "aria-haspopup": "menu",
1637
+ "aria-expanded": open,
1638
+ title: triggerLabel,
1639
+ disabled: busy,
1640
+ onClick: () => {
1641
+ if (open) setOpen(false);
1642
+ else show();
1643
+ },
1644
+ children: triggerLabel
1645
+ })]
1646
+ });
1647
+ }
1648
+ const styles = {
1649
+ root: {
1650
+ position: "relative",
1651
+ display: "inline-flex"
1652
+ },
1653
+ trigger: {
1654
+ border: "1px solid var(--dsw-alias-border-l2)",
1655
+ borderRadius: 8,
1656
+ background: "transparent",
1657
+ color: "var(--dsw-alias-label-secondary)",
1658
+ font: "inherit",
1659
+ fontSize: 12,
1660
+ lineHeight: "18px",
1661
+ padding: "2px 8px",
1662
+ cursor: "pointer",
1663
+ whiteSpace: "nowrap"
1664
+ },
1665
+ menu: {
1666
+ position: "absolute",
1667
+ bottom: "100%",
1668
+ right: 0,
1669
+ marginBottom: 4,
1670
+ minWidth: 180,
1671
+ padding: 4,
1672
+ zIndex: 20,
1673
+ background: "var(--dsw-alias-bg-layer-1)",
1674
+ border: "1px solid var(--dsw-alias-border-l2)",
1675
+ borderRadius: 8,
1676
+ display: "flex",
1677
+ flexDirection: "column",
1678
+ gap: 2
1679
+ },
1680
+ item: {
1681
+ display: "flex",
1682
+ alignItems: "flex-start",
1683
+ gap: 6,
1684
+ width: "100%",
1685
+ border: "none",
1686
+ borderRadius: 6,
1687
+ background: "transparent",
1688
+ padding: "6px 8px",
1689
+ cursor: "pointer",
1690
+ font: "inherit",
1691
+ textAlign: "left"
1692
+ },
1693
+ itemCheck: {
1694
+ width: 14,
1695
+ flexShrink: 0,
1696
+ fontSize: 12,
1697
+ lineHeight: "18px",
1698
+ color: "var(--dsw-alias-label-primary)"
1699
+ },
1700
+ itemText: {
1701
+ display: "flex",
1702
+ flexDirection: "column"
1703
+ },
1704
+ itemName: {
1705
+ fontSize: 12,
1706
+ lineHeight: "18px",
1707
+ color: "var(--dsw-alias-label-primary)"
1708
+ },
1709
+ itemDescription: {
1710
+ fontSize: 11,
1711
+ lineHeight: "16px",
1712
+ color: "var(--dsw-alias-label-tertiary)"
1713
+ }
1714
+ };
1715
+
1154
1716
  //#endregion
1155
1717
  //#region src/client/index.ts
1156
1718
  /** Dictionary namespace owned by this plugin. */
@@ -1203,6 +1765,46 @@ function apply(ctx) {
1203
1765
  locale: NS,
1204
1766
  inject: videoToolviewInjected
1205
1767
  }, VideoGenerateToolview));
1768
+ ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
1769
+ name: "conversation.input.right",
1770
+ id: "codex-speed",
1771
+ order: 0,
1772
+ locale: NS,
1773
+ inject: (sessionId) => ({
1774
+ loadSpeed: createSpeedLoader(connection, sessionId),
1775
+ setSpeed: createSpeedSetter(connection, sessionId)
1776
+ })
1777
+ }, SpeedSelect));
1778
+ ctx.inject(["commandUi"], (scope) => {
1779
+ const command = scope.get("commandUi");
1780
+ scope.effect(() => command.register({
1781
+ name: "fast",
1782
+ description: t("commandFast"),
1783
+ available: () => true,
1784
+ ui: {
1785
+ kind: "popupSelect",
1786
+ options: async (session) => {
1787
+ const state = await createSpeedLoader(connection, session.sessionId)();
1788
+ if (!state.visible) throw new Error(t("commandFastUnavailable"));
1789
+ return [{
1790
+ id: "standard",
1791
+ label: t("speedStandard"),
1792
+ detail: t("speedStandardDescription")
1793
+ }, {
1794
+ id: "fast",
1795
+ label: t("speedFast"),
1796
+ detail: t("speedFastDescription")
1797
+ }].map((option) => ({
1798
+ ...option,
1799
+ active: option.id === state.tier
1800
+ }));
1801
+ },
1802
+ onSelect: async (option, session) => {
1803
+ await createSpeedSetter(connection, session.sessionId)(option.id);
1804
+ }
1805
+ }
1806
+ }), "dsh-plugin-subscriptions: /fast contribution");
1807
+ });
1206
1808
  }
1207
1809
 
1208
1810
  //#endregion