minecodex 0.1.20 → 0.2.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.
@@ -37,7 +37,7 @@ export function responsiveSummaryVisibleSurface({ displayMode, isPinned, isPopov
37
37
  }
38
38
 
39
39
  export function responsiveSummaryContentShift({ displayMode, isPinned }, layout = RESPONSIVE_SUMMARY_LAYOUT) {
40
- return displayMode === "shift" && isPinned
40
+ return displayMode !== "overlay" && isPinned
41
41
  ? -(layout.panelWidth + layout.panelInset) / 2
42
42
  : 0;
43
43
  }
@@ -78,6 +78,18 @@ function requireDeclaredLoopbackSurface(feature, value) {
78
78
  return url;
79
79
  }
80
80
 
81
+ export function pageScriptServiceOrigin(feature) {
82
+ if (!feature.pageScript || !feature.surfaceUrl || !feature.healthUrl) return null;
83
+ const surfaceUrl = new URL(feature.surfaceUrl);
84
+ const healthUrl = new URL(feature.healthUrl);
85
+ if (
86
+ surfaceUrl.origin !== healthUrl.origin
87
+ || surfaceUrl.protocol !== "http:"
88
+ || !LOOPBACK_SURFACE_HOSTS.has(surfaceUrl.hostname)
89
+ ) return null;
90
+ return surfaceUrl.origin;
91
+ }
92
+
81
93
  function findFrameByName(frameTree, frameName) {
82
94
  if (frameTree.frame?.name === frameName) return frameTree.frame;
83
95
  for (const child of frameTree.childFrames ?? []) {
@@ -127,9 +139,6 @@ export function createInjectionSource(features, {
127
139
  const pinnedSurfaceMarker = "data-codex-personal-pinned-summary";
128
140
  const modalSurfaceMarker = "data-codex-personal-modal";
129
141
  const promptPreviewMarker = "data-codex-personal-prompt-preview";
130
- const threadShiftStyle = document.createElement("style");
131
- threadShiftStyle.setAttribute("data-codex-personal-summary-shift-style", "");
132
- threadShiftStyle.textContent = "[data-codex-personal-summary-shift] { transform: none !important; --thread-wide-block-inline-shift: 0px !important; }";
133
142
  const nativeLabels = {
134
143
  sites: ["sites", "站点"],
135
144
  scheduled: ["scheduled", "计划任务"],
@@ -254,7 +263,7 @@ export function createInjectionSource(features, {
254
263
  const modelSelectorFeature = features.find(
255
264
  (feature) => feature.entry?.kind === "composer-model-selector",
256
265
  ) ?? null;
257
- const pageScriptFeatures = features.filter((feature) => feature.entry?.kind === "page-script");
266
+ const pageScriptFeatures = features.filter((feature) => feature.pageScript);
258
267
  const pageSurfaces = new Map();
259
268
  const pinnedSurfaces = new Map();
260
269
  const surfaceRecords = new Map();
@@ -273,9 +282,9 @@ export function createInjectionSource(features, {
273
282
  let savedComposerThread = null;
274
283
  let currentThread = null;
275
284
  let currentSummaryDisplayMode = null;
276
- let shiftedThreadContent = null;
277
- let shiftedThreadOriginal = null;
285
+ let shiftedConversationOwners = [];
278
286
  let mainContentObserver = null;
287
+ let toolbarReadiness = null;
279
288
  const domObservers = [];
280
289
  const pageScriptCleanups = [];
281
290
  const pendingHostActions = new Map();
@@ -304,6 +313,7 @@ export function createInjectionSource(features, {
304
313
  let modelSelectorFastControlVisible = false;
305
314
  // 持久化脏标记:仅在图标实际变化时写一次 localStorage。
306
315
  let modelSelectorNativeFastIconsDirty = false;
316
+ const modelSelectorFastIconStorageKey = "codex-model-slider:fast-icons:v1";
307
317
  const modelSelectorStyle = document.createElement("style");
308
318
  modelSelectorStyle.setAttribute("data-codex-model-slider-style", "");
309
319
  modelSelectorStyle.textContent = `
@@ -384,7 +394,7 @@ export function createInjectionSource(features, {
384
394
  document.documentElement.append(modelSelectorStyle);
385
395
  // 启动阶段就解析 Fast 图标:先读持久化的原生克隆,再从 app bundle 静态提取兜底,
386
396
  // 避免首次会话必须等用户点开弹窗后 DOM 里才出现图标。
387
- const persistedIcons = loadPersistedNativeFastIcons();
397
+ const persistedIcons = loadPersistedNativeFastIcons(modelSelectorFastIconStorageKey);
388
398
  if (persistedIcons) {
389
399
  modelSelectorNativeFastIcons = persistedIcons;
390
400
  modelSelectorNativeFastIconSources = {};
@@ -404,9 +414,16 @@ export function createInjectionSource(features, {
404
414
  "window",
405
415
  "document",
406
416
  "MutationObserver",
417
+ "config",
407
418
  `"use strict";\n${source}`,
408
419
  );
409
- const cleanup = installer(lifetime.signal, window, document, MutationObserver);
420
+ const cleanup = installer(
421
+ lifetime.signal,
422
+ window,
423
+ document,
424
+ MutationObserver,
425
+ feature.pageScript.config ?? {},
426
+ );
410
427
  if (typeof cleanup === "function") pageScriptCleanups.push(cleanup);
411
428
  } catch (error) {
412
429
  console.warn(`[codex-personal] page-script feature failed: ${feature.id}`, error);
@@ -471,6 +488,57 @@ export function createInjectionSource(features, {
471
488
  }) ?? null;
472
489
  }
473
490
 
491
+ function structuralToolbarAnchor() {
492
+ const buttons = Array.from(document.querySelectorAll("button"));
493
+ return buttons.find((button) => /temporary chat/i.test(button.getAttribute("aria-label") ?? ""))
494
+ ?? buttons.find((button) => new Set(["Toggle summary", "Toggle pinned summary"])
495
+ .has(button.getAttribute("aria-label")))
496
+ ?? buttons.find((button) => button.getAttribute("aria-label") === "Toggle bottom panel")
497
+ ?? buttons.find((button) => button.getAttribute("aria-label") === "Toggle side panel")
498
+ ?? null;
499
+ }
500
+
501
+ function stopToolbarReadiness() {
502
+ if (!toolbarReadiness) return;
503
+ toolbarReadiness.observer.disconnect();
504
+ if (toolbarReadiness.retryTimer != null) clearTimeout(toolbarReadiness.retryTimer);
505
+ if (toolbarReadiness.deadlineTimer != null) clearTimeout(toolbarReadiness.deadlineTimer);
506
+ toolbarReadiness = null;
507
+ }
508
+
509
+ function ensureToolbarReadiness(candidate) {
510
+ const thread = threadIdentity();
511
+ if (toolbarReadiness?.candidate === candidate && toolbarReadiness.thread === thread) return;
512
+ stopToolbarReadiness();
513
+ const record = {
514
+ candidate,
515
+ thread,
516
+ attempts: 0,
517
+ retryTimer: null,
518
+ deadlineTimer: null,
519
+ observer: null,
520
+ };
521
+ const wake = () => {
522
+ if (toolbarReadiness !== record) return;
523
+ queueEnsure();
524
+ };
525
+ record.observer = new ResizeObserver(wake);
526
+ record.observer.observe(candidate);
527
+ const header = candidate.closest("header");
528
+ if (header) record.observer.observe(header);
529
+ const retry = () => {
530
+ if (toolbarReadiness !== record || record.attempts >= 8) return;
531
+ record.attempts += 1;
532
+ wake();
533
+ record.retryTimer = setTimeout(retry, 75);
534
+ };
535
+ record.retryTimer = setTimeout(retry, 75);
536
+ record.deadlineTimer = setTimeout(() => {
537
+ if (toolbarReadiness === record) stopToolbarReadiness();
538
+ }, 1_500);
539
+ toolbarReadiness = record;
540
+ }
541
+
474
542
  function toolbarControlRoot(button) {
475
543
  return button?.parentElement?.parentElement ?? button?.parentElement ?? button ?? null;
476
544
  }
@@ -525,8 +593,10 @@ export function createInjectionSource(features, {
525
593
  }
526
594
 
527
595
  function mainContentViewport() {
528
- return document.querySelector("[data-app-shell-main-content-layout]")
596
+ const composer = currentComposer();
597
+ return composer?.closest("main[data-app-shell-main-surface]")
529
598
  ?? document.querySelector("main[data-app-shell-main-surface]")
599
+ ?? document.querySelector("[data-app-shell-main-content-layout]")
530
600
  ?? document.querySelector("main");
531
601
  }
532
602
 
@@ -666,8 +736,6 @@ export function createInjectionSource(features, {
666
736
  return null;
667
737
  }
668
738
 
669
- const modelSelectorFastIconStorageKey = "codex-model-slider:fast-icons:v1";
670
-
671
739
  function persistNativeFastIcons() {
672
740
  if (!modelSelectorNativeFastIcons || !modelSelectorNativeFastIconsDirty) return;
673
741
  try {
@@ -682,22 +750,29 @@ export function createInjectionSource(features, {
682
750
  }
683
751
  }
684
752
 
685
- function loadPersistedNativeFastIcons() {
753
+ function loadPersistedNativeFastIcons(storageKey) {
754
+ let stored;
686
755
  try {
687
- const payload = JSON.parse(localStorage.getItem(modelSelectorFastIconStorageKey) ?? "null");
688
- if (!payload || typeof payload !== "object") return null;
689
- const icons = {};
690
- for (const [state, markup] of Object.entries(payload)) {
691
- if (typeof markup !== "string") continue;
692
- const container = document.createElement("template");
693
- container.innerHTML = markup;
694
- const svg = container.content.querySelector("svg");
695
- if (svg) icons[state] = svg;
696
- }
697
- return Object.keys(icons).length ? icons : null;
756
+ stored = localStorage.getItem(storageKey);
698
757
  } catch {
699
758
  return null;
700
759
  }
760
+ let payload;
761
+ try {
762
+ payload = JSON.parse(stored ?? "null");
763
+ } catch {
764
+ return null;
765
+ }
766
+ if (!payload || typeof payload !== "object") return null;
767
+ const icons = {};
768
+ for (const [state, markup] of Object.entries(payload)) {
769
+ if (typeof markup !== "string") continue;
770
+ const container = document.createElement("template");
771
+ container.innerHTML = markup;
772
+ const svg = container.content.querySelector("svg");
773
+ if (svg) icons[state] = svg;
774
+ }
775
+ return Object.keys(icons).length ? icons : null;
701
776
  }
702
777
 
703
778
  function extractFastIconPath(source, prefix) {
@@ -911,14 +986,16 @@ export function createInjectionSource(features, {
911
986
  (option) => String(option.thinkingEffort ?? "") === String(selected.thinkingEffort ?? ""),
912
987
  );
913
988
  const selectedEffort = selectedOption ? effortFor(selectedOption) : (efforts[0] ?? "instant");
989
+ const defaultEffort = String(version.defaultReasoningEffort ?? efforts[0] ?? "instant").toLowerCase();
914
990
  const models = [{
915
991
  model: String(selected.slug ?? ""),
916
992
  displayName: String(version?.label ?? selected.slug ?? ""),
917
- defaultReasoningEffort: selectedEffort,
993
+ defaultReasoningEffort: defaultEffort,
918
994
  supportedReasoningEfforts: efforts.map((effort) => ({ reasoningEffort: effort })),
919
995
  }];
920
996
  return {
921
997
  models,
998
+ reasoningEffort: selectedEffort,
922
999
  onSelectReasoningEffort(effort) {
923
1000
  const target = options.find((option) => effortFor(option) === String(effort).toLowerCase());
924
1001
  if (!target) return;
@@ -930,8 +1007,9 @@ export function createInjectionSource(features, {
930
1007
  };
931
1008
  }
932
1009
 
933
- function nativeCatalogModelFor(identity) {
934
- const models = nativeComposerModelController()?.models ?? [];
1010
+ function nativeCatalogModelFor(identity, selector = modelSelectorFeature?.modelSelector) {
1011
+ const controller = nativeComposerModelController();
1012
+ const models = controller?.models ?? [];
935
1013
  const exact = models.find((model) => String(model.model ?? "").toLowerCase() === identity.raw.toLowerCase());
936
1014
  const target = comparableModelLabel(identity.backend);
937
1015
  const aliases = exact ? [] : models.filter((model) => (
@@ -939,13 +1017,25 @@ export function createInjectionSource(features, {
939
1017
  ));
940
1018
  const model = exact ?? (aliases.length === 1 ? aliases[0] : null);
941
1019
  if (!model) return null;
1020
+ const nativeReasoningLevels = (model.supportedReasoningEfforts ?? []).map((level) => ({
1021
+ effort: String(level?.reasoningEffort ?? level?.effort ?? level ?? "").toLowerCase(),
1022
+ })).filter((level) => level.effort);
1023
+ const allowed = selector?.reasoningEffortOverrides?.[identity.raw.toLowerCase()];
1024
+ const supportedReasoningLevels = allowed
1025
+ ? nativeReasoningLevels.filter((level) => allowed.includes(level.effort))
1026
+ : nativeReasoningLevels;
1027
+ const currentReasoningLevel = String(controller?.reasoningEffort ?? "").toLowerCase();
942
1028
  return {
943
1029
  slug: String(model.model ?? ""),
944
1030
  displayName: String(model.displayName ?? model.model ?? ""),
945
- defaultReasoningLevel: String(model.defaultReasoningEffort ?? ""),
946
- supportedReasoningLevels: (model.supportedReasoningEfforts ?? []).map((level) => ({
947
- effort: String(level?.reasoningEffort ?? level?.effort ?? level ?? ""),
948
- })).filter((level) => level.effort),
1031
+ currentReasoningLevel: nativeReasoningLevels.some(
1032
+ (level) => level.effort === currentReasoningLevel,
1033
+ ) ? currentReasoningLevel : null,
1034
+ currentReasoningLevelVisible: supportedReasoningLevels.some(
1035
+ (level) => level.effort === currentReasoningLevel,
1036
+ ),
1037
+ defaultReasoningLevel: String(model.defaultReasoningEffort ?? "").toLowerCase(),
1038
+ supportedReasoningLevels,
949
1039
  };
950
1040
  }
951
1041
 
@@ -1591,6 +1681,9 @@ export function createInjectionSource(features, {
1591
1681
  }
1592
1682
 
1593
1683
  function selectNativeEffort(parentMenu, effort) {
1684
+ const identity = modelIdentity(nativeModelValue(parentMenu));
1685
+ const model = modelDefinitionFor(identity, modelSelectorFeature?.modelSelector);
1686
+ if (!model?.supportedReasoningLevels?.some((level) => level.effort === String(effort).toLowerCase())) return;
1594
1687
  const controller = nativeComposerModelController();
1595
1688
  if (typeof controller?.onSelectReasoningEffort === "function") {
1596
1689
  controller.onSelectReasoningEffort(effort);
@@ -1666,6 +1759,24 @@ export function createInjectionSource(features, {
1666
1759
  rail.setAttribute("aria-valuenow", String(index));
1667
1760
  }
1668
1761
 
1762
+ function neutralizeStaleSliderVisual(rail) {
1763
+ for (const selector of [
1764
+ "[data-codex-model-slider-native-range]",
1765
+ "[data-codex-model-slider-native-thumb]",
1766
+ "[data-codex-model-slider-range]",
1767
+ "[data-codex-model-slider-thumb]",
1768
+ ]) {
1769
+ rail.querySelectorAll(selector).forEach((element) => {
1770
+ element.style.setProperty("visibility", "hidden");
1771
+ });
1772
+ }
1773
+ rail.querySelectorAll("[data-codex-model-slider-tick]").forEach((tick) => {
1774
+ tick.removeAttribute("data-selected");
1775
+ });
1776
+ rail.removeAttribute("aria-valuenow");
1777
+ rail.style.removeProperty("--codex-model-slider-progress");
1778
+ }
1779
+
1669
1780
  function nativeParticleSeed(index, salt) {
1670
1781
  const value = Math.sin((index + 1) * 12.9898 + salt * 78.233) * 43758.5453;
1671
1782
  return value - Math.floor(value);
@@ -1792,7 +1903,8 @@ export function createInjectionSource(features, {
1792
1903
 
1793
1904
  function createGenericModelSlider(parentMenu, selector, identity, model, selectedEffort) {
1794
1905
  const efforts = model.supportedReasoningLevels.map((level) => level.effort);
1795
- const selectedIndex = Math.max(0, efforts.indexOf(selectedEffort));
1906
+ const selectedIndex = efforts.indexOf(selectedEffort);
1907
+ const selectedEffortVisible = selectedIndex >= 0;
1796
1908
  const classes = Object.fromEntries([
1797
1909
  "Menu",
1798
1910
  "ViewToggle",
@@ -1890,7 +2002,20 @@ export function createInjectionSource(features, {
1890
2002
  rail.setAttribute("aria-label", locale === "zh-CN" ? "推理强度" : "Reasoning effort");
1891
2003
  rail.setAttribute("aria-valuemin", "0");
1892
2004
  rail.setAttribute("aria-valuemax", String(efforts.length - 1));
1893
- rail.setAttribute("aria-valuetext", effortLabel(efforts[selectedIndex]));
2005
+ rail.setAttribute("aria-valuetext", selectedEffortVisible
2006
+ ? effortLabel(selectedEffort)
2007
+ : (locale === "zh-CN" ? "当前档位不可用" : "Current effort unavailable"));
2008
+ rail.toggleAttribute("data-stale-reasoning-effort", !selectedEffortVisible);
2009
+ if (!selectedEffortVisible) {
2010
+ rail.setAttribute("aria-disabled", "true");
2011
+ rail.removeAttribute("tabindex");
2012
+ neutralizeStaleSliderVisual(rail);
2013
+ const staleView = document.createElement("div");
2014
+ staleView.className = classes.SimpleView;
2015
+ staleView.append(nativeSlider ?? rail);
2016
+ shell.append(viewControls, staleView);
2017
+ return shell;
2018
+ }
1894
2019
  if (!nativeSlider) {
1895
2020
  const track = document.createElement("div");
1896
2021
  track.setAttribute("data-codex-model-slider-track", "");
@@ -1986,7 +2111,7 @@ export function createInjectionSource(features, {
1986
2111
  // 控制器推导的任务内实际 effort 优先,避免子任务/主任务切换后触发器属性滞后;
1987
2112
  // 触发器属性仅在控制器无法解析时作为实时回退,静态目录默认值最后兜底。
1988
2113
  const nativeModel = nativeCatalogModelFor(identity);
1989
- const selectedEffort = nativeModel?.defaultReasoningLevel
2114
+ const selectedEffort = nativeModel?.currentReasoningLevel
1990
2115
  ?? document.querySelector("[data-codex-intelligence-trigger]")
1991
2116
  ?.getAttribute("data-selected-reasoning-effort")
1992
2117
  ?? model.defaultReasoningLevel
@@ -2357,9 +2482,11 @@ export function createInjectionSource(features, {
2357
2482
  }
2358
2483
  if (next === currentThread) return;
2359
2484
  currentThread = next;
2485
+ stopToolbarReadiness();
2360
2486
  savedComposerRange = null;
2361
2487
  savedComposerThread = null;
2362
2488
  hidePinnedSurfaces();
2489
+ queueEnsure();
2363
2490
  }
2364
2491
 
2365
2492
  function currentComposer() {
@@ -2560,24 +2687,23 @@ export function createInjectionSource(features, {
2560
2687
  }
2561
2688
  }
2562
2689
 
2563
- function threadScrollContent() {
2564
- const scrollContainer = mainContentViewport()?.querySelector(".thread-scroll-container");
2565
- return scrollContainer?.firstElementChild instanceof HTMLElement
2566
- ? scrollContainer.firstElementChild
2567
- : null;
2568
- }
2569
-
2570
2690
  function restoreThreadContentShift() {
2571
- if (!shiftedThreadContent || !shiftedThreadOriginal) return;
2572
- const { element, position, left, transition } = shiftedThreadOriginal;
2573
- if (element.isConnected) {
2574
- element.style.setProperty("position", position.value, position.priority);
2575
- element.style.setProperty("left", left.value, left.priority);
2576
- element.style.setProperty("transition", transition.value, transition.priority);
2691
+ for (const { element, shift, translate } of shiftedConversationOwners) {
2692
+ if (!element.isConnected) continue;
2693
+ if (shift.value) {
2694
+ element.style.setProperty(
2695
+ "--thread-wide-block-inline-shift",
2696
+ shift.value,
2697
+ shift.priority,
2698
+ );
2699
+ } else {
2700
+ element.style.removeProperty("--thread-wide-block-inline-shift");
2701
+ }
2702
+ if (translate.value) element.style.setProperty("translate", translate.value, translate.priority);
2703
+ else element.style.removeProperty("translate");
2577
2704
  element.removeAttribute("data-codex-personal-summary-shift");
2578
2705
  }
2579
- shiftedThreadContent = null;
2580
- shiftedThreadOriginal = null;
2706
+ shiftedConversationOwners = [];
2581
2707
  }
2582
2708
 
2583
2709
  function applyThreadContentShift(value) {
@@ -2585,45 +2711,40 @@ export function createInjectionSource(features, {
2585
2711
  restoreThreadContentShift();
2586
2712
  return;
2587
2713
  }
2588
- if (!threadShiftStyle.isConnected) {
2589
- (document.head ?? document.documentElement)?.append(threadShiftStyle);
2590
- }
2591
- const content = threadScrollContent();
2592
- if (!content) {
2714
+ const viewport = mainContentViewport();
2715
+ const owners = viewport
2716
+ ? Array.from(viewport.querySelectorAll('[style*="--thread-wide-block-inline-shift"]'))
2717
+ : [];
2718
+ if (!owners.length) {
2593
2719
  restoreThreadContentShift();
2594
2720
  return;
2595
2721
  }
2596
- if (shiftedThreadContent !== content) {
2722
+ if (
2723
+ shiftedConversationOwners.length !== owners.length
2724
+ || owners.some((owner, index) => shiftedConversationOwners[index]?.element !== owner)
2725
+ ) {
2597
2726
  restoreThreadContentShift();
2598
- shiftedThreadContent = content;
2599
- shiftedThreadOriginal = {
2600
- element: content,
2601
- position: {
2602
- value: content.style.getPropertyValue("position"),
2603
- priority: content.style.getPropertyPriority("position"),
2727
+ shiftedConversationOwners = owners.map((owner) => ({
2728
+ element: owner,
2729
+ shift: {
2730
+ value: owner.style.getPropertyValue("--thread-wide-block-inline-shift"),
2731
+ priority: owner.style.getPropertyPriority("--thread-wide-block-inline-shift"),
2604
2732
  },
2605
- left: {
2606
- value: content.style.getPropertyValue("left"),
2607
- priority: content.style.getPropertyPriority("left"),
2733
+ translate: {
2734
+ value: owner.style.getPropertyValue("translate"),
2735
+ priority: owner.style.getPropertyPriority("translate"),
2608
2736
  },
2609
- transition: {
2610
- value: content.style.getPropertyValue("transition"),
2611
- priority: content.style.getPropertyPriority("transition"),
2612
- },
2613
- };
2737
+ }));
2738
+ }
2739
+ for (const owner of owners) {
2740
+ owner.style.setProperty(
2741
+ "--thread-wide-block-inline-shift",
2742
+ `${Number(value)}px`,
2743
+ "important",
2744
+ );
2745
+ owner.style.setProperty("translate", `${Number(value)}px 0`, "important");
2746
+ owner.setAttribute("data-codex-personal-summary-shift", String(value));
2614
2747
  }
2615
- const duration = reducedMotion() ? 0 : 300;
2616
- content.style.setProperty("transition", duration
2617
- ? "left 300ms cubic-bezier(.2,.8,.2,1)"
2618
- : "none", "important");
2619
- content.style.setProperty("position", "relative", "important");
2620
- content.style.setProperty("left", `${value}px`, "important");
2621
- content.setAttribute("data-codex-personal-summary-shift", String(value));
2622
- if (duration) window.setTimeout(() => {
2623
- if (content.getAttribute("data-codex-personal-summary-shift") !== String(value)) return;
2624
- content.style.setProperty("transition", "none", "important");
2625
- content.style.setProperty("left", `${value}px`, "important");
2626
- }, duration + 50);
2627
2748
  }
2628
2749
 
2629
2750
  function setSummarySurfaceVisible(surface, visible, mode) {
@@ -2728,7 +2849,7 @@ export function createInjectionSource(features, {
2728
2849
  surface.setAttribute("data-codex-personal-summary-presentation", displayMode);
2729
2850
  setSummarySurfaceVisible(surface, true, displayMode);
2730
2851
 
2731
- const shift = displayMode === "shift"
2852
+ const shift = displayMode !== "overlay"
2732
2853
  ? -(layout.panelWidth + layout.panelInset) / 2
2733
2854
  : 0;
2734
2855
  applyThreadContentShift(shift);
@@ -3373,8 +3494,12 @@ export function createInjectionSource(features, {
3373
3494
  const detail = feature?.detailTabs?.find((candidate) => candidate.id === detailId);
3374
3495
  if (!feature || !detail) throw new Error("The requested detail Tab is not registered");
3375
3496
  const result = await openNativeDetailTab(feature, detail);
3376
- if (!result) throw nativeTabCapabilityError
3377
- ?? new Error("Codex native right-panel tabs are unavailable");
3497
+ if (!result) {
3498
+ const error = nativeTabCapabilityError
3499
+ ?? new Error("Codex native right-panel tabs are unavailable");
3500
+ error.code = "NATIVE_TAB_UNAVAILABLE";
3501
+ throw error;
3502
+ }
3378
3503
  return result;
3379
3504
  }
3380
3505
 
@@ -3489,13 +3614,20 @@ export function createInjectionSource(features, {
3489
3614
  }
3490
3615
 
3491
3616
  function ensureToolbarEntries() {
3492
- if (!toolbarFeatures.length) return;
3617
+ if (!toolbarFeatures.length) {
3618
+ stopToolbarReadiness();
3619
+ return;
3620
+ }
3493
3621
  const temporaryChatButton = nativeTemporaryChatButton();
3494
3622
  const summaryButton = nativeSummaryButton();
3495
3623
  const bottomPanelButton = nativeBottomPanelButton();
3496
3624
  const sidePanelButton = nativeSidePanelButton();
3497
3625
  const anchorButton = temporaryChatButton ?? summaryButton ?? bottomPanelButton ?? sidePanelButton;
3498
- if (!anchorButton) return;
3626
+ if (!anchorButton) {
3627
+ const candidate = structuralToolbarAnchor();
3628
+ if (candidate) ensureToolbarReadiness(candidate);
3629
+ return;
3630
+ }
3499
3631
  const anchorRoot = toolbarControlRoot(anchorButton);
3500
3632
  // The bottom/side-panel toggles sit in narrow fixed containers that
3501
3633
  // travel with the native panels. Entries anchored to them must live in
@@ -3506,7 +3638,10 @@ export function createInjectionSource(features, {
3506
3638
  const useFallbackAnchor = anchorButton === sidePanelButton || anchorButton === bottomPanelButton;
3507
3639
  const summaryGroup = useFallbackAnchor ? null : nativeSummaryToolbarGroup(anchorButton);
3508
3640
  let targetGroup = summaryGroup ?? ensureFallbackSummaryToolbarGroup(anchorButton);
3509
- if (!targetGroup) return;
3641
+ if (!targetGroup) {
3642
+ ensureToolbarReadiness(anchorButton);
3643
+ return;
3644
+ }
3510
3645
  let cursor = useFallbackAnchor ? null : anchorRoot;
3511
3646
  for (const feature of toolbarFeatures) {
3512
3647
  const entries = Array.from(document.querySelectorAll(`[${entryMarker}="${feature.id}"]`));
@@ -3542,6 +3677,11 @@ export function createInjectionSource(features, {
3542
3677
  if (!used) group.remove();
3543
3678
  });
3544
3679
  }
3680
+ if (toolbarFeatures.every((feature) => findEntry(feature.id)?.isConnected)) {
3681
+ stopToolbarReadiness();
3682
+ } else {
3683
+ ensureToolbarReadiness(anchorButton);
3684
+ }
3545
3685
  }
3546
3686
 
3547
3687
  function ensureEntries() {
@@ -3600,6 +3740,13 @@ export function createInjectionSource(features, {
3600
3740
  }, record.origin);
3601
3741
  }
3602
3742
 
3743
+ function hostActionFailure(error) {
3744
+ return {
3745
+ code: typeof error?.code === "string" ? error.code : "HOST_ACTION_FAILED",
3746
+ message: error?.message ?? "Host action failed",
3747
+ };
3748
+ }
3749
+
3603
3750
  async function handleSurfaceHostAction(record, message) {
3604
3751
  const feature = featureById.get(record.featureId);
3605
3752
  const { requestId, action, payload = {} } = message;
@@ -3663,7 +3810,7 @@ export function createInjectionSource(features, {
3663
3810
  } catch (error) {
3664
3811
  respondToSurface(record, requestId, {
3665
3812
  ok: false,
3666
- error: { code: "HOST_ACTION_FAILED", message: error.message },
3813
+ error: hostActionFailure(error),
3667
3814
  });
3668
3815
  }
3669
3816
  }
@@ -3831,9 +3978,9 @@ export function createInjectionSource(features, {
3831
3978
  if (ensureFrame != null) cancelAnimationFrame(ensureFrame);
3832
3979
  if (ensureTimer != null) clearTimeout(ensureTimer);
3833
3980
  for (const observer of domObservers) observer.disconnect();
3981
+ stopToolbarReadiness();
3834
3982
  mainContentObserver?.observer.disconnect();
3835
3983
  restoreThreadContentShift();
3836
- threadShiftStyle.remove();
3837
3984
  for (const root of document.querySelectorAll("[data-codex-personal-toolbar-entry]")) root.remove();
3838
3985
  for (const group of document.querySelectorAll("[data-codex-personal-summary-toolbar-group]")) group.remove();
3839
3986
  for (const surface of pageSurfaces.values()) surface.remove();
@@ -3864,7 +4011,15 @@ export function createInjectionSource(features, {
3864
4011
  detailTabs: feature.detailTabs ?? [],
3865
4012
  hostActions: feature.hostActions ?? [],
3866
4013
  modelSelector: feature.modelSelector ?? null,
3867
- pageScript: feature.pageScript ?? null,
4014
+ pageScript: feature.pageScript ? {
4015
+ ...feature.pageScript,
4016
+ config: {
4017
+ serviceOrigin: pageScriptServiceOrigin(feature),
4018
+ bindingName,
4019
+ bindingToken,
4020
+ featureId: feature.id,
4021
+ },
4022
+ } : null,
3868
4023
  }));
3869
4024
  return `(${install.toString()})(${JSON.stringify({
3870
4025
  version: RUNTIME_VERSION,
@@ -3886,7 +4041,9 @@ export function createRuntimeReadyContract({
3886
4041
  runtimeSessionId,
3887
4042
  }) {
3888
4043
  const discovered = Boolean(renderer.discovered);
4044
+ const discoveredTargets = Math.max(0, Number(renderer.discoveredTargets) || 0);
3889
4045
  const injectedTargets = Math.max(0, Number(renderer.injectedTargets) || 0);
4046
+ const targetFailures = Array.isArray(renderer.targetFailures) ? renderer.targetFailures : [];
3890
4047
  const active = Boolean(renderer.active) && discovered && injectedTargets > 0;
3891
4048
  return {
3892
4049
  pid,
@@ -3904,8 +4061,11 @@ export function createRuntimeReadyContract({
3904
4061
  })),
3905
4062
  renderer: {
3906
4063
  discovered,
4064
+ discoveredTargets,
3907
4065
  injectedTargets,
3908
4066
  active,
4067
+ healthy: active && targetFailures.length === 0 && injectedTargets === discoveredTargets,
4068
+ targetFailures,
3909
4069
  runtimeSessionId: renderer.runtimeSessionId ?? runtimeSessionId,
3910
4070
  },
3911
4071
  };
@@ -4106,6 +4266,8 @@ export class CodexRuntime {
4106
4266
  this.onStatusChange = onStatusChange;
4107
4267
  this.onManagedCodexPidChange = onManagedCodexPidChange;
4108
4268
  this.injectedTargets = new Set();
4269
+ this.rendererTargetIds = new Set();
4270
+ this.targetFailures = new Map();
4109
4271
  this.clients = new Map();
4110
4272
  this.scriptIds = new Map();
4111
4273
  this.bindingTokens = new Map();
@@ -4159,10 +4321,21 @@ export class CodexRuntime {
4159
4321
 
4160
4322
  getRendererStatus() {
4161
4323
  const injectedTargets = this.injectedTargets.size;
4324
+ const discoveredTargets = this.rendererTargetIds.size;
4325
+ const targetFailures = Array.from(this.targetFailures, ([targetId, failure]) => ({
4326
+ targetId,
4327
+ attempts: failure.attempts,
4328
+ state: "retrying",
4329
+ lastFailure: failure.lastFailure,
4330
+ })).sort((left, right) => left.targetId.localeCompare(right.targetId));
4331
+ const active = !this.stopping && this.rendererDiscovered && injectedTargets > 0;
4162
4332
  return {
4163
4333
  discovered: this.rendererDiscovered,
4334
+ discoveredTargets,
4164
4335
  injectedTargets,
4165
- active: !this.stopping && this.rendererDiscovered && injectedTargets > 0,
4336
+ active,
4337
+ healthy: active && targetFailures.length === 0 && injectedTargets === discoveredTargets,
4338
+ targetFailures,
4166
4339
  runtimeSessionId: this.runtimeSessionId,
4167
4340
  };
4168
4341
  }
@@ -4173,8 +4346,11 @@ export class CodexRuntime {
4173
4346
  if (
4174
4347
  previous
4175
4348
  && previous.discovered === status.discovered
4349
+ && previous.discoveredTargets === status.discoveredTargets
4176
4350
  && previous.injectedTargets === status.injectedTargets
4177
4351
  && previous.active === status.active
4352
+ && previous.healthy === status.healthy
4353
+ && JSON.stringify(previous.targetFailures) === JSON.stringify(status.targetFailures)
4178
4354
  && previous.runtimeSessionId === status.runtimeSessionId
4179
4355
  ) return;
4180
4356
  this.lastRendererStatus = status;
@@ -4218,6 +4394,8 @@ export class CodexRuntime {
4218
4394
  this.bindingTokens.clear();
4219
4395
  this.hostActionChains.clear();
4220
4396
  this.injectedTargets.clear();
4397
+ this.rendererTargetIds.clear();
4398
+ this.targetFailures.clear();
4221
4399
  this.rendererDiscovered = false;
4222
4400
  this.browserClient?.socket.close();
4223
4401
  this.browserClient = null;
@@ -4581,6 +4759,10 @@ export class CodexRuntime {
4581
4759
  });
4582
4760
  return;
4583
4761
  }
4762
+ if (request.action === "import-generated-image") {
4763
+ await this.importGeneratedImage(feature, request.payload);
4764
+ return;
4765
+ }
4584
4766
  try {
4585
4767
  if (request.action !== "attach-file") throw new Error("Unsupported native Host action");
4586
4768
  const result = await this.attachFileToComposer(client, request.payload?.path, request.requestId);
@@ -4593,6 +4775,22 @@ export class CodexRuntime {
4593
4775
  }
4594
4776
  }
4595
4777
 
4778
+ async importGeneratedImage(feature, payload) {
4779
+ const origin = pageScriptServiceOrigin(feature);
4780
+ if (!origin) throw new Error("Generated image import service is unavailable");
4781
+ const response = await this.fetchImpl(new URL("/api/import-generated-image", origin), {
4782
+ method: "POST",
4783
+ headers: {
4784
+ "Content-Type": "application/json",
4785
+ Origin: CODEX_APP_ORIGIN,
4786
+ },
4787
+ body: JSON.stringify(payload),
4788
+ });
4789
+ if (!response?.ok) {
4790
+ throw new Error(`Generated image import returned HTTP ${response?.status ?? "error"}`);
4791
+ }
4792
+ }
4793
+
4596
4794
  async refreshTargets() {
4597
4795
  let response;
4598
4796
  try {
@@ -4619,8 +4817,14 @@ export class CodexRuntime {
4619
4817
  .filter((target) => target.type === "page" && target.url === "app://-/index.html")
4620
4818
  .map((target) => target.id),
4621
4819
  );
4820
+ this.rendererTargetIds = liveRendererIds;
4622
4821
  let removedTarget = false;
4623
- for (const id of this.injectedTargets) {
4822
+ const trackedTargetIds = new Set([
4823
+ ...this.injectedTargets,
4824
+ ...this.clients.keys(),
4825
+ ...this.targetFailures.keys(),
4826
+ ]);
4827
+ for (const id of trackedTargetIds) {
4624
4828
  if (liveRendererIds.has(id)) continue;
4625
4829
  removedTarget = true;
4626
4830
  this.clients.get(id)?.socket.close();
@@ -4629,6 +4833,7 @@ export class CodexRuntime {
4629
4833
  this.bindingTokens.delete(id);
4630
4834
  this.hostActionChains.delete(id);
4631
4835
  this.injectedTargets.delete(id);
4836
+ this.targetFailures.delete(id);
4632
4837
  }
4633
4838
  const rendererDiscovered = liveRendererIds.size > 0;
4634
4839
  const rendererChanged = rendererDiscovered !== this.rendererDiscovered;
@@ -4639,8 +4844,12 @@ export class CodexRuntime {
4639
4844
  if (target.type !== "page" || target.url !== "app://-/index.html" || this.injectedTargets.has(target.id)) {
4640
4845
  continue;
4641
4846
  }
4642
- const client = await this.connectClient(target.webSocketDebuggerUrl);
4847
+ const previousFailure = this.targetFailures.get(target.id);
4848
+ if (previousFailure && Date.now() < previousFailure.nextRetryAt) continue;
4849
+ let client;
4850
+ let scriptIdentifier = null;
4643
4851
  try {
4852
+ client = await this.connectClient(target.webSocketDebuggerUrl);
4644
4853
  await Promise.all([
4645
4854
  client.send("Page.enable"),
4646
4855
  client.send("Runtime.enable"),
@@ -4662,6 +4871,7 @@ export class CodexRuntime {
4662
4871
  const script = await client.send("Page.addScriptToEvaluateOnNewDocument", {
4663
4872
  source: createDocumentBootstrapSource(source),
4664
4873
  });
4874
+ scriptIdentifier = script.identifier;
4665
4875
  await waitForExpression(
4666
4876
  client,
4667
4877
  `document.readyState === "interactive" || document.readyState === "complete"`,
@@ -4674,15 +4884,33 @@ export class CodexRuntime {
4674
4884
  + ` && window.__codexPersonalRuntime?.sessionId === ${JSON.stringify(this.runtimeSessionId)}`,
4675
4885
  );
4676
4886
  this.clients.set(target.id, client);
4677
- this.scriptIds.set(target.id, script.identifier);
4887
+ this.scriptIds.set(target.id, scriptIdentifier);
4678
4888
  this.injectedTargets.add(target.id);
4889
+ this.targetFailures.delete(target.id);
4679
4890
  this.notifyStatusChange();
4680
4891
  } catch (error) {
4681
4892
  this.bindingTokens.delete(target.id);
4682
- client.socket.close();
4683
- throw error;
4893
+ if (client) {
4894
+ if (scriptIdentifier) {
4895
+ await client.send("Page.removeScriptToEvaluateOnNewDocument", {
4896
+ identifier: scriptIdentifier,
4897
+ }).catch(() => {});
4898
+ }
4899
+ await client.send("Runtime.removeBinding", { name: HOST_BINDING_NAME }).catch(() => {});
4900
+ client.socket.close();
4901
+ }
4902
+ const attempts = (previousFailure?.attempts ?? 0) + 1;
4903
+ const retryDelay = Math.min(5_000, 250 * (2 ** Math.min(attempts - 1, 5)));
4904
+ this.targetFailures.set(target.id, {
4905
+ attempts,
4906
+ lastFailure: error.message,
4907
+ nextRetryAt: Date.now() + retryDelay,
4908
+ });
4909
+ this.logger.warn?.(`Renderer injection failed for ${target.id}; retrying`, error.message);
4910
+ this.notifyStatusChange();
4684
4911
  }
4685
4912
  }
4913
+ this.notifyStatusChange();
4686
4914
  }
4687
4915
 
4688
4916
  async stop() {
@@ -4712,6 +4940,8 @@ export class CodexRuntime {
4712
4940
  this.bindingTokens.clear();
4713
4941
  this.hostActionChains.clear();
4714
4942
  this.injectedTargets.clear();
4943
+ this.rendererTargetIds.clear();
4944
+ this.targetFailures.clear();
4715
4945
  this.rendererDiscovered = false;
4716
4946
  this.notifyStatusChange();
4717
4947
  this.monitorPromise = null;