minecodex 1.2.0 → 1.2.2

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.
@@ -3,7 +3,7 @@
3
3
  // CodexRuntime 与 CDP 客户端在 codex-runtime.mjs / codex-cdp.mjs。
4
4
 
5
5
  import { randomBytes } from "node:crypto";
6
- import { CODEX_THEME_TOKENS } from "./codex-design-contract.mjs";
6
+ import { CODEX_THEME_TOKENS, CODEX_THEME_TOKEN_ALIASES } from "./codex-design-contract.mjs";
7
7
 
8
8
  export const RUNTIME_VERSION = 101;
9
9
  export const HOST_BINDING_NAME = "__codexPersonalHostAction";
@@ -158,6 +158,7 @@ export function createInjectionSource(features, {
158
158
  temporaryChat: ["temporary chat", "临时聊天", "临时对话"],
159
159
  };
160
160
  const themeTokenNames = config.themeTokens;
161
+ const themeTokenAliases = config.themeTokenAliases;
161
162
 
162
163
  function normalizedLocale(value) {
163
164
  return String(value ?? "").toLowerCase().startsWith("zh") ? "zh-CN" : "en";
@@ -251,10 +252,6 @@ export function createInjectionSource(features, {
251
252
  let modelSelectorObservedTrigger = null;
252
253
  let modelSelectorControllerMenu = null;
253
254
  let modelSelectorNativeFastIcons = null;
254
- // Fast 图标来源标记:bundle 静态提取 vs DOM 克隆,DOM 克隆优先以保持像素一致。
255
- let modelSelectorNativeFastIconSources = null;
256
- // DOM 克隆可见性门控:仅在 Fast 控件从无到有时扫描一次,常驻会话不逐帧扫描。
257
- let modelSelectorFastControlVisible = false;
258
255
  // 持久化脏标记:仅在图标实际变化时写一次 localStorage。
259
256
  let modelSelectorNativeFastIconsDirty = false;
260
257
  const modelSelectorFastIconStorageKey = "codex-model-slider:fast-icons:v1";
@@ -344,10 +341,12 @@ export function createInjectionSource(features, {
344
341
  }
345
342
  [data-codex-model-slider-generic] { box-sizing: border-box; width: 100%; }
346
343
  [data-codex-model-slider-fast] { border: 0; background: transparent; cursor: pointer; }
344
+ [data-codex-model-slider-fast]:not(:hover) [data-codex-model-slider-fast-content] { background: transparent; }
347
345
  [data-codex-model-slider-fast]:focus-visible { outline: 2px solid var(--color-border-focus); outline-offset: 1px; }
348
346
  [data-codex-model-slider-rail][data-codex-model-slider-fallback] { position: relative; height: 38px; margin: 1px 8px 0; outline: none; cursor: pointer; }
349
347
  [data-codex-model-slider-track] { position: absolute; top: 12px; right: 0; left: 0; height: 16px; border-radius: 999px; background: var(--color-background-control); }
350
348
  [data-codex-model-slider-range] { position: absolute; top: 0; bottom: 0; left: 0; width: var(--codex-model-slider-progress); border-radius: inherit; background: var(--color-text-accent); }
349
+ [data-codex-model-slider-rail][data-max="true"] [data-codex-model-slider-range] { background: var(--color-chart-purple); }
351
350
  [data-codex-model-slider-fallback] [data-codex-model-slider-tick] {
352
351
  position: absolute; top: 16px; width: 6px; height: 6px; transform: translate(-50%, -50%); border: 0; border-radius: 50%;
353
352
  padding: 0; background: var(--color-text-foreground-tertiary); pointer-events: none;
@@ -371,10 +370,6 @@ export function createInjectionSource(features, {
371
370
  const persistedIcons = loadPersistedNativeFastIcons(modelSelectorFastIconStorageKey);
372
371
  if (persistedIcons) {
373
372
  modelSelectorNativeFastIcons = persistedIcons;
374
- modelSelectorNativeFastIconSources = {};
375
- for (const state of Object.keys(persistedIcons)) {
376
- modelSelectorNativeFastIconSources[state] = "persisted";
377
- }
378
373
  }
379
374
  void loadNativeFastIconFromBundle();
380
375
  }
@@ -789,7 +784,6 @@ export function createInjectionSource(features, {
789
784
  const source = await fetch(sourceLink.href).then((response) => response.text());
790
785
  if (!modelSelectorNativeFastIcons) {
791
786
  modelSelectorNativeFastIcons = {};
792
- modelSelectorNativeFastIconSources = {};
793
787
  }
794
788
  let gained = false;
795
789
  const define = (state, prefix, definition) => {
@@ -806,7 +800,6 @@ export function createInjectionSource(features, {
806
800
  svg.setAttribute("width", "20");
807
801
  svg.setAttribute("height", "20");
808
802
  modelSelectorNativeFastIcons[state] = svg;
809
- modelSelectorNativeFastIconSources[state] = "bundle";
810
803
  gained = true;
811
804
  };
812
805
  define("active", "M11\\.9125 21\\.4125", { viewBox: "0 0 24 24" });
@@ -822,50 +815,24 @@ export function createInjectionSource(features, {
822
815
  }
823
816
  }
824
817
 
825
- function hasVisibleFastControl() {
826
- return Boolean(
827
- document.querySelector("[data-codex-model-slider-fast]")
828
- || document.querySelector('[class*="_FastModeToggle_"]')
829
- || document.querySelector('[class*="_ModelPickerTriggerFastIndicator_"] svg'),
830
- );
831
- }
832
-
833
818
  function captureNativeFastIcon() {
834
- // Fast 图标只在 Fast 控件渲染后才出现在 DOM(打开菜单或 GPT 模型);
835
- // 只在控件可见性从无到有时做一次全页扫描(升级为 DOM 克隆),
836
- // 常驻会话不逐帧扫描整个文档;缺图标时由 bundle/持久化兜底。
837
- const visible = hasVisibleFastControl();
838
- const justBecameVisible = visible && !modelSelectorFastControlVisible;
839
- modelSelectorFastControlVisible = visible;
840
- if (visible && justBecameVisible) {
841
- const capture = (state, pathPrefix) => {
842
- const current = modelSelectorNativeFastIcons?.[state];
843
- const currentSource = modelSelectorNativeFastIconSources?.[state];
844
- if (current && currentSource === "dom") return;
845
- const path = Array.from(document.querySelectorAll("svg path")).find(
846
- (candidate) => candidate.getAttribute("d")?.startsWith(pathPrefix),
847
- );
848
- const svg = path?.closest("svg");
849
- if (!svg) return;
850
- const template = svg.cloneNode(true);
851
- template.removeAttribute("id");
852
- // 统一呈现尺寸:保留原生 viewBox,固定 20px。DOM 克隆来源是
853
- // 原生 active(24px)/inactive(20px) 两套 SVG,若不归一化,
854
- // active 24px 放进 26px 的 Fast content 容器会显得大一圈。
855
- template.setAttribute("width", "20");
856
- template.setAttribute("height", "20");
857
- if (!modelSelectorNativeFastIcons) {
858
- modelSelectorNativeFastIcons = {};
859
- modelSelectorNativeFastIconSources = {};
860
- }
861
- modelSelectorNativeFastIcons[state] = template;
862
- modelSelectorNativeFastIconSources[state] = "dom";
863
- modelSelectorNativeFastIconsDirty = true;
864
- };
865
- capture("active", "M11.9125 21.4125");
866
- // 现版 app 的 inactive bolt 为 20x20,路径以 M9.80999 开头;
867
- // 旧的 M7.38 前缀已匹配不到任何图标,导致 inactive 缺失。
868
- capture("inactive", "M9.80999 17.8302");
819
+ // 原生控件每次只渲染当前状态;状态切换后按控件本身更新缓存。
820
+ const nativeContent = Array.from(document.querySelectorAll('[class*="_FastModeToggleContent_"]'))
821
+ .find((content) => !content.closest("[data-codex-model-slider-generic]"));
822
+ const toggle = nativeContent?.closest('[class*="_FastModeToggle_"]');
823
+ const state = toggle?.getAttribute("data-fast-mode-enabled") === "true" ? "active" : "inactive";
824
+ const svg = nativeContent?.querySelector("svg");
825
+ const current = modelSelectorNativeFastIcons?.[state];
826
+ if (svg && (current?.innerHTML !== svg.innerHTML
827
+ || current.getAttribute("viewBox") !== svg.getAttribute("viewBox")
828
+ || current.getAttribute("width") !== svg.getAttribute("width"))) {
829
+ const template = svg.cloneNode(true);
830
+ template.removeAttribute("id");
831
+ if (!modelSelectorNativeFastIcons) {
832
+ modelSelectorNativeFastIcons = {};
833
+ }
834
+ modelSelectorNativeFastIcons[state] = template;
835
+ modelSelectorNativeFastIconsDirty = true;
869
836
  }
870
837
  const ready = Boolean(
871
838
  modelSelectorNativeFastIcons?.active && modelSelectorNativeFastIcons?.inactive,
@@ -874,31 +841,42 @@ export function createInjectionSource(features, {
874
841
  persistNativeFastIcons();
875
842
  applyNativeFastIconsToShells();
876
843
  }
877
- return modelSelectorNativeFastIcons;
878
844
  }
879
845
 
880
846
  function applyNativeFastIconsToShells() {
881
847
  document.querySelectorAll("[data-codex-model-slider-fast]").forEach((button) => {
882
848
  const content = button.querySelector("[data-codex-model-slider-fast-content]");
883
- // 幂等补装:只给还没有图标的按钮补上,避免每次 ensure 都重建节点。
884
- if (!content || content.querySelector("svg")) return;
849
+ if (!content) return;
885
850
  const icon = createNativeFastIcon(button.getAttribute("aria-pressed") === "true");
886
- if (icon) content.replaceChildren(icon);
851
+ if (!icon) return;
852
+ const current = content.querySelector("svg");
853
+ if (!current || current.innerHTML !== icon.innerHTML
854
+ || current.getAttribute("viewBox") !== icon.getAttribute("viewBox")
855
+ || current.getAttribute("width") !== icon.getAttribute("width")) {
856
+ content.replaceChildren(icon);
857
+ }
887
858
  });
888
859
  }
889
860
 
890
861
  function createNativeFastIcon(active) {
891
862
  // 直接读取已就绪的图标存储(启动时由 bundle/持久化填充,运行期由
892
863
  // captureNativeFastIcon 升级为 DOM 克隆),避免再次触发捕获造成递归。
893
- // 优先克隆当前面板的图标,保留原生尺寸和路径,不使用旧版图标规格。
894
- const nativeContent = Array.from(document.querySelectorAll('[class*="_FastModeToggleContent_"]'))
895
- .find((content) => !content.closest("[data-codex-model-slider-generic]")
896
- && (content.getAttribute("data-fast-mode-enabled")
897
- ?? content.closest('[class*="_FastModeToggle_"]')?.getAttribute("data-fast-mode-enabled")) === String(active));
864
+ // 优先克隆当前面板的图标;切换瞬间使用缓存时,也沿用原生图标尺寸。
865
+ const nativeContents = Array.from(document.querySelectorAll('[class*="_FastModeToggleContent_"]'))
866
+ .filter((content) => !content.closest("[data-codex-model-slider-generic]"));
867
+ const nativeContent = nativeContents.find((content) => (
868
+ (content.getAttribute("data-fast-mode-enabled")
869
+ ?? content.closest('[class*="_FastModeToggle_"]')?.getAttribute("data-fast-mode-enabled")) === String(active)
870
+ ));
871
+ const nativeSvg = nativeContent?.querySelector("svg") ?? nativeContents[0]?.querySelector("svg");
898
872
  const template = nativeContent?.querySelector("svg")
899
873
  ?? modelSelectorNativeFastIcons?.[active ? "active" : "inactive"];
900
874
  if (!template) return null;
901
875
  const icon = template.cloneNode(true);
876
+ if (nativeSvg) {
877
+ icon.setAttribute("width", nativeSvg.getAttribute("width") ?? "16");
878
+ icon.setAttribute("height", nativeSvg.getAttribute("height") ?? "16");
879
+ }
902
880
  icon.setAttribute("aria-hidden", "true");
903
881
  return icon;
904
882
  }
@@ -994,9 +972,25 @@ export function createInjectionSource(features, {
994
972
  function nativeComposerModelController() {
995
973
  const trigger = nativeComposerTrigger();
996
974
  let fiber = currentReactFiber(trigger);
975
+ let powerController = null;
997
976
  for (let depth = 0; fiber && depth < 40; depth += 1, fiber = fiber.return) {
998
977
  const props = fiber.memoizedProps;
978
+ if (
979
+ Array.isArray(props?.powerSelections)
980
+ && typeof props.onCommitPower === "function"
981
+ ) powerController ??= props;
999
982
  if (Array.isArray(props?.models) && typeof props.onSelectReasoningEffort === "function") return props;
983
+ if (Array.isArray(props?.models) && typeof props.onCommitPower === "function") {
984
+ const power = powerController ?? props;
985
+ return {
986
+ ...props,
987
+ powerSelections: power.powerSelections ?? props.powerSelections,
988
+ selectedPowerSelection: power.selectedPowerSelection ?? props.selectedPowerSelection,
989
+ fallbackPowerSelection: power.fallbackPowerSelection ?? props.fallbackPowerSelection,
990
+ onSelectPower: power.onSelectPower ?? props.onSelectPower,
991
+ onCommitPower: power.onCommitPower,
992
+ };
993
+ }
1000
994
  // Chat/Temporary Chat 使用原生模型选择器,不进入任务模型适配。
1001
995
  if (props?.selectedModel && typeof props.onModelChange === "function") return null;
1002
996
  }
@@ -1663,7 +1657,11 @@ export function createInjectionSource(features, {
1663
1657
  if (!wrapper?.hasAttribute("data-radix-popper-content-wrapper")) return;
1664
1658
  const existing = modelSelectorPositionObservers.get(wrapper);
1665
1659
  if (existing) {
1666
- existing.menu = menu;
1660
+ if (existing.menu !== menu) {
1661
+ existing.resizeObserver.unobserve(existing.menu);
1662
+ existing.resizeObserver.observe(menu);
1663
+ existing.menu = menu;
1664
+ }
1667
1665
  return;
1668
1666
  }
1669
1667
  const record = {
@@ -1713,6 +1711,32 @@ export function createInjectionSource(features, {
1713
1711
  }
1714
1712
  }
1715
1713
 
1714
+ function restoreGenericModelPicker(container) {
1715
+ const original = modelSelectorGenericContainers.get(container);
1716
+ if (!original) return;
1717
+ original.effectMirror?.disconnect();
1718
+ if (container.isConnected) {
1719
+ const menu = container.parentElement;
1720
+ menu?.querySelector(":scope > [data-codex-model-slider-generic]")?.remove();
1721
+ menu?.removeAttribute("data-codex-model-slider-generic-menu");
1722
+ menu?.style.removeProperty("--codex-model-slider-menu-center-shift");
1723
+ container.style.position = original.position;
1724
+ container.style.inset = original.inset;
1725
+ container.style.opacity = original.opacity;
1726
+ container.style.pointerEvents = original.pointerEvents;
1727
+ if (original.ariaHidden == null) container.removeAttribute("aria-hidden");
1728
+ else container.setAttribute("aria-hidden", original.ariaHidden);
1729
+ container.inert = original.inert;
1730
+ }
1731
+ modelSelectorGenericContainers.delete(container);
1732
+ }
1733
+
1734
+ function pruneGenericModelPickerContainers() {
1735
+ for (const container of modelSelectorGenericContainers.keys()) {
1736
+ if (!container.isConnected) restoreGenericModelPicker(container);
1737
+ }
1738
+ }
1739
+
1716
1740
  function closeControllerModelMenu() {
1717
1741
  const record = modelSelectorControllerMenu;
1718
1742
  if (!record) return false;
@@ -1853,22 +1877,6 @@ export function createInjectionSource(features, {
1853
1877
  // 原生 effort 子菜单与滑条视觉 — effort 事务、Fast/通用滑条视觉与状态同步
1854
1878
  // ========================================================================
1855
1879
 
1856
- function openNativeModelSubmenu(parentMenu) {
1857
- let attempts = 0;
1858
- const open = () => {
1859
- attempts += 1;
1860
- const currentMenu = nativeModelPickerMenu() ?? parentMenu;
1861
- const row = nativeModelPickerRow(currentMenu);
1862
- if (row) {
1863
- setReactControlledMenuOpen(row, true);
1864
- setTimeout(queueEnsure, 0);
1865
- return;
1866
- }
1867
- if (attempts < 12) requestAnimationFrame(open);
1868
- };
1869
- setTimeout(open, 80);
1870
- }
1871
-
1872
1880
  function nativeEffortRow(parentMenu) {
1873
1881
  return Array.from(parentMenu?.querySelectorAll('[role="menuitem"][aria-haspopup="menu"]') ?? [])
1874
1882
  .filter((row) => !row.hasAttribute("data-model-picker-view-toggle"))[1] ?? null;
@@ -1895,6 +1903,30 @@ export function createInjectionSource(features, {
1895
1903
  queueEnsure();
1896
1904
  return;
1897
1905
  }
1906
+ if (typeof controller?.onCommitPower === "function") {
1907
+ const normalizedEffort = String(effort).toLowerCase();
1908
+ const activeModel = String(controller.model ?? nativeCatalogModelFor(identity)?.slug ?? identity.raw);
1909
+ const next = controller.powerSelections?.find((selection) => (
1910
+ String(selection?.model ?? "") === activeModel
1911
+ && String(selection?.reasoningEffort ?? "").toLowerCase() === normalizedEffort
1912
+ ));
1913
+ const previous = controller.selectedPowerSelection
1914
+ ?? controller.powerSelections?.find((selection) => (
1915
+ String(selection?.model ?? "") === activeModel
1916
+ && String(selection?.reasoningEffort ?? "").toLowerCase()
1917
+ === String(controller.reasoningEffort ?? "").toLowerCase()
1918
+ ))
1919
+ ?? controller.fallbackPowerSelection;
1920
+ if (!next || !previous) return;
1921
+ controller.onSelectPower?.(next);
1922
+ controller.onCommitPower(next, previous);
1923
+ if (modelSelectorPendingOutsideDismiss) {
1924
+ closeReactControlledMenu(nativeComposerTrigger());
1925
+ modelSelectorPendingOutsideDismiss = false;
1926
+ }
1927
+ queueEnsure();
1928
+ return;
1929
+ }
1898
1930
  const row = nativeEffortRow(parentMenu);
1899
1931
  if (!row) return;
1900
1932
  beginNativeEffortTransaction();
@@ -1960,6 +1992,19 @@ export function createInjectionSource(features, {
1960
1992
  rail.setAttribute("aria-valuenow", String(index));
1961
1993
  }
1962
1994
 
1995
+ function syncGenericMaximumVisual(rail, effortText, fastButton, maximum) {
1996
+ const value = String(maximum);
1997
+ rail?.setAttribute("data-highlighted", value);
1998
+ rail?.setAttribute("data-max", value);
1999
+ effortText?.setAttribute("data-maximum", value);
2000
+ fastButton?.setAttribute("data-max-power-selection", value);
2001
+ const maxEffects = rail?.querySelector('[class*="_MaxEffects_"]');
2002
+ if (maxEffects) {
2003
+ maxEffects.style.opacity = maximum ? "1" : "0";
2004
+ if (maximum) maxEffects.style.transform = "translateX(0)";
2005
+ }
2006
+ }
2007
+
1963
2008
  function neutralizeStaleSliderVisual(rail) {
1964
2009
  for (const selector of [
1965
2010
  "[data-codex-model-slider-native-range]",
@@ -1983,9 +2028,21 @@ export function createInjectionSource(features, {
1983
2028
  return value - Math.floor(value);
1984
2029
  }
1985
2030
 
1986
- function syncNativeFastParticles(rail, enabled) {
2031
+ function syncNativeFastParticles(rail, enabled, entering = false) {
1987
2032
  const track = Array.from(rail.children).find((element) => String(element.className).includes("_Track_"));
1988
2033
  if (!track) return;
2034
+ const staticParticles = rail.querySelector('[class*="_ParticleTransition_"]');
2035
+ if (staticParticles && !matchMedia("(prefers-reduced-motion: reduce)").matches) {
2036
+ staticParticles.__codexStaticParticleFade?.cancel();
2037
+ staticParticles.style.opacity = enabled ? "0" : "1";
2038
+ if (entering) {
2039
+ // 动态粒子渐显时,让原有星点在同一时间淡出。
2040
+ staticParticles.__codexStaticParticleFade = staticParticles.animate(
2041
+ [{ opacity: 1 }, { opacity: 0 }],
2042
+ { duration: 240, easing: "ease-out" },
2043
+ );
2044
+ }
2045
+ }
1989
2046
  track.querySelectorAll('[class*="_FastParticleClip_"]').forEach((element) => element.remove());
1990
2047
  if (!enabled || matchMedia("(prefers-reduced-motion: reduce)").matches) return;
1991
2048
  const clipClass = nativeCssModuleClass("FastParticleClip");
@@ -2025,6 +2082,10 @@ export function createInjectionSource(features, {
2025
2082
  clip.append(particles);
2026
2083
  const tickRail = Array.from(track.children).find((element) => String(element.className).includes("_TickRail_"));
2027
2084
  track.insertBefore(clip, tickRail ?? null);
2085
+ if (entering) {
2086
+ // 首帧沿用关闭 Fast 时的星点,再渐显流动粒子。
2087
+ clip.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 240, easing: "ease-out" });
2088
+ }
2028
2089
  }
2029
2090
 
2030
2091
  function syncGenericFastVisual(shell, enabled, maxPowerSelection, animate = false) {
@@ -2033,7 +2094,12 @@ export function createInjectionSource(features, {
2033
2094
  if (!button || !rail) return;
2034
2095
  button.setAttribute("aria-pressed", String(enabled));
2035
2096
  button.setAttribute("data-fast-mode-enabled", String(enabled));
2036
- button.setAttribute("data-max-power-selection", String(maxPowerSelection));
2097
+ syncGenericMaximumVisual(
2098
+ rail,
2099
+ shell.querySelector("[data-codex-model-slider-effort-row] [data-maximum]"),
2100
+ button,
2101
+ maxPowerSelection,
2102
+ );
2037
2103
  button.setAttribute("aria-label", enabled
2038
2104
  ? (locale === "zh-CN" ? "关闭 Fast 模式" : "Disable Fast mode")
2039
2105
  : (locale === "zh-CN" ? "开启 Fast 模式" : "Enable Fast mode"));
@@ -2042,8 +2108,18 @@ export function createInjectionSource(features, {
2042
2108
  if (icon && content) content.replaceChildren(icon);
2043
2109
  rail.setAttribute("data-fast-mode", String(enabled));
2044
2110
  rail.setAttribute("data-fast-mode-dot-transition", animate ? (enabled ? "entering" : "exiting") : (enabled ? "active" : "inactive"));
2045
- syncNativeFastParticles(rail, enabled);
2111
+ syncNativeFastParticles(rail, enabled, animate && enabled);
2046
2112
  setGenericSliderVisual(rail, Number(rail.getAttribute("aria-valuenow") ?? 0), Number(rail.getAttribute("aria-valuemax") ?? 0) + 1);
2113
+ if (animate && enabled) {
2114
+ shell.setAttribute("data-codex-model-slider-fast-transitioning", "");
2115
+ setTimeout(() => {
2116
+ if (!shell.isConnected) return;
2117
+ shell.removeAttribute("data-codex-model-slider-fast-transitioning");
2118
+ queueEnsure();
2119
+ }, 260);
2120
+ } else {
2121
+ shell.removeAttribute("data-codex-model-slider-fast-transitioning");
2122
+ }
2047
2123
  if (animate) {
2048
2124
  setTimeout(() => {
2049
2125
  if (rail.isConnected && rail.getAttribute("data-fast-mode") === String(enabled)) {
@@ -2102,6 +2178,88 @@ export function createInjectionSource(features, {
2102
2178
  return clone;
2103
2179
  }
2104
2180
 
2181
+ function syncClonedPowerEffects(nativeContainer, shell) {
2182
+ const source = nativeContainer.querySelector("[data-model-picker-power-slider]");
2183
+ const clone = shell.querySelector("[data-codex-model-slider-native-slider]");
2184
+ if (!source || !clone) return null;
2185
+ const sourceCanvas = source.querySelector("canvas");
2186
+ const cloneCanvas = clone.querySelector("canvas");
2187
+ let canvasTimer = null;
2188
+ let observer = null;
2189
+ const disconnect = () => {
2190
+ observer?.disconnect();
2191
+ if (canvasTimer != null) clearInterval(canvasTimer);
2192
+ };
2193
+ if (sourceCanvas && cloneCanvas) {
2194
+ // 原生 canvas 可能在首次克隆时尚未绘制;持续同步其高光纹理。
2195
+ const scratch = document.createElement("canvas");
2196
+ const scratchContext = scratch.getContext("2d");
2197
+ const cloneContext = cloneCanvas.getContext("2d");
2198
+ const copyCanvas = () => {
2199
+ if (!source.isConnected || !shell.isConnected) {
2200
+ disconnect();
2201
+ return;
2202
+ }
2203
+ if (scratch.width !== sourceCanvas.width || scratch.height !== sourceCanvas.height) {
2204
+ scratch.width = sourceCanvas.width;
2205
+ scratch.height = sourceCanvas.height;
2206
+ cloneCanvas.width = sourceCanvas.width;
2207
+ cloneCanvas.height = sourceCanvas.height;
2208
+ }
2209
+ scratchContext.clearRect(0, 0, scratch.width, scratch.height);
2210
+ scratchContext.drawImage(sourceCanvas, 0, 0);
2211
+ const y = Math.floor(scratch.height / 2);
2212
+ const hasDrawing = [0.25, 0.5, 0.75].some((ratio) => (
2213
+ scratchContext.getImageData(Math.floor(scratch.width * ratio), y, 1, 1).data[3] > 0
2214
+ ));
2215
+ if (!hasDrawing) return;
2216
+ cloneContext.clearRect(0, 0, cloneCanvas.width, cloneCanvas.height);
2217
+ cloneContext.drawImage(scratch, 0, 0);
2218
+ };
2219
+ copyCanvas();
2220
+ canvasTimer = setInterval(copyCanvas, 80);
2221
+ }
2222
+ const sourceEffects = source.querySelector('[class*="_MaxEffects_"]');
2223
+ const cloneEffects = clone.querySelector('[class*="_MaxEffects_"]');
2224
+ if (!sourceEffects || !cloneEffects) return { disconnect };
2225
+ const sync = () => {
2226
+ if (!source.isConnected || !shell.isConnected) {
2227
+ disconnect();
2228
+ return;
2229
+ }
2230
+ const sourceTransition = sourceEffects.querySelector('[class*="_ParticleTransition_"]');
2231
+ let cloneTransition = cloneEffects.querySelector('[class*="_ParticleTransition_"]');
2232
+ if (!sourceTransition) {
2233
+ if (clone.querySelector("[data-fast-mode]")?.getAttribute("data-fast-mode") !== "true") {
2234
+ cloneTransition?.remove();
2235
+ }
2236
+ return;
2237
+ }
2238
+ const sourceParticles = sourceTransition.querySelectorAll('[class*="_TrackParticle_"]');
2239
+ let cloneParticles = cloneTransition?.querySelectorAll('[class*="_TrackParticle_"]');
2240
+ if (!cloneTransition || sourceParticles.length !== cloneParticles.length) {
2241
+ cloneTransition?.remove();
2242
+ cloneTransition = sourceTransition.cloneNode(true);
2243
+ cloneEffects.append(cloneTransition);
2244
+ cloneParticles = cloneTransition.querySelectorAll('[class*="_TrackParticle_"]');
2245
+ }
2246
+ cloneTransition.setAttribute("style", sourceTransition.getAttribute("style") ?? "");
2247
+ sourceParticles.forEach((particle, index) => {
2248
+ const style = particle.getAttribute("style") ?? "";
2249
+ if (cloneParticles[index].getAttribute("style") !== style) {
2250
+ cloneParticles[index].setAttribute("style", style);
2251
+ }
2252
+ });
2253
+ if (!matchMedia("(prefers-reduced-motion: reduce)").matches) {
2254
+ cloneTransition.style.opacity = clone.querySelector("[data-fast-mode]")?.getAttribute("data-fast-mode") === "true" ? "0" : "1";
2255
+ }
2256
+ };
2257
+ observer = new MutationObserver(sync);
2258
+ observer.observe(sourceEffects, { attributes: true, attributeFilter: ["style"], childList: true, subtree: true });
2259
+ sync();
2260
+ return { disconnect };
2261
+ }
2262
+
2105
2263
  function createGenericModelSlider(parentMenu, selector, identity, model, selectedEffort) {
2106
2264
  const efforts = model.supportedReasoningLevels.map((level) => level.effort);
2107
2265
  const selectedIndex = efforts.indexOf(selectedEffort);
@@ -2158,9 +2316,21 @@ export function createInjectionSource(features, {
2158
2316
  buttonLabel.className = classes.ViewToggleModelLabel;
2159
2317
  buttonLabel.setAttribute("data-codex-model-slider-model-label", "");
2160
2318
  buttonLabel.textContent = modelDisplayLabel(identity, selector);
2319
+ const modelRow = document.createElement("span");
2320
+ modelRow.setAttribute("data-codex-model-slider-model-row", "");
2321
+ modelRow.style.display = "inline-flex";
2322
+ modelRow.style.alignItems = "center";
2323
+ modelRow.style.justifyContent = "center";
2324
+ modelRow.style.gap = "calc(var(--spacing) / 2)";
2325
+ modelRow.style.minWidth = "0";
2326
+ modelRow.style.maxWidth = "100%";
2327
+ modelRow.append(buttonLabel);
2161
2328
  const effortText = selectedEffortVisible ? document.createElement("span") : null;
2162
2329
  if (effortText) {
2163
2330
  effortText.className = classes.ViewToggleEffortLabel;
2331
+ // 原生标签会跨满整行;收回中列,使上下两行各自居中。
2332
+ effortText.style.gridColumn = "2";
2333
+ effortText.style.gridRow = "1";
2164
2334
  effortText.textContent = effortLabel(selectedEffort);
2165
2335
  effortText.setAttribute("data-accent", "true");
2166
2336
  effortText.setAttribute("data-effort-only", "false");
@@ -2174,13 +2344,13 @@ export function createInjectionSource(features, {
2174
2344
  )?.cloneNode(true);
2175
2345
  if (nativeChevron) {
2176
2346
  nativeChevron.classList.add(classes.ViewToggleIcon);
2347
+ nativeChevron.style.gridArea = "auto";
2348
+ nativeChevron.style.flexShrink = "0";
2177
2349
  nativeChevron.setAttribute("aria-hidden", "true");
2178
- if (effortText) effortRow.append(effortText);
2179
- effortRow.append(nativeChevron);
2180
- } else {
2181
- if (effortText) effortRow.append(effortText);
2350
+ modelRow.append(nativeChevron);
2182
2351
  }
2183
- buttonContent.append(effortRow, buttonLabel);
2352
+ if (effortText) effortRow.append(effortText);
2353
+ buttonContent.append(effortRow, modelRow);
2184
2354
  modelButton.append(buttonContent);
2185
2355
  const openModels = () => openControllerModelMenu(parentMenu, modelButton, selector);
2186
2356
  modelButton.addEventListener("click", openModels);
@@ -2297,7 +2467,12 @@ export function createInjectionSource(features, {
2297
2467
  }
2298
2468
  setGenericSliderVisual(rail, selectedIndex, efforts.length);
2299
2469
  syncNativeFastParticles(rail, fastEnabled);
2300
- setGenericSliderVisual(rail, selectedIndex, efforts.length);
2470
+ syncGenericMaximumVisual(
2471
+ rail,
2472
+ effortText,
2473
+ shell.querySelector("[data-codex-model-slider-fast]"),
2474
+ selectedEffort === "ultra",
2475
+ );
2301
2476
 
2302
2477
  let pendingIndex = selectedIndex;
2303
2478
  let dragging = false;
@@ -2316,10 +2491,9 @@ export function createInjectionSource(features, {
2316
2491
  rail.setAttribute("aria-valuetext", effortLabel(effort));
2317
2492
  if (effortText) {
2318
2493
  effortText.textContent = effortLabel(effort);
2319
- effortText.setAttribute("data-maximum", String(effort === "ultra"));
2320
2494
  }
2321
2495
  const fastButton = shell.querySelector("[data-codex-model-slider-fast]");
2322
- fastButton?.setAttribute("data-max-power-selection", String(effort === "ultra"));
2496
+ syncGenericMaximumVisual(rail, effortText, fastButton, effort === "ultra");
2323
2497
  };
2324
2498
  const preview = (clientX) => previewIndex(indexAt(clientX));
2325
2499
  rail.addEventListener("pointerdown", (event) => {
@@ -2421,7 +2595,8 @@ export function createInjectionSource(features, {
2421
2595
  const selectedServiceTier = nativeComposerModelController()?.selectedServiceTier;
2422
2596
  const serviceTierKey = selectedServiceTier?.id ?? selectedServiceTier ?? "standard";
2423
2597
  const selectionMode = nativeComposerModelController()?.selectionMode ?? "";
2424
- const key = `${identity.raw}:${selectedEffortValue}:${serviceTierKey}:${selectionMode}:${efforts.map((level) => level.effort).join(",")}`;
2598
+ const structureKey = `${identity.raw}:${selectedEffortValue}:${selectionMode}:${efforts.map((level) => level.effort).join(",")}`;
2599
+ const key = `${structureKey}:${serviceTierKey}`;
2425
2600
  const existing = parentMenu.querySelector(":scope > [data-codex-model-slider-generic]");
2426
2601
  const nativeTemplateReady = Boolean(
2427
2602
  parentMenu.firstElementChild
@@ -2439,11 +2614,24 @@ export function createInjectionSource(features, {
2439
2614
  const canUpgradeFallback = Boolean(
2440
2615
  existing?.querySelector("[data-codex-model-slider-fallback]") && nativeTemplateReady,
2441
2616
  );
2617
+ if (existing?.getAttribute("data-codex-model-slider-structure-key") === structureKey
2618
+ && existing.hasAttribute("data-codex-model-slider-fast-transitioning") && !canUpgradeFallback) {
2619
+ centerGenericModelPicker(parentMenu);
2620
+ return true;
2621
+ }
2622
+ const currentFast = existing?.querySelector("[data-codex-model-slider-rail]")?.getAttribute("data-fast-mode");
2623
+ const nativeFast = parentMenu.firstElementChild
2624
+ ?.querySelector("[data-model-picker-power-slider] [data-fast-mode]")?.getAttribute("data-fast-mode");
2625
+ if (existing?.getAttribute("data-codex-model-slider-structure-key") === structureKey
2626
+ && currentFast != null && currentFast === nativeFast && !canUpgradeFallback) {
2627
+ existing.setAttribute("data-codex-model-slider-generic", key);
2628
+ centerGenericModelPicker(parentMenu);
2629
+ return true;
2630
+ }
2442
2631
  if (existing?.getAttribute("data-codex-model-slider-generic") === key && !canUpgradeFallback) {
2443
2632
  centerGenericModelPicker(parentMenu);
2444
2633
  return true;
2445
2634
  }
2446
- existing?.remove();
2447
2635
  const nativeContainer = parentMenu.firstElementChild;
2448
2636
  if (!nativeContainer) return false;
2449
2637
  const shell = createGenericModelSlider(
@@ -2453,29 +2641,37 @@ export function createInjectionSource(features, {
2453
2641
  displayModel,
2454
2642
  selectedEffortValue,
2455
2643
  );
2456
- if (!shell) return false;
2644
+ if (!shell) {
2645
+ restoreGenericModelPicker(nativeContainer);
2646
+ return false;
2647
+ }
2648
+ modelSelectorGenericContainers.get(nativeContainer)?.effectMirror?.disconnect();
2649
+ existing?.remove();
2457
2650
  if (!modelSelectorGenericContainers.has(nativeContainer)) {
2458
- modelSelectorGenericContainers.set(nativeContainer, nativeContainer.style.display);
2651
+ modelSelectorGenericContainers.set(nativeContainer, {
2652
+ position: nativeContainer.style.position,
2653
+ inset: nativeContainer.style.inset,
2654
+ opacity: nativeContainer.style.opacity,
2655
+ pointerEvents: nativeContainer.style.pointerEvents,
2656
+ ariaHidden: nativeContainer.getAttribute("aria-hidden"),
2657
+ inert: nativeContainer.inert,
2658
+ });
2459
2659
  }
2460
2660
  parentMenu.setAttribute("data-codex-model-slider-generic-menu", "");
2461
- nativeContainer.style.display = "none";
2661
+ nativeContainer.style.position = "absolute";
2662
+ nativeContainer.style.inset = "0";
2663
+ nativeContainer.style.opacity = "0";
2664
+ nativeContainer.style.pointerEvents = "none";
2665
+ nativeContainer.setAttribute("aria-hidden", "true");
2666
+ nativeContainer.inert = true;
2462
2667
  shell.setAttribute("data-codex-model-slider-generic", key);
2668
+ shell.setAttribute("data-codex-model-slider-structure-key", structureKey);
2463
2669
  parentMenu.append(shell);
2670
+ modelSelectorGenericContainers.get(nativeContainer).effectMirror = syncClonedPowerEffects(nativeContainer, shell);
2464
2671
  centerGenericModelPicker(parentMenu);
2465
2672
  return true;
2466
2673
  }
2467
2674
 
2468
- function restoreGenericModelPicker(parentMenu) {
2469
- parentMenu.querySelector(":scope > [data-codex-model-slider-generic]")?.remove();
2470
- parentMenu.removeAttribute("data-codex-model-slider-generic-menu");
2471
- parentMenu.style.removeProperty("--codex-model-slider-menu-center-shift");
2472
- const nativeContainer = parentMenu.firstElementChild;
2473
- if (nativeContainer && modelSelectorGenericContainers.has(nativeContainer)) {
2474
- nativeContainer.style.display = modelSelectorGenericContainers.get(nativeContainer);
2475
- modelSelectorGenericContainers.delete(nativeContainer);
2476
- }
2477
- }
2478
-
2479
2675
  function enhanceNativeModelPicker(parentMenu, selector) {
2480
2676
  if (enhanceGenericModelPicker(parentMenu, selector)) return;
2481
2677
  const toggle = parentMenu.querySelector("[data-model-picker-view-toggle]");
@@ -2597,6 +2793,7 @@ export function createInjectionSource(features, {
2597
2793
  }
2598
2794
  pruneModelSubmenuPositionObservers();
2599
2795
  pruneNativeModelToggleHandlers();
2796
+ pruneGenericModelPickerContainers();
2600
2797
  document.documentElement.setAttribute("data-codex-model-slider-catalog-ready", "");
2601
2798
  if (!modelSelectorStyle.isConnected) document.head?.append(modelSelectorStyle);
2602
2799
  enhanceComposerModelTrigger(selector);
@@ -2667,8 +2864,8 @@ export function createInjectionSource(features, {
2667
2864
  if (handlers.originalAriaExpanded == null) toggle.removeAttribute("aria-expanded");
2668
2865
  else toggle.setAttribute("aria-expanded", handlers.originalAriaExpanded);
2669
2866
  }
2670
- for (const [container, display] of modelSelectorGenericContainers) {
2671
- if (container.isConnected) container.style.display = display;
2867
+ for (const container of modelSelectorGenericContainers.keys()) {
2868
+ restoreGenericModelPicker(container);
2672
2869
  }
2673
2870
  document.querySelectorAll("[data-codex-model-slider-divider]")
2674
2871
  .forEach((element) => element.remove());
@@ -2685,7 +2882,6 @@ export function createInjectionSource(features, {
2685
2882
  document.documentElement.removeAttribute("data-codex-model-slider-catalog-ready");
2686
2883
  modelSelectorStyle.remove();
2687
2884
  modelSelectorOriginalToggleLabels.clear();
2688
- modelSelectorGenericContainers.clear();
2689
2885
  modelSelectorNativeToggleHandlers.clear();
2690
2886
  }
2691
2887
 
@@ -2728,6 +2924,10 @@ export function createInjectionSource(features, {
2728
2924
  // CDP 重连会轮换 token;保留已有页面和草稿,只重试未就绪的页面。
2729
2925
  config.bindingName = bindingName;
2730
2926
  config.bindingToken = bindingToken;
2927
+ for (const feature of pageScriptFeatures) {
2928
+ feature.pageScript.config.bindingName = bindingName;
2929
+ feature.pageScript.config.bindingToken = bindingToken;
2930
+ }
2731
2931
  for (const [requestId, pending] of pendingHostActions) {
2732
2932
  if (pending.kind === "surface-load") pendingHostActions.delete(requestId);
2733
2933
  }
@@ -2821,7 +3021,17 @@ export function createInjectionSource(features, {
2821
3021
  .map((name) => [name, style.getPropertyValue(name).trim()])
2822
3022
  .filter(([, value]) => value),
2823
3023
  );
2824
- const mode = root.classList.contains("electron-dark") || style.colorScheme === "dark"
3024
+ // 按语义和命名空间解析,不依赖构建哈希或猜测近似名称。
3025
+ for (const name of themeTokenNames) {
3026
+ if (tokens[name]) continue;
3027
+ const semanticNames = [name, ...(themeTokenAliases[name] ?? [])];
3028
+ const candidates = semanticNames.flatMap((candidate) => candidate.startsWith("--color-")
3029
+ ? [candidate, candidate.replace("--color-", "--app-color-")]
3030
+ : [candidate]);
3031
+ const value = candidates.map((candidate) => style.getPropertyValue(candidate).trim()).find(Boolean);
3032
+ if (value) tokens[name] = value;
3033
+ }
3034
+ const mode = root.dataset.theme === "dark" || root.classList.contains("electron-dark") || style.colorScheme === "dark"
2825
3035
  ? "dark"
2826
3036
  : "light";
2827
3037
  return { root, style, tokens, mode };
@@ -3091,6 +3301,8 @@ export function createInjectionSource(features, {
3091
3301
  frame.title = title;
3092
3302
  frame.allow = "clipboard-write";
3093
3303
  frame.style.cssText = "width:100%;height:100%;border:0;display:block;background:transparent";
3304
+ // 图片页覆盖原生标题栏,避免控件点击被窗口拖拽区域吞掉。
3305
+ if (featureId === "images") frame.style.setProperty("-webkit-app-region", "no-drag");
3094
3306
  return frame;
3095
3307
  }
3096
3308
 
@@ -4290,7 +4502,7 @@ export function createInjectionSource(features, {
4290
4502
  respondToSurface(record, requestId, { ok: true, result: { paths } });
4291
4503
  return;
4292
4504
  }
4293
- if (["attach-file", "create-image-edit-thread"].includes(action)) {
4505
+ if (["attach-file", "create-image-edit-thread", "list-cloud-images", "import-cloud-image"].includes(action)) {
4294
4506
  if (typeof globalThis[config.bindingName] !== "function") {
4295
4507
  throw new Error("The native file bridge is unavailable");
4296
4508
  }
@@ -4472,7 +4684,7 @@ export function createInjectionSource(features, {
4472
4684
  const themeObserver = new MutationObserver(queueTheme);
4473
4685
  themeObserver.observe(document.documentElement, {
4474
4686
  attributes: true,
4475
- attributeFilter: ["class", "style"],
4687
+ attributeFilter: ["class", "style", "data-theme"],
4476
4688
  });
4477
4689
  domObservers.push(themeObserver);
4478
4690
 
@@ -4559,6 +4771,7 @@ export function createInjectionSource(features, {
4559
4771
  summaryLayout: RESPONSIVE_SUMMARY_LAYOUT,
4560
4772
  rightPanelRouteKinds: RIGHT_PANEL_ROUTE_KINDS,
4561
4773
  themeTokens: CODEX_THEME_TOKENS,
4774
+ themeTokenAliases: CODEX_THEME_TOKEN_ALIASES,
4562
4775
  features: runtimeFeatures,
4563
4776
  })})`;
4564
4777
  }