minecodex 0.1.19 → 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();
@@ -300,6 +309,11 @@ export function createInjectionSource(features, {
300
309
  let modelSelectorNativeFastIcons = null;
301
310
  // Fast 图标来源标记:bundle 静态提取 vs DOM 克隆,DOM 克隆优先以保持像素一致。
302
311
  let modelSelectorNativeFastIconSources = null;
312
+ // DOM 克隆可见性门控:仅在 Fast 控件从无到有时扫描一次,常驻会话不逐帧扫描。
313
+ let modelSelectorFastControlVisible = false;
314
+ // 持久化脏标记:仅在图标实际变化时写一次 localStorage。
315
+ let modelSelectorNativeFastIconsDirty = false;
316
+ const modelSelectorFastIconStorageKey = "codex-model-slider:fast-icons:v1";
303
317
  const modelSelectorStyle = document.createElement("style");
304
318
  modelSelectorStyle.setAttribute("data-codex-model-slider-style", "");
305
319
  modelSelectorStyle.textContent = `
@@ -380,7 +394,7 @@ export function createInjectionSource(features, {
380
394
  document.documentElement.append(modelSelectorStyle);
381
395
  // 启动阶段就解析 Fast 图标:先读持久化的原生克隆,再从 app bundle 静态提取兜底,
382
396
  // 避免首次会话必须等用户点开弹窗后 DOM 里才出现图标。
383
- const persistedIcons = loadPersistedNativeFastIcons();
397
+ const persistedIcons = loadPersistedNativeFastIcons(modelSelectorFastIconStorageKey);
384
398
  if (persistedIcons) {
385
399
  modelSelectorNativeFastIcons = persistedIcons;
386
400
  modelSelectorNativeFastIconSources = {};
@@ -400,9 +414,16 @@ export function createInjectionSource(features, {
400
414
  "window",
401
415
  "document",
402
416
  "MutationObserver",
417
+ "config",
403
418
  `"use strict";\n${source}`,
404
419
  );
405
- 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
+ );
406
427
  if (typeof cleanup === "function") pageScriptCleanups.push(cleanup);
407
428
  } catch (error) {
408
429
  console.warn(`[codex-personal] page-script feature failed: ${feature.id}`, error);
@@ -467,6 +488,57 @@ export function createInjectionSource(features, {
467
488
  }) ?? null;
468
489
  }
469
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
+
470
542
  function toolbarControlRoot(button) {
471
543
  return button?.parentElement?.parentElement ?? button?.parentElement ?? button ?? null;
472
544
  }
@@ -521,8 +593,10 @@ export function createInjectionSource(features, {
521
593
  }
522
594
 
523
595
  function mainContentViewport() {
524
- return document.querySelector("[data-app-shell-main-content-layout]")
596
+ const composer = currentComposer();
597
+ return composer?.closest("main[data-app-shell-main-surface]")
525
598
  ?? document.querySelector("main[data-app-shell-main-surface]")
599
+ ?? document.querySelector("[data-app-shell-main-content-layout]")
526
600
  ?? document.querySelector("main");
527
601
  }
528
602
 
@@ -662,37 +736,43 @@ export function createInjectionSource(features, {
662
736
  return null;
663
737
  }
664
738
 
665
- const modelSelectorFastIconStorageKey = "codex-model-slider:fast-icons:v1";
666
-
667
739
  function persistNativeFastIcons() {
668
- if (!modelSelectorNativeFastIcons) return;
740
+ if (!modelSelectorNativeFastIcons || !modelSelectorNativeFastIconsDirty) return;
669
741
  try {
670
742
  const payload = {};
671
743
  for (const [state, template] of Object.entries(modelSelectorNativeFastIcons)) {
672
744
  payload[state] = template.outerHTML;
673
745
  }
674
746
  localStorage.setItem(modelSelectorFastIconStorageKey, JSON.stringify(payload));
747
+ modelSelectorNativeFastIconsDirty = false;
675
748
  } catch {
676
749
  // localStorage 不可用时静默降级,仅本次会话内生效。
677
750
  }
678
751
  }
679
752
 
680
- function loadPersistedNativeFastIcons() {
753
+ function loadPersistedNativeFastIcons(storageKey) {
754
+ let stored;
681
755
  try {
682
- const payload = JSON.parse(localStorage.getItem(modelSelectorFastIconStorageKey) ?? "null");
683
- if (!payload || typeof payload !== "object") return null;
684
- const icons = {};
685
- for (const [state, markup] of Object.entries(payload)) {
686
- if (typeof markup !== "string") continue;
687
- const container = document.createElement("template");
688
- container.innerHTML = markup;
689
- const svg = container.content.querySelector("svg");
690
- if (svg) icons[state] = svg;
691
- }
692
- return Object.keys(icons).length ? icons : null;
756
+ stored = localStorage.getItem(storageKey);
693
757
  } catch {
694
758
  return null;
695
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;
696
776
  }
697
777
 
698
778
  function extractFastIconPath(source, prefix) {
@@ -701,6 +781,8 @@ export function createInjectionSource(features, {
701
781
  }
702
782
 
703
783
  async function loadNativeFastIconFromBundle() {
784
+ // 两个图标均已就绪(持久化或 DOM 克隆)时不再拉取整包 app-initial 资源。
785
+ if (modelSelectorNativeFastIcons?.active && modelSelectorNativeFastIcons?.inactive) return;
704
786
  const sourceLink = document.querySelector('link[href*="/assets/app-initial-"][href$=".js"]');
705
787
  if (!sourceLink?.href) return;
706
788
  try {
@@ -728,6 +810,7 @@ export function createInjectionSource(features, {
728
810
  define("active", "M11\\.9125 21\\.4125", { viewBox: "0 0 24 24" });
729
811
  define("inactive", "M7\\.38 16\\.2207", { viewBox: "0 0 20 20", transform: "translate(2.43 1.609)" });
730
812
  if (gained) {
813
+ modelSelectorNativeFastIconsDirty = true;
731
814
  persistNativeFastIcons();
732
815
  applyNativeFastIconsToShells();
733
816
  queueEnsure();
@@ -747,32 +830,36 @@ export function createInjectionSource(features, {
747
830
 
748
831
  function captureNativeFastIcon() {
749
832
  // Fast 图标只在 Fast 控件渲染后才出现在 DOM(打开菜单或 GPT 模型);
750
- // 启动阶段已有 bundle/持久化兜底,因此无可见 Fast 控件时不做全页扫描。
751
- if (!hasVisibleFastControl()) {
752
- return modelSelectorNativeFastIcons;
753
- }
754
- const capture = (state, pathPrefix) => {
755
- const current = modelSelectorNativeFastIcons?.[state];
756
- const currentSource = modelSelectorNativeFastIconSources?.[state];
757
- if (current && currentSource === "dom") return;
758
- const path = Array.from(document.querySelectorAll("svg path")).find(
759
- (candidate) => candidate.getAttribute("d")?.startsWith(pathPrefix),
760
- );
761
- const svg = path?.closest("svg");
762
- if (!svg) return;
763
- const template = svg.cloneNode(true);
764
- template.removeAttribute("id");
765
- template.removeAttribute("width");
766
- template.removeAttribute("height");
767
- if (!modelSelectorNativeFastIcons) {
768
- modelSelectorNativeFastIcons = {};
769
- modelSelectorNativeFastIconSources = {};
770
- }
771
- modelSelectorNativeFastIcons[state] = template;
772
- modelSelectorNativeFastIconSources[state] = "dom";
773
- };
774
- capture("active", "M11.9125 21.4125");
775
- capture("inactive", "M7.38 16.2207");
833
+ // 只在控件可见性从无到有时做一次全页扫描(升级为 DOM 克隆),
834
+ // 常驻会话不逐帧扫描整个文档;缺图标时由 bundle/持久化兜底。
835
+ const visible = hasVisibleFastControl();
836
+ const justBecameVisible = visible && !modelSelectorFastControlVisible;
837
+ modelSelectorFastControlVisible = visible;
838
+ if (visible && justBecameVisible) {
839
+ const capture = (state, pathPrefix) => {
840
+ const current = modelSelectorNativeFastIcons?.[state];
841
+ const currentSource = modelSelectorNativeFastIconSources?.[state];
842
+ if (current && currentSource === "dom") return;
843
+ const path = Array.from(document.querySelectorAll("svg path")).find(
844
+ (candidate) => candidate.getAttribute("d")?.startsWith(pathPrefix),
845
+ );
846
+ const svg = path?.closest("svg");
847
+ if (!svg) return;
848
+ const template = svg.cloneNode(true);
849
+ template.removeAttribute("id");
850
+ template.removeAttribute("width");
851
+ template.removeAttribute("height");
852
+ if (!modelSelectorNativeFastIcons) {
853
+ modelSelectorNativeFastIcons = {};
854
+ modelSelectorNativeFastIconSources = {};
855
+ }
856
+ modelSelectorNativeFastIcons[state] = template;
857
+ modelSelectorNativeFastIconSources[state] = "dom";
858
+ modelSelectorNativeFastIconsDirty = true;
859
+ };
860
+ capture("active", "M11.9125 21.4125");
861
+ capture("inactive", "M7.38 16.2207");
862
+ }
776
863
  const ready = Boolean(
777
864
  modelSelectorNativeFastIcons?.active && modelSelectorNativeFastIcons?.inactive,
778
865
  );
@@ -785,14 +872,18 @@ export function createInjectionSource(features, {
785
872
 
786
873
  function applyNativeFastIconsToShells() {
787
874
  document.querySelectorAll("[data-codex-model-slider-fast]").forEach((button) => {
788
- const icon = createNativeFastIcon(button.getAttribute("aria-pressed") === "true");
789
875
  const content = button.querySelector("[data-codex-model-slider-fast-content]");
790
- if (icon && content) content.replaceChildren(icon);
876
+ // 幂等补装:只给还没有图标的按钮补上,避免每次 ensure 都重建节点。
877
+ if (!content || content.querySelector("svg")) return;
878
+ const icon = createNativeFastIcon(button.getAttribute("aria-pressed") === "true");
879
+ if (icon) content.replaceChildren(icon);
791
880
  });
792
881
  }
793
882
 
794
883
  function createNativeFastIcon(active) {
795
- const template = captureNativeFastIcon()?.[active ? "active" : "inactive"];
884
+ // 直接读取已就绪的图标存储(启动时由 bundle/持久化填充,运行期由
885
+ // captureNativeFastIcon 升级为 DOM 克隆),避免再次触发捕获造成递归。
886
+ const template = modelSelectorNativeFastIcons?.[active ? "active" : "inactive"];
796
887
  if (!template) return null;
797
888
  const icon = template.cloneNode(true);
798
889
  icon.setAttribute("aria-hidden", "true");
@@ -895,14 +986,16 @@ export function createInjectionSource(features, {
895
986
  (option) => String(option.thinkingEffort ?? "") === String(selected.thinkingEffort ?? ""),
896
987
  );
897
988
  const selectedEffort = selectedOption ? effortFor(selectedOption) : (efforts[0] ?? "instant");
989
+ const defaultEffort = String(version.defaultReasoningEffort ?? efforts[0] ?? "instant").toLowerCase();
898
990
  const models = [{
899
991
  model: String(selected.slug ?? ""),
900
992
  displayName: String(version?.label ?? selected.slug ?? ""),
901
- defaultReasoningEffort: selectedEffort,
993
+ defaultReasoningEffort: defaultEffort,
902
994
  supportedReasoningEfforts: efforts.map((effort) => ({ reasoningEffort: effort })),
903
995
  }];
904
996
  return {
905
997
  models,
998
+ reasoningEffort: selectedEffort,
906
999
  onSelectReasoningEffort(effort) {
907
1000
  const target = options.find((option) => effortFor(option) === String(effort).toLowerCase());
908
1001
  if (!target) return;
@@ -914,8 +1007,9 @@ export function createInjectionSource(features, {
914
1007
  };
915
1008
  }
916
1009
 
917
- function nativeCatalogModelFor(identity) {
918
- const models = nativeComposerModelController()?.models ?? [];
1010
+ function nativeCatalogModelFor(identity, selector = modelSelectorFeature?.modelSelector) {
1011
+ const controller = nativeComposerModelController();
1012
+ const models = controller?.models ?? [];
919
1013
  const exact = models.find((model) => String(model.model ?? "").toLowerCase() === identity.raw.toLowerCase());
920
1014
  const target = comparableModelLabel(identity.backend);
921
1015
  const aliases = exact ? [] : models.filter((model) => (
@@ -923,13 +1017,25 @@ export function createInjectionSource(features, {
923
1017
  ));
924
1018
  const model = exact ?? (aliases.length === 1 ? aliases[0] : null);
925
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();
926
1028
  return {
927
1029
  slug: String(model.model ?? ""),
928
1030
  displayName: String(model.displayName ?? model.model ?? ""),
929
- defaultReasoningLevel: String(model.defaultReasoningEffort ?? ""),
930
- supportedReasoningLevels: (model.supportedReasoningEfforts ?? []).map((level) => ({
931
- effort: String(level?.reasoningEffort ?? level?.effort ?? level ?? ""),
932
- })).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,
933
1039
  };
934
1040
  }
935
1041
 
@@ -1362,18 +1468,19 @@ export function createInjectionSource(features, {
1362
1468
  }
1363
1469
 
1364
1470
  function enhanceModelItem(item, menu, selector, index) {
1365
- if (item.querySelector("[data-codex-model-slider-row]")) return;
1366
1471
  const observedRaw = modelItemRawValue(item);
1367
1472
  if (!observedRaw) return;
1368
- if (!modelSelectorOriginalItems.has(item)) modelSelectorOriginalItems.set(item, item.innerHTML);
1369
1473
  const observedIdentity = modelIdentity(observedRaw);
1370
1474
  const nativeDescriptor = nativeModelDescriptorForItem(item, observedRaw);
1371
1475
  const catalogModel = catalogModelFor(observedIdentity, selector);
1372
1476
  const raw = catalogModel?.slug ?? nativeDescriptor?.slug ?? observedRaw;
1477
+ // 先刷新 dataset:React 原地复用菜单节点时,raw/排序/收藏键不能基于过期值。
1373
1478
  item.dataset.codexModelSliderRaw = raw;
1374
1479
  item.dataset.codexModelSliderItem = "";
1375
1480
  item.dataset.codexModelSliderOrder = String(index);
1376
1481
  item.dataset.codexModelSliderIdentitySource = catalogModel ? "catalog" : nativeDescriptor ? "native" : "visible";
1482
+ if (item.querySelector("[data-codex-model-slider-row]")) return;
1483
+ if (!modelSelectorOriginalItems.has(item)) modelSelectorOriginalItems.set(item, item.innerHTML);
1377
1484
 
1378
1485
  const identity = modelIdentity(raw);
1379
1486
  const display = modelDisplayLabel(identity, selector);
@@ -1574,6 +1681,9 @@ export function createInjectionSource(features, {
1574
1681
  }
1575
1682
 
1576
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;
1577
1687
  const controller = nativeComposerModelController();
1578
1688
  if (typeof controller?.onSelectReasoningEffort === "function") {
1579
1689
  controller.onSelectReasoningEffort(effort);
@@ -1649,6 +1759,24 @@ export function createInjectionSource(features, {
1649
1759
  rail.setAttribute("aria-valuenow", String(index));
1650
1760
  }
1651
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
+
1652
1780
  function nativeParticleSeed(index, salt) {
1653
1781
  const value = Math.sin((index + 1) * 12.9898 + salt * 78.233) * 43758.5453;
1654
1782
  return value - Math.floor(value);
@@ -1775,7 +1903,8 @@ export function createInjectionSource(features, {
1775
1903
 
1776
1904
  function createGenericModelSlider(parentMenu, selector, identity, model, selectedEffort) {
1777
1905
  const efforts = model.supportedReasoningLevels.map((level) => level.effort);
1778
- const selectedIndex = Math.max(0, efforts.indexOf(selectedEffort));
1906
+ const selectedIndex = efforts.indexOf(selectedEffort);
1907
+ const selectedEffortVisible = selectedIndex >= 0;
1779
1908
  const classes = Object.fromEntries([
1780
1909
  "Menu",
1781
1910
  "ViewToggle",
@@ -1873,7 +2002,20 @@ export function createInjectionSource(features, {
1873
2002
  rail.setAttribute("aria-label", locale === "zh-CN" ? "推理强度" : "Reasoning effort");
1874
2003
  rail.setAttribute("aria-valuemin", "0");
1875
2004
  rail.setAttribute("aria-valuemax", String(efforts.length - 1));
1876
- 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
+ }
1877
2019
  if (!nativeSlider) {
1878
2020
  const track = document.createElement("div");
1879
2021
  track.setAttribute("data-codex-model-slider-track", "");
@@ -1969,7 +2111,7 @@ export function createInjectionSource(features, {
1969
2111
  // 控制器推导的任务内实际 effort 优先,避免子任务/主任务切换后触发器属性滞后;
1970
2112
  // 触发器属性仅在控制器无法解析时作为实时回退,静态目录默认值最后兜底。
1971
2113
  const nativeModel = nativeCatalogModelFor(identity);
1972
- const selectedEffort = nativeModel?.defaultReasoningLevel
2114
+ const selectedEffort = nativeModel?.currentReasoningLevel
1973
2115
  ?? document.querySelector("[data-codex-intelligence-trigger]")
1974
2116
  ?.getAttribute("data-selected-reasoning-effort")
1975
2117
  ?? model.defaultReasoningLevel
@@ -2077,7 +2219,6 @@ export function createInjectionSource(features, {
2077
2219
  function ensureModelSelector() {
2078
2220
  const selector = modelSelectorFeature?.modelSelector;
2079
2221
  if (!selector) return;
2080
- captureNativeFastIcon();
2081
2222
  pruneModelSubmenuPositionObservers();
2082
2223
  if (!modelSelectorStyle.isConnected) document.head?.append(modelSelectorStyle);
2083
2224
  enhanceComposerModelTrigger(selector);
@@ -2089,11 +2230,17 @@ export function createInjectionSource(features, {
2089
2230
  returnToNativeModelSlider();
2090
2231
  }, 80);
2091
2232
  }
2092
- return;
2233
+ } else {
2234
+ enhanceNativeModelPicker(parentMenu, selector);
2235
+ const submenu = nativeModelSubmenu(parentMenu);
2236
+ if (submenu) enhanceNativeModelSubmenu(submenu, selector);
2237
+ }
2238
+ try {
2239
+ // 图标工作与菜单增强解耦:放在最后并隔离异常,任何失败都不影响标签与菜单。
2240
+ captureNativeFastIcon();
2241
+ } catch (error) {
2242
+ console.warn("[codex-personal] fast icon capture failed", error);
2093
2243
  }
2094
- enhanceNativeModelPicker(parentMenu, selector);
2095
- const submenu = nativeModelSubmenu(parentMenu);
2096
- if (submenu) enhanceNativeModelSubmenu(submenu, selector);
2097
2244
  }
2098
2245
 
2099
2246
  function restoreModelSelectorEnhancements() {
@@ -2335,9 +2482,11 @@ export function createInjectionSource(features, {
2335
2482
  }
2336
2483
  if (next === currentThread) return;
2337
2484
  currentThread = next;
2485
+ stopToolbarReadiness();
2338
2486
  savedComposerRange = null;
2339
2487
  savedComposerThread = null;
2340
2488
  hidePinnedSurfaces();
2489
+ queueEnsure();
2341
2490
  }
2342
2491
 
2343
2492
  function currentComposer() {
@@ -2399,16 +2548,23 @@ export function createInjectionSource(features, {
2399
2548
  ]);
2400
2549
  for (const element of selected) {
2401
2550
  if (element.hasAttribute(entryMarker) || alreadySuppressed.has(element)) continue;
2402
- suppressedSelections.push({
2551
+ const selection = {
2403
2552
  element,
2404
2553
  ariaCurrent: element.getAttribute("aria-current"),
2405
2554
  threadSelected: element.getAttribute("data-app-action-sidebar-thread-selected"),
2555
+ hadPrimaryGhost: element.classList.contains("bg-primary-ghost-hover"),
2556
+ hadHoverPrimaryGhost: element.classList.contains("hover:bg-primary-ghost-hover"),
2406
2557
  hadActiveClass: element.classList.contains("bg-token-list-hover-background"),
2407
2558
  hadHoverClass: element.classList.contains("hover:bg-token-list-hover-background"),
2408
- });
2559
+ };
2560
+ suppressedSelections.push(selection);
2409
2561
  element.removeAttribute("aria-current");
2410
2562
  element.removeAttribute("data-app-action-sidebar-thread-selected");
2563
+ // 原生选中态(灰底)现在由 bg-primary-ghost-hover 承载,需一并移除;
2564
+ // 旧 token 类仍保留处理,以便兼容旧主题。
2565
+ element.classList.remove("bg-primary-ghost-hover");
2411
2566
  element.classList.remove("bg-token-list-hover-background");
2567
+ if (!selection.hadHoverPrimaryGhost) element.classList.add("hover:bg-primary-ghost-hover");
2412
2568
  element.classList.add("hover:bg-token-list-hover-background");
2413
2569
  }
2414
2570
  }
@@ -2421,6 +2577,8 @@ export function createInjectionSource(features, {
2421
2577
  if (selection.threadSelected !== null) {
2422
2578
  element.setAttribute("data-app-action-sidebar-thread-selected", selection.threadSelected);
2423
2579
  }
2580
+ element.classList.toggle("bg-primary-ghost-hover", Boolean(selection.hadPrimaryGhost));
2581
+ element.classList.toggle("hover:bg-primary-ghost-hover", Boolean(selection.hadHoverPrimaryGhost));
2424
2582
  element.classList.toggle("bg-token-list-hover-background", selection.hadActiveClass);
2425
2583
  element.classList.toggle("hover:bg-token-list-hover-background", selection.hadHoverClass);
2426
2584
  }
@@ -2432,6 +2590,7 @@ export function createInjectionSource(features, {
2432
2590
  const entry = findEntry(feature.id);
2433
2591
  if (!entry) continue;
2434
2592
  const selected = feature.id === activePageFeatureId;
2593
+ entry.classList.toggle("bg-primary-ghost-hover", selected);
2435
2594
  entry.classList.toggle("bg-token-list-hover-background", selected);
2436
2595
  entry.classList.toggle("hover:bg-token-list-hover-background", !selected);
2437
2596
  if (selected) entry.setAttribute("aria-current", "page");
@@ -2528,24 +2687,23 @@ export function createInjectionSource(features, {
2528
2687
  }
2529
2688
  }
2530
2689
 
2531
- function threadScrollContent() {
2532
- const scrollContainer = mainContentViewport()?.querySelector(".thread-scroll-container");
2533
- return scrollContainer?.firstElementChild instanceof HTMLElement
2534
- ? scrollContainer.firstElementChild
2535
- : null;
2536
- }
2537
-
2538
2690
  function restoreThreadContentShift() {
2539
- if (!shiftedThreadContent || !shiftedThreadOriginal) return;
2540
- const { element, position, left, transition } = shiftedThreadOriginal;
2541
- if (element.isConnected) {
2542
- element.style.setProperty("position", position.value, position.priority);
2543
- element.style.setProperty("left", left.value, left.priority);
2544
- 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");
2545
2704
  element.removeAttribute("data-codex-personal-summary-shift");
2546
2705
  }
2547
- shiftedThreadContent = null;
2548
- shiftedThreadOriginal = null;
2706
+ shiftedConversationOwners = [];
2549
2707
  }
2550
2708
 
2551
2709
  function applyThreadContentShift(value) {
@@ -2553,45 +2711,40 @@ export function createInjectionSource(features, {
2553
2711
  restoreThreadContentShift();
2554
2712
  return;
2555
2713
  }
2556
- if (!threadShiftStyle.isConnected) {
2557
- (document.head ?? document.documentElement)?.append(threadShiftStyle);
2558
- }
2559
- const content = threadScrollContent();
2560
- 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) {
2561
2719
  restoreThreadContentShift();
2562
2720
  return;
2563
2721
  }
2564
- if (shiftedThreadContent !== content) {
2722
+ if (
2723
+ shiftedConversationOwners.length !== owners.length
2724
+ || owners.some((owner, index) => shiftedConversationOwners[index]?.element !== owner)
2725
+ ) {
2565
2726
  restoreThreadContentShift();
2566
- shiftedThreadContent = content;
2567
- shiftedThreadOriginal = {
2568
- element: content,
2569
- position: {
2570
- value: content.style.getPropertyValue("position"),
2571
- priority: content.style.getPropertyPriority("position"),
2572
- },
2573
- left: {
2574
- value: content.style.getPropertyValue("left"),
2575
- priority: content.style.getPropertyPriority("left"),
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"),
2576
2732
  },
2577
- transition: {
2578
- value: content.style.getPropertyValue("transition"),
2579
- priority: content.style.getPropertyPriority("transition"),
2733
+ translate: {
2734
+ value: owner.style.getPropertyValue("translate"),
2735
+ priority: owner.style.getPropertyPriority("translate"),
2580
2736
  },
2581
- };
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));
2582
2747
  }
2583
- const duration = reducedMotion() ? 0 : 300;
2584
- content.style.setProperty("transition", duration
2585
- ? "left 300ms cubic-bezier(.2,.8,.2,1)"
2586
- : "none", "important");
2587
- content.style.setProperty("position", "relative", "important");
2588
- content.style.setProperty("left", `${value}px`, "important");
2589
- content.setAttribute("data-codex-personal-summary-shift", String(value));
2590
- if (duration) window.setTimeout(() => {
2591
- if (content.getAttribute("data-codex-personal-summary-shift") !== String(value)) return;
2592
- content.style.setProperty("transition", "none", "important");
2593
- content.style.setProperty("left", `${value}px`, "important");
2594
- }, duration + 50);
2595
2748
  }
2596
2749
 
2597
2750
  function setSummarySurfaceVisible(surface, visible, mode) {
@@ -2696,7 +2849,7 @@ export function createInjectionSource(features, {
2696
2849
  surface.setAttribute("data-codex-personal-summary-presentation", displayMode);
2697
2850
  setSummarySurfaceVisible(surface, true, displayMode);
2698
2851
 
2699
- const shift = displayMode === "shift"
2852
+ const shift = displayMode !== "overlay"
2700
2853
  ? -(layout.panelWidth + layout.panelInset) / 2
2701
2854
  : 0;
2702
2855
  applyThreadContentShift(shift);
@@ -3341,8 +3494,12 @@ export function createInjectionSource(features, {
3341
3494
  const detail = feature?.detailTabs?.find((candidate) => candidate.id === detailId);
3342
3495
  if (!feature || !detail) throw new Error("The requested detail Tab is not registered");
3343
3496
  const result = await openNativeDetailTab(feature, detail);
3344
- if (!result) throw nativeTabCapabilityError
3345
- ?? 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
+ }
3346
3503
  return result;
3347
3504
  }
3348
3505
 
@@ -3457,13 +3614,20 @@ export function createInjectionSource(features, {
3457
3614
  }
3458
3615
 
3459
3616
  function ensureToolbarEntries() {
3460
- if (!toolbarFeatures.length) return;
3617
+ if (!toolbarFeatures.length) {
3618
+ stopToolbarReadiness();
3619
+ return;
3620
+ }
3461
3621
  const temporaryChatButton = nativeTemporaryChatButton();
3462
3622
  const summaryButton = nativeSummaryButton();
3463
3623
  const bottomPanelButton = nativeBottomPanelButton();
3464
3624
  const sidePanelButton = nativeSidePanelButton();
3465
3625
  const anchorButton = temporaryChatButton ?? summaryButton ?? bottomPanelButton ?? sidePanelButton;
3466
- if (!anchorButton) return;
3626
+ if (!anchorButton) {
3627
+ const candidate = structuralToolbarAnchor();
3628
+ if (candidate) ensureToolbarReadiness(candidate);
3629
+ return;
3630
+ }
3467
3631
  const anchorRoot = toolbarControlRoot(anchorButton);
3468
3632
  // The bottom/side-panel toggles sit in narrow fixed containers that
3469
3633
  // travel with the native panels. Entries anchored to them must live in
@@ -3474,7 +3638,10 @@ export function createInjectionSource(features, {
3474
3638
  const useFallbackAnchor = anchorButton === sidePanelButton || anchorButton === bottomPanelButton;
3475
3639
  const summaryGroup = useFallbackAnchor ? null : nativeSummaryToolbarGroup(anchorButton);
3476
3640
  let targetGroup = summaryGroup ?? ensureFallbackSummaryToolbarGroup(anchorButton);
3477
- if (!targetGroup) return;
3641
+ if (!targetGroup) {
3642
+ ensureToolbarReadiness(anchorButton);
3643
+ return;
3644
+ }
3478
3645
  let cursor = useFallbackAnchor ? null : anchorRoot;
3479
3646
  for (const feature of toolbarFeatures) {
3480
3647
  const entries = Array.from(document.querySelectorAll(`[${entryMarker}="${feature.id}"]`));
@@ -3510,6 +3677,11 @@ export function createInjectionSource(features, {
3510
3677
  if (!used) group.remove();
3511
3678
  });
3512
3679
  }
3680
+ if (toolbarFeatures.every((feature) => findEntry(feature.id)?.isConnected)) {
3681
+ stopToolbarReadiness();
3682
+ } else {
3683
+ ensureToolbarReadiness(anchorButton);
3684
+ }
3513
3685
  }
3514
3686
 
3515
3687
  function ensureEntries() {
@@ -3568,6 +3740,13 @@ export function createInjectionSource(features, {
3568
3740
  }, record.origin);
3569
3741
  }
3570
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
+
3571
3750
  async function handleSurfaceHostAction(record, message) {
3572
3751
  const feature = featureById.get(record.featureId);
3573
3752
  const { requestId, action, payload = {} } = message;
@@ -3631,7 +3810,7 @@ export function createInjectionSource(features, {
3631
3810
  } catch (error) {
3632
3811
  respondToSurface(record, requestId, {
3633
3812
  ok: false,
3634
- error: { code: "HOST_ACTION_FAILED", message: error.message },
3813
+ error: hostActionFailure(error),
3635
3814
  });
3636
3815
  }
3637
3816
  }
@@ -3799,9 +3978,9 @@ export function createInjectionSource(features, {
3799
3978
  if (ensureFrame != null) cancelAnimationFrame(ensureFrame);
3800
3979
  if (ensureTimer != null) clearTimeout(ensureTimer);
3801
3980
  for (const observer of domObservers) observer.disconnect();
3981
+ stopToolbarReadiness();
3802
3982
  mainContentObserver?.observer.disconnect();
3803
3983
  restoreThreadContentShift();
3804
- threadShiftStyle.remove();
3805
3984
  for (const root of document.querySelectorAll("[data-codex-personal-toolbar-entry]")) root.remove();
3806
3985
  for (const group of document.querySelectorAll("[data-codex-personal-summary-toolbar-group]")) group.remove();
3807
3986
  for (const surface of pageSurfaces.values()) surface.remove();
@@ -3832,7 +4011,15 @@ export function createInjectionSource(features, {
3832
4011
  detailTabs: feature.detailTabs ?? [],
3833
4012
  hostActions: feature.hostActions ?? [],
3834
4013
  modelSelector: feature.modelSelector ?? null,
3835
- 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,
3836
4023
  }));
3837
4024
  return `(${install.toString()})(${JSON.stringify({
3838
4025
  version: RUNTIME_VERSION,
@@ -3854,7 +4041,9 @@ export function createRuntimeReadyContract({
3854
4041
  runtimeSessionId,
3855
4042
  }) {
3856
4043
  const discovered = Boolean(renderer.discovered);
4044
+ const discoveredTargets = Math.max(0, Number(renderer.discoveredTargets) || 0);
3857
4045
  const injectedTargets = Math.max(0, Number(renderer.injectedTargets) || 0);
4046
+ const targetFailures = Array.isArray(renderer.targetFailures) ? renderer.targetFailures : [];
3858
4047
  const active = Boolean(renderer.active) && discovered && injectedTargets > 0;
3859
4048
  return {
3860
4049
  pid,
@@ -3872,8 +4061,11 @@ export function createRuntimeReadyContract({
3872
4061
  })),
3873
4062
  renderer: {
3874
4063
  discovered,
4064
+ discoveredTargets,
3875
4065
  injectedTargets,
3876
4066
  active,
4067
+ healthy: active && targetFailures.length === 0 && injectedTargets === discoveredTargets,
4068
+ targetFailures,
3877
4069
  runtimeSessionId: renderer.runtimeSessionId ?? runtimeSessionId,
3878
4070
  },
3879
4071
  };
@@ -4074,6 +4266,8 @@ export class CodexRuntime {
4074
4266
  this.onStatusChange = onStatusChange;
4075
4267
  this.onManagedCodexPidChange = onManagedCodexPidChange;
4076
4268
  this.injectedTargets = new Set();
4269
+ this.rendererTargetIds = new Set();
4270
+ this.targetFailures = new Map();
4077
4271
  this.clients = new Map();
4078
4272
  this.scriptIds = new Map();
4079
4273
  this.bindingTokens = new Map();
@@ -4127,10 +4321,21 @@ export class CodexRuntime {
4127
4321
 
4128
4322
  getRendererStatus() {
4129
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;
4130
4332
  return {
4131
4333
  discovered: this.rendererDiscovered,
4334
+ discoveredTargets,
4132
4335
  injectedTargets,
4133
- active: !this.stopping && this.rendererDiscovered && injectedTargets > 0,
4336
+ active,
4337
+ healthy: active && targetFailures.length === 0 && injectedTargets === discoveredTargets,
4338
+ targetFailures,
4134
4339
  runtimeSessionId: this.runtimeSessionId,
4135
4340
  };
4136
4341
  }
@@ -4141,8 +4346,11 @@ export class CodexRuntime {
4141
4346
  if (
4142
4347
  previous
4143
4348
  && previous.discovered === status.discovered
4349
+ && previous.discoveredTargets === status.discoveredTargets
4144
4350
  && previous.injectedTargets === status.injectedTargets
4145
4351
  && previous.active === status.active
4352
+ && previous.healthy === status.healthy
4353
+ && JSON.stringify(previous.targetFailures) === JSON.stringify(status.targetFailures)
4146
4354
  && previous.runtimeSessionId === status.runtimeSessionId
4147
4355
  ) return;
4148
4356
  this.lastRendererStatus = status;
@@ -4186,6 +4394,8 @@ export class CodexRuntime {
4186
4394
  this.bindingTokens.clear();
4187
4395
  this.hostActionChains.clear();
4188
4396
  this.injectedTargets.clear();
4397
+ this.rendererTargetIds.clear();
4398
+ this.targetFailures.clear();
4189
4399
  this.rendererDiscovered = false;
4190
4400
  this.browserClient?.socket.close();
4191
4401
  this.browserClient = null;
@@ -4549,6 +4759,10 @@ export class CodexRuntime {
4549
4759
  });
4550
4760
  return;
4551
4761
  }
4762
+ if (request.action === "import-generated-image") {
4763
+ await this.importGeneratedImage(feature, request.payload);
4764
+ return;
4765
+ }
4552
4766
  try {
4553
4767
  if (request.action !== "attach-file") throw new Error("Unsupported native Host action");
4554
4768
  const result = await this.attachFileToComposer(client, request.payload?.path, request.requestId);
@@ -4561,6 +4775,22 @@ export class CodexRuntime {
4561
4775
  }
4562
4776
  }
4563
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
+
4564
4794
  async refreshTargets() {
4565
4795
  let response;
4566
4796
  try {
@@ -4587,8 +4817,14 @@ export class CodexRuntime {
4587
4817
  .filter((target) => target.type === "page" && target.url === "app://-/index.html")
4588
4818
  .map((target) => target.id),
4589
4819
  );
4820
+ this.rendererTargetIds = liveRendererIds;
4590
4821
  let removedTarget = false;
4591
- 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) {
4592
4828
  if (liveRendererIds.has(id)) continue;
4593
4829
  removedTarget = true;
4594
4830
  this.clients.get(id)?.socket.close();
@@ -4597,6 +4833,7 @@ export class CodexRuntime {
4597
4833
  this.bindingTokens.delete(id);
4598
4834
  this.hostActionChains.delete(id);
4599
4835
  this.injectedTargets.delete(id);
4836
+ this.targetFailures.delete(id);
4600
4837
  }
4601
4838
  const rendererDiscovered = liveRendererIds.size > 0;
4602
4839
  const rendererChanged = rendererDiscovered !== this.rendererDiscovered;
@@ -4607,8 +4844,12 @@ export class CodexRuntime {
4607
4844
  if (target.type !== "page" || target.url !== "app://-/index.html" || this.injectedTargets.has(target.id)) {
4608
4845
  continue;
4609
4846
  }
4610
- 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;
4611
4851
  try {
4852
+ client = await this.connectClient(target.webSocketDebuggerUrl);
4612
4853
  await Promise.all([
4613
4854
  client.send("Page.enable"),
4614
4855
  client.send("Runtime.enable"),
@@ -4630,6 +4871,7 @@ export class CodexRuntime {
4630
4871
  const script = await client.send("Page.addScriptToEvaluateOnNewDocument", {
4631
4872
  source: createDocumentBootstrapSource(source),
4632
4873
  });
4874
+ scriptIdentifier = script.identifier;
4633
4875
  await waitForExpression(
4634
4876
  client,
4635
4877
  `document.readyState === "interactive" || document.readyState === "complete"`,
@@ -4642,15 +4884,33 @@ export class CodexRuntime {
4642
4884
  + ` && window.__codexPersonalRuntime?.sessionId === ${JSON.stringify(this.runtimeSessionId)}`,
4643
4885
  );
4644
4886
  this.clients.set(target.id, client);
4645
- this.scriptIds.set(target.id, script.identifier);
4887
+ this.scriptIds.set(target.id, scriptIdentifier);
4646
4888
  this.injectedTargets.add(target.id);
4889
+ this.targetFailures.delete(target.id);
4647
4890
  this.notifyStatusChange();
4648
4891
  } catch (error) {
4649
4892
  this.bindingTokens.delete(target.id);
4650
- client.socket.close();
4651
- 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();
4652
4911
  }
4653
4912
  }
4913
+ this.notifyStatusChange();
4654
4914
  }
4655
4915
 
4656
4916
  async stop() {
@@ -4680,6 +4940,8 @@ export class CodexRuntime {
4680
4940
  this.bindingTokens.clear();
4681
4941
  this.hostActionChains.clear();
4682
4942
  this.injectedTargets.clear();
4943
+ this.rendererTargetIds.clear();
4944
+ this.targetFailures.clear();
4683
4945
  this.rendererDiscovered = false;
4684
4946
  this.notifyStatusChange();
4685
4947
  this.monitorPromise = null;