dsh-plugin-subscriptions 0.4.2 → 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,7 +530,7 @@ 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 });
533
+ const response = await callSubscriptionsAuth(rpc, "login", { provider });
521
534
  if (typeof response.authorizeUrl === "string" && response.authorizeUrl === "") {
522
535
  await refresh();
523
536
  return;
@@ -547,7 +560,7 @@ function SubscriptionsSection(props) {
547
560
  if (rpc === void 0) return;
548
561
  stopPolling(provider);
549
562
  try {
550
- await callSubscriptionsAuth$1(rpc, "cancel", { provider });
563
+ await callSubscriptionsAuth(rpc, "cancel", { provider });
551
564
  } catch (error) {
552
565
  setProviderError(provider, messageOf(error));
553
566
  }
@@ -564,7 +577,7 @@ function SubscriptionsSection(props) {
564
577
  if (input === "") return;
565
578
  setProviderError(provider, void 0);
566
579
  try {
567
- await callSubscriptionsAuth$1(rpc, "manual", {
580
+ await callSubscriptionsAuth(rpc, "manual", {
568
581
  provider,
569
582
  input
570
583
  });
@@ -587,7 +600,7 @@ function SubscriptionsSection(props) {
587
600
  if (!window.confirm(t("logoutConfirm", { provider: name }))) return;
588
601
  setProviderError(provider, void 0);
589
602
  try {
590
- await callSubscriptionsAuth$1(rpc, "logout", { provider });
603
+ await callSubscriptionsAuth(rpc, "logout", { provider });
591
604
  } catch (error) {
592
605
  setProviderError(provider, messageOf(error));
593
606
  }
@@ -599,13 +612,13 @@ function SubscriptionsSection(props) {
599
612
  refresh
600
613
  ]);
601
614
  if (rpc === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
602
- style: styles$2.intro,
615
+ style: styles$4.intro,
603
616
  children: t("unavailable")
604
617
  });
605
618
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
606
- style: styles$2.section,
619
+ style: styles$4.section,
607
620
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
608
- style: styles$2.intro,
621
+ style: styles$4.intro,
609
622
  children: t("intro")
610
623
  }), PROVIDERS.map(({ id, name }) => {
611
624
  const status = statuses[id];
@@ -614,36 +627,36 @@ function SubscriptionsSection(props) {
614
627
  const usageError = usageErrors[id];
615
628
  const showUsage = status?.loggedIn === true && usage?.supported !== false && (usage !== void 0 || usageError !== void 0 || usageLoading[id] === true);
616
629
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
617
- style: styles$2.card,
630
+ style: styles$4.card,
618
631
  children: [
619
632
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
620
- style: styles$2.cardHeader,
633
+ style: styles$4.cardHeader,
621
634
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
622
- ...styles$2.dot,
635
+ ...styles$4.dot,
623
636
  background: dotColor(status)
624
637
  } }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
625
- style: styles$2.name,
638
+ style: styles$4.name,
626
639
  children: name
627
640
  })]
628
641
  }),
629
642
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
630
- style: styles$2.statusLine,
643
+ style: styles$4.statusLine,
631
644
  children: statusText(t, status)
632
645
  }),
633
646
  status?.detail !== void 0 && status.detail !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
634
- style: styles$2.statusLine,
647
+ style: styles$4.statusLine,
635
648
  children: status.detail
636
649
  }),
637
650
  errors[id] !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
638
- style: styles$2.errorLine,
651
+ style: styles$4.errorLine,
639
652
  children: errors[id]
640
653
  }),
641
654
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
642
- style: styles$2.actions,
655
+ style: styles$4.actions,
643
656
  children: [
644
657
  !busy && status?.loggedIn !== true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
645
658
  type: "button",
646
- style: styles$2.button,
659
+ style: styles$4.button,
647
660
  onClick: () => {
648
661
  login(id);
649
662
  },
@@ -651,7 +664,7 @@ function SubscriptionsSection(props) {
651
664
  }),
652
665
  busy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
653
666
  type: "button",
654
- style: styles$2.button,
667
+ style: styles$4.button,
655
668
  onClick: () => {
656
669
  cancel(id);
657
670
  },
@@ -659,7 +672,7 @@ function SubscriptionsSection(props) {
659
672
  }),
660
673
  status?.loggedIn === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
661
674
  type: "button",
662
- style: styles$2.button,
675
+ style: styles$4.button,
663
676
  onClick: () => {
664
677
  logout(id, name);
665
678
  },
@@ -668,23 +681,23 @@ function SubscriptionsSection(props) {
668
681
  ]
669
682
  }),
670
683
  showUsage && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
671
- style: styles$2.usage,
684
+ style: styles$4.usage,
672
685
  children: [
673
686
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
674
- style: styles$2.usageHeader,
687
+ style: styles$4.usageHeader,
675
688
  children: [
676
689
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
677
- style: styles$2.usageTitle,
690
+ style: styles$4.usageTitle,
678
691
  children: t("usageTitle")
679
692
  }),
680
693
  usage?.plan !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
681
- style: styles$2.usagePlan,
694
+ style: styles$4.usagePlan,
682
695
  children: t("usagePlan", { plan: usage.plan })
683
696
  }),
684
697
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
685
698
  type: "button",
686
699
  style: {
687
- ...styles$2.usageRefresh,
700
+ ...styles$4.usageRefresh,
688
701
  ...usageLoading[id] === true ? {
689
702
  opacity: .5,
690
703
  cursor: "default"
@@ -699,28 +712,28 @@ function SubscriptionsSection(props) {
699
712
  ]
700
713
  }),
701
714
  usage === void 0 && usageError === void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
702
- style: styles$2.statusLine,
715
+ style: styles$4.statusLine,
703
716
  children: t("usageLoading")
704
717
  }),
705
718
  usageError !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
706
- style: styles$2.errorLine,
719
+ style: styles$4.errorLine,
707
720
  children: t("usageError", { message: usageError })
708
721
  }),
709
722
  usage?.windows !== void 0 && usage.windows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
710
- style: styles$2.statusLine,
723
+ style: styles$4.statusLine,
711
724
  children: t("usageEmpty")
712
725
  }),
713
726
  (usage?.windows ?? []).map((window$1, index) => {
714
727
  const percent = Math.min(100, Math.max(0, window$1.usedPercent));
715
728
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
716
- style: styles$2.usageRow,
729
+ style: styles$4.usageRow,
717
730
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
718
- style: styles$2.usageMeta,
731
+ style: styles$4.usageMeta,
719
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() })}`] })]
720
733
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
721
- style: styles$2.usageTrack,
734
+ style: styles$4.usageTrack,
722
735
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
723
- ...styles$2.usageFill,
736
+ ...styles$4.usageFill,
724
737
  width: `${String(percent)}%`,
725
738
  background: usageBarColor(percent)
726
739
  } })
@@ -730,11 +743,11 @@ function SubscriptionsSection(props) {
730
743
  ]
731
744
  }),
732
745
  busy && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
733
- style: styles$2.manual,
746
+ style: styles$4.manual,
734
747
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: t("manualSummary") }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
735
- style: styles$2.manualRow,
748
+ style: styles$4.manualRow,
736
749
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
737
- style: styles$2.manualInput,
750
+ style: styles$4.manualInput,
738
751
  value: manualDrafts[id],
739
752
  placeholder: t("manualPlaceholder"),
740
753
  onChange: (event) => setManualDrafts((prev) => ({
@@ -743,7 +756,7 @@ function SubscriptionsSection(props) {
743
756
  }))
744
757
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
745
758
  type: "button",
746
- style: styles$2.button,
759
+ style: styles$4.button,
747
760
  onClick: () => {
748
761
  submitManual(id);
749
762
  },
@@ -757,6 +770,229 @@ function SubscriptionsSection(props) {
757
770
  });
758
771
  }
759
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
+
760
996
  //#endregion
761
997
  //#region src/client/ImageGenerateToolview.tsx
762
998
  /** Logical RPC channel served by the node half of this plugin. */
@@ -770,7 +1006,7 @@ const PROMPT_MAX_LENGTH$1 = 60;
770
1006
  * @param payload - channel-owned request payload.
771
1007
  * @returns the success value, cast by the caller to the endpoint's shape.
772
1008
  */
773
- async function callSubscriptionsAuth(rpc, endpoint, payload) {
1009
+ async function callSubscriptionsAuth$1(rpc, endpoint, payload) {
774
1010
  const result = await rpc.call(SUBSCRIPTIONS_AUTH_CHANNEL$1, endpoint, payload);
775
1011
  if (!result.ok) throw new Error(result.error.message);
776
1012
  return result.value;
@@ -781,7 +1017,7 @@ async function callSubscriptionsAuth(rpc, endpoint, payload) {
781
1017
  * @returns loader resolving an attachment ref to a data URL.
782
1018
  */
783
1019
  function createImageLoader(rpc) {
784
- 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}`);
785
1021
  }
786
1022
  /**
787
1023
  * English-dictionary fallback for a missing locale seat (standalone renders);
@@ -790,7 +1026,7 @@ function createImageLoader(rpc) {
790
1026
  * @param params - `{name}` template params.
791
1027
  * @returns the template with params substituted.
792
1028
  */
793
- function fallbackTranslate$1(key, params) {
1029
+ function fallbackTranslate$2(key, params) {
794
1030
  let text = en[key];
795
1031
  for (const [name, value] of Object.entries(params ?? {})) text = text.replaceAll(`{${name}}`, String(value));
796
1032
  return text;
@@ -830,7 +1066,7 @@ function resultImages(block) {
830
1066
  for (const part of block.content) if (part.type === "image") images.push({ attachment: part.attachment });
831
1067
  return images;
832
1068
  }
833
- const styles$1 = {
1069
+ const styles$2 = {
834
1070
  container: {
835
1071
  display: "flex",
836
1072
  flexDirection: "column",
@@ -884,7 +1120,7 @@ const styles$1 = {
884
1120
  */
885
1121
  function ImageGenerateToolview(props) {
886
1122
  const { block, load } = props;
887
- const t = props.t ?? fallbackTranslate$1;
1123
+ const t = props.t ?? fallbackTranslate$2;
888
1124
  if (block === void 0) return null;
889
1125
  const settled = "kind" in block;
890
1126
  const title = `image_generate: ${derivePrompt$1((settled ? block.call?.argsRaw : block.argsRaw) ?? "")}`;
@@ -902,34 +1138,33 @@ function ImageGenerateToolview(props) {
902
1138
  }
903
1139
  };
904
1140
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
905
- style: styles$1.container,
1141
+ style: styles$2.container,
906
1142
  children: [
907
1143
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
908
- style: styles$1.row,
1144
+ style: styles$2.row,
909
1145
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
910
- style: styles$1.icon,
1146
+ style: styles$2.icon,
911
1147
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconSparkle16, { size: 14 })
912
1148
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
913
- style: styles$1.title,
1149
+ style: styles$2.title,
914
1150
  children: title
915
1151
  })]
916
1152
  }),
917
1153
  !settled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
918
- style: styles$1.subtle,
1154
+ style: styles$2.subtle,
919
1155
  children: t("generating")
920
1156
  }),
921
1157
  settled && block.isError && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
922
- style: styles$1.error,
1158
+ style: styles$2.error,
923
1159
  children: text.split("\n", 1)[0]
924
1160
  }),
925
- 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, {
926
1162
  images,
927
1163
  load,
928
- align: "start",
929
1164
  labels
930
1165
  }),
931
1166
  settled && !block.isError && images.length === 0 && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
932
- style: styles$1.output,
1167
+ style: styles$2.output,
933
1168
  children: text
934
1169
  })
935
1170
  ]
@@ -958,7 +1193,7 @@ function createVideoLoader(rpc) {
958
1193
  * English-dictionary fallback for a missing locale seat (standalone renders);
959
1194
  * the framework always supplies the namespace-bound one.
960
1195
  */
961
- function fallbackTranslate(key, params) {
1196
+ function fallbackTranslate$1(key, params) {
962
1197
  let text = en[key];
963
1198
  for (const [name, value] of Object.entries(params ?? {})) text = text.replaceAll(`{${name}}`, String(value));
964
1199
  return text;
@@ -1015,7 +1250,7 @@ function base64Bytes(dataBase64) {
1015
1250
  for (let index = 0; index < binary.length; index++) bytes[index] = binary.charCodeAt(index);
1016
1251
  return bytes;
1017
1252
  }
1018
- const styles = {
1253
+ const styles$1 = {
1019
1254
  container: {
1020
1255
  display: "flex",
1021
1256
  flexDirection: "column",
@@ -1076,7 +1311,7 @@ const styles = {
1076
1311
  */
1077
1312
  function VideoGenerateToolview(props) {
1078
1313
  const { block, loadVideo } = props;
1079
- const t = props.t ?? fallbackTranslate;
1314
+ const t = props.t ?? fallbackTranslate$1;
1080
1315
  const settled = block !== void 0 && "kind" in block;
1081
1316
  const isError = settled && block.isError;
1082
1317
  const fileName = block !== void 0 && settled && !isError ? resolveFileName(block) : void 0;
@@ -1109,48 +1344,285 @@ function VideoGenerateToolview(props) {
1109
1344
  const title = `video_generate: ${derivePrompt((settled ? block.call?.argsRaw : block.argsRaw) ?? "")}`;
1110
1345
  const text = settled ? resultText(block) : "";
1111
1346
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1112
- style: styles.container,
1347
+ style: styles$1.container,
1113
1348
  children: [
1114
1349
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1115
- style: styles.row,
1350
+ style: styles$1.row,
1116
1351
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1117
- style: styles.icon,
1352
+ style: styles$1.icon,
1118
1353
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(__deepseek_ai_dsh_client_ui_primitives.IconSparkle16, { size: 14 })
1119
1354
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1120
- style: styles.title,
1355
+ style: styles$1.title,
1121
1356
  children: title
1122
1357
  })]
1123
1358
  }),
1124
1359
  !settled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1125
- style: styles.subtle,
1360
+ style: styles$1.subtle,
1126
1361
  children: t("generatingVideo")
1127
1362
  }),
1128
1363
  settled && isError && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1129
- style: styles.error,
1364
+ style: styles$1.error,
1130
1365
  children: text.split("\n", 1)[0]
1131
1366
  }),
1132
1367
  settled && !isError && fileName !== void 0 && load.phase === "loading" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1133
- style: styles.subtle,
1368
+ style: styles$1.subtle,
1134
1369
  children: t("videoLoading")
1135
1370
  }),
1136
1371
  settled && !isError && fileName !== void 0 && load.phase === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1137
- style: styles.error,
1372
+ style: styles$1.error,
1138
1373
  children: t("videoLoadFailed", { message: load.message })
1139
1374
  }),
1140
1375
  settled && !isError && fileName !== void 0 && load.phase === "ready" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("video", {
1141
- style: styles.video,
1376
+ style: styles$1.video,
1142
1377
  src: load.url,
1143
1378
  controls: true,
1144
1379
  preload: "metadata"
1145
1380
  }),
1146
1381
  settled && !isError && fileName === void 0 && text !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1147
- style: styles.output,
1382
+ style: styles$1.output,
1148
1383
  children: text
1149
1384
  })
1150
1385
  ]
1151
1386
  });
1152
1387
  }
1153
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
+
1154
1626
  //#endregion
1155
1627
  //#region src/client/index.ts
1156
1628
  /** Dictionary namespace owned by this plugin. */
@@ -1203,6 +1675,46 @@ function apply(ctx) {
1203
1675
  locale: NS,
1204
1676
  inject: videoToolviewInjected
1205
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
+ });
1206
1718
  }
1207
1719
 
1208
1720
  //#endregion