minecodex 1.2.1 → 2.0.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.
Files changed (99) hide show
  1. package/README.md +9 -3
  2. package/features/images/README.md +16 -98
  3. package/features/images/codex-feature.json +6 -14
  4. package/features/images/src/http-server.mjs +15 -5
  5. package/features/images/src/image-library.mjs +37 -9
  6. package/features/images/src/library-prompt-page.mjs +239 -0
  7. package/features/images/src/prompt-index.mjs +27 -24
  8. package/features/images/src/prompt-source.mjs +100 -0
  9. package/features/images/web/app.js +180 -67
  10. package/features/images/web/i18n.mjs +5 -0
  11. package/features/images/web/index.html +29 -24
  12. package/features/images/web/styles.css +169 -139
  13. package/features/model-slider/README.md +13 -1
  14. package/features/model-slider/THIRD_PARTY_NOTICES.md +1 -1
  15. package/features/notes/DESIGN.md +66 -0
  16. package/features/notes/README.md +18 -99
  17. package/features/notes/assets/icons/book-open-nav.svg +15 -0
  18. package/features/notes/assets/icons/book-open.svg +16 -0
  19. package/features/notes/assets/native-emojis.json +1 -0
  20. package/features/notes/assets/native-prompt-icons.json +28 -0
  21. package/features/notes/codex-feature.json +12 -70
  22. package/features/notes/src/main.mjs +2 -2
  23. package/features/notes/src/product.mjs +287 -613
  24. package/features/notes/src/prompt-schema.mjs +117 -0
  25. package/features/notes/src/quick-prompts-page.mjs +1559 -0
  26. package/features/notes/src/quick-prompts-server.mjs +167 -0
  27. package/features/session-status/DESIGN.md +10 -2
  28. package/features/session-status/README.md +13 -3
  29. package/features/session-status/src/session-status.mjs +329 -24
  30. package/features/sidebar-focus/DESIGN.md +16 -0
  31. package/features/sidebar-focus/codex-feature.json +20 -0
  32. package/features/sidebar-focus/src/sidebar-focus.mjs +134 -0
  33. package/features/sidebar-focus/vendor/lucide-LICENSE +43 -0
  34. package/features/unread-inbox/DESIGN.md +28 -0
  35. package/features/unread-inbox/codex-feature.json +20 -0
  36. package/features/unread-inbox/src/unread-inbox.mjs +360 -0
  37. package/features/usage-meter/DESIGN.md +49 -0
  38. package/features/usage-meter/README.md +18 -0
  39. package/features/usage-meter/codex-feature.json +13 -0
  40. package/features/usage-meter/src/usage-meter.mjs +200 -0
  41. package/package.json +21 -6
  42. package/packages/cli/src/config.mjs +4 -1
  43. package/packages/cli/src/control-server.mjs +2 -2
  44. package/packages/runtime-host/src/cloud-images.mjs +123 -0
  45. package/packages/runtime-host/src/codex-cdp.mjs +42 -8
  46. package/packages/runtime-host/src/codex-design-contract.mjs +10 -0
  47. package/packages/runtime-host/src/codex-injection.mjs +364 -116
  48. package/packages/runtime-host/src/codex-runtime.mjs +105 -28
  49. package/packages/runtime-host/src/feature-registry.mjs +3 -0
  50. package/packages/runtime-host/src/session-status-reader.mjs +65 -24
  51. package/features/notes/assets/icons/circle.svg +0 -15
  52. package/features/notes/assets/icons/ellipsis.svg +0 -17
  53. package/features/notes/assets/icons/external-link.svg +0 -17
  54. package/features/notes/assets/icons/list-todo.svg +0 -19
  55. package/features/notes/assets/icons/message-square-quote.svg +0 -17
  56. package/features/notes/assets/icons/paperclip.svg +0 -15
  57. package/features/notes/assets/icons/star-off.svg +0 -17
  58. package/features/notes/assets/icons/star.svg +0 -15
  59. package/features/notes/assets/icons/sticky-note.svg +0 -16
  60. package/features/notes/assets/icons/trash-2.svg +0 -19
  61. package/features/notes/src/http-server.mjs +0 -580
  62. package/features/notes/web/app.js +0 -1202
  63. package/features/notes/web/codex-artifact-document.svg +0 -1
  64. package/features/notes/web/codex-attachment.svg +0 -1
  65. package/features/notes/web/codex-build.svg +0 -1
  66. package/features/notes/web/codex-code.svg +0 -1
  67. package/features/notes/web/codex-cplusplus.svg +0 -1
  68. package/features/notes/web/codex-css.svg +0 -1
  69. package/features/notes/web/codex-document.svg +0 -1
  70. package/features/notes/web/codex-file.svg +0 -1
  71. package/features/notes/web/codex-folder.svg +0 -1
  72. package/features/notes/web/codex-hashes.svg +0 -1
  73. package/features/notes/web/codex-html.svg +0 -1
  74. package/features/notes/web/codex-image.svg +0 -1
  75. package/features/notes/web/codex-java.svg +0 -1
  76. package/features/notes/web/codex-javascript.svg +0 -1
  77. package/features/notes/web/codex-json.svg +0 -1
  78. package/features/notes/web/codex-notebook.svg +0 -1
  79. package/features/notes/web/codex-pdf.svg +0 -1
  80. package/features/notes/web/codex-pencil.svg +0 -3
  81. package/features/notes/web/codex-php.svg +0 -1
  82. package/features/notes/web/codex-plus.svg +0 -3
  83. package/features/notes/web/codex-presentation.svg +0 -1
  84. package/features/notes/web/codex-python.svg +0 -1
  85. package/features/notes/web/codex-react.svg +0 -1
  86. package/features/notes/web/codex-rust.svg +0 -1
  87. package/features/notes/web/codex-shell.svg +0 -1
  88. package/features/notes/web/codex-skill.svg +0 -1
  89. package/features/notes/web/codex-spreadsheet.svg +0 -1
  90. package/features/notes/web/codex-task.svg +0 -3
  91. package/features/notes/web/codex-terminal.svg +0 -1
  92. package/features/notes/web/codex-toml.svg +0 -1
  93. package/features/notes/web/codex-typescript.svg +0 -1
  94. package/features/notes/web/codex-view-all.svg +0 -3
  95. package/features/notes/web/file-icons.mjs +0 -68
  96. package/features/notes/web/i18n.mjs +0 -237
  97. package/features/notes/web/index.html +0 -15
  98. package/features/notes/web/styles.css +0 -1155
  99. package/features/notes/web/todo-drag.mjs +0 -18
@@ -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
  }
@@ -395,12 +390,14 @@ export function createInjectionSource(features, {
395
390
  "config",
396
391
  `"use strict";\n${source}`,
397
392
  );
393
+ const pageConfig = feature.pageScript.config ??= {};
394
+ pageConfig.requestHostAction = (action, payload) => requestPageHostAction(feature.id, action, payload);
398
395
  const cleanup = installer(
399
396
  lifetime.signal,
400
397
  window,
401
398
  document,
402
399
  MutationObserver,
403
- feature.pageScript.config ?? {},
400
+ pageConfig,
404
401
  );
405
402
  if (typeof cleanup === "function") pageScriptCleanups.push(cleanup);
406
403
  } catch (error) {
@@ -789,7 +786,6 @@ export function createInjectionSource(features, {
789
786
  const source = await fetch(sourceLink.href).then((response) => response.text());
790
787
  if (!modelSelectorNativeFastIcons) {
791
788
  modelSelectorNativeFastIcons = {};
792
- modelSelectorNativeFastIconSources = {};
793
789
  }
794
790
  let gained = false;
795
791
  const define = (state, prefix, definition) => {
@@ -806,7 +802,6 @@ export function createInjectionSource(features, {
806
802
  svg.setAttribute("width", "20");
807
803
  svg.setAttribute("height", "20");
808
804
  modelSelectorNativeFastIcons[state] = svg;
809
- modelSelectorNativeFastIconSources[state] = "bundle";
810
805
  gained = true;
811
806
  };
812
807
  define("active", "M11\\.9125 21\\.4125", { viewBox: "0 0 24 24" });
@@ -822,50 +817,24 @@ export function createInjectionSource(features, {
822
817
  }
823
818
  }
824
819
 
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
820
  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");
821
+ // 原生控件每次只渲染当前状态;状态切换后按控件本身更新缓存。
822
+ const nativeContent = Array.from(document.querySelectorAll('[class*="_FastModeToggleContent_"]'))
823
+ .find((content) => !content.closest("[data-codex-model-slider-generic]"));
824
+ const toggle = nativeContent?.closest('[class*="_FastModeToggle_"]');
825
+ const state = toggle?.getAttribute("data-fast-mode-enabled") === "true" ? "active" : "inactive";
826
+ const svg = nativeContent?.querySelector("svg");
827
+ const current = modelSelectorNativeFastIcons?.[state];
828
+ if (svg && (current?.innerHTML !== svg.innerHTML
829
+ || current.getAttribute("viewBox") !== svg.getAttribute("viewBox")
830
+ || current.getAttribute("width") !== svg.getAttribute("width"))) {
831
+ const template = svg.cloneNode(true);
832
+ template.removeAttribute("id");
833
+ if (!modelSelectorNativeFastIcons) {
834
+ modelSelectorNativeFastIcons = {};
835
+ }
836
+ modelSelectorNativeFastIcons[state] = template;
837
+ modelSelectorNativeFastIconsDirty = true;
869
838
  }
870
839
  const ready = Boolean(
871
840
  modelSelectorNativeFastIcons?.active && modelSelectorNativeFastIcons?.inactive,
@@ -874,31 +843,42 @@ export function createInjectionSource(features, {
874
843
  persistNativeFastIcons();
875
844
  applyNativeFastIconsToShells();
876
845
  }
877
- return modelSelectorNativeFastIcons;
878
846
  }
879
847
 
880
848
  function applyNativeFastIconsToShells() {
881
849
  document.querySelectorAll("[data-codex-model-slider-fast]").forEach((button) => {
882
850
  const content = button.querySelector("[data-codex-model-slider-fast-content]");
883
- // 幂等补装:只给还没有图标的按钮补上,避免每次 ensure 都重建节点。
884
- if (!content || content.querySelector("svg")) return;
851
+ if (!content) return;
885
852
  const icon = createNativeFastIcon(button.getAttribute("aria-pressed") === "true");
886
- if (icon) content.replaceChildren(icon);
853
+ if (!icon) return;
854
+ const current = content.querySelector("svg");
855
+ if (!current || current.innerHTML !== icon.innerHTML
856
+ || current.getAttribute("viewBox") !== icon.getAttribute("viewBox")
857
+ || current.getAttribute("width") !== icon.getAttribute("width")) {
858
+ content.replaceChildren(icon);
859
+ }
887
860
  });
888
861
  }
889
862
 
890
863
  function createNativeFastIcon(active) {
891
864
  // 直接读取已就绪的图标存储(启动时由 bundle/持久化填充,运行期由
892
865
  // 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));
866
+ // 优先克隆当前面板的图标;切换瞬间使用缓存时,也沿用原生图标尺寸。
867
+ const nativeContents = Array.from(document.querySelectorAll('[class*="_FastModeToggleContent_"]'))
868
+ .filter((content) => !content.closest("[data-codex-model-slider-generic]"));
869
+ const nativeContent = nativeContents.find((content) => (
870
+ (content.getAttribute("data-fast-mode-enabled")
871
+ ?? content.closest('[class*="_FastModeToggle_"]')?.getAttribute("data-fast-mode-enabled")) === String(active)
872
+ ));
873
+ const nativeSvg = nativeContent?.querySelector("svg") ?? nativeContents[0]?.querySelector("svg");
898
874
  const template = nativeContent?.querySelector("svg")
899
875
  ?? modelSelectorNativeFastIcons?.[active ? "active" : "inactive"];
900
876
  if (!template) return null;
901
877
  const icon = template.cloneNode(true);
878
+ if (nativeSvg) {
879
+ icon.setAttribute("width", nativeSvg.getAttribute("width") ?? "16");
880
+ icon.setAttribute("height", nativeSvg.getAttribute("height") ?? "16");
881
+ }
902
882
  icon.setAttribute("aria-hidden", "true");
903
883
  return icon;
904
884
  }
@@ -994,9 +974,25 @@ export function createInjectionSource(features, {
994
974
  function nativeComposerModelController() {
995
975
  const trigger = nativeComposerTrigger();
996
976
  let fiber = currentReactFiber(trigger);
977
+ let powerController = null;
997
978
  for (let depth = 0; fiber && depth < 40; depth += 1, fiber = fiber.return) {
998
979
  const props = fiber.memoizedProps;
980
+ if (
981
+ Array.isArray(props?.powerSelections)
982
+ && typeof props.onCommitPower === "function"
983
+ ) powerController ??= props;
999
984
  if (Array.isArray(props?.models) && typeof props.onSelectReasoningEffort === "function") return props;
985
+ if (Array.isArray(props?.models) && typeof props.onCommitPower === "function") {
986
+ const power = powerController ?? props;
987
+ return {
988
+ ...props,
989
+ powerSelections: power.powerSelections ?? props.powerSelections,
990
+ selectedPowerSelection: power.selectedPowerSelection ?? props.selectedPowerSelection,
991
+ fallbackPowerSelection: power.fallbackPowerSelection ?? props.fallbackPowerSelection,
992
+ onSelectPower: power.onSelectPower ?? props.onSelectPower,
993
+ onCommitPower: power.onCommitPower,
994
+ };
995
+ }
1000
996
  // Chat/Temporary Chat 使用原生模型选择器,不进入任务模型适配。
1001
997
  if (props?.selectedModel && typeof props.onModelChange === "function") return null;
1002
998
  }
@@ -1663,7 +1659,11 @@ export function createInjectionSource(features, {
1663
1659
  if (!wrapper?.hasAttribute("data-radix-popper-content-wrapper")) return;
1664
1660
  const existing = modelSelectorPositionObservers.get(wrapper);
1665
1661
  if (existing) {
1666
- existing.menu = menu;
1662
+ if (existing.menu !== menu) {
1663
+ existing.resizeObserver.unobserve(existing.menu);
1664
+ existing.resizeObserver.observe(menu);
1665
+ existing.menu = menu;
1666
+ }
1667
1667
  return;
1668
1668
  }
1669
1669
  const record = {
@@ -1713,6 +1713,32 @@ export function createInjectionSource(features, {
1713
1713
  }
1714
1714
  }
1715
1715
 
1716
+ function restoreGenericModelPicker(container) {
1717
+ const original = modelSelectorGenericContainers.get(container);
1718
+ if (!original) return;
1719
+ original.effectMirror?.disconnect();
1720
+ if (container.isConnected) {
1721
+ const menu = container.parentElement;
1722
+ menu?.querySelector(":scope > [data-codex-model-slider-generic]")?.remove();
1723
+ menu?.removeAttribute("data-codex-model-slider-generic-menu");
1724
+ menu?.style.removeProperty("--codex-model-slider-menu-center-shift");
1725
+ container.style.position = original.position;
1726
+ container.style.inset = original.inset;
1727
+ container.style.opacity = original.opacity;
1728
+ container.style.pointerEvents = original.pointerEvents;
1729
+ if (original.ariaHidden == null) container.removeAttribute("aria-hidden");
1730
+ else container.setAttribute("aria-hidden", original.ariaHidden);
1731
+ container.inert = original.inert;
1732
+ }
1733
+ modelSelectorGenericContainers.delete(container);
1734
+ }
1735
+
1736
+ function pruneGenericModelPickerContainers() {
1737
+ for (const container of modelSelectorGenericContainers.keys()) {
1738
+ if (!container.isConnected) restoreGenericModelPicker(container);
1739
+ }
1740
+ }
1741
+
1716
1742
  function closeControllerModelMenu() {
1717
1743
  const record = modelSelectorControllerMenu;
1718
1744
  if (!record) return false;
@@ -1853,22 +1879,6 @@ export function createInjectionSource(features, {
1853
1879
  // 原生 effort 子菜单与滑条视觉 — effort 事务、Fast/通用滑条视觉与状态同步
1854
1880
  // ========================================================================
1855
1881
 
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
1882
  function nativeEffortRow(parentMenu) {
1873
1883
  return Array.from(parentMenu?.querySelectorAll('[role="menuitem"][aria-haspopup="menu"]') ?? [])
1874
1884
  .filter((row) => !row.hasAttribute("data-model-picker-view-toggle"))[1] ?? null;
@@ -1895,6 +1905,30 @@ export function createInjectionSource(features, {
1895
1905
  queueEnsure();
1896
1906
  return;
1897
1907
  }
1908
+ if (typeof controller?.onCommitPower === "function") {
1909
+ const normalizedEffort = String(effort).toLowerCase();
1910
+ const activeModel = String(controller.model ?? nativeCatalogModelFor(identity)?.slug ?? identity.raw);
1911
+ const next = controller.powerSelections?.find((selection) => (
1912
+ String(selection?.model ?? "") === activeModel
1913
+ && String(selection?.reasoningEffort ?? "").toLowerCase() === normalizedEffort
1914
+ ));
1915
+ const previous = controller.selectedPowerSelection
1916
+ ?? controller.powerSelections?.find((selection) => (
1917
+ String(selection?.model ?? "") === activeModel
1918
+ && String(selection?.reasoningEffort ?? "").toLowerCase()
1919
+ === String(controller.reasoningEffort ?? "").toLowerCase()
1920
+ ))
1921
+ ?? controller.fallbackPowerSelection;
1922
+ if (!next || !previous) return;
1923
+ controller.onSelectPower?.(next);
1924
+ controller.onCommitPower(next, previous);
1925
+ if (modelSelectorPendingOutsideDismiss) {
1926
+ closeReactControlledMenu(nativeComposerTrigger());
1927
+ modelSelectorPendingOutsideDismiss = false;
1928
+ }
1929
+ queueEnsure();
1930
+ return;
1931
+ }
1898
1932
  const row = nativeEffortRow(parentMenu);
1899
1933
  if (!row) return;
1900
1934
  beginNativeEffortTransaction();
@@ -1960,6 +1994,19 @@ export function createInjectionSource(features, {
1960
1994
  rail.setAttribute("aria-valuenow", String(index));
1961
1995
  }
1962
1996
 
1997
+ function syncGenericMaximumVisual(rail, effortText, fastButton, maximum) {
1998
+ const value = String(maximum);
1999
+ rail?.setAttribute("data-highlighted", value);
2000
+ rail?.setAttribute("data-max", value);
2001
+ effortText?.setAttribute("data-maximum", value);
2002
+ fastButton?.setAttribute("data-max-power-selection", value);
2003
+ const maxEffects = rail?.querySelector('[class*="_MaxEffects_"]');
2004
+ if (maxEffects) {
2005
+ maxEffects.style.opacity = maximum ? "1" : "0";
2006
+ if (maximum) maxEffects.style.transform = "translateX(0)";
2007
+ }
2008
+ }
2009
+
1963
2010
  function neutralizeStaleSliderVisual(rail) {
1964
2011
  for (const selector of [
1965
2012
  "[data-codex-model-slider-native-range]",
@@ -1983,9 +2030,21 @@ export function createInjectionSource(features, {
1983
2030
  return value - Math.floor(value);
1984
2031
  }
1985
2032
 
1986
- function syncNativeFastParticles(rail, enabled) {
2033
+ function syncNativeFastParticles(rail, enabled, entering = false) {
1987
2034
  const track = Array.from(rail.children).find((element) => String(element.className).includes("_Track_"));
1988
2035
  if (!track) return;
2036
+ const staticParticles = rail.querySelector('[class*="_ParticleTransition_"]');
2037
+ if (staticParticles && !matchMedia("(prefers-reduced-motion: reduce)").matches) {
2038
+ staticParticles.__codexStaticParticleFade?.cancel();
2039
+ staticParticles.style.opacity = enabled ? "0" : "1";
2040
+ if (entering) {
2041
+ // 动态粒子渐显时,让原有星点在同一时间淡出。
2042
+ staticParticles.__codexStaticParticleFade = staticParticles.animate(
2043
+ [{ opacity: 1 }, { opacity: 0 }],
2044
+ { duration: 240, easing: "ease-out" },
2045
+ );
2046
+ }
2047
+ }
1989
2048
  track.querySelectorAll('[class*="_FastParticleClip_"]').forEach((element) => element.remove());
1990
2049
  if (!enabled || matchMedia("(prefers-reduced-motion: reduce)").matches) return;
1991
2050
  const clipClass = nativeCssModuleClass("FastParticleClip");
@@ -2025,6 +2084,10 @@ export function createInjectionSource(features, {
2025
2084
  clip.append(particles);
2026
2085
  const tickRail = Array.from(track.children).find((element) => String(element.className).includes("_TickRail_"));
2027
2086
  track.insertBefore(clip, tickRail ?? null);
2087
+ if (entering) {
2088
+ // 首帧沿用关闭 Fast 时的星点,再渐显流动粒子。
2089
+ clip.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 240, easing: "ease-out" });
2090
+ }
2028
2091
  }
2029
2092
 
2030
2093
  function syncGenericFastVisual(shell, enabled, maxPowerSelection, animate = false) {
@@ -2033,7 +2096,12 @@ export function createInjectionSource(features, {
2033
2096
  if (!button || !rail) return;
2034
2097
  button.setAttribute("aria-pressed", String(enabled));
2035
2098
  button.setAttribute("data-fast-mode-enabled", String(enabled));
2036
- button.setAttribute("data-max-power-selection", String(maxPowerSelection));
2099
+ syncGenericMaximumVisual(
2100
+ rail,
2101
+ shell.querySelector("[data-codex-model-slider-effort-row] [data-maximum]"),
2102
+ button,
2103
+ maxPowerSelection,
2104
+ );
2037
2105
  button.setAttribute("aria-label", enabled
2038
2106
  ? (locale === "zh-CN" ? "关闭 Fast 模式" : "Disable Fast mode")
2039
2107
  : (locale === "zh-CN" ? "开启 Fast 模式" : "Enable Fast mode"));
@@ -2042,8 +2110,18 @@ export function createInjectionSource(features, {
2042
2110
  if (icon && content) content.replaceChildren(icon);
2043
2111
  rail.setAttribute("data-fast-mode", String(enabled));
2044
2112
  rail.setAttribute("data-fast-mode-dot-transition", animate ? (enabled ? "entering" : "exiting") : (enabled ? "active" : "inactive"));
2045
- syncNativeFastParticles(rail, enabled);
2113
+ syncNativeFastParticles(rail, enabled, animate && enabled);
2046
2114
  setGenericSliderVisual(rail, Number(rail.getAttribute("aria-valuenow") ?? 0), Number(rail.getAttribute("aria-valuemax") ?? 0) + 1);
2115
+ if (animate && enabled) {
2116
+ shell.setAttribute("data-codex-model-slider-fast-transitioning", "");
2117
+ setTimeout(() => {
2118
+ if (!shell.isConnected) return;
2119
+ shell.removeAttribute("data-codex-model-slider-fast-transitioning");
2120
+ queueEnsure();
2121
+ }, 260);
2122
+ } else {
2123
+ shell.removeAttribute("data-codex-model-slider-fast-transitioning");
2124
+ }
2047
2125
  if (animate) {
2048
2126
  setTimeout(() => {
2049
2127
  if (rail.isConnected && rail.getAttribute("data-fast-mode") === String(enabled)) {
@@ -2102,6 +2180,88 @@ export function createInjectionSource(features, {
2102
2180
  return clone;
2103
2181
  }
2104
2182
 
2183
+ function syncClonedPowerEffects(nativeContainer, shell) {
2184
+ const source = nativeContainer.querySelector("[data-model-picker-power-slider]");
2185
+ const clone = shell.querySelector("[data-codex-model-slider-native-slider]");
2186
+ if (!source || !clone) return null;
2187
+ const sourceCanvas = source.querySelector("canvas");
2188
+ const cloneCanvas = clone.querySelector("canvas");
2189
+ let canvasTimer = null;
2190
+ let observer = null;
2191
+ const disconnect = () => {
2192
+ observer?.disconnect();
2193
+ if (canvasTimer != null) clearInterval(canvasTimer);
2194
+ };
2195
+ if (sourceCanvas && cloneCanvas) {
2196
+ // 原生 canvas 可能在首次克隆时尚未绘制;持续同步其高光纹理。
2197
+ const scratch = document.createElement("canvas");
2198
+ const scratchContext = scratch.getContext("2d");
2199
+ const cloneContext = cloneCanvas.getContext("2d");
2200
+ const copyCanvas = () => {
2201
+ if (!source.isConnected || !shell.isConnected) {
2202
+ disconnect();
2203
+ return;
2204
+ }
2205
+ if (scratch.width !== sourceCanvas.width || scratch.height !== sourceCanvas.height) {
2206
+ scratch.width = sourceCanvas.width;
2207
+ scratch.height = sourceCanvas.height;
2208
+ cloneCanvas.width = sourceCanvas.width;
2209
+ cloneCanvas.height = sourceCanvas.height;
2210
+ }
2211
+ scratchContext.clearRect(0, 0, scratch.width, scratch.height);
2212
+ scratchContext.drawImage(sourceCanvas, 0, 0);
2213
+ const y = Math.floor(scratch.height / 2);
2214
+ const hasDrawing = [0.25, 0.5, 0.75].some((ratio) => (
2215
+ scratchContext.getImageData(Math.floor(scratch.width * ratio), y, 1, 1).data[3] > 0
2216
+ ));
2217
+ if (!hasDrawing) return;
2218
+ cloneContext.clearRect(0, 0, cloneCanvas.width, cloneCanvas.height);
2219
+ cloneContext.drawImage(scratch, 0, 0);
2220
+ };
2221
+ copyCanvas();
2222
+ canvasTimer = setInterval(copyCanvas, 80);
2223
+ }
2224
+ const sourceEffects = source.querySelector('[class*="_MaxEffects_"]');
2225
+ const cloneEffects = clone.querySelector('[class*="_MaxEffects_"]');
2226
+ if (!sourceEffects || !cloneEffects) return { disconnect };
2227
+ const sync = () => {
2228
+ if (!source.isConnected || !shell.isConnected) {
2229
+ disconnect();
2230
+ return;
2231
+ }
2232
+ const sourceTransition = sourceEffects.querySelector('[class*="_ParticleTransition_"]');
2233
+ let cloneTransition = cloneEffects.querySelector('[class*="_ParticleTransition_"]');
2234
+ if (!sourceTransition) {
2235
+ if (clone.querySelector("[data-fast-mode]")?.getAttribute("data-fast-mode") !== "true") {
2236
+ cloneTransition?.remove();
2237
+ }
2238
+ return;
2239
+ }
2240
+ const sourceParticles = sourceTransition.querySelectorAll('[class*="_TrackParticle_"]');
2241
+ let cloneParticles = cloneTransition?.querySelectorAll('[class*="_TrackParticle_"]');
2242
+ if (!cloneTransition || sourceParticles.length !== cloneParticles.length) {
2243
+ cloneTransition?.remove();
2244
+ cloneTransition = sourceTransition.cloneNode(true);
2245
+ cloneEffects.append(cloneTransition);
2246
+ cloneParticles = cloneTransition.querySelectorAll('[class*="_TrackParticle_"]');
2247
+ }
2248
+ cloneTransition.setAttribute("style", sourceTransition.getAttribute("style") ?? "");
2249
+ sourceParticles.forEach((particle, index) => {
2250
+ const style = particle.getAttribute("style") ?? "";
2251
+ if (cloneParticles[index].getAttribute("style") !== style) {
2252
+ cloneParticles[index].setAttribute("style", style);
2253
+ }
2254
+ });
2255
+ if (!matchMedia("(prefers-reduced-motion: reduce)").matches) {
2256
+ cloneTransition.style.opacity = clone.querySelector("[data-fast-mode]")?.getAttribute("data-fast-mode") === "true" ? "0" : "1";
2257
+ }
2258
+ };
2259
+ observer = new MutationObserver(sync);
2260
+ observer.observe(sourceEffects, { attributes: true, attributeFilter: ["style"], childList: true, subtree: true });
2261
+ sync();
2262
+ return { disconnect };
2263
+ }
2264
+
2105
2265
  function createGenericModelSlider(parentMenu, selector, identity, model, selectedEffort) {
2106
2266
  const efforts = model.supportedReasoningLevels.map((level) => level.effort);
2107
2267
  const selectedIndex = efforts.indexOf(selectedEffort);
@@ -2158,9 +2318,21 @@ export function createInjectionSource(features, {
2158
2318
  buttonLabel.className = classes.ViewToggleModelLabel;
2159
2319
  buttonLabel.setAttribute("data-codex-model-slider-model-label", "");
2160
2320
  buttonLabel.textContent = modelDisplayLabel(identity, selector);
2321
+ const modelRow = document.createElement("span");
2322
+ modelRow.setAttribute("data-codex-model-slider-model-row", "");
2323
+ modelRow.style.display = "inline-flex";
2324
+ modelRow.style.alignItems = "center";
2325
+ modelRow.style.justifyContent = "center";
2326
+ modelRow.style.gap = "calc(var(--spacing) / 2)";
2327
+ modelRow.style.minWidth = "0";
2328
+ modelRow.style.maxWidth = "100%";
2329
+ modelRow.append(buttonLabel);
2161
2330
  const effortText = selectedEffortVisible ? document.createElement("span") : null;
2162
2331
  if (effortText) {
2163
2332
  effortText.className = classes.ViewToggleEffortLabel;
2333
+ // 原生标签会跨满整行;收回中列,使上下两行各自居中。
2334
+ effortText.style.gridColumn = "2";
2335
+ effortText.style.gridRow = "1";
2164
2336
  effortText.textContent = effortLabel(selectedEffort);
2165
2337
  effortText.setAttribute("data-accent", "true");
2166
2338
  effortText.setAttribute("data-effort-only", "false");
@@ -2174,13 +2346,13 @@ export function createInjectionSource(features, {
2174
2346
  )?.cloneNode(true);
2175
2347
  if (nativeChevron) {
2176
2348
  nativeChevron.classList.add(classes.ViewToggleIcon);
2349
+ nativeChevron.style.gridArea = "auto";
2350
+ nativeChevron.style.flexShrink = "0";
2177
2351
  nativeChevron.setAttribute("aria-hidden", "true");
2178
- if (effortText) effortRow.append(effortText);
2179
- effortRow.append(nativeChevron);
2180
- } else {
2181
- if (effortText) effortRow.append(effortText);
2352
+ modelRow.append(nativeChevron);
2182
2353
  }
2183
- buttonContent.append(effortRow, buttonLabel);
2354
+ if (effortText) effortRow.append(effortText);
2355
+ buttonContent.append(effortRow, modelRow);
2184
2356
  modelButton.append(buttonContent);
2185
2357
  const openModels = () => openControllerModelMenu(parentMenu, modelButton, selector);
2186
2358
  modelButton.addEventListener("click", openModels);
@@ -2297,7 +2469,12 @@ export function createInjectionSource(features, {
2297
2469
  }
2298
2470
  setGenericSliderVisual(rail, selectedIndex, efforts.length);
2299
2471
  syncNativeFastParticles(rail, fastEnabled);
2300
- setGenericSliderVisual(rail, selectedIndex, efforts.length);
2472
+ syncGenericMaximumVisual(
2473
+ rail,
2474
+ effortText,
2475
+ shell.querySelector("[data-codex-model-slider-fast]"),
2476
+ selectedEffort === "ultra",
2477
+ );
2301
2478
 
2302
2479
  let pendingIndex = selectedIndex;
2303
2480
  let dragging = false;
@@ -2316,10 +2493,9 @@ export function createInjectionSource(features, {
2316
2493
  rail.setAttribute("aria-valuetext", effortLabel(effort));
2317
2494
  if (effortText) {
2318
2495
  effortText.textContent = effortLabel(effort);
2319
- effortText.setAttribute("data-maximum", String(effort === "ultra"));
2320
2496
  }
2321
2497
  const fastButton = shell.querySelector("[data-codex-model-slider-fast]");
2322
- fastButton?.setAttribute("data-max-power-selection", String(effort === "ultra"));
2498
+ syncGenericMaximumVisual(rail, effortText, fastButton, effort === "ultra");
2323
2499
  };
2324
2500
  const preview = (clientX) => previewIndex(indexAt(clientX));
2325
2501
  rail.addEventListener("pointerdown", (event) => {
@@ -2421,7 +2597,8 @@ export function createInjectionSource(features, {
2421
2597
  const selectedServiceTier = nativeComposerModelController()?.selectedServiceTier;
2422
2598
  const serviceTierKey = selectedServiceTier?.id ?? selectedServiceTier ?? "standard";
2423
2599
  const selectionMode = nativeComposerModelController()?.selectionMode ?? "";
2424
- const key = `${identity.raw}:${selectedEffortValue}:${serviceTierKey}:${selectionMode}:${efforts.map((level) => level.effort).join(",")}`;
2600
+ const structureKey = `${identity.raw}:${selectedEffortValue}:${selectionMode}:${efforts.map((level) => level.effort).join(",")}`;
2601
+ const key = `${structureKey}:${serviceTierKey}`;
2425
2602
  const existing = parentMenu.querySelector(":scope > [data-codex-model-slider-generic]");
2426
2603
  const nativeTemplateReady = Boolean(
2427
2604
  parentMenu.firstElementChild
@@ -2439,11 +2616,24 @@ export function createInjectionSource(features, {
2439
2616
  const canUpgradeFallback = Boolean(
2440
2617
  existing?.querySelector("[data-codex-model-slider-fallback]") && nativeTemplateReady,
2441
2618
  );
2619
+ if (existing?.getAttribute("data-codex-model-slider-structure-key") === structureKey
2620
+ && existing.hasAttribute("data-codex-model-slider-fast-transitioning") && !canUpgradeFallback) {
2621
+ centerGenericModelPicker(parentMenu);
2622
+ return true;
2623
+ }
2624
+ const currentFast = existing?.querySelector("[data-codex-model-slider-rail]")?.getAttribute("data-fast-mode");
2625
+ const nativeFast = parentMenu.firstElementChild
2626
+ ?.querySelector("[data-model-picker-power-slider] [data-fast-mode]")?.getAttribute("data-fast-mode");
2627
+ if (existing?.getAttribute("data-codex-model-slider-structure-key") === structureKey
2628
+ && currentFast != null && currentFast === nativeFast && !canUpgradeFallback) {
2629
+ existing.setAttribute("data-codex-model-slider-generic", key);
2630
+ centerGenericModelPicker(parentMenu);
2631
+ return true;
2632
+ }
2442
2633
  if (existing?.getAttribute("data-codex-model-slider-generic") === key && !canUpgradeFallback) {
2443
2634
  centerGenericModelPicker(parentMenu);
2444
2635
  return true;
2445
2636
  }
2446
- existing?.remove();
2447
2637
  const nativeContainer = parentMenu.firstElementChild;
2448
2638
  if (!nativeContainer) return false;
2449
2639
  const shell = createGenericModelSlider(
@@ -2453,29 +2643,37 @@ export function createInjectionSource(features, {
2453
2643
  displayModel,
2454
2644
  selectedEffortValue,
2455
2645
  );
2456
- if (!shell) return false;
2646
+ if (!shell) {
2647
+ restoreGenericModelPicker(nativeContainer);
2648
+ return false;
2649
+ }
2650
+ modelSelectorGenericContainers.get(nativeContainer)?.effectMirror?.disconnect();
2651
+ existing?.remove();
2457
2652
  if (!modelSelectorGenericContainers.has(nativeContainer)) {
2458
- modelSelectorGenericContainers.set(nativeContainer, nativeContainer.style.display);
2653
+ modelSelectorGenericContainers.set(nativeContainer, {
2654
+ position: nativeContainer.style.position,
2655
+ inset: nativeContainer.style.inset,
2656
+ opacity: nativeContainer.style.opacity,
2657
+ pointerEvents: nativeContainer.style.pointerEvents,
2658
+ ariaHidden: nativeContainer.getAttribute("aria-hidden"),
2659
+ inert: nativeContainer.inert,
2660
+ });
2459
2661
  }
2460
2662
  parentMenu.setAttribute("data-codex-model-slider-generic-menu", "");
2461
- nativeContainer.style.display = "none";
2663
+ nativeContainer.style.position = "absolute";
2664
+ nativeContainer.style.inset = "0";
2665
+ nativeContainer.style.opacity = "0";
2666
+ nativeContainer.style.pointerEvents = "none";
2667
+ nativeContainer.setAttribute("aria-hidden", "true");
2668
+ nativeContainer.inert = true;
2462
2669
  shell.setAttribute("data-codex-model-slider-generic", key);
2670
+ shell.setAttribute("data-codex-model-slider-structure-key", structureKey);
2463
2671
  parentMenu.append(shell);
2672
+ modelSelectorGenericContainers.get(nativeContainer).effectMirror = syncClonedPowerEffects(nativeContainer, shell);
2464
2673
  centerGenericModelPicker(parentMenu);
2465
2674
  return true;
2466
2675
  }
2467
2676
 
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
2677
  function enhanceNativeModelPicker(parentMenu, selector) {
2480
2678
  if (enhanceGenericModelPicker(parentMenu, selector)) return;
2481
2679
  const toggle = parentMenu.querySelector("[data-model-picker-view-toggle]");
@@ -2583,9 +2781,10 @@ export function createInjectionSource(features, {
2583
2781
  }
2584
2782
  // 原生开合的临时 min-width 钉定与弹层水平居中都基于对 Measurement
2585
2783
  // 探针的测量;把探针文本同步为 MCX 显示标签,原生机制即可测得
2586
- // 真实显示宽度(含 110px 名称上限与 160px Content 上限)。
2784
+ // 真实显示宽度。保留 React 持有的子节点,避免切模型时卸载失败。
2587
2785
  const measurement = trigger.querySelector('[class*="_ModelPickerTriggerMeasurement_"]');
2588
- if (measurement && measurement.textContent !== label) measurement.textContent = label;
2786
+ const measurementText = measurement && firstVisibleTextNode(measurement);
2787
+ if (measurementText && measurementText.nodeValue !== label) measurementText.nodeValue = label;
2589
2788
  }
2590
2789
 
2591
2790
  function ensureModelSelector() {
@@ -2597,6 +2796,7 @@ export function createInjectionSource(features, {
2597
2796
  }
2598
2797
  pruneModelSubmenuPositionObservers();
2599
2798
  pruneNativeModelToggleHandlers();
2799
+ pruneGenericModelPickerContainers();
2600
2800
  document.documentElement.setAttribute("data-codex-model-slider-catalog-ready", "");
2601
2801
  if (!modelSelectorStyle.isConnected) document.head?.append(modelSelectorStyle);
2602
2802
  enhanceComposerModelTrigger(selector);
@@ -2667,8 +2867,8 @@ export function createInjectionSource(features, {
2667
2867
  if (handlers.originalAriaExpanded == null) toggle.removeAttribute("aria-expanded");
2668
2868
  else toggle.setAttribute("aria-expanded", handlers.originalAriaExpanded);
2669
2869
  }
2670
- for (const [container, display] of modelSelectorGenericContainers) {
2671
- if (container.isConnected) container.style.display = display;
2870
+ for (const container of modelSelectorGenericContainers.keys()) {
2871
+ restoreGenericModelPicker(container);
2672
2872
  }
2673
2873
  document.querySelectorAll("[data-codex-model-slider-divider]")
2674
2874
  .forEach((element) => element.remove());
@@ -2685,7 +2885,6 @@ export function createInjectionSource(features, {
2685
2885
  document.documentElement.removeAttribute("data-codex-model-slider-catalog-ready");
2686
2886
  modelSelectorStyle.remove();
2687
2887
  modelSelectorOriginalToggleLabels.clear();
2688
- modelSelectorGenericContainers.clear();
2689
2888
  modelSelectorNativeToggleHandlers.clear();
2690
2889
  }
2691
2890
 
@@ -2728,6 +2927,10 @@ export function createInjectionSource(features, {
2728
2927
  // CDP 重连会轮换 token;保留已有页面和草稿,只重试未就绪的页面。
2729
2928
  config.bindingName = bindingName;
2730
2929
  config.bindingToken = bindingToken;
2930
+ for (const feature of pageScriptFeatures) {
2931
+ feature.pageScript.config.bindingName = bindingName;
2932
+ feature.pageScript.config.bindingToken = bindingToken;
2933
+ }
2731
2934
  for (const [requestId, pending] of pendingHostActions) {
2732
2935
  if (pending.kind === "surface-load") pendingHostActions.delete(requestId);
2733
2936
  }
@@ -2821,7 +3024,17 @@ export function createInjectionSource(features, {
2821
3024
  .map((name) => [name, style.getPropertyValue(name).trim()])
2822
3025
  .filter(([, value]) => value),
2823
3026
  );
2824
- const mode = root.classList.contains("electron-dark") || style.colorScheme === "dark"
3027
+ // 按语义和命名空间解析,不依赖构建哈希或猜测近似名称。
3028
+ for (const name of themeTokenNames) {
3029
+ if (tokens[name]) continue;
3030
+ const semanticNames = [name, ...(themeTokenAliases[name] ?? [])];
3031
+ const candidates = semanticNames.flatMap((candidate) => candidate.startsWith("--color-")
3032
+ ? [candidate, candidate.replace("--color-", "--app-color-")]
3033
+ : [candidate]);
3034
+ const value = candidates.map((candidate) => style.getPropertyValue(candidate).trim()).find(Boolean);
3035
+ if (value) tokens[name] = value;
3036
+ }
3037
+ const mode = root.dataset.theme === "dark" || root.classList.contains("electron-dark") || style.colorScheme === "dark"
2825
3038
  ? "dark"
2826
3039
  : "light";
2827
3040
  return { root, style, tokens, mode };
@@ -3091,6 +3304,8 @@ export function createInjectionSource(features, {
3091
3304
  frame.title = title;
3092
3305
  frame.allow = "clipboard-write";
3093
3306
  frame.style.cssText = "width:100%;height:100%;border:0;display:block;background:transparent";
3307
+ // 图片页覆盖原生标题栏,避免控件点击被窗口拖拽区域吞掉。
3308
+ if (featureId === "images") frame.style.setProperty("-webkit-app-region", "no-drag");
3094
3309
  return frame;
3095
3310
  }
3096
3311
 
@@ -4290,7 +4505,7 @@ export function createInjectionSource(features, {
4290
4505
  respondToSurface(record, requestId, { ok: true, result: { paths } });
4291
4506
  return;
4292
4507
  }
4293
- if (["attach-file", "create-image-edit-thread"].includes(action)) {
4508
+ if (["attach-file", "create-image-edit-thread", "list-cloud-images", "import-cloud-image"].includes(action)) {
4294
4509
  if (typeof globalThis[config.bindingName] !== "function") {
4295
4510
  throw new Error("The native file bridge is unavailable");
4296
4511
  }
@@ -4313,10 +4528,36 @@ export function createInjectionSource(features, {
4313
4528
  }
4314
4529
  }
4315
4530
 
4531
+ function requestPageHostAction(featureId, action, payload) {
4532
+ if (!featureById.get(featureId)?.hostActions.has(action)) return Promise.reject(new Error("Host action is not registered"));
4533
+ if (typeof globalThis[config.bindingName] !== "function") return Promise.reject(new Error("Host bridge is unavailable"));
4534
+ return new Promise((resolve, reject) => {
4535
+ const requestId = crypto.randomUUID();
4536
+ const timer = setTimeout(() => {
4537
+ pendingHostActions.delete(requestId);
4538
+ reject(new Error("Host request timed out"));
4539
+ }, 10_000);
4540
+ pendingHostActions.set(requestId, { kind: "page-script", resolve, reject, timer });
4541
+ try {
4542
+ globalThis[config.bindingName](JSON.stringify({ token: config.bindingToken, featureId, requestId, action, payload }));
4543
+ } catch (error) {
4544
+ clearTimeout(timer);
4545
+ pendingHostActions.delete(requestId);
4546
+ reject(error);
4547
+ }
4548
+ });
4549
+ }
4550
+
4316
4551
  function resolveHostAction(requestId, response) {
4317
4552
  const pending = pendingHostActions.get(requestId);
4318
4553
  if (!pending) return false;
4319
4554
  pendingHostActions.delete(requestId);
4555
+ if (pending.kind === "page-script") {
4556
+ clearTimeout(pending.timer);
4557
+ if (response.ok) pending.resolve(response.result);
4558
+ else pending.reject(new Error(response.error?.message ?? "Host request failed"));
4559
+ return true;
4560
+ }
4320
4561
  const record = surfaceRecords.get(pending.recordKey);
4321
4562
  if (!record) return false;
4322
4563
  if (pending.kind === "surface-load") {
@@ -4472,7 +4713,7 @@ export function createInjectionSource(features, {
4472
4713
  const themeObserver = new MutationObserver(queueTheme);
4473
4714
  themeObserver.observe(document.documentElement, {
4474
4715
  attributes: true,
4475
- attributeFilter: ["class", "style"],
4716
+ attributeFilter: ["class", "style", "data-theme"],
4476
4717
  });
4477
4718
  domObservers.push(themeObserver);
4478
4719
 
@@ -4499,6 +4740,12 @@ export function createInjectionSource(features, {
4499
4740
  resolveHostAction,
4500
4741
  dispose: () => {
4501
4742
  lifetime.abort();
4743
+ for (const [id, pending] of pendingHostActions) {
4744
+ if (pending.kind !== "page-script") continue;
4745
+ clearTimeout(pending.timer);
4746
+ pending.reject(new Error("Runtime disposed"));
4747
+ pendingHostActions.delete(id);
4748
+ }
4502
4749
  for (const cleanup of pageScriptCleanups.splice(0)) {
4503
4750
  try { cleanup(); } catch (error) {
4504
4751
  console.warn("[codex-personal] page-script cleanup failed", error);
@@ -4559,6 +4806,7 @@ export function createInjectionSource(features, {
4559
4806
  summaryLayout: RESPONSIVE_SUMMARY_LAYOUT,
4560
4807
  rightPanelRouteKinds: RIGHT_PANEL_ROUTE_KINDS,
4561
4808
  themeTokens: CODEX_THEME_TOKENS,
4809
+ themeTokenAliases: CODEX_THEME_TOKEN_ALIASES,
4562
4810
  features: runtimeFeatures,
4563
4811
  })})`;
4564
4812
  }