minecodex 0.1.20 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
 
@@ -1003,6 +1093,19 @@ export function createInjectionSource(features, {
1003
1093
  return formatIdentifier(identity.backend).toLowerCase() === nativeLabel ? native : thirdParty;
1004
1094
  }
1005
1095
 
1096
+ function visibleReasoningLevelsFor(model) {
1097
+ if (!model) return [];
1098
+ const capability = model.modelSelectorCapability;
1099
+ if (capability && Array.isArray(capability.levels)) {
1100
+ const allowed = new Set(capability.levels);
1101
+ return (model.supportedReasoningLevels ?? []).filter((level) => {
1102
+ const effort = String(level?.effort ?? level ?? "").toLowerCase();
1103
+ return allowed.has(effort);
1104
+ });
1105
+ }
1106
+ return model.supportedReasoningLevels ?? [];
1107
+ }
1108
+
1006
1109
  function loadModelFavorites(selector) {
1007
1110
  try {
1008
1111
  const value = JSON.parse(localStorage.getItem(selector.favoriteStorageKey) ?? "[]");
@@ -1591,6 +1694,9 @@ export function createInjectionSource(features, {
1591
1694
  }
1592
1695
 
1593
1696
  function selectNativeEffort(parentMenu, effort) {
1697
+ const identity = modelIdentity(nativeModelValue(parentMenu));
1698
+ const model = modelDefinitionFor(identity, modelSelectorFeature?.modelSelector);
1699
+ if (!model?.supportedReasoningLevels?.some((level) => level.effort === String(effort).toLowerCase())) return;
1594
1700
  const controller = nativeComposerModelController();
1595
1701
  if (typeof controller?.onSelectReasoningEffort === "function") {
1596
1702
  controller.onSelectReasoningEffort(effort);
@@ -1666,6 +1772,24 @@ export function createInjectionSource(features, {
1666
1772
  rail.setAttribute("aria-valuenow", String(index));
1667
1773
  }
1668
1774
 
1775
+ function neutralizeStaleSliderVisual(rail) {
1776
+ for (const selector of [
1777
+ "[data-codex-model-slider-native-range]",
1778
+ "[data-codex-model-slider-native-thumb]",
1779
+ "[data-codex-model-slider-range]",
1780
+ "[data-codex-model-slider-thumb]",
1781
+ ]) {
1782
+ rail.querySelectorAll(selector).forEach((element) => {
1783
+ element.style.setProperty("visibility", "hidden");
1784
+ });
1785
+ }
1786
+ rail.querySelectorAll("[data-codex-model-slider-tick]").forEach((tick) => {
1787
+ tick.removeAttribute("data-selected");
1788
+ });
1789
+ rail.removeAttribute("aria-valuenow");
1790
+ rail.style.removeProperty("--codex-model-slider-progress");
1791
+ }
1792
+
1669
1793
  function nativeParticleSeed(index, salt) {
1670
1794
  const value = Math.sin((index + 1) * 12.9898 + salt * 78.233) * 43758.5453;
1671
1795
  return value - Math.floor(value);
@@ -1792,7 +1916,8 @@ export function createInjectionSource(features, {
1792
1916
 
1793
1917
  function createGenericModelSlider(parentMenu, selector, identity, model, selectedEffort) {
1794
1918
  const efforts = model.supportedReasoningLevels.map((level) => level.effort);
1795
- const selectedIndex = Math.max(0, efforts.indexOf(selectedEffort));
1919
+ const selectedIndex = efforts.indexOf(selectedEffort);
1920
+ const selectedEffortVisible = selectedIndex >= 0;
1796
1921
  const classes = Object.fromEntries([
1797
1922
  "Menu",
1798
1923
  "ViewToggle",
@@ -1890,7 +2015,20 @@ export function createInjectionSource(features, {
1890
2015
  rail.setAttribute("aria-label", locale === "zh-CN" ? "推理强度" : "Reasoning effort");
1891
2016
  rail.setAttribute("aria-valuemin", "0");
1892
2017
  rail.setAttribute("aria-valuemax", String(efforts.length - 1));
1893
- rail.setAttribute("aria-valuetext", effortLabel(efforts[selectedIndex]));
2018
+ rail.setAttribute("aria-valuetext", selectedEffortVisible
2019
+ ? effortLabel(selectedEffort)
2020
+ : (locale === "zh-CN" ? "当前档位不可用" : "Current effort unavailable"));
2021
+ rail.toggleAttribute("data-stale-reasoning-effort", !selectedEffortVisible);
2022
+ if (!selectedEffortVisible) {
2023
+ rail.setAttribute("aria-disabled", "true");
2024
+ rail.removeAttribute("tabindex");
2025
+ neutralizeStaleSliderVisual(rail);
2026
+ const staleView = document.createElement("div");
2027
+ staleView.className = classes.SimpleView;
2028
+ staleView.append(nativeSlider ?? rail);
2029
+ shell.append(viewControls, staleView);
2030
+ return shell;
2031
+ }
1894
2032
  if (!nativeSlider) {
1895
2033
  const track = document.createElement("div");
1896
2034
  track.setAttribute("data-codex-model-slider-track", "");
@@ -1981,19 +2119,35 @@ export function createInjectionSource(features, {
1981
2119
  function enhanceGenericModelPicker(parentMenu, selector) {
1982
2120
  const identity = modelIdentity(nativeModelValue(parentMenu));
1983
2121
  const model = modelDefinitionFor(identity, selector);
1984
- const efforts = model?.supportedReasoningLevels ?? [];
2122
+ const catalogModel = catalogModelFor(identity, selector);
2123
+ const modelWithCapability = model?.modelSelectorCapability ? model : {
2124
+ ...(model ?? {}),
2125
+ ...(catalogModel ?? {}),
2126
+ supportedReasoningLevels: model?.supportedReasoningLevels ?? catalogModel?.supportedReasoningLevels ?? [],
2127
+ };
2128
+ const efforts = visibleReasoningLevelsFor(modelWithCapability);
1985
2129
  if (!model || efforts.length < 2) return false;
1986
2130
  // 控制器推导的任务内实际 effort 优先,避免子任务/主任务切换后触发器属性滞后;
1987
2131
  // 触发器属性仅在控制器无法解析时作为实时回退,静态目录默认值最后兜底。
1988
2132
  const nativeModel = nativeCatalogModelFor(identity);
1989
- const selectedEffort = nativeModel?.defaultReasoningLevel
2133
+ const selectedEffort = nativeModel?.currentReasoningLevel
1990
2134
  ?? document.querySelector("[data-codex-intelligence-trigger]")
1991
2135
  ?.getAttribute("data-selected-reasoning-effort")
1992
2136
  ?? model.defaultReasoningLevel
1993
2137
  ?? efforts[0].effort;
2138
+ const effortRank = (value) => {
2139
+ const order = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
2140
+ const index = order.indexOf(String(value ?? "").toLowerCase());
2141
+ return index < 0 ? -1 : index;
2142
+ };
2143
+ const visibleEffort = efforts.find((level) => level.effort === selectedEffort)?.effort
2144
+ ?? efforts.filter((level) => effortRank(level.effort) <= effortRank(selectedEffort))
2145
+ .sort((left, right) => effortRank(right.effort) - effortRank(left.effort))[0]?.effort
2146
+ ?? efforts[0].effort;
2147
+ const selectedEffortValue = visibleEffort;
1994
2148
  const selectedServiceTier = nativeComposerModelController()?.selectedServiceTier;
1995
2149
  const serviceTierKey = selectedServiceTier?.id ?? selectedServiceTier ?? "standard";
1996
- const key = `${identity.raw}:${selectedEffort}:${serviceTierKey}:${efforts.map((level) => level.effort).join(",")}`;
2150
+ const key = `${identity.raw}:${selectedEffortValue}:${serviceTierKey}:${efforts.map((level) => level.effort).join(",")}`;
1997
2151
  const existing = parentMenu.querySelector(":scope > [data-codex-model-slider-generic]");
1998
2152
  const nativeTemplateReady = Boolean(
1999
2153
  parentMenu.firstElementChild
@@ -2015,7 +2169,7 @@ export function createInjectionSource(features, {
2015
2169
  existing?.remove();
2016
2170
  const nativeContainer = parentMenu.firstElementChild;
2017
2171
  if (!nativeContainer) return false;
2018
- const shell = createGenericModelSlider(parentMenu, selector, identity, model, selectedEffort);
2172
+ const shell = createGenericModelSlider(parentMenu, selector, identity, model, selectedEffortValue);
2019
2173
  if (!shell) return false;
2020
2174
  if (!modelSelectorGenericContainers.has(nativeContainer)) {
2021
2175
  modelSelectorGenericContainers.set(nativeContainer, nativeContainer.style.display);
@@ -2357,9 +2511,11 @@ export function createInjectionSource(features, {
2357
2511
  }
2358
2512
  if (next === currentThread) return;
2359
2513
  currentThread = next;
2514
+ stopToolbarReadiness();
2360
2515
  savedComposerRange = null;
2361
2516
  savedComposerThread = null;
2362
2517
  hidePinnedSurfaces();
2518
+ queueEnsure();
2363
2519
  }
2364
2520
 
2365
2521
  function currentComposer() {
@@ -2560,24 +2716,23 @@ export function createInjectionSource(features, {
2560
2716
  }
2561
2717
  }
2562
2718
 
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
2719
  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);
2720
+ for (const { element, shift, translate } of shiftedConversationOwners) {
2721
+ if (!element.isConnected) continue;
2722
+ if (shift.value) {
2723
+ element.style.setProperty(
2724
+ "--thread-wide-block-inline-shift",
2725
+ shift.value,
2726
+ shift.priority,
2727
+ );
2728
+ } else {
2729
+ element.style.removeProperty("--thread-wide-block-inline-shift");
2730
+ }
2731
+ if (translate.value) element.style.setProperty("translate", translate.value, translate.priority);
2732
+ else element.style.removeProperty("translate");
2577
2733
  element.removeAttribute("data-codex-personal-summary-shift");
2578
2734
  }
2579
- shiftedThreadContent = null;
2580
- shiftedThreadOriginal = null;
2735
+ shiftedConversationOwners = [];
2581
2736
  }
2582
2737
 
2583
2738
  function applyThreadContentShift(value) {
@@ -2585,45 +2740,40 @@ export function createInjectionSource(features, {
2585
2740
  restoreThreadContentShift();
2586
2741
  return;
2587
2742
  }
2588
- if (!threadShiftStyle.isConnected) {
2589
- (document.head ?? document.documentElement)?.append(threadShiftStyle);
2590
- }
2591
- const content = threadScrollContent();
2592
- if (!content) {
2743
+ const viewport = mainContentViewport();
2744
+ const owners = viewport
2745
+ ? Array.from(viewport.querySelectorAll('[style*="--thread-wide-block-inline-shift"]'))
2746
+ : [];
2747
+ if (!owners.length) {
2593
2748
  restoreThreadContentShift();
2594
2749
  return;
2595
2750
  }
2596
- if (shiftedThreadContent !== content) {
2751
+ if (
2752
+ shiftedConversationOwners.length !== owners.length
2753
+ || owners.some((owner, index) => shiftedConversationOwners[index]?.element !== owner)
2754
+ ) {
2597
2755
  restoreThreadContentShift();
2598
- shiftedThreadContent = content;
2599
- shiftedThreadOriginal = {
2600
- element: content,
2601
- position: {
2602
- value: content.style.getPropertyValue("position"),
2603
- priority: content.style.getPropertyPriority("position"),
2604
- },
2605
- left: {
2606
- value: content.style.getPropertyValue("left"),
2607
- priority: content.style.getPropertyPriority("left"),
2756
+ shiftedConversationOwners = owners.map((owner) => ({
2757
+ element: owner,
2758
+ shift: {
2759
+ value: owner.style.getPropertyValue("--thread-wide-block-inline-shift"),
2760
+ priority: owner.style.getPropertyPriority("--thread-wide-block-inline-shift"),
2608
2761
  },
2609
- transition: {
2610
- value: content.style.getPropertyValue("transition"),
2611
- priority: content.style.getPropertyPriority("transition"),
2762
+ translate: {
2763
+ value: owner.style.getPropertyValue("translate"),
2764
+ priority: owner.style.getPropertyPriority("translate"),
2612
2765
  },
2613
- };
2766
+ }));
2767
+ }
2768
+ for (const owner of owners) {
2769
+ owner.style.setProperty(
2770
+ "--thread-wide-block-inline-shift",
2771
+ `${Number(value)}px`,
2772
+ "important",
2773
+ );
2774
+ owner.style.setProperty("translate", `${Number(value)}px 0`, "important");
2775
+ owner.setAttribute("data-codex-personal-summary-shift", String(value));
2614
2776
  }
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
2777
  }
2628
2778
 
2629
2779
  function setSummarySurfaceVisible(surface, visible, mode) {
@@ -2728,7 +2878,7 @@ export function createInjectionSource(features, {
2728
2878
  surface.setAttribute("data-codex-personal-summary-presentation", displayMode);
2729
2879
  setSummarySurfaceVisible(surface, true, displayMode);
2730
2880
 
2731
- const shift = displayMode === "shift"
2881
+ const shift = displayMode !== "overlay"
2732
2882
  ? -(layout.panelWidth + layout.panelInset) / 2
2733
2883
  : 0;
2734
2884
  applyThreadContentShift(shift);
@@ -3373,8 +3523,12 @@ export function createInjectionSource(features, {
3373
3523
  const detail = feature?.detailTabs?.find((candidate) => candidate.id === detailId);
3374
3524
  if (!feature || !detail) throw new Error("The requested detail Tab is not registered");
3375
3525
  const result = await openNativeDetailTab(feature, detail);
3376
- if (!result) throw nativeTabCapabilityError
3377
- ?? new Error("Codex native right-panel tabs are unavailable");
3526
+ if (!result) {
3527
+ const error = nativeTabCapabilityError
3528
+ ?? new Error("Codex native right-panel tabs are unavailable");
3529
+ error.code = "NATIVE_TAB_UNAVAILABLE";
3530
+ throw error;
3531
+ }
3378
3532
  return result;
3379
3533
  }
3380
3534
 
@@ -3489,13 +3643,20 @@ export function createInjectionSource(features, {
3489
3643
  }
3490
3644
 
3491
3645
  function ensureToolbarEntries() {
3492
- if (!toolbarFeatures.length) return;
3646
+ if (!toolbarFeatures.length) {
3647
+ stopToolbarReadiness();
3648
+ return;
3649
+ }
3493
3650
  const temporaryChatButton = nativeTemporaryChatButton();
3494
3651
  const summaryButton = nativeSummaryButton();
3495
3652
  const bottomPanelButton = nativeBottomPanelButton();
3496
3653
  const sidePanelButton = nativeSidePanelButton();
3497
3654
  const anchorButton = temporaryChatButton ?? summaryButton ?? bottomPanelButton ?? sidePanelButton;
3498
- if (!anchorButton) return;
3655
+ if (!anchorButton) {
3656
+ const candidate = structuralToolbarAnchor();
3657
+ if (candidate) ensureToolbarReadiness(candidate);
3658
+ return;
3659
+ }
3499
3660
  const anchorRoot = toolbarControlRoot(anchorButton);
3500
3661
  // The bottom/side-panel toggles sit in narrow fixed containers that
3501
3662
  // travel with the native panels. Entries anchored to them must live in
@@ -3506,7 +3667,10 @@ export function createInjectionSource(features, {
3506
3667
  const useFallbackAnchor = anchorButton === sidePanelButton || anchorButton === bottomPanelButton;
3507
3668
  const summaryGroup = useFallbackAnchor ? null : nativeSummaryToolbarGroup(anchorButton);
3508
3669
  let targetGroup = summaryGroup ?? ensureFallbackSummaryToolbarGroup(anchorButton);
3509
- if (!targetGroup) return;
3670
+ if (!targetGroup) {
3671
+ ensureToolbarReadiness(anchorButton);
3672
+ return;
3673
+ }
3510
3674
  let cursor = useFallbackAnchor ? null : anchorRoot;
3511
3675
  for (const feature of toolbarFeatures) {
3512
3676
  const entries = Array.from(document.querySelectorAll(`[${entryMarker}="${feature.id}"]`));
@@ -3542,6 +3706,11 @@ export function createInjectionSource(features, {
3542
3706
  if (!used) group.remove();
3543
3707
  });
3544
3708
  }
3709
+ if (toolbarFeatures.every((feature) => findEntry(feature.id)?.isConnected)) {
3710
+ stopToolbarReadiness();
3711
+ } else {
3712
+ ensureToolbarReadiness(anchorButton);
3713
+ }
3545
3714
  }
3546
3715
 
3547
3716
  function ensureEntries() {
@@ -3600,6 +3769,13 @@ export function createInjectionSource(features, {
3600
3769
  }, record.origin);
3601
3770
  }
3602
3771
 
3772
+ function hostActionFailure(error) {
3773
+ return {
3774
+ code: typeof error?.code === "string" ? error.code : "HOST_ACTION_FAILED",
3775
+ message: error?.message ?? "Host action failed",
3776
+ };
3777
+ }
3778
+
3603
3779
  async function handleSurfaceHostAction(record, message) {
3604
3780
  const feature = featureById.get(record.featureId);
3605
3781
  const { requestId, action, payload = {} } = message;
@@ -3663,7 +3839,7 @@ export function createInjectionSource(features, {
3663
3839
  } catch (error) {
3664
3840
  respondToSurface(record, requestId, {
3665
3841
  ok: false,
3666
- error: { code: "HOST_ACTION_FAILED", message: error.message },
3842
+ error: hostActionFailure(error),
3667
3843
  });
3668
3844
  }
3669
3845
  }
@@ -3831,9 +4007,9 @@ export function createInjectionSource(features, {
3831
4007
  if (ensureFrame != null) cancelAnimationFrame(ensureFrame);
3832
4008
  if (ensureTimer != null) clearTimeout(ensureTimer);
3833
4009
  for (const observer of domObservers) observer.disconnect();
4010
+ stopToolbarReadiness();
3834
4011
  mainContentObserver?.observer.disconnect();
3835
4012
  restoreThreadContentShift();
3836
- threadShiftStyle.remove();
3837
4013
  for (const root of document.querySelectorAll("[data-codex-personal-toolbar-entry]")) root.remove();
3838
4014
  for (const group of document.querySelectorAll("[data-codex-personal-summary-toolbar-group]")) group.remove();
3839
4015
  for (const surface of pageSurfaces.values()) surface.remove();
@@ -3864,7 +4040,15 @@ export function createInjectionSource(features, {
3864
4040
  detailTabs: feature.detailTabs ?? [],
3865
4041
  hostActions: feature.hostActions ?? [],
3866
4042
  modelSelector: feature.modelSelector ?? null,
3867
- pageScript: feature.pageScript ?? null,
4043
+ pageScript: feature.pageScript ? {
4044
+ ...feature.pageScript,
4045
+ config: {
4046
+ serviceOrigin: pageScriptServiceOrigin(feature),
4047
+ bindingName,
4048
+ bindingToken,
4049
+ featureId: feature.id,
4050
+ },
4051
+ } : null,
3868
4052
  }));
3869
4053
  return `(${install.toString()})(${JSON.stringify({
3870
4054
  version: RUNTIME_VERSION,
@@ -3886,7 +4070,9 @@ export function createRuntimeReadyContract({
3886
4070
  runtimeSessionId,
3887
4071
  }) {
3888
4072
  const discovered = Boolean(renderer.discovered);
4073
+ const discoveredTargets = Math.max(0, Number(renderer.discoveredTargets) || 0);
3889
4074
  const injectedTargets = Math.max(0, Number(renderer.injectedTargets) || 0);
4075
+ const targetFailures = Array.isArray(renderer.targetFailures) ? renderer.targetFailures : [];
3890
4076
  const active = Boolean(renderer.active) && discovered && injectedTargets > 0;
3891
4077
  return {
3892
4078
  pid,
@@ -3904,8 +4090,11 @@ export function createRuntimeReadyContract({
3904
4090
  })),
3905
4091
  renderer: {
3906
4092
  discovered,
4093
+ discoveredTargets,
3907
4094
  injectedTargets,
3908
4095
  active,
4096
+ healthy: active && targetFailures.length === 0 && injectedTargets === discoveredTargets,
4097
+ targetFailures,
3909
4098
  runtimeSessionId: renderer.runtimeSessionId ?? runtimeSessionId,
3910
4099
  },
3911
4100
  };
@@ -4106,6 +4295,8 @@ export class CodexRuntime {
4106
4295
  this.onStatusChange = onStatusChange;
4107
4296
  this.onManagedCodexPidChange = onManagedCodexPidChange;
4108
4297
  this.injectedTargets = new Set();
4298
+ this.rendererTargetIds = new Set();
4299
+ this.targetFailures = new Map();
4109
4300
  this.clients = new Map();
4110
4301
  this.scriptIds = new Map();
4111
4302
  this.bindingTokens = new Map();
@@ -4159,10 +4350,21 @@ export class CodexRuntime {
4159
4350
 
4160
4351
  getRendererStatus() {
4161
4352
  const injectedTargets = this.injectedTargets.size;
4353
+ const discoveredTargets = this.rendererTargetIds.size;
4354
+ const targetFailures = Array.from(this.targetFailures, ([targetId, failure]) => ({
4355
+ targetId,
4356
+ attempts: failure.attempts,
4357
+ state: "retrying",
4358
+ lastFailure: failure.lastFailure,
4359
+ })).sort((left, right) => left.targetId.localeCompare(right.targetId));
4360
+ const active = !this.stopping && this.rendererDiscovered && injectedTargets > 0;
4162
4361
  return {
4163
4362
  discovered: this.rendererDiscovered,
4363
+ discoveredTargets,
4164
4364
  injectedTargets,
4165
- active: !this.stopping && this.rendererDiscovered && injectedTargets > 0,
4365
+ active,
4366
+ healthy: active && targetFailures.length === 0 && injectedTargets === discoveredTargets,
4367
+ targetFailures,
4166
4368
  runtimeSessionId: this.runtimeSessionId,
4167
4369
  };
4168
4370
  }
@@ -4173,8 +4375,11 @@ export class CodexRuntime {
4173
4375
  if (
4174
4376
  previous
4175
4377
  && previous.discovered === status.discovered
4378
+ && previous.discoveredTargets === status.discoveredTargets
4176
4379
  && previous.injectedTargets === status.injectedTargets
4177
4380
  && previous.active === status.active
4381
+ && previous.healthy === status.healthy
4382
+ && JSON.stringify(previous.targetFailures) === JSON.stringify(status.targetFailures)
4178
4383
  && previous.runtimeSessionId === status.runtimeSessionId
4179
4384
  ) return;
4180
4385
  this.lastRendererStatus = status;
@@ -4218,6 +4423,8 @@ export class CodexRuntime {
4218
4423
  this.bindingTokens.clear();
4219
4424
  this.hostActionChains.clear();
4220
4425
  this.injectedTargets.clear();
4426
+ this.rendererTargetIds.clear();
4427
+ this.targetFailures.clear();
4221
4428
  this.rendererDiscovered = false;
4222
4429
  this.browserClient?.socket.close();
4223
4430
  this.browserClient = null;
@@ -4581,6 +4788,10 @@ export class CodexRuntime {
4581
4788
  });
4582
4789
  return;
4583
4790
  }
4791
+ if (request.action === "import-generated-image") {
4792
+ await this.importGeneratedImage(feature, request.payload);
4793
+ return;
4794
+ }
4584
4795
  try {
4585
4796
  if (request.action !== "attach-file") throw new Error("Unsupported native Host action");
4586
4797
  const result = await this.attachFileToComposer(client, request.payload?.path, request.requestId);
@@ -4593,6 +4804,22 @@ export class CodexRuntime {
4593
4804
  }
4594
4805
  }
4595
4806
 
4807
+ async importGeneratedImage(feature, payload) {
4808
+ const origin = pageScriptServiceOrigin(feature);
4809
+ if (!origin) throw new Error("Generated image import service is unavailable");
4810
+ const response = await this.fetchImpl(new URL("/api/import-generated-image", origin), {
4811
+ method: "POST",
4812
+ headers: {
4813
+ "Content-Type": "application/json",
4814
+ Origin: CODEX_APP_ORIGIN,
4815
+ },
4816
+ body: JSON.stringify(payload),
4817
+ });
4818
+ if (!response?.ok) {
4819
+ throw new Error(`Generated image import returned HTTP ${response?.status ?? "error"}`);
4820
+ }
4821
+ }
4822
+
4596
4823
  async refreshTargets() {
4597
4824
  let response;
4598
4825
  try {
@@ -4619,8 +4846,14 @@ export class CodexRuntime {
4619
4846
  .filter((target) => target.type === "page" && target.url === "app://-/index.html")
4620
4847
  .map((target) => target.id),
4621
4848
  );
4849
+ this.rendererTargetIds = liveRendererIds;
4622
4850
  let removedTarget = false;
4623
- for (const id of this.injectedTargets) {
4851
+ const trackedTargetIds = new Set([
4852
+ ...this.injectedTargets,
4853
+ ...this.clients.keys(),
4854
+ ...this.targetFailures.keys(),
4855
+ ]);
4856
+ for (const id of trackedTargetIds) {
4624
4857
  if (liveRendererIds.has(id)) continue;
4625
4858
  removedTarget = true;
4626
4859
  this.clients.get(id)?.socket.close();
@@ -4629,6 +4862,7 @@ export class CodexRuntime {
4629
4862
  this.bindingTokens.delete(id);
4630
4863
  this.hostActionChains.delete(id);
4631
4864
  this.injectedTargets.delete(id);
4865
+ this.targetFailures.delete(id);
4632
4866
  }
4633
4867
  const rendererDiscovered = liveRendererIds.size > 0;
4634
4868
  const rendererChanged = rendererDiscovered !== this.rendererDiscovered;
@@ -4639,8 +4873,12 @@ export class CodexRuntime {
4639
4873
  if (target.type !== "page" || target.url !== "app://-/index.html" || this.injectedTargets.has(target.id)) {
4640
4874
  continue;
4641
4875
  }
4642
- const client = await this.connectClient(target.webSocketDebuggerUrl);
4876
+ const previousFailure = this.targetFailures.get(target.id);
4877
+ if (previousFailure && Date.now() < previousFailure.nextRetryAt) continue;
4878
+ let client;
4879
+ let scriptIdentifier = null;
4643
4880
  try {
4881
+ client = await this.connectClient(target.webSocketDebuggerUrl);
4644
4882
  await Promise.all([
4645
4883
  client.send("Page.enable"),
4646
4884
  client.send("Runtime.enable"),
@@ -4662,6 +4900,7 @@ export class CodexRuntime {
4662
4900
  const script = await client.send("Page.addScriptToEvaluateOnNewDocument", {
4663
4901
  source: createDocumentBootstrapSource(source),
4664
4902
  });
4903
+ scriptIdentifier = script.identifier;
4665
4904
  await waitForExpression(
4666
4905
  client,
4667
4906
  `document.readyState === "interactive" || document.readyState === "complete"`,
@@ -4674,15 +4913,33 @@ export class CodexRuntime {
4674
4913
  + ` && window.__codexPersonalRuntime?.sessionId === ${JSON.stringify(this.runtimeSessionId)}`,
4675
4914
  );
4676
4915
  this.clients.set(target.id, client);
4677
- this.scriptIds.set(target.id, script.identifier);
4916
+ this.scriptIds.set(target.id, scriptIdentifier);
4678
4917
  this.injectedTargets.add(target.id);
4918
+ this.targetFailures.delete(target.id);
4679
4919
  this.notifyStatusChange();
4680
4920
  } catch (error) {
4681
4921
  this.bindingTokens.delete(target.id);
4682
- client.socket.close();
4683
- throw error;
4922
+ if (client) {
4923
+ if (scriptIdentifier) {
4924
+ await client.send("Page.removeScriptToEvaluateOnNewDocument", {
4925
+ identifier: scriptIdentifier,
4926
+ }).catch(() => {});
4927
+ }
4928
+ await client.send("Runtime.removeBinding", { name: HOST_BINDING_NAME }).catch(() => {});
4929
+ client.socket.close();
4930
+ }
4931
+ const attempts = (previousFailure?.attempts ?? 0) + 1;
4932
+ const retryDelay = Math.min(5_000, 250 * (2 ** Math.min(attempts - 1, 5)));
4933
+ this.targetFailures.set(target.id, {
4934
+ attempts,
4935
+ lastFailure: error.message,
4936
+ nextRetryAt: Date.now() + retryDelay,
4937
+ });
4938
+ this.logger.warn?.(`Renderer injection failed for ${target.id}; retrying`, error.message);
4939
+ this.notifyStatusChange();
4684
4940
  }
4685
4941
  }
4942
+ this.notifyStatusChange();
4686
4943
  }
4687
4944
 
4688
4945
  async stop() {
@@ -4712,6 +4969,8 @@ export class CodexRuntime {
4712
4969
  this.bindingTokens.clear();
4713
4970
  this.hostActionChains.clear();
4714
4971
  this.injectedTargets.clear();
4972
+ this.rendererTargetIds.clear();
4973
+ this.targetFailures.clear();
4715
4974
  this.rendererDiscovered = false;
4716
4975
  this.notifyStatusChange();
4717
4976
  this.monitorPromise = null;