minecodex 0.2.2 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -165,7 +165,11 @@ export function createInjectionSource(features, {
165
165
  "--color-token-bg-primary",
166
166
  "--color-token-bg-secondary",
167
167
  "--color-token-bg-tertiary",
168
+ "--color-surface-tertiary",
169
+ "--color-surface-elevated-secondary",
168
170
  "--color-background-elevated-primary-opaque",
171
+ "--color-background-primary-ghost-hover",
172
+ "--color-chart-blue",
169
173
  "--color-background-panel",
170
174
  "--color-background-control",
171
175
  "--color-background-primary-solid",
@@ -181,11 +185,13 @@ export function createInjectionSource(features, {
181
185
  "--color-text-foreground",
182
186
  "--color-text-foreground-secondary",
183
187
  "--color-text-foreground-tertiary",
188
+ "--color-text-inverse",
184
189
  "--color-text-on-accent",
185
190
  "--color-text-button-primary",
186
191
  "--color-text-accent",
187
192
  "--color-token-text-link-foreground",
188
193
  "--color-border",
194
+ "--color-border-subtle",
189
195
  "--color-border-heavy",
190
196
  "--color-border-focus",
191
197
  "--color-token-scrollbar-slider-background",
@@ -306,6 +312,7 @@ export function createInjectionSource(features, {
306
312
  let modelSelectorSuppressOutsidePointerSequence = false;
307
313
  let modelSelectorTriggerObserver = null;
308
314
  let modelSelectorObservedTrigger = null;
315
+ let modelSelectorControllerMenu = null;
309
316
  let modelSelectorNativeFastIcons = null;
310
317
  // Fast 图标来源标记:bundle 静态提取 vs DOM 克隆,DOM 克隆优先以保持像素一致。
311
318
  let modelSelectorNativeFastIconSources = null;
@@ -318,6 +325,15 @@ export function createInjectionSource(features, {
318
325
  modelSelectorStyle.setAttribute("data-codex-model-slider-style", "");
319
326
  modelSelectorStyle.textContent = `
320
327
  [data-codex-model-slider-menu] { width: 264px !important; min-width: 264px; overflow-x: hidden; }
328
+ [data-codex-model-slider-controller-menu] {
329
+ position: absolute !important;
330
+ right: 0;
331
+ bottom: 0;
332
+ z-index: 1;
333
+ background-color: var(--color-surface-elevated-secondary);
334
+ -webkit-backdrop-filter: none;
335
+ backdrop-filter: none;
336
+ }
321
337
  [data-codex-model-slider-menu][data-codex-model-slider-overflow] {
322
338
  max-height: var(--codex-model-slider-menu-max-height) !important;
323
339
  overflow-y: auto;
@@ -935,7 +951,7 @@ export function createInjectionSource(features, {
935
951
  function modelDisplayLabel(identity, selector) {
936
952
  const model = catalogModelFor(identity, selector) ?? nativeCatalogModelFor(identity);
937
953
  const source = model?.displayName || model?.slug || identity.backend;
938
- return formatIdentifier(modelIdentity(source).backend);
954
+ return formatIdentifier(source.slice(source.lastIndexOf("/") + 1));
939
955
  }
940
956
 
941
957
  function brandForModel(identity, selector) {
@@ -952,10 +968,21 @@ export function createInjectionSource(features, {
952
968
  .trim();
953
969
  }
954
970
 
971
+ function currentReactFiber(element) {
972
+ const fiberKey = Object.getOwnPropertyNames(element ?? {}).find((key) => key.startsWith("__reactFiber$"));
973
+ const propsKey = Object.getOwnPropertyNames(element ?? {}).find((key) => key.startsWith("__reactProps$"));
974
+ let fiber = fiberKey ? element[fiberKey] : null;
975
+ if (
976
+ propsKey
977
+ && fiber?.alternate?.memoizedProps === element[propsKey]
978
+ && fiber.memoizedProps !== element[propsKey]
979
+ ) return fiber.alternate;
980
+ return fiber;
981
+ }
982
+
955
983
  function nativeComposerModelController() {
956
984
  const trigger = document.querySelector("[data-codex-intelligence-trigger]");
957
- const fiberKey = Object.getOwnPropertyNames(trigger ?? {}).find((key) => key.startsWith("__reactFiber$"));
958
- let fiber = fiberKey ? trigger[fiberKey] : null;
985
+ let fiber = currentReactFiber(trigger);
959
986
  for (let depth = 0; fiber && depth < 40; depth += 1, fiber = fiber.return) {
960
987
  const props = fiber.memoizedProps;
961
988
  if (Array.isArray(props?.models) && typeof props.onSelectReasoningEffort === "function") return props;
@@ -1480,8 +1507,12 @@ export function createInjectionSource(features, {
1480
1507
  renderEnhancedModelMenu(menu, selector);
1481
1508
  }
1482
1509
 
1483
- function enhanceModelItem(item, menu, selector, index) {
1484
- const observedRaw = modelItemRawValue(item);
1510
+ function enhanceModelItem(item, menu, selector, index, {
1511
+ rawValue = null,
1512
+ selected = null,
1513
+ scheduleReturn = true,
1514
+ } = {}) {
1515
+ const observedRaw = rawValue ?? modelItemRawValue(item);
1485
1516
  if (!observedRaw) return;
1486
1517
  const observedIdentity = modelIdentity(observedRaw);
1487
1518
  const nativeDescriptor = nativeModelDescriptorForItem(item, observedRaw);
@@ -1499,7 +1530,10 @@ export function createInjectionSource(features, {
1499
1530
  const display = modelDisplayLabel(identity, selector);
1500
1531
  const provider = providerLabel(identity.provider, selector);
1501
1532
  const brand = brandForModel(identity, selector);
1502
- item.toggleAttribute("data-codex-model-slider-selected", Boolean(item.querySelector("svg")));
1533
+ item.toggleAttribute(
1534
+ "data-codex-model-slider-selected",
1535
+ selected ?? Boolean(item.querySelector("svg")),
1536
+ );
1503
1537
  const favorites = loadModelFavorites(selector);
1504
1538
  const starred = favorites.has(raw);
1505
1539
  const row = document.createElement("span");
@@ -1528,7 +1562,7 @@ export function createInjectionSource(features, {
1528
1562
  star.append(createModelSelectorIcon(starred ? selector.icons.unstar : selector.icons.star));
1529
1563
  row.append(star);
1530
1564
  item.replaceChildren(row);
1531
- if (!item.hasAttribute("data-codex-model-slider-return-listener")) {
1565
+ if (scheduleReturn && !item.hasAttribute("data-codex-model-slider-return-listener")) {
1532
1566
  item.setAttribute("data-codex-model-slider-return-listener", "");
1533
1567
  item.addEventListener("click", (event) => {
1534
1568
  if (!event.target.closest?.("[data-codex-model-slider-star]")) scheduleModelSliderReturn();
@@ -1665,6 +1699,106 @@ export function createInjectionSource(features, {
1665
1699
  }
1666
1700
  }
1667
1701
 
1702
+ function closeControllerModelMenu() {
1703
+ const record = modelSelectorControllerMenu;
1704
+ if (!record) return false;
1705
+ record.button.setAttribute("aria-expanded", "false");
1706
+ record.parentMenu.style.position = record.position;
1707
+ record.parentMenu.style.overflow = record.overflow;
1708
+ for (const item of modelMenuItems(record.menu)) modelSelectorOriginalItems.delete(item);
1709
+ record.menu.remove();
1710
+ modelSelectorControllerMenu = null;
1711
+ return true;
1712
+ }
1713
+
1714
+ function controllerModelReasoningEffort(controller, model) {
1715
+ const current = String(controller?.reasoningEffort ?? "").toLowerCase();
1716
+ const supported = (model?.supportedReasoningEfforts ?? []).map((entry) => (
1717
+ String(entry?.reasoningEffort ?? entry ?? "").toLowerCase()
1718
+ )).filter(Boolean);
1719
+ return supported.includes(current)
1720
+ ? current
1721
+ : String(model?.defaultReasoningEffort ?? supported[0] ?? "none").toLowerCase();
1722
+ }
1723
+
1724
+ function openControllerModelMenu(parentMenu, modelButton, selector) {
1725
+ if (modelSelectorControllerMenu) {
1726
+ closeControllerModelMenu();
1727
+ return;
1728
+ }
1729
+ const controller = nativeComposerModelController();
1730
+ if (!Array.isArray(controller?.models) || typeof controller.onSelectModel !== "function") return;
1731
+ const models = controller.models.filter((model) => !model.hidden && String(model.model ?? "").trim());
1732
+ if (!models.length) return;
1733
+
1734
+ const menu = document.createElement("div");
1735
+ menu.className = parentMenu.className;
1736
+ menu.setAttribute("role", "menu");
1737
+ menu.setAttribute("data-state", "open");
1738
+ menu.setAttribute("data-codex-model-slider-menu", "");
1739
+ menu.setAttribute("data-codex-model-slider-controller-menu", "");
1740
+ models.forEach((model, index) => {
1741
+ const item = document.createElement("div");
1742
+ item.className = "no-drag outline-hidden rounded-lg px-[var(--padding-row-x)] py-[var(--padding-row-y)] text-sm text-default group hover:bg-primary-ghost-hover focus:bg-primary-ghost-hover cursor-interaction flex flex-col";
1743
+ item.setAttribute("role", "menuitem");
1744
+ item.setAttribute("tabindex", "-1");
1745
+ enhanceModelItem(item, menu, selector, index, {
1746
+ rawValue: String(model.model),
1747
+ selected: String(model.model) === String(controller.model),
1748
+ scheduleReturn: false,
1749
+ });
1750
+ menu.append(item);
1751
+ });
1752
+ menu.addEventListener("click", (event) => {
1753
+ const star = event.target.closest?.("[data-codex-model-slider-star]");
1754
+ const item = event.target.closest?.("[data-codex-model-slider-item]");
1755
+ if (!item) return;
1756
+ if (star) {
1757
+ const raw = item.dataset.codexModelSliderRaw;
1758
+ const label = item.querySelector("[data-codex-model-slider-name]")?.textContent ?? raw;
1759
+ toggleModelFavorite(event, menu, selector, raw, label);
1760
+ return;
1761
+ }
1762
+ const currentController = nativeComposerModelController();
1763
+ const raw = item.dataset.codexModelSliderRaw;
1764
+ const model = currentController?.models?.find((candidate) => String(candidate.model) === raw);
1765
+ if (!model || typeof currentController.onSelectModel !== "function") return;
1766
+ const reasoningEffort = controllerModelReasoningEffort(currentController, model);
1767
+ currentController.onSelectModel(raw, reasoningEffort);
1768
+ closeControllerModelMenu();
1769
+ queueEnsure();
1770
+ });
1771
+ menu.addEventListener("keydown", (event) => {
1772
+ if (event.key === "Escape") {
1773
+ event.preventDefault();
1774
+ closeControllerModelMenu();
1775
+ modelButton.focus();
1776
+ return;
1777
+ }
1778
+ if (event.key !== "Enter" && event.key !== " ") return;
1779
+ const item = event.target.closest?.("[data-codex-model-slider-item]");
1780
+ if (!item) return;
1781
+ event.preventDefault();
1782
+ item.click();
1783
+ });
1784
+ renderEnhancedModelMenu(menu, selector);
1785
+
1786
+ modelSelectorControllerMenu = {
1787
+ menu,
1788
+ parentMenu,
1789
+ button: modelButton,
1790
+ position: parentMenu.style.position,
1791
+ overflow: parentMenu.style.overflow,
1792
+ };
1793
+ parentMenu.style.position = "relative";
1794
+ parentMenu.style.overflow = "visible";
1795
+ parentMenu.append(menu);
1796
+ sizeModelMenuViewport(menu, selector.maxVisibleItems);
1797
+ modelButton.setAttribute("aria-expanded", "true");
1798
+ (menu.querySelector('[data-codex-model-slider-selected]')
1799
+ ?? menu.querySelector('[role="menuitem"]'))?.focus();
1800
+ }
1801
+
1668
1802
  function openNativeModelSubmenu(parentMenu) {
1669
1803
  let attempts = 0;
1670
1804
  const open = () => {
@@ -1942,6 +2076,8 @@ export function createInjectionSource(features, {
1942
2076
  modelButton.setAttribute("tabindex", "0");
1943
2077
  modelButton.setAttribute("data-codex-model-slider-model-button", "");
1944
2078
  modelButton.setAttribute("aria-label", `Model ${modelDisplayLabel(identity, selector)}`);
2079
+ modelButton.setAttribute("aria-haspopup", "menu");
2080
+ modelButton.setAttribute("aria-expanded", "false");
1945
2081
  const buttonContent = document.createElement("span");
1946
2082
  buttonContent.className = classes.ViewToggleContent;
1947
2083
  const buttonLabel = document.createElement("span");
@@ -1955,7 +2091,7 @@ export function createInjectionSource(features, {
1955
2091
  buttonContent.append(buttonLabel);
1956
2092
  }
1957
2093
  modelButton.append(buttonContent);
1958
- const openModels = () => openNativeModelSubmenu(parentMenu);
2094
+ const openModels = () => openControllerModelMenu(parentMenu, modelButton, selector);
1959
2095
  modelButton.addEventListener("click", openModels);
1960
2096
  modelButton.addEventListener("keydown", (event) => {
1961
2097
  if (event.key !== "Enter" && event.key !== " ") return;
@@ -2006,6 +2142,11 @@ export function createInjectionSource(features, {
2006
2142
  viewControls.append(modelButton);
2007
2143
  }
2008
2144
 
2145
+ if (efforts.length < 2) {
2146
+ shell.append(viewControls);
2147
+ return shell;
2148
+ }
2149
+
2009
2150
  const nativeSlider = cloneNativePowerSlider(parentMenu, efforts.length, fastEnabled);
2010
2151
  const rail = nativeSlider?.querySelector('[role="slider"]') ?? document.createElement("div");
2011
2152
  rail.setAttribute("data-codex-model-slider-rail", "");
@@ -2126,7 +2267,7 @@ export function createInjectionSource(features, {
2126
2267
  supportedReasoningLevels: model?.supportedReasoningLevels ?? catalogModel?.supportedReasoningLevels ?? [],
2127
2268
  };
2128
2269
  const efforts = visibleReasoningLevelsFor(modelWithCapability);
2129
- if (!model || efforts.length < 2) return false;
2270
+ if (!model) return false;
2130
2271
  // 控制器推导的任务内实际 effort 优先,避免子任务/主任务切换后触发器属性滞后;
2131
2272
  // 触发器属性仅在控制器无法解析时作为实时回退,静态目录默认值最后兜底。
2132
2273
  const nativeModel = nativeCatalogModelFor(identity);
@@ -2134,7 +2275,8 @@ export function createInjectionSource(features, {
2134
2275
  ?? document.querySelector("[data-codex-intelligence-trigger]")
2135
2276
  ?.getAttribute("data-selected-reasoning-effort")
2136
2277
  ?? model.defaultReasoningLevel
2137
- ?? efforts[0].effort;
2278
+ ?? efforts[0]?.effort
2279
+ ?? "none";
2138
2280
  const effortRank = (value) => {
2139
2281
  const order = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
2140
2282
  const index = order.indexOf(String(value ?? "").toLowerCase());
@@ -2143,7 +2285,8 @@ export function createInjectionSource(features, {
2143
2285
  const visibleEffort = efforts.find((level) => level.effort === selectedEffort)?.effort
2144
2286
  ?? efforts.filter((level) => effortRank(level.effort) <= effortRank(selectedEffort))
2145
2287
  .sort((left, right) => effortRank(right.effort) - effortRank(left.effort))[0]?.effort
2146
- ?? efforts[0].effort;
2288
+ ?? efforts[0]?.effort
2289
+ ?? "none";
2147
2290
  const selectedEffortValue = visibleEffort;
2148
2291
  const selectedServiceTier = nativeComposerModelController()?.selectedServiceTier;
2149
2292
  const serviceTierKey = selectedServiceTier?.id ?? selectedServiceTier ?? "standard";
@@ -2252,12 +2395,18 @@ export function createInjectionSource(features, {
2252
2395
  if (!modelSelectorStyle.isConnected) document.head?.append(modelSelectorStyle);
2253
2396
  enhanceComposerModelTrigger(selector);
2254
2397
  const parentMenu = nativeModelPickerMenu();
2398
+ if (modelSelectorControllerMenu && modelSelectorControllerMenu.parentMenu !== parentMenu) {
2399
+ closeControllerModelMenu();
2400
+ }
2255
2401
  if (!parentMenu) {
2256
- if (modelSelectorPendingReturn && !visibleBlockingDialog() && modelSelectorReturnTimer == null) {
2257
- modelSelectorReturnTimer = setTimeout(() => {
2258
- modelSelectorReturnTimer = null;
2259
- returnToNativeModelSlider();
2260
- }, 80);
2402
+ if (
2403
+ modelSelectorPendingReturn
2404
+ && !visibleBlockingDialog()
2405
+ && nativeModelPickerSubmenus().length === 0
2406
+ ) {
2407
+ clearTimeout(modelSelectorReturnTimer);
2408
+ modelSelectorReturnTimer = null;
2409
+ returnToNativeModelSlider();
2261
2410
  }
2262
2411
  } else {
2263
2412
  enhanceNativeModelPicker(parentMenu, selector);
@@ -2273,6 +2422,7 @@ export function createInjectionSource(features, {
2273
2422
  }
2274
2423
 
2275
2424
  function restoreModelSelectorEnhancements() {
2425
+ closeControllerModelMenu();
2276
2426
  clearTimeout(modelSelectorReturnTimer);
2277
2427
  clearTimeout(modelSelectorEffortTransactionTimer);
2278
2428
  modelSelectorTriggerObserver?.disconnect();
@@ -2432,6 +2582,8 @@ export function createInjectionSource(features, {
2432
2582
  const dialogShadow = "0 16px 32px -8px rgba(0,0,0,.19)";
2433
2583
  const surfaceTokens = {
2434
2584
  ...tokens,
2585
+ // 部分 Codex 版本仅提供等价的正文前景 token。
2586
+ "--color-token-input-foreground": tokens["--color-token-input-foreground"] ?? tokens["--color-text-foreground"],
2435
2587
  "--codex-summary-section-title-font-size": "14px",
2436
2588
  "--codex-summary-section-title-line-height": "21px",
2437
2589
  "--codex-summary-section-title-font-weight": tokens["--font-weight-normal"] ?? "400",
@@ -2687,8 +2839,9 @@ export function createInjectionSource(features, {
2687
2839
  postSurfaceActive(record, false);
2688
2840
  }
2689
2841
  const existing = pageSurfaces.get(featureId);
2690
- const surface = existing ?? createPageSurface(feature);
2691
- if (existing) reloadSurfaceIfUnready(surfaceKey(feature.id, "page"), feature.surfaceUrl);
2842
+ const reusable = existing?.isConnected ? existing : null;
2843
+ const surface = reusable ?? createPageSurface(feature);
2844
+ if (reusable) reloadSurfaceIfUnready(surfaceKey(feature.id, "page"), feature.surfaceUrl);
2692
2845
  surface.hidden = false;
2693
2846
  activePageFeatureId = featureId;
2694
2847
  const record = surfaceRecords.get(surfaceKey(feature.id, "page"));
@@ -3821,7 +3974,7 @@ export function createInjectionSource(features, {
3821
3974
  respondToSurface(record, requestId, { ok: true, result: { paths } });
3822
3975
  return;
3823
3976
  }
3824
- if (action === "attach-file") {
3977
+ if (["attach-file", "create-image-edit-thread"].includes(action)) {
3825
3978
  if (typeof globalThis[config.bindingName] !== "function") {
3826
3979
  throw new Error("The native file bridge is unavailable");
3827
3980
  }
@@ -3863,6 +4016,11 @@ export function createInjectionSource(features, {
3863
4016
  document.addEventListener(
3864
4017
  "click",
3865
4018
  (event) => {
4019
+ if (
4020
+ modelSelectorControllerMenu
4021
+ && !modelSelectorControllerMenu.menu.contains(event.target)
4022
+ && !modelSelectorControllerMenu.button.contains(event.target)
4023
+ ) closeControllerModelMenu();
3866
4024
  if (isNativeSummaryButton(event.target) && activePinnedFeatureId) hidePinnedSurfaces();
3867
4025
  if (activePinnedFeatureId && summaryDisplayMode() === "overlay") {
3868
4026
  const state = summaryState(activePinnedFeatureId);
@@ -4010,6 +4168,7 @@ export function createInjectionSource(features, {
4010
4168
  stopToolbarReadiness();
4011
4169
  mainContentObserver?.observer.disconnect();
4012
4170
  restoreThreadContentShift();
4171
+ for (const entry of document.querySelectorAll(`[${entryMarker}]`)) entry.remove();
4013
4172
  for (const root of document.querySelectorAll("[data-codex-personal-toolbar-entry]")) root.remove();
4014
4173
  for (const group of document.querySelectorAll("[data-codex-personal-summary-toolbar-group]")) group.remove();
4015
4174
  for (const surface of pageSurfaces.values()) surface.remove();
@@ -4659,6 +4818,143 @@ export class CodexRuntime {
4659
4818
  }
4660
4819
  }
4661
4820
 
4821
+ async attachFilesToComposer(client, filePaths, requestId) {
4822
+ if (!Array.isArray(filePaths) || !filePaths.length || filePaths.length > 2) {
4823
+ throw new Error("One or two image edit attachments are required");
4824
+ }
4825
+ for (const filePath of filePaths) {
4826
+ if (typeof filePath !== "string" || !path.isAbsolute(filePath) || !(await stat(filePath)).isFile()) {
4827
+ throw new Error("Only absolute regular files can be attached");
4828
+ }
4829
+ }
4830
+ const fileNames = filePaths.map((filePath) => path.basename(filePath));
4831
+ const requiredLabels = fileNames.reduce((counts, fileName) => {
4832
+ const label = `Remove ${fileName}`;
4833
+ counts[label] = (counts[label] ?? 0) + 1;
4834
+ return counts;
4835
+ }, {});
4836
+ const marker = `codex-personal-${requestId}-batch`;
4837
+
4838
+ try {
4839
+ evaluationValue(await client.send("Runtime.evaluate", {
4840
+ expression: `(() => {
4841
+ document.querySelectorAll('[data-codex-personal-file-input]').forEach((input) => input.remove());
4842
+ const input = document.createElement("input");
4843
+ input.type = "file";
4844
+ input.multiple = true;
4845
+ input.dataset.codexPersonalFileInput = ${JSON.stringify(marker)};
4846
+ input.style.display = "none";
4847
+ document.body.append(input);
4848
+ return true;
4849
+ })()`,
4850
+ returnByValue: true,
4851
+ }));
4852
+ const { root } = await client.send("DOM.getDocument", { depth: 0 });
4853
+ const { nodeId } = await client.send("DOM.querySelector", {
4854
+ nodeId: root.nodeId,
4855
+ selector: `[data-codex-personal-file-input="${marker}"]`,
4856
+ });
4857
+ if (!nodeId) throw new Error("The temporary multi-file bridge could not be resolved");
4858
+ await client.send("DOM.setFileInputFiles", { files: filePaths, nodeId });
4859
+ const dropped = evaluationValue(await client.send("Runtime.evaluate", {
4860
+ expression: `(() => {
4861
+ const input = document.querySelector(${JSON.stringify(`[data-codex-personal-file-input="${marker}"]`)});
4862
+ const composer = document.querySelector('[data-codex-composer="true"][contenteditable="true"]');
4863
+ if (!input?.files?.length || !composer) return false;
4864
+ const transfer = new DataTransfer();
4865
+ for (const file of input.files) transfer.items.add(file);
4866
+ for (const type of ["dragenter", "dragover", "drop"]) {
4867
+ composer.dispatchEvent(new DragEvent(type, {
4868
+ bubbles: true,
4869
+ cancelable: true,
4870
+ composed: true,
4871
+ dataTransfer: transfer,
4872
+ }));
4873
+ }
4874
+ return transfer.files.length === ${filePaths.length};
4875
+ })()`,
4876
+ returnByValue: true,
4877
+ }));
4878
+ if (!dropped) throw new Error("The native Composer rejected the image edit drop");
4879
+ await waitForExpression(client, `(() => {
4880
+ const required = ${JSON.stringify(requiredLabels)};
4881
+ const observed = {};
4882
+ for (const button of document.querySelectorAll('button[aria-label^="Remove "]')) {
4883
+ const label = button.getAttribute("aria-label");
4884
+ observed[label] = (observed[label] ?? 0) + 1;
4885
+ }
4886
+ return Object.entries(required).every(([label, count]) => (observed[label] ?? 0) >= count);
4887
+ })()`, 10_000);
4888
+ return fileNames.map((name) => ({ mode: "attach", name }));
4889
+ } finally {
4890
+ await client.send("Runtime.evaluate", {
4891
+ expression: `document.querySelectorAll('[data-codex-personal-file-input]').forEach((input) => input.remove())`,
4892
+ }).catch(() => {});
4893
+ }
4894
+ }
4895
+
4896
+ async createImageEditThread(client, feature, payload, requestId) {
4897
+ const draftId = typeof payload?.draftId === "string" ? payload.draftId : "";
4898
+ const prompt = typeof payload?.prompt === "string" ? payload.prompt.trim() : "";
4899
+ if (!/^[a-f0-9-]{36}$/.test(draftId) || !prompt || prompt.length > 2_000 || !feature?.surfaceUrl) {
4900
+ throw Object.assign(new Error("The image edit request is incomplete"), { code: "INVALID_EDIT_REQUEST" });
4901
+ }
4902
+ const draftResponse = await this.fetchImpl(new URL(`/api/image-edit-draft/${draftId}`, feature.surfaceUrl), {
4903
+ headers: { Origin: CODEX_APP_ORIGIN },
4904
+ });
4905
+ if (!draftResponse?.ok) {
4906
+ throw Object.assign(new Error("The image edit draft is unavailable"), { code: "EDIT_DRAFT_UNAVAILABLE" });
4907
+ }
4908
+ const draft = await draftResponse.json();
4909
+ const paths = [draft?.sourcePath, draft?.auxiliaryPath].filter(Boolean);
4910
+ if (!paths.length || paths.length > 2) {
4911
+ throw Object.assign(new Error("The image edit attachments are unavailable"), { code: "INVALID_EDIT_ATTACHMENT" });
4912
+ }
4913
+ for (const filePath of paths) {
4914
+ if (typeof filePath !== "string" || !path.isAbsolute(filePath) || !(await stat(filePath)).isFile()) {
4915
+ throw Object.assign(new Error("The image edit attachment is unavailable"), { code: "INVALID_EDIT_ATTACHMENT" });
4916
+ }
4917
+ }
4918
+
4919
+ const opened = evaluationValue(await client.send("Runtime.evaluate", {
4920
+ expression: `(() => {
4921
+ const candidates = Array.from(document.querySelectorAll('button, [role="button"]'));
4922
+ const trigger = candidates.find((element) => /^new chat$/i.test(
4923
+ (element.getAttribute('aria-label') || element.textContent || '').trim(),
4924
+ ));
4925
+ if (!trigger) return false;
4926
+ trigger.click();
4927
+ return true;
4928
+ })()`,
4929
+ returnByValue: true,
4930
+ }));
4931
+ if (!opened) throw Object.assign(new Error("The native New chat control is unavailable"), {
4932
+ code: "NEW_CHAT_UNAVAILABLE",
4933
+ });
4934
+ await waitForExpression(client, `Boolean(document.querySelector('[data-testid="home-icon"]')) && Boolean(document.querySelector('[data-codex-composer="true"][contenteditable="true"]'))`);
4935
+ const attached = await this.attachFilesToComposer(client, paths, requestId);
4936
+ evaluationValue(await client.send("Runtime.evaluate", {
4937
+ expression: `window.__codexPersonalRuntime?.insertText(${JSON.stringify(prompt)})`,
4938
+ returnByValue: true,
4939
+ }));
4940
+ const sent = evaluationValue(await client.send("Runtime.evaluate", {
4941
+ expression: `(() => {
4942
+ const send = document.querySelector('button[aria-label="Send message"]')
4943
+ || document.querySelector('button[aria-label="Send"]')
4944
+ || document.querySelector('button[data-testid="send-button"]');
4945
+ if (!send || send.disabled) return false;
4946
+ send.click();
4947
+ return true;
4948
+ })()`,
4949
+ returnByValue: true,
4950
+ }));
4951
+ if (!sent) throw Object.assign(new Error("The new Chat is ready but its Send button is unavailable"), {
4952
+ code: "SEND_UNAVAILABLE",
4953
+ });
4954
+ await waitForExpression(client, `!document.querySelector('[data-testid="home-icon"]') && Boolean(document.querySelector('[data-codex-composer="true"][contenteditable="true"]'))`);
4955
+ return { attached, threadStarted: true };
4956
+ }
4957
+
4662
4958
  async fetchSurfaceResource(url, label) {
4663
4959
  const controller = new AbortController();
4664
4960
  const timeout = setTimeout(() => controller.abort(), this.surfaceLoadTimeoutMs);
@@ -4793,8 +5089,9 @@ export class CodexRuntime {
4793
5089
  return;
4794
5090
  }
4795
5091
  try {
4796
- if (request.action !== "attach-file") throw new Error("Unsupported native Host action");
4797
- const result = await this.attachFileToComposer(client, request.payload?.path, request.requestId);
5092
+ const result = request.action === "create-image-edit-thread"
5093
+ ? await this.createImageEditThread(client, feature, request.payload, request.requestId)
5094
+ : await this.attachFileToComposer(client, request.payload?.path, request.requestId);
4798
5095
  await this.resolveHostAction(client, request.requestId, { ok: true, result });
4799
5096
  } catch (error) {
4800
5097
  await this.resolveHostAction(client, request.requestId, {
@@ -12,6 +12,7 @@ const HOST_ACTIONS = new Set([
12
12
  "open-editor-modal",
13
13
  "resolve-file-paths",
14
14
  "import-generated-image",
15
+ "create-image-edit-thread",
15
16
  ]);
16
17
 
17
18
  const PAGE_SCRIPT_ENTRY_KINDS = new Set(["page-script"]);
@@ -359,6 +360,12 @@ export async function loadConfiguredModelCatalog(
359
360
  id: String(tier.id ?? "").trim(),
360
361
  name: String(tier.name ?? "").trim(),
361
362
  })),
363
+ capabilityKey: [
364
+ model.opencodex_capability_provenance?.provider,
365
+ model.opencodex_capability_provenance?.model_id,
366
+ ].every((value) => typeof value === "string" && value.trim())
367
+ ? `${model.opencodex_capability_provenance.provider.trim()}/${model.opencodex_capability_provenance.model_id.trim()}`
368
+ : null,
362
369
  })).filter((model) => model.slug);
363
370
  } catch (error) {
364
371
  if (error.code === "ENOENT") return [];
@@ -29,7 +29,7 @@ for (const feature of features) {
29
29
  feature.modelSelector.models = modelCatalog.map((model) => (
30
30
  applyVisibleReasoningLevels(
31
31
  model,
32
- modelCapabilities.get(String(model.slug ?? "").toLowerCase()),
32
+ modelCapabilities.get(String(model.capabilityKey ?? model.slug ?? "").toLowerCase()),
33
33
  model.supportedReasoningLevels,
34
34
  )
35
35
  ));