dsh-plugin-subscriptions 0.4.1 → 0.5.0

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
@@ -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. */
@@ -74,7 +72,14 @@ const en = {
74
72
  imageClose: "Close",
75
73
  generatingVideo: "Generating video…",
76
74
  videoLoading: "Loading video…",
77
- videoLoadFailed: "Video failed to load: {message}"
75
+ videoLoadFailed: "Video failed to load: {message}",
76
+ speed: "Speed",
77
+ speedStandard: "Standard",
78
+ speedStandardDescription: "Default speed",
79
+ speedFast: "Fast",
80
+ speedFastDescription: "1.5x speed, more usage",
81
+ commandFast: "Switch the Codex speed tier (Standard/Fast)",
82
+ commandFastUnavailable: "The current model has no fast tier; /fast only works on Codex models whose catalog advertises one"
78
83
  };
79
84
  /** zh strings, one per {@link en} key. */
80
85
  const zh = {
@@ -116,7 +121,14 @@ const zh = {
116
121
  imageClose: "关闭",
117
122
  generatingVideo: "正在生成视频…",
118
123
  videoLoading: "视频加载中…",
119
- videoLoadFailed: "视频加载失败:{message}"
124
+ videoLoadFailed: "视频加载失败:{message}",
125
+ speed: "速度",
126
+ speedStandard: "标准",
127
+ speedStandardDescription: "默认速度",
128
+ speedFast: "快速",
129
+ speedFastDescription: "约 1.5 倍速度,消耗更多用量",
130
+ commandFast: "切换 Codex 速度档(标准/快速)",
131
+ commandFastUnavailable: "当前模型不支持快速档;/fast 仅对目录声明了 fast tier 的 Codex 模型可用"
120
132
  };
121
133
 
122
134
  //#endregion
@@ -124,7 +136,7 @@ const zh = {
124
136
  /** Logical RPC channel served by the node half of this plugin. */
125
137
  const SUBSCRIPTIONS_AUTH_CHANNEL$2 = "/subscriptions-auth";
126
138
  /** Poll cadence while a provider login attempt is busy. */
127
- const POLL_INTERVAL_MS = 2e3;
139
+ const POLL_INTERVAL_MS$1 = 2e3;
128
140
  /** Card display metadata, in page order (names are brand names, not translated). */
129
141
  const PROVIDERS = [
130
142
  {
@@ -144,12 +156,13 @@ const PROVIDERS = [
144
156
  var SubscriptionsAuthError = class extends Error {};
145
157
  /**
146
158
  * Call one `/subscriptions-auth` endpoint and unwrap the business result.
159
+ * Shared by the settings section and the composer Speed toggle.
147
160
  * @param rpc - Connection RPC caller.
148
161
  * @param endpoint - channel-relative endpoint.
149
162
  * @param payload - channel-owned request payload.
150
163
  * @returns the success value, cast by the caller to the endpoint's shape.
151
164
  */
152
- async function callSubscriptionsAuth$1(rpc, endpoint, payload) {
165
+ async function callSubscriptionsAuth(rpc, endpoint, payload) {
153
166
  let result;
154
167
  try {
155
168
  result = await rpc.call(SUBSCRIPTIONS_AUTH_CHANNEL$2, endpoint, payload);
@@ -170,12 +183,12 @@ function messageOf(error) {
170
183
  * @param params - `{name}` template params.
171
184
  * @returns the template with params substituted.
172
185
  */
173
- function fallbackTranslate$2(key, params) {
186
+ function fallbackTranslate$3(key, params) {
174
187
  let text = en[key];
175
188
  for (const [name, value] of Object.entries(params ?? {})) text = text.replaceAll(`{${name}}`, String(value));
176
189
  return text;
177
190
  }
178
- const styles$2 = {
191
+ const styles$4 = {
179
192
  section: {
180
193
  display: "flex",
181
194
  flexDirection: "column",
@@ -388,7 +401,7 @@ function usageBarColor(usedPercent) {
388
401
  */
389
402
  function SubscriptionsSection(props) {
390
403
  const { rpc } = props;
391
- const t = props.t ?? fallbackTranslate$2;
404
+ const t = props.t ?? fallbackTranslate$3;
392
405
  const [statuses, setStatuses] = (0, react.useState)({});
393
406
  const [errors, setErrors] = (0, react.useState)({});
394
407
  const [manualDrafts, setManualDrafts] = (0, react.useState)({
@@ -424,7 +437,7 @@ function SubscriptionsSection(props) {
424
437
  if (rpc === void 0) return;
425
438
  let response;
426
439
  try {
427
- response = await callSubscriptionsAuth$1(rpc, "status", {});
440
+ response = await callSubscriptionsAuth(rpc, "status", {});
428
441
  } catch {
429
442
  return;
430
443
  }
@@ -439,7 +452,7 @@ function SubscriptionsSection(props) {
439
452
  if (pollersRef.current.has(provider)) return;
440
453
  pollersRef.current.set(provider, setInterval(() => {
441
454
  refresh();
442
- }, POLL_INTERVAL_MS));
455
+ }, POLL_INTERVAL_MS$1));
443
456
  }, [refresh]);
444
457
  (0, react.useEffect)(() => {
445
458
  mountedRef.current = true;
@@ -464,7 +477,7 @@ function SubscriptionsSection(props) {
464
477
  [provider]: true
465
478
  }));
466
479
  try {
467
- const usage = await callSubscriptionsAuth$1(rpc, "usage", { provider });
480
+ const usage = await callSubscriptionsAuth(rpc, "usage", { provider });
468
481
  if (!mountedRef.current) return;
469
482
  setUsages((prev) => ({
470
483
  ...prev,
@@ -517,8 +530,12 @@ function SubscriptionsSection(props) {
517
530
  if (rpc === void 0) return;
518
531
  setProviderError(provider, void 0);
519
532
  try {
520
- const response = await callSubscriptionsAuth$1(rpc, "login", { provider });
521
- if (typeof response.authorizeUrl !== "string" || response.authorizeUrl === "") throw new SubscriptionsAuthError(t("loginMissingUrl"));
533
+ const response = await callSubscriptionsAuth(rpc, "login", { provider });
534
+ if (typeof response.authorizeUrl === "string" && response.authorizeUrl === "") {
535
+ await refresh();
536
+ return;
537
+ }
538
+ if (typeof response.authorizeUrl !== "string") throw new SubscriptionsAuthError(t("loginMissingUrl"));
522
539
  window.open(response.authorizeUrl, "_blank", "noopener");
523
540
  if (!mountedRef.current) return;
524
541
  setStatuses((prev) => ({
@@ -543,7 +560,7 @@ function SubscriptionsSection(props) {
543
560
  if (rpc === void 0) return;
544
561
  stopPolling(provider);
545
562
  try {
546
- await callSubscriptionsAuth$1(rpc, "cancel", { provider });
563
+ await callSubscriptionsAuth(rpc, "cancel", { provider });
547
564
  } catch (error) {
548
565
  setProviderError(provider, messageOf(error));
549
566
  }
@@ -560,7 +577,7 @@ function SubscriptionsSection(props) {
560
577
  if (input === "") return;
561
578
  setProviderError(provider, void 0);
562
579
  try {
563
- await callSubscriptionsAuth$1(rpc, "manual", {
580
+ await callSubscriptionsAuth(rpc, "manual", {
564
581
  provider,
565
582
  input
566
583
  });
@@ -583,7 +600,7 @@ function SubscriptionsSection(props) {
583
600
  if (!window.confirm(t("logoutConfirm", { provider: name }))) return;
584
601
  setProviderError(provider, void 0);
585
602
  try {
586
- await callSubscriptionsAuth$1(rpc, "logout", { provider });
603
+ await callSubscriptionsAuth(rpc, "logout", { provider });
587
604
  } catch (error) {
588
605
  setProviderError(provider, messageOf(error));
589
606
  }
@@ -595,13 +612,13 @@ function SubscriptionsSection(props) {
595
612
  refresh
596
613
  ]);
597
614
  if (rpc === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
598
- style: styles$2.intro,
615
+ style: styles$4.intro,
599
616
  children: t("unavailable")
600
617
  });
601
618
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
602
- style: styles$2.section,
619
+ style: styles$4.section,
603
620
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
604
- style: styles$2.intro,
621
+ style: styles$4.intro,
605
622
  children: t("intro")
606
623
  }), PROVIDERS.map(({ id, name }) => {
607
624
  const status = statuses[id];
@@ -610,36 +627,36 @@ function SubscriptionsSection(props) {
610
627
  const usageError = usageErrors[id];
611
628
  const showUsage = status?.loggedIn === true && usage?.supported !== false && (usage !== void 0 || usageError !== void 0 || usageLoading[id] === true);
612
629
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
613
- style: styles$2.card,
630
+ style: styles$4.card,
614
631
  children: [
615
632
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
616
- style: styles$2.cardHeader,
633
+ style: styles$4.cardHeader,
617
634
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
618
- ...styles$2.dot,
635
+ ...styles$4.dot,
619
636
  background: dotColor(status)
620
637
  } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
621
- style: styles$2.name,
638
+ style: styles$4.name,
622
639
  children: name
623
640
  })]
624
641
  }),
625
642
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
626
- style: styles$2.statusLine,
643
+ style: styles$4.statusLine,
627
644
  children: statusText(t, status)
628
645
  }),
629
646
  status?.detail !== void 0 && status.detail !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
630
- style: styles$2.statusLine,
647
+ style: styles$4.statusLine,
631
648
  children: status.detail
632
649
  }),
633
650
  errors[id] !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
634
- style: styles$2.errorLine,
651
+ style: styles$4.errorLine,
635
652
  children: errors[id]
636
653
  }),
637
654
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
638
- style: styles$2.actions,
655
+ style: styles$4.actions,
639
656
  children: [
640
657
  !busy && status?.loggedIn !== true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
641
658
  type: "button",
642
- style: styles$2.button,
659
+ style: styles$4.button,
643
660
  onClick: () => {
644
661
  login(id);
645
662
  },
@@ -647,7 +664,7 @@ function SubscriptionsSection(props) {
647
664
  }),
648
665
  busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
649
666
  type: "button",
650
- style: styles$2.button,
667
+ style: styles$4.button,
651
668
  onClick: () => {
652
669
  cancel(id);
653
670
  },
@@ -655,7 +672,7 @@ function SubscriptionsSection(props) {
655
672
  }),
656
673
  status?.loggedIn === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
657
674
  type: "button",
658
- style: styles$2.button,
675
+ style: styles$4.button,
659
676
  onClick: () => {
660
677
  logout(id, name);
661
678
  },
@@ -664,23 +681,23 @@ function SubscriptionsSection(props) {
664
681
  ]
665
682
  }),
666
683
  showUsage && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
667
- style: styles$2.usage,
684
+ style: styles$4.usage,
668
685
  children: [
669
686
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
670
- style: styles$2.usageHeader,
687
+ style: styles$4.usageHeader,
671
688
  children: [
672
689
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
673
- style: styles$2.usageTitle,
690
+ style: styles$4.usageTitle,
674
691
  children: t("usageTitle")
675
692
  }),
676
693
  usage?.plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
677
- style: styles$2.usagePlan,
694
+ style: styles$4.usagePlan,
678
695
  children: t("usagePlan", { plan: usage.plan })
679
696
  }),
680
697
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
681
698
  type: "button",
682
699
  style: {
683
- ...styles$2.usageRefresh,
700
+ ...styles$4.usageRefresh,
684
701
  ...usageLoading[id] === true ? {
685
702
  opacity: .5,
686
703
  cursor: "default"
@@ -695,28 +712,28 @@ function SubscriptionsSection(props) {
695
712
  ]
696
713
  }),
697
714
  usage === void 0 && usageError === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
698
- style: styles$2.statusLine,
715
+ style: styles$4.statusLine,
699
716
  children: t("usageLoading")
700
717
  }),
701
718
  usageError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
702
- style: styles$2.errorLine,
719
+ style: styles$4.errorLine,
703
720
  children: t("usageError", { message: usageError })
704
721
  }),
705
722
  usage?.windows !== void 0 && usage.windows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
706
- style: styles$2.statusLine,
723
+ style: styles$4.statusLine,
707
724
  children: t("usageEmpty")
708
725
  }),
709
726
  (usage?.windows ?? []).map((window$1, index) => {
710
727
  const percent = Math.min(100, Math.max(0, window$1.usedPercent));
711
728
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
712
- style: styles$2.usageRow,
729
+ style: styles$4.usageRow,
713
730
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
714
- style: styles$2.usageMeta,
731
+ style: styles$4.usageMeta,
715
732
  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() })}`] })]
716
733
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
717
- style: styles$2.usageTrack,
734
+ style: styles$4.usageTrack,
718
735
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
719
- ...styles$2.usageFill,
736
+ ...styles$4.usageFill,
720
737
  width: `${String(percent)}%`,
721
738
  background: usageBarColor(percent)
722
739
  } })
@@ -726,11 +743,11 @@ function SubscriptionsSection(props) {
726
743
  ]
727
744
  }),
728
745
  busy && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
729
- style: styles$2.manual,
746
+ style: styles$4.manual,
730
747
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("manualSummary") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
731
- style: styles$2.manualRow,
748
+ style: styles$4.manualRow,
732
749
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
733
- style: styles$2.manualInput,
750
+ style: styles$4.manualInput,
734
751
  value: manualDrafts[id],
735
752
  placeholder: t("manualPlaceholder"),
736
753
  onChange: (event) => setManualDrafts((prev) => ({
@@ -739,7 +756,7 @@ function SubscriptionsSection(props) {
739
756
  }))
740
757
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
741
758
  type: "button",
742
- style: styles$2.button,
759
+ style: styles$4.button,
743
760
  onClick: () => {
744
761
  submitManual(id);
745
762
  },
@@ -753,6 +770,229 @@ function SubscriptionsSection(props) {
753
770
  });
754
771
  }
755
772
 
773
+ //#endregion
774
+ //#region src/client/ImageGallery.tsx
775
+ /** Display box for a lone image (platform rule): long edge 240px with the
776
+ * rendered aspect ratio clamped to [0.25, 4] — the overflow is cropped by
777
+ * `object-fit: cover` — and never upscaled past the image's natural size. The
778
+ * crop anchor keeps the top of very tall images and the left of very wide
779
+ * ones, where the informative content usually starts. */
780
+ function singleFit(attachment) {
781
+ const natural = attachment.width / attachment.height;
782
+ const ratio = Math.min(4, Math.max(.25, natural));
783
+ const box = ratio >= 1 ? {
784
+ width: 240,
785
+ height: 240 / ratio
786
+ } : {
787
+ width: 240 * ratio,
788
+ height: 240
789
+ };
790
+ const scale = Math.min(1, attachment.width / box.width, attachment.height / box.height);
791
+ return {
792
+ width: Math.max(1, Math.round(box.width * scale)),
793
+ height: Math.max(1, Math.round(box.height * scale)),
794
+ objectPosition: natural < .25 ? "center top" : natural > 4 ? "left center" : "center"
795
+ };
796
+ }
797
+ const styles$3 = {
798
+ gallery: {
799
+ display: "flex",
800
+ flexWrap: "wrap",
801
+ gap: 8,
802
+ justifyContent: "flex-start"
803
+ },
804
+ frame: {
805
+ display: "grid",
806
+ placeItems: "center",
807
+ overflow: "hidden",
808
+ padding: 0,
809
+ border: "1px solid var(--dsw-alias-border-l2-darkmode-thin)",
810
+ borderRadius: 8,
811
+ background: "var(--dsw-alias-interactive-bg-hover-solid)",
812
+ cursor: "zoom-in"
813
+ },
814
+ tile: {
815
+ width: 64,
816
+ height: 64
817
+ },
818
+ img: {
819
+ width: "100%",
820
+ height: "100%",
821
+ objectFit: "cover",
822
+ display: "block"
823
+ },
824
+ loading: {
825
+ fontSize: 12,
826
+ color: "var(--dsw-alias-label-tertiary)",
827
+ padding: "0 8px"
828
+ },
829
+ error: {
830
+ fontSize: 12,
831
+ color: "var(--dsw-alias-state-error-primary)",
832
+ cursor: "pointer",
833
+ border: "1px solid var(--dsw-alias-border-l2-darkmode-thin)",
834
+ borderRadius: 8,
835
+ background: "transparent",
836
+ padding: "6px 10px"
837
+ },
838
+ overlay: {
839
+ position: "fixed",
840
+ inset: 0,
841
+ zIndex: 1e3,
842
+ display: "grid",
843
+ placeItems: "center",
844
+ background: "rgba(0, 0, 0, 0.72)",
845
+ padding: 24
846
+ },
847
+ overlayImg: {
848
+ maxWidth: "92vw",
849
+ maxHeight: "92vh",
850
+ objectFit: "contain",
851
+ borderRadius: 4
852
+ },
853
+ close: {
854
+ position: "absolute",
855
+ top: 12,
856
+ right: 12,
857
+ width: 32,
858
+ height: 32,
859
+ display: "grid",
860
+ placeItems: "center",
861
+ border: "none",
862
+ borderRadius: "50%",
863
+ cursor: "pointer",
864
+ background: "rgba(255, 255, 255, 0.16)",
865
+ color: "#fff",
866
+ fontSize: 16,
867
+ lineHeight: 1
868
+ }
869
+ };
870
+ /**
871
+ * Full-viewport original-image preview: backdrop or close-control click and
872
+ * Escape all dismiss; the image itself is inert so a click on it does not
873
+ * fall through to the backdrop dismissal.
874
+ */
875
+ function ImageLightbox({ src, alt, labels, onClose }) {
876
+ (0, react.useEffect)(() => {
877
+ const onKey = (event) => {
878
+ if (event.key === "Escape") onClose();
879
+ };
880
+ window.addEventListener("keydown", onKey);
881
+ return () => {
882
+ window.removeEventListener("keydown", onKey);
883
+ };
884
+ }, [onClose]);
885
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
886
+ role: "dialog",
887
+ "aria-label": labels.dialog,
888
+ style: styles$3.overlay,
889
+ onClick: onClose,
890
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
891
+ src,
892
+ alt,
893
+ style: styles$3.overlayImg,
894
+ onClick: (event) => {
895
+ event.stopPropagation();
896
+ }
897
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
898
+ type: "button",
899
+ "aria-label": labels.close,
900
+ style: styles$3.close,
901
+ onClick: onClose,
902
+ children: "×"
903
+ })]
904
+ });
905
+ }
906
+ /**
907
+ * Compact history renderer with retryable loading and click-to-open original
908
+ * preview. A lone image renders at its `singleFit` size; an image among
909
+ * several renders as a fixed 64px square tile.
910
+ */
911
+ function MessageImage({ attachment, load, variant, labels }) {
912
+ const [src, setSrc] = (0, react.useState)(null);
913
+ const [error, setError] = (0, react.useState)(false);
914
+ const [open, setOpen] = (0, react.useState)(false);
915
+ const [attempt, setAttempt] = (0, react.useState)(0);
916
+ const retry = (0, react.useCallback)(() => {
917
+ setAttempt((a) => a + 1);
918
+ }, []);
919
+ const close = (0, react.useCallback)(() => {
920
+ setOpen(false);
921
+ }, []);
922
+ const fit = (0, react.useMemo)(() => variant === "single" ? singleFit(attachment) : void 0, [attachment, variant]);
923
+ (0, react.useEffect)(() => {
924
+ let live = true;
925
+ setError(false);
926
+ setSrc(null);
927
+ load(attachment).then((url) => {
928
+ if (live) setSrc(url);
929
+ }).catch(() => {
930
+ if (live) setError(true);
931
+ });
932
+ return () => {
933
+ live = false;
934
+ };
935
+ }, [
936
+ attachment,
937
+ load,
938
+ attempt
939
+ ]);
940
+ const label = attachment.name ?? labels.image;
941
+ if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
942
+ type: "button",
943
+ style: styles$3.error,
944
+ onClick: retry,
945
+ children: labels.loadFailed
946
+ });
947
+ const box = fit === void 0 ? styles$3.tile : {
948
+ width: fit.width,
949
+ height: fit.height
950
+ };
951
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
952
+ type: "button",
953
+ style: {
954
+ ...styles$3.frame,
955
+ ...box
956
+ },
957
+ title: labels.open,
958
+ "aria-label": labels.openNamed(label),
959
+ onClick: () => {
960
+ if (src !== null) setOpen(true);
961
+ },
962
+ children: src === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
963
+ style: styles$3.loading,
964
+ children: labels.loading
965
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
966
+ src,
967
+ alt: label,
968
+ style: {
969
+ ...styles$3.img,
970
+ objectPosition: fit?.objectPosition
971
+ }
972
+ })
973
+ }), open && src !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageLightbox, {
974
+ src,
975
+ alt: label,
976
+ labels: labels.lightbox,
977
+ onClose: close
978
+ })] });
979
+ }
980
+ /** Wrapping image group: a lone image renders large, several render as 64px
981
+ * square tiles (same rule as the platform gallery). */
982
+ function ImageGallery({ images, load, labels }) {
983
+ if (images.length === 0) return null;
984
+ const variant = images.length === 1 ? "single" : "tile";
985
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
986
+ style: styles$3.gallery,
987
+ children: images.map((image, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MessageImage, {
988
+ attachment: image.attachment,
989
+ load,
990
+ variant,
991
+ labels
992
+ }, `${image.attachment.attachmentId}:${index}`))
993
+ });
994
+ }
995
+
756
996
  //#endregion
757
997
  //#region src/client/ImageGenerateToolview.tsx
758
998
  /** Logical RPC channel served by the node half of this plugin. */
@@ -766,7 +1006,7 @@ const PROMPT_MAX_LENGTH$1 = 60;
766
1006
  * @param payload - channel-owned request payload.
767
1007
  * @returns the success value, cast by the caller to the endpoint's shape.
768
1008
  */
769
- async function callSubscriptionsAuth(rpc, endpoint, payload) {
1009
+ async function callSubscriptionsAuth$1(rpc, endpoint, payload) {
770
1010
  const result = await rpc.call(SUBSCRIPTIONS_AUTH_CHANNEL$1, endpoint, payload);
771
1011
  if (!result.ok) throw new Error(result.error.message);
772
1012
  return result.value;
@@ -777,7 +1017,7 @@ async function callSubscriptionsAuth(rpc, endpoint, payload) {
777
1017
  * @returns loader resolving an attachment ref to a data URL.
778
1018
  */
779
1019
  function createImageLoader(rpc) {
780
- return (attachment) => callSubscriptionsAuth(rpc, "image", { ...attachment }).then((result) => `data:${result.mediaType};base64,${result.dataBase64}`);
1020
+ return (attachment) => callSubscriptionsAuth$1(rpc, "image", { ...attachment }).then((result) => `data:${result.mediaType};base64,${result.dataBase64}`);
781
1021
  }
782
1022
  /**
783
1023
  * English-dictionary fallback for a missing locale seat (standalone renders);
@@ -786,7 +1026,7 @@ function createImageLoader(rpc) {
786
1026
  * @param params - `{name}` template params.
787
1027
  * @returns the template with params substituted.
788
1028
  */
789
- function fallbackTranslate$1(key, params) {
1029
+ function fallbackTranslate$2(key, params) {
790
1030
  let text = en[key];
791
1031
  for (const [name, value] of Object.entries(params ?? {})) text = text.replaceAll(`{${name}}`, String(value));
792
1032
  return text;
@@ -826,7 +1066,7 @@ function resultImages(block) {
826
1066
  for (const part of block.content) if (part.type === "image") images.push({ attachment: part.attachment });
827
1067
  return images;
828
1068
  }
829
- const styles$1 = {
1069
+ const styles$2 = {
830
1070
  container: {
831
1071
  display: "flex",
832
1072
  flexDirection: "column",
@@ -880,7 +1120,7 @@ const styles$1 = {
880
1120
  */
881
1121
  function ImageGenerateToolview(props) {
882
1122
  const { block, load } = props;
883
- const t = props.t ?? fallbackTranslate$1;
1123
+ const t = props.t ?? fallbackTranslate$2;
884
1124
  if (block === void 0) return null;
885
1125
  const settled = "kind" in block;
886
1126
  const title = `image_generate: ${derivePrompt$1((settled ? block.call?.argsRaw : block.argsRaw) ?? "")}`;
@@ -898,34 +1138,33 @@ function ImageGenerateToolview(props) {
898
1138
  }
899
1139
  };
900
1140
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
901
- style: styles$1.container,
1141
+ style: styles$2.container,
902
1142
  children: [
903
1143
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
904
- style: styles$1.row,
1144
+ style: styles$2.row,
905
1145
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
906
- style: styles$1.icon,
1146
+ style: styles$2.icon,
907
1147
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconSparkle16, { size: 14 })
908
1148
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
909
- style: styles$1.title,
1149
+ style: styles$2.title,
910
1150
  children: title
911
1151
  })]
912
1152
  }),
913
1153
  !settled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
914
- style: styles$1.subtle,
1154
+ style: styles$2.subtle,
915
1155
  children: t("generating")
916
1156
  }),
917
1157
  settled && block.isError && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
918
- style: styles$1.error,
1158
+ style: styles$2.error,
919
1159
  children: text.split("\n", 1)[0]
920
1160
  }),
921
- settled && !block.isError && images.length > 0 && load !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_attachment.ImageGallery, {
1161
+ settled && !block.isError && images.length > 0 && load !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageGallery, {
922
1162
  images,
923
1163
  load,
924
- align: "start",
925
1164
  labels
926
1165
  }),
927
1166
  settled && !block.isError && images.length === 0 && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
928
- style: styles$1.output,
1167
+ style: styles$2.output,
929
1168
  children: text
930
1169
  })
931
1170
  ]
@@ -954,7 +1193,7 @@ function createVideoLoader(rpc) {
954
1193
  * English-dictionary fallback for a missing locale seat (standalone renders);
955
1194
  * the framework always supplies the namespace-bound one.
956
1195
  */
957
- function fallbackTranslate(key, params) {
1196
+ function fallbackTranslate$1(key, params) {
958
1197
  let text = en[key];
959
1198
  for (const [name, value] of Object.entries(params ?? {})) text = text.replaceAll(`{${name}}`, String(value));
960
1199
  return text;
@@ -1011,7 +1250,7 @@ function base64Bytes(dataBase64) {
1011
1250
  for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
1012
1251
  return bytes;
1013
1252
  }
1014
- const styles = {
1253
+ const styles$1 = {
1015
1254
  container: {
1016
1255
  display: "flex",
1017
1256
  flexDirection: "column",
@@ -1072,7 +1311,7 @@ const styles = {
1072
1311
  */
1073
1312
  function VideoGenerateToolview(props) {
1074
1313
  const { block, loadVideo } = props;
1075
- const t = props.t ?? fallbackTranslate;
1314
+ const t = props.t ?? fallbackTranslate$1;
1076
1315
  const settled = block !== void 0 && "kind" in block;
1077
1316
  const isError = settled && block.isError;
1078
1317
  const fileName = block !== void 0 && settled && !isError ? resolveFileName(block) : void 0;
@@ -1105,48 +1344,285 @@ function VideoGenerateToolview(props) {
1105
1344
  const title = `video_generate: ${derivePrompt((settled ? block.call?.argsRaw : block.argsRaw) ?? "")}`;
1106
1345
  const text = settled ? resultText(block) : "";
1107
1346
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1108
- style: styles.container,
1347
+ style: styles$1.container,
1109
1348
  children: [
1110
1349
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1111
- style: styles.row,
1350
+ style: styles$1.row,
1112
1351
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1113
- style: styles.icon,
1352
+ style: styles$1.icon,
1114
1353
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconSparkle16, { size: 14 })
1115
1354
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1116
- style: styles.title,
1355
+ style: styles$1.title,
1117
1356
  children: title
1118
1357
  })]
1119
1358
  }),
1120
1359
  !settled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1121
- style: styles.subtle,
1360
+ style: styles$1.subtle,
1122
1361
  children: t("generatingVideo")
1123
1362
  }),
1124
1363
  settled && isError && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1125
- style: styles.error,
1364
+ style: styles$1.error,
1126
1365
  children: text.split("\n", 1)[0]
1127
1366
  }),
1128
1367
  settled && !isError && fileName !== void 0 && load.phase === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1129
- style: styles.subtle,
1368
+ style: styles$1.subtle,
1130
1369
  children: t("videoLoading")
1131
1370
  }),
1132
1371
  settled && !isError && fileName !== void 0 && load.phase === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1133
- style: styles.error,
1372
+ style: styles$1.error,
1134
1373
  children: t("videoLoadFailed", { message: load.message })
1135
1374
  }),
1136
1375
  settled && !isError && fileName !== void 0 && load.phase === "ready" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("video", {
1137
- style: styles.video,
1376
+ style: styles$1.video,
1138
1377
  src: load.url,
1139
1378
  controls: true,
1140
1379
  preload: "metadata"
1141
1380
  }),
1142
1381
  settled && !isError && fileName === void 0 && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1143
- style: styles.output,
1382
+ style: styles$1.output,
1144
1383
  children: text
1145
1384
  })
1146
1385
  ]
1147
1386
  });
1148
1387
  }
1149
1388
 
1389
+ //#endregion
1390
+ //#region src/client/SpeedSelect.tsx
1391
+ /**
1392
+ * The `loadSpeed` half of the inject face: the plugin's own speed state plus
1393
+ * the host's current model selection (the visibility gate). A model-RPC
1394
+ * failure throws rather than answering "hidden" — the caller keeps its last
1395
+ * known state, so a transient failure never locks the toggle away.
1396
+ *
1397
+ * `sessionId` is a plain string: slot and command contexts brand it through
1398
+ * different dsh-session copies, and only the API-client boundary needs one.
1399
+ */
1400
+ function createSpeedLoader(connection, sessionId) {
1401
+ return async () => {
1402
+ const state = await callSubscriptionsAuth(connection.rpc, "speed", { sessionId });
1403
+ const { result } = await connection.api.sessions.models({ sessionId });
1404
+ if (!result.ok) throw new Error(`session.models failed: ${result.error.code}: ${result.error.message}`);
1405
+ const current = result.value.current;
1406
+ return {
1407
+ visible: current !== null && current.provider === "codex" && state.fastModels.includes(current.model),
1408
+ tier: state.tier
1409
+ };
1410
+ };
1411
+ }
1412
+ /** The `setSpeed` half of the inject face: boolean outcome for the component's busy state. */
1413
+ function createSpeedSetter(connection, sessionId) {
1414
+ return (tier) => callSubscriptionsAuth(connection.rpc, "setSpeed", {
1415
+ sessionId,
1416
+ tier
1417
+ }).then(() => true, () => false);
1418
+ }
1419
+ /** English-dictionary fallback for a missing inject `t` (standalone renders). */
1420
+ function fallbackTranslate(key) {
1421
+ return en[key];
1422
+ }
1423
+ const TIERS = ["standard", "fast"];
1424
+ /**
1425
+ * The composer Speed control: a trigger reading `速度 · 快速`/`速度 · 标准`
1426
+ * that opens a two-row menu (standard/fast with descriptions, check mark on
1427
+ * the current tier). Mount and every open reload the host state so a model
1428
+ * switch made since the last open self-corrects.
1429
+ */
1430
+ /** How often the control re-reads the host state (model switches arrive only by asking). */
1431
+ const POLL_INTERVAL_MS = 3e3;
1432
+ /**
1433
+ * The composer Speed control: a trigger reading `速度 · 快速`/`速度 · 标准`
1434
+ * that opens a two-row menu (standard/fast with descriptions, check mark on
1435
+ * the current tier). The host pushes nothing on a model switch, so the
1436
+ * control re-reads on a slow poll with a single-flight guard; a failed read
1437
+ * keeps the last known state, so a transient RPC failure can never lock the
1438
+ * toggle away (the earlier mount-only load had no recovery path).
1439
+ */
1440
+ function SpeedSelect({ loadSpeed, setSpeed, t }) {
1441
+ const translate = t ?? fallbackTranslate;
1442
+ const [state, setState] = (0, react.useState)(null);
1443
+ const [open, setOpen] = (0, react.useState)(false);
1444
+ const [busy, setBusy] = (0, react.useState)(false);
1445
+ const rootRef = (0, react.useRef)(null);
1446
+ const loadRef = (0, react.useRef)(loadSpeed);
1447
+ loadRef.current = loadSpeed;
1448
+ (0, react.useEffect)(() => {
1449
+ if (loadRef.current === void 0) return;
1450
+ let cancelled = false;
1451
+ let inflight = false;
1452
+ const reload = () => {
1453
+ const load = loadRef.current;
1454
+ if (load === void 0 || inflight) return;
1455
+ inflight = true;
1456
+ load().then((loaded) => {
1457
+ if (!cancelled) setState(loaded);
1458
+ }, () => {}).finally(() => {
1459
+ inflight = false;
1460
+ });
1461
+ };
1462
+ reload();
1463
+ const timer = setInterval(reload, POLL_INTERVAL_MS);
1464
+ return () => {
1465
+ cancelled = true;
1466
+ clearInterval(timer);
1467
+ };
1468
+ }, []);
1469
+ (0, react.useEffect)(() => {
1470
+ if (!open) return;
1471
+ const closeOutside = (event) => {
1472
+ if (!rootRef.current?.contains(event.target)) setOpen(false);
1473
+ };
1474
+ document.addEventListener("mousedown", closeOutside);
1475
+ return () => {
1476
+ document.removeEventListener("mousedown", closeOutside);
1477
+ };
1478
+ }, [open]);
1479
+ if (loadSpeed === void 0 || setSpeed === void 0 || state === null || !state.visible) return null;
1480
+ const choose = (tier) => {
1481
+ if (busy) return;
1482
+ if (tier === state.tier) {
1483
+ setOpen(false);
1484
+ return;
1485
+ }
1486
+ setBusy(true);
1487
+ setSpeed(tier).then((ok) => {
1488
+ setBusy(false);
1489
+ if (ok) {
1490
+ setState({
1491
+ visible: true,
1492
+ tier
1493
+ });
1494
+ setOpen(false);
1495
+ }
1496
+ });
1497
+ };
1498
+ const show = () => {
1499
+ setOpen(true);
1500
+ const load = loadRef.current;
1501
+ if (load === void 0) return;
1502
+ load().then(setState, () => {});
1503
+ };
1504
+ const tierName = (tier) => translate(tier === "fast" ? "speedFast" : "speedStandard");
1505
+ const tierDescription = (tier) => translate(tier === "fast" ? "speedFastDescription" : "speedStandardDescription");
1506
+ const triggerLabel = `${translate("speed")} · ${tierName(state.tier)}`;
1507
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1508
+ ref: rootRef,
1509
+ style: styles.root,
1510
+ onKeyDown: (event) => {
1511
+ if (event.key === "Escape" && open) {
1512
+ event.preventDefault();
1513
+ setOpen(false);
1514
+ }
1515
+ },
1516
+ children: [open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1517
+ style: styles.menu,
1518
+ role: "menu",
1519
+ "aria-label": translate("speed"),
1520
+ children: TIERS.map((tier) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1521
+ type: "button",
1522
+ role: "menuitemradio",
1523
+ "aria-checked": tier === state.tier,
1524
+ style: styles.item,
1525
+ disabled: busy,
1526
+ onClick: () => {
1527
+ choose(tier);
1528
+ },
1529
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1530
+ style: styles.itemCheck,
1531
+ children: tier === state.tier ? "✓" : ""
1532
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1533
+ style: styles.itemText,
1534
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1535
+ style: styles.itemName,
1536
+ children: tierName(tier)
1537
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1538
+ style: styles.itemDescription,
1539
+ children: tierDescription(tier)
1540
+ })]
1541
+ })]
1542
+ }, tier))
1543
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1544
+ type: "button",
1545
+ style: styles.trigger,
1546
+ "aria-haspopup": "menu",
1547
+ "aria-expanded": open,
1548
+ title: triggerLabel,
1549
+ disabled: busy,
1550
+ onClick: () => {
1551
+ if (open) setOpen(false);
1552
+ else show();
1553
+ },
1554
+ children: triggerLabel
1555
+ })]
1556
+ });
1557
+ }
1558
+ const styles = {
1559
+ root: {
1560
+ position: "relative",
1561
+ display: "inline-flex"
1562
+ },
1563
+ trigger: {
1564
+ border: "1px solid var(--dsw-alias-border-l2)",
1565
+ borderRadius: 8,
1566
+ background: "transparent",
1567
+ color: "var(--dsw-alias-label-secondary)",
1568
+ font: "inherit",
1569
+ fontSize: 12,
1570
+ lineHeight: "18px",
1571
+ padding: "2px 8px",
1572
+ cursor: "pointer",
1573
+ whiteSpace: "nowrap"
1574
+ },
1575
+ menu: {
1576
+ position: "absolute",
1577
+ bottom: "100%",
1578
+ right: 0,
1579
+ marginBottom: 4,
1580
+ minWidth: 180,
1581
+ padding: 4,
1582
+ zIndex: 20,
1583
+ background: "var(--dsw-alias-bg-layer-1)",
1584
+ border: "1px solid var(--dsw-alias-border-l2)",
1585
+ borderRadius: 8,
1586
+ display: "flex",
1587
+ flexDirection: "column",
1588
+ gap: 2
1589
+ },
1590
+ item: {
1591
+ display: "flex",
1592
+ alignItems: "flex-start",
1593
+ gap: 6,
1594
+ width: "100%",
1595
+ border: "none",
1596
+ borderRadius: 6,
1597
+ background: "transparent",
1598
+ padding: "6px 8px",
1599
+ cursor: "pointer",
1600
+ font: "inherit",
1601
+ textAlign: "left"
1602
+ },
1603
+ itemCheck: {
1604
+ width: 14,
1605
+ flexShrink: 0,
1606
+ fontSize: 12,
1607
+ lineHeight: "18px",
1608
+ color: "var(--dsw-alias-label-primary)"
1609
+ },
1610
+ itemText: {
1611
+ display: "flex",
1612
+ flexDirection: "column"
1613
+ },
1614
+ itemName: {
1615
+ fontSize: 12,
1616
+ lineHeight: "18px",
1617
+ color: "var(--dsw-alias-label-primary)"
1618
+ },
1619
+ itemDescription: {
1620
+ fontSize: 11,
1621
+ lineHeight: "16px",
1622
+ color: "var(--dsw-alias-label-tertiary)"
1623
+ }
1624
+ };
1625
+
1150
1626
  //#endregion
1151
1627
  //#region src/client/index.ts
1152
1628
  /** Dictionary namespace owned by this plugin. */
@@ -1199,6 +1675,46 @@ function apply(ctx) {
1199
1675
  locale: NS,
1200
1676
  inject: videoToolviewInjected
1201
1677
  }, VideoGenerateToolview));
1678
+ ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
1679
+ name: "conversation.input.right",
1680
+ id: "codex-speed",
1681
+ order: 0,
1682
+ locale: NS,
1683
+ inject: (sessionId) => ({
1684
+ loadSpeed: createSpeedLoader(connection, sessionId),
1685
+ setSpeed: createSpeedSetter(connection, sessionId)
1686
+ })
1687
+ }, SpeedSelect));
1688
+ ctx.inject(["commandUi"], (scope) => {
1689
+ const command = scope.get("commandUi");
1690
+ scope.effect(() => command.register({
1691
+ name: "fast",
1692
+ description: t("commandFast"),
1693
+ available: () => true,
1694
+ ui: {
1695
+ kind: "popupSelect",
1696
+ options: async (session) => {
1697
+ const state = await createSpeedLoader(connection, session.sessionId)();
1698
+ if (!state.visible) throw new Error(t("commandFastUnavailable"));
1699
+ return [{
1700
+ id: "standard",
1701
+ label: t("speedStandard"),
1702
+ detail: t("speedStandardDescription")
1703
+ }, {
1704
+ id: "fast",
1705
+ label: t("speedFast"),
1706
+ detail: t("speedFastDescription")
1707
+ }].map((option) => ({
1708
+ ...option,
1709
+ active: option.id === state.tier
1710
+ }));
1711
+ },
1712
+ onSelect: async (option, session) => {
1713
+ await createSpeedSetter(connection, session.sessionId)(option.id);
1714
+ }
1715
+ }
1716
+ }), "dsh-plugin-subscriptions: /fast contribution");
1717
+ });
1202
1718
  }
1203
1719
 
1204
1720
  //#endregion