minecodex 2.0.55 → 2.1.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.
Files changed (65) hide show
  1. package/README.md +90 -26
  2. package/apps/dashboard/app.js +20 -14
  3. package/apps/dashboard/assets/chevron-left.svg +13 -0
  4. package/apps/dashboard/assets/chevron-right.svg +13 -0
  5. package/apps/dashboard/assets/feature-showcase.js +185 -0
  6. package/apps/dashboard/assets/pause.svg +14 -0
  7. package/apps/dashboard/assets/play.svg +13 -0
  8. package/apps/dashboard/assets/previews/composer-expand.jpg +0 -0
  9. package/apps/dashboard/assets/previews/composer-expand.mp4 +0 -0
  10. package/apps/dashboard/assets/previews/file-navigation.jpg +0 -0
  11. package/apps/dashboard/assets/previews/file-navigation.mp4 +0 -0
  12. package/apps/dashboard/assets/previews/files-auto-refresh.jpg +0 -0
  13. package/apps/dashboard/assets/previews/files-auto-refresh.mp4 +0 -0
  14. package/apps/dashboard/assets/previews/images.jpg +0 -0
  15. package/apps/dashboard/assets/previews/images.mp4 +0 -0
  16. package/apps/dashboard/assets/previews/manifest.json +67 -1
  17. package/apps/dashboard/assets/previews/markdown-reading.jpg +0 -0
  18. package/apps/dashboard/assets/previews/markdown-reading.mp4 +0 -0
  19. package/apps/dashboard/assets/previews/mermaid-diagrams.jpg +0 -0
  20. package/apps/dashboard/assets/previews/mermaid-diagrams.mp4 +0 -0
  21. package/apps/dashboard/assets/previews/model-slider.jpg +0 -0
  22. package/apps/dashboard/assets/previews/model-slider.mp4 +0 -0
  23. package/apps/dashboard/assets/previews/notes.jpg +0 -0
  24. package/apps/dashboard/assets/previews/notes.mp4 +0 -0
  25. package/apps/dashboard/assets/previews/quick-chat.jpg +0 -0
  26. package/apps/dashboard/assets/previews/quick-chat.mp4 +0 -0
  27. package/apps/dashboard/assets/previews/session-status.jpg +0 -0
  28. package/apps/dashboard/assets/previews/session-status.mp4 +0 -0
  29. package/apps/dashboard/assets/previews/sidebar-focus.jpg +0 -0
  30. package/apps/dashboard/assets/previews/sidebar-focus.mp4 +0 -0
  31. package/apps/dashboard/assets/previews/sites.jpg +0 -0
  32. package/apps/dashboard/assets/previews/sites.mp4 +0 -0
  33. package/apps/dashboard/assets/previews/usage-meter.jpg +0 -0
  34. package/apps/dashboard/assets/previews/usage-meter.mp4 +0 -0
  35. package/apps/dashboard/index.html +26 -2
  36. package/apps/dashboard/styles.css +58 -39
  37. package/assets/readme/hero-dark.svg +50 -0
  38. package/assets/readme/hero.svg +36 -79
  39. package/features/images/src/adapters/codex/rspack-modules.mjs +9 -3
  40. package/features/images/src/library-prompt-source.mjs +2 -2
  41. package/features/notes/codex-feature.json +1 -0
  42. package/features/quick-chat/src/adapters/codex/renderer.mjs +3 -1
  43. package/package.json +3 -3
  44. package/packages/cli/src/config.mjs +1 -2
  45. package/packages/cli/src/control-server.mjs +1 -1
  46. package/packages/plugin/assets/app.js +35 -35
  47. package/packages/runtime-host/src/codex-cdp.mjs +0 -7
  48. package/packages/runtime-host/src/codex-injection.mjs +50 -2114
  49. package/packages/runtime-host/src/codex-runtime.mjs +4 -203
  50. package/packages/runtime-host/src/feature-registry.mjs +10 -91
  51. package/packages/runtime-host/src/main.mjs +0 -1
  52. package/packages/ui/src/native/tokens.mjs +0 -28
  53. package/apps/dashboard/assets/feature-previews.js +0 -206
  54. package/features/images/src/adapters/codex/cloud-import.mjs +0 -52
  55. package/features/images/src/backend/cloud-library.mjs +0 -12
  56. package/features/images/src/chatgpt-image-source-page.mjs +0 -207
  57. package/features/images/src/chatgpt-image-source.mjs +0 -42
  58. package/features/images/src/http-server.mjs +0 -487
  59. package/features/images/src/main.mjs +0 -52
  60. package/features/images/src/save-as.mjs +0 -69
  61. package/features/images/web/app.js +0 -1780
  62. package/features/images/web/i18n.mjs +0 -132
  63. package/features/images/web/index.html +0 -187
  64. package/features/images/web/styles.css +0 -1114
  65. package/packages/runtime-host/src/host-actions.mjs +0 -221
@@ -11,82 +11,15 @@ import { MCP_NAVIGATION_ICONS } from "../../plugin/src/navigation-icons.mjs";
11
11
  import { randomBytes } from "node:crypto";
12
12
  import { CODEX_THEME_TOKENS, CODEX_THEME_TOKEN_ALIASES, MINECODEX_THEME_BINDINGS } from "../../ui/src/native/tokens.mjs";
13
13
 
14
- export const RUNTIME_VERSION = 114;
14
+
15
+ export const RUNTIME_VERSION = 115;
15
16
  export const HOST_BINDING_NAME = "__codexPersonalHostAction";
16
17
  export const CSP_BOOTSTRAP_VERSION = 1;
17
18
  export const CSP_BOOTSTRAP_KEY = "__mineCodexCspBootstrapVersion";
18
19
  export const CODEX_APP_ORIGIN = "app://-";
19
- export const SURFACE_LOAD_ACTION = "load-surface";
20
- export const SURFACE_FRAME_PREFIX = "minecodex-surface-";
21
- export const LOOPBACK_SURFACE_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
22
- export const RIGHT_PANEL_ROUTE_KINDS = Object.freeze(["home", "local-thread"]);
23
-
24
- export const RESPONSIVE_SUMMARY_LAYOUT = Object.freeze({
25
- contentBaseWidth: 736,
26
- overlayClearance: 180,
27
- gutterClearance: 400,
28
- panelWidth: 300,
29
- panelInset: 16,
30
- verticalInset: 12,
31
- popoverOffset: 8,
32
- popoverBottomInset: 6,
33
- });
34
-
35
- export function responsiveSummaryDisplayMode(mainContentTargetWidth, layout = RESPONSIVE_SUMMARY_LAYOUT) {
36
- const clearance = (Number(mainContentTargetWidth) - layout.contentBaseWidth) / 2;
37
- if (clearance < layout.overlayClearance) return "overlay";
38
- if (clearance < layout.gutterClearance) return "shift";
39
- return "gutter";
40
- }
41
-
42
- export function responsiveSummaryVisibleSurface({ displayMode, isPinned, isPopoverOpen }) {
43
- if (displayMode !== "overlay" && isPinned) return "inline";
44
- if (displayMode === "overlay" && isPopoverOpen) return "popover";
45
- return null;
46
- }
47
-
48
- export function responsiveSummaryContentShift({ displayMode, isPinned }, layout = RESPONSIVE_SUMMARY_LAYOUT) {
49
- return displayMode !== "overlay" && isPinned
50
- ? -(layout.panelWidth + layout.panelInset) / 2
51
- : 0;
52
- }
53
-
54
- function declaredSurfaceUrls(feature) {
55
- return new Set([
56
- feature.surfaceUrl,
57
- feature.pinnedSummary?.surfaceUrl,
58
- ...(feature.detailTabs ?? []).map((detail) => detail.surfaceUrl),
59
- ].filter(Boolean));
60
- }
61
-
62
- export function requireDeclaredLoopbackSurface(feature, value) {
63
- if (typeof value !== "string") {
64
- throw Object.assign(new Error("Surface URL is not declared by this feature"), {
65
- code: "SURFACE_URL_NOT_ALLOWED",
66
- });
67
- }
68
- const url = new URL(value);
69
- const matchesDeclaredDocument = Array.from(declaredSurfaceUrls(feature), (declared) => new URL(declared))
70
- .some((declared) => (
71
- declared.origin === url.origin
72
- && declared.username === url.username
73
- && declared.password === url.password
74
- && declared.pathname === url.pathname
75
- && declared.hash === url.hash
76
- ));
77
- if (!matchesDeclaredDocument) {
78
- throw Object.assign(new Error("Surface URL is not declared by this feature"), {
79
- code: "SURFACE_URL_NOT_ALLOWED",
80
- });
81
- }
82
- if (url.protocol !== "http:" || !LOOPBACK_SURFACE_HOSTS.has(url.hostname)) {
83
- throw Object.assign(new Error("Surface URL must use an exact loopback HTTP origin"), {
84
- code: "SURFACE_URL_NOT_ALLOWED",
85
- });
86
- }
87
- return url;
88
- }
20
+ const LOOPBACK_SERVICE_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
89
21
 
22
+ // 带本机服务的 page-script 只能经 RuntimeHost 访问与 healthUrl 同源的 loopback HTTP 服务。
90
23
  export function pageScriptServiceOrigin(feature) {
91
24
  if (!feature.pageScript || !feature.surfaceUrl || !feature.healthUrl) return null;
92
25
  const surfaceUrl = new URL(feature.surfaceUrl);
@@ -94,30 +27,11 @@ export function pageScriptServiceOrigin(feature) {
94
27
  if (
95
28
  surfaceUrl.origin !== healthUrl.origin
96
29
  || surfaceUrl.protocol !== "http:"
97
- || !LOOPBACK_SURFACE_HOSTS.has(surfaceUrl.hostname)
30
+ || !LOOPBACK_SERVICE_HOSTS.has(surfaceUrl.hostname)
98
31
  ) return null;
99
32
  return surfaceUrl.origin;
100
33
  }
101
34
 
102
- export function findFrameByName(frameTree, frameName) {
103
- if (frameTree.frame?.name === frameName) return frameTree.frame;
104
- for (const child of frameTree.childFrames ?? []) {
105
- const match = findFrameByName(child, frameName);
106
- if (match) return match;
107
- }
108
- return null;
109
- }
110
-
111
- export function documentWithBase(html, surfaceUrl) {
112
- const head = /<head(?:\s[^>]*)?>/i;
113
- if (!head.test(html)) {
114
- throw Object.assign(new Error("Surface document has no head element"), {
115
- code: "SURFACE_DOCUMENT_INVALID",
116
- });
117
- }
118
- return html.replace(head, (match) => `${match}<base href=${JSON.stringify(surfaceUrl)}>`);
119
- }
120
-
121
35
  // createInjectionSource:产出注入 Codex Renderer 的自包含脚本字符串。
122
36
  // 返回的脚本经 CDP 注入后独立执行(install 函数体必须自包含,不能引用模块外部标识符)。
123
37
  export function createInjectionSource(features, {
@@ -146,25 +60,6 @@ export function createInjectionSource(features, {
146
60
  const lifetime = new AbortController();
147
61
  const codex = createCodexEnvironment();
148
62
  const featureUI = installUI(lifetime.signal, {codex,mcpServer:"minecodex",mcpPluginId:"minecodex@minecodex-local",mcpNavigationIcons:config.mcpNavigationIcons,themeTokens:config.themeTokens,themeTokenAliases:config.themeTokenAliases,themeBindings:config.themeBindings});
149
- const rightPanelRouteKinds = new Set(config.rightPanelRouteKinds);
150
-
151
- const entryMarker = "data-codex-personal-feature";
152
- const pageSurfaceMarker = "data-codex-personal-surface";
153
- const pinnedSurfaceMarker = "data-codex-personal-pinned-summary";
154
- const modalSurfaceMarker = "data-codex-personal-modal";
155
- const promptPreviewMarker = "data-codex-personal-prompt-preview";
156
- const nativeLabels = {
157
- sites: ["sites", "站点"],
158
- scheduled: ["scheduled", "计划任务"],
159
- plugins: ["plugins", "插件"],
160
- };
161
- const nativeToolbarLabels = {
162
- summary: ["Toggle summary", "切换摘要"],
163
- pinnedSummary: ["Toggle pinned summary", "切换固定摘要"],
164
- bottomPanel: ["Toggle bottom panel", "切换底部面板"],
165
- sidePanel: ["Toggle side panel", "切换侧边面板"],
166
- temporaryChat: ["temporary chat", "临时聊天", "临时对话"],
167
- };
168
63
 
169
64
  function normalizedLocale(value) {
170
65
  return String(value ?? "").toLowerCase().startsWith("zh") ? "zh-CN" : "en";
@@ -179,26 +74,12 @@ export function createInjectionSource(features, {
179
74
  }
180
75
 
181
76
  // ========================================================================
182
- // 配置派生与运行状态 — feature 分类、surface/卡片状态、观察器与计时器
77
+ // 配置派生与运行状态
183
78
  // ========================================================================
184
79
 
185
80
  const features = config.features.map((feature) => ({
186
81
  ...feature,
187
82
  label: localizedText(feature.label, feature.labels),
188
- entry: feature.entry ? {
189
- ...feature.entry,
190
- ariaLabel: localizedText(feature.entry.ariaLabel, feature.entry.ariaLabels),
191
- } : { kind: "sidebar" },
192
- pinnedSummary: feature.pinnedSummary ? {
193
- ...feature.pinnedSummary,
194
- label: localizedText(feature.pinnedSummary.label, feature.pinnedSummary.labels),
195
- overlayLabel: localizedText(feature.pinnedSummary.overlayLabel, feature.pinnedSummary.overlayLabels),
196
- pinnedLabel: localizedText(feature.pinnedSummary.pinnedLabel, feature.pinnedSummary.pinnedLabels),
197
- } : null,
198
- detailTabs: (feature.detailTabs ?? []).map((detail) => ({
199
- ...detail,
200
- label: localizedText(detail.label, detail.labels),
201
- })),
202
83
  modelSelector: feature.modelSelector ? {
203
84
  ...feature.modelSelector,
204
85
  strings: Object.fromEntries(Object.entries(feature.modelSelector.strings ?? {}).map(
@@ -209,42 +90,36 @@ export function createInjectionSource(features, {
209
90
  }));
210
91
  const selfCheck = createRendererSelfCheck(features, config.sessionId, featureUI.theme);
211
92
  const featureById = new Map(features.map((feature) => [feature.id, feature]));
212
- const sidebarFeatures = features.filter((feature) => feature.entry?.kind === "sidebar");
213
- const toolbarFeatures = features.filter((feature) => feature.entry?.kind === "conversation-toolbar");
214
93
  const modelSelectorFeature = features.find(
215
94
  (feature) => feature.entry?.kind === "composer-model-selector",
216
95
  ) ?? null;
217
96
  const pageScriptFeatures = features.filter((feature) => feature.pageScript);
218
- const pageSurfaces = new Map();
219
- const pinnedSurfaces = new Map();
220
- const surfaceRecords = new Map();
221
- const nativeDetailComponents = new Map();
222
- const nativeDetailSurfaceKeys = new Map();
223
- const nativeOpenTabSessions = new Set();
224
- const summaryStates = new Map();
225
- let activePageFeatureId = null;
226
- let activePinnedFeatureId = null;
227
97
  let ensureQueued = false;
228
98
  let ensureFrame = null;
229
99
  let ensureTimer = null;
230
- let themeQueued = false;
231
- let suppressedSelections = [];
232
100
  let savedComposerRange = null;
233
101
  let savedComposerThread = null;
234
- let currentThread = null;
235
- let currentSummaryDisplayMode = null;
236
- let shiftedConversationOwners = [];
237
- let mainContentObserver = null;
238
- let toolbarReadiness = null;
239
102
  const domObservers = [];
240
103
  const pageScriptCleanups = [];
241
104
  const pendingHostActions = new Map();
242
- let activeModalRecordKey = null;
243
- let activePromptPreview = null;
244
- let promptPreviewDismissTimer = null;
245
- let nativeTabCapabilityPromise = null;
246
- let nativeTabCapability = null;
247
- let nativeTabCapabilityError = null;
105
+
106
+ function createMarkupIcon(iconDefinition, className, size = 16) {
107
+ if (!iconDefinition?.markup) return null;
108
+ const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
109
+ icon.setAttribute("width", String(size));
110
+ icon.setAttribute("height", String(size));
111
+ icon.setAttribute("viewBox", iconDefinition.viewBox ?? "0 0 24 24");
112
+ icon.setAttribute("fill", "none");
113
+ icon.setAttribute("stroke", "currentColor");
114
+ icon.setAttribute("stroke-width", "1.5");
115
+ icon.setAttribute("stroke-linecap", "round");
116
+ icon.setAttribute("stroke-linejoin", "round");
117
+ icon.setAttribute("aria-hidden", "true");
118
+ if (className) icon.setAttribute("class", className);
119
+ icon.innerHTML = iconDefinition.markup;
120
+ return icon;
121
+ }
122
+
248
123
  let modelSlider = {ensure() {}, dispose() {}};
249
124
  try {
250
125
  modelSlider = installModelSliderFeature(lifetime.signal, {
@@ -296,515 +171,20 @@ export function createInjectionSource(features, {
296
171
  }
297
172
  }
298
173
 
299
- // ========================================================================
300
- // Toolbar 入口定位与就绪 — 识别原生 Toolbar/Summary 控件并等待其就绪
301
- // ========================================================================
302
-
303
- function findEntry(featureId) {
304
- return Array.from(document.querySelectorAll(`[${entryMarker}]`)).find(
305
- (entry) => entry.getAttribute(entryMarker) === featureId,
306
- );
307
- }
308
-
309
- function findNativeEntry(key) {
310
- const labels = nativeLabels[key] ?? [String(key).toLowerCase()];
311
- return Array.from(document.querySelectorAll("a, button")).find((element) => (
312
- labels.includes(element.textContent?.trim().toLowerCase())
313
- ));
314
- }
315
-
316
- function findReferenceEntry() {
317
- return findNativeEntry("plugins");
318
- }
319
-
320
- function isVisibleToolbarControl(element) {
321
- if (!(element instanceof HTMLElement)) return false;
322
- const style = getComputedStyle(element);
323
- const rect = element.getBoundingClientRect();
324
- return style.display !== "none"
325
- && style.visibility !== "hidden"
326
- && rect.width > 0
327
- && rect.height > 0;
328
- }
329
-
330
- function hasNativeToolbarLabel(button, key) {
331
- const label = String(button?.getAttribute("aria-label") ?? "").trim().toLowerCase();
332
- return nativeToolbarLabels[key].some((candidate) => (
333
- key === "temporaryChat"
334
- ? label.includes(candidate.toLowerCase())
335
- : label === candidate.toLowerCase()
336
- ));
337
- }
338
-
339
- function nativeSummaryButton() {
340
- return Array.from(document.querySelectorAll("button")).find((button) => (
341
- (
342
- hasNativeToolbarLabel(button, "summary")
343
- || hasNativeToolbarLabel(button, "pinnedSummary")
344
- )
345
- && isVisibleToolbarControl(button)
346
- )) ?? null;
347
- }
348
-
349
- function nativeBottomPanelButton() {
350
- return Array.from(document.querySelectorAll("button")).find((button) => {
351
- if (!hasNativeToolbarLabel(button, "bottomPanel")) return false;
352
- return isVisibleToolbarControl(button);
353
- }) ?? null;
354
- }
355
-
356
- function nativeSidePanelButton() {
357
- return Array.from(document.querySelectorAll("button")).find((button) => {
358
- if (!hasNativeToolbarLabel(button, "sidePanel")) return false;
359
- return isVisibleToolbarControl(button);
360
- }) ?? null;
361
- }
362
-
363
- function nativeTemporaryChatButton() {
364
- return Array.from(document.querySelectorAll("button")).find((button) => {
365
- if (!hasNativeToolbarLabel(button, "temporaryChat")) return false;
366
- return isVisibleToolbarControl(button);
367
- }) ?? null;
368
- }
369
-
370
- function structuralToolbarAnchor() {
371
- const buttons = Array.from(document.querySelectorAll("button"));
372
- return buttons.find((button) => hasNativeToolbarLabel(button, "temporaryChat"))
373
- ?? buttons.find((button) => (
374
- hasNativeToolbarLabel(button, "summary")
375
- || hasNativeToolbarLabel(button, "pinnedSummary")
376
- ))
377
- ?? buttons.find((button) => hasNativeToolbarLabel(button, "bottomPanel"))
378
- ?? buttons.find((button) => hasNativeToolbarLabel(button, "sidePanel"))
379
- ?? null;
380
- }
381
-
382
- function stopToolbarReadiness() {
383
- if (!toolbarReadiness) return;
384
- toolbarReadiness.observer.disconnect();
385
- if (toolbarReadiness.retryTimer != null) clearTimeout(toolbarReadiness.retryTimer);
386
- if (toolbarReadiness.deadlineTimer != null) clearTimeout(toolbarReadiness.deadlineTimer);
387
- toolbarReadiness = null;
388
- }
389
-
390
- function ensureToolbarReadiness(candidate) {
391
- const thread = threadIdentity();
392
- if (toolbarReadiness?.candidate === candidate && toolbarReadiness.thread === thread) return;
393
- stopToolbarReadiness();
394
- const record = {
395
- candidate,
396
- thread,
397
- attempts: 0,
398
- retryTimer: null,
399
- deadlineTimer: null,
400
- observer: null,
401
- };
402
- const wake = () => {
403
- if (toolbarReadiness !== record) return;
404
- queueEnsure();
405
- };
406
- record.observer = new ResizeObserver(wake);
407
- record.observer.observe(candidate);
408
- const header = candidate.closest("header");
409
- if (header) record.observer.observe(header);
410
- const retry = () => {
411
- if (toolbarReadiness !== record || record.attempts >= 8) return;
412
- record.attempts += 1;
413
- wake();
414
- record.retryTimer = setTimeout(retry, 75);
415
- };
416
- record.retryTimer = setTimeout(retry, 75);
417
- record.deadlineTimer = setTimeout(() => {
418
- if (toolbarReadiness === record) stopToolbarReadiness();
419
- }, 1_500);
420
- toolbarReadiness = record;
421
- }
422
-
423
- function toolbarControlRoot(button) {
424
- return button?.parentElement?.parentElement ?? button?.parentElement ?? button ?? null;
425
- }
426
-
427
- function nativeSummaryToolbarGroup(nativeButton) {
428
- return toolbarControlRoot(nativeButton)?.parentElement ?? null;
429
- }
430
-
431
- function conversationToolbarHost(nativeButton) {
432
- const header = nativeButton?.closest("header");
433
- if (!header) return null;
434
- return Array.from(header.children).find((child) => {
435
- if (!(child instanceof HTMLElement) || !isVisibleToolbarControl(child)) return false;
436
- return Number.parseFloat(getComputedStyle(child).flexGrow) > 0;
437
- }) ?? null;
438
- }
439
-
440
- function ensureFallbackSummaryToolbarGroup(nativeButton) {
441
- const toolbarHost = conversationToolbarHost(nativeButton);
442
- if (!toolbarHost) return null;
443
- let group = toolbarHost.querySelector(":scope > [data-codex-personal-summary-toolbar-group]");
444
- if (!group) {
445
- group = document.createElement("div");
446
- group.setAttribute("data-codex-personal-summary-toolbar-group", "fallback");
447
- // The host toolbar strip is pointer-events:none; without this the
448
- // fallback entries would be visible but unclickable.
449
- group.className = "ms-auto flex shrink-0 items-center gap-1.5 pointer-events-auto";
450
- // Same structure as the native tool group (e.g. the temporary-chat
451
- // group in Chat mode): an in-flow ms-auto strip inside the flexible
452
- // header area. It sits at the right edge while the side panel is
453
- // closed and, when the side panel widens its host container, moves
454
- // together with the native layout exactly like Chat mode does.
455
- toolbarHost.append(group);
456
- }
457
- return group;
458
- }
459
-
460
- function toolbarEntryFitsHeader(entryRoot) {
461
- const header = entryRoot.closest("header");
462
- if (!header) return true;
463
- const headerRect = header.getBoundingClientRect();
464
- const entryRect = entryRoot.getBoundingClientRect();
465
- if (entryRect.width <= 0 || entryRect.height <= 0) return false;
466
- return entryRect.right <= headerRect.right + 1 && entryRect.left >= headerRect.left - 1;
467
- }
468
-
469
- function isNativeSummaryButton(element) {
470
- if (!(element instanceof Element)) return false;
471
- const button = element.closest("button");
472
- return hasNativeToolbarLabel(button, "summary")
473
- || hasNativeToolbarLabel(button, "pinnedSummary");
474
- }
475
-
476
- function mainContentViewport() {
477
- const composer = currentComposer();
478
- return composer?.closest("main[data-app-shell-main-surface]")
479
- ?? document.querySelector("main[data-app-shell-main-surface]")
480
- ?? document.querySelector("[data-app-shell-main-content-layout]")
481
- ?? document.querySelector("main");
482
- }
483
-
484
- function visibleRightWorkspacePanels() {
485
- return Array.from(document.querySelectorAll('[data-app-shell-tab-panel-controller="right"]'))
486
- .filter((panel) => {
487
- if (!(panel instanceof HTMLElement) || panel.hidden) return false;
488
- const style = getComputedStyle(panel);
489
- const rect = panel.getBoundingClientRect();
490
- return style.display !== "none" && style.visibility !== "hidden" && rect.width > 0;
491
- });
492
- }
493
-
494
- function mainContentTargetGeometry() {
495
- const viewport = mainContentViewport();
496
- const rect = viewport?.getBoundingClientRect() ?? {
497
- left: Math.max(0, sidebarRight()),
498
- right: innerWidth,
499
- top: 0,
500
- bottom: innerHeight,
501
- width: Math.max(0, innerWidth - sidebarRight()),
502
- };
503
- let right = rect.right;
504
- for (const panel of visibleRightWorkspacePanels()) {
505
- const panelRect = panel.getBoundingClientRect();
506
- if (panelRect.left > rect.left && panelRect.left < right) right = panelRect.left;
507
- }
508
- return {
509
- viewport,
510
- left: rect.left,
511
- right,
512
- top: rect.top,
513
- bottom: rect.bottom,
514
- width: Math.max(0, right - rect.left),
515
- };
516
- }
517
-
518
- function summaryDisplayMode() {
519
- const layout = config.summaryLayout;
520
- const clearance = (mainContentTargetGeometry().width - layout.contentBaseWidth) / 2;
521
- if (clearance < layout.overlayClearance) return "overlay";
522
- if (clearance < layout.gutterClearance) return "shift";
523
- return "gutter";
524
- }
525
-
526
- function summaryState(featureId) {
527
- let state = summaryStates.get(featureId);
528
- if (!state) {
529
- state = { isPinned: false, isPopoverOpen: false };
530
- summaryStates.set(featureId, state);
531
- }
532
- return state;
533
- }
534
-
535
- function visibleSummarySurface(featureId, displayMode = summaryDisplayMode()) {
536
- const state = summaryState(featureId);
537
- if (displayMode !== "overlay" && state.isPinned) return "inline";
538
- if (displayMode === "overlay" && state.isPopoverOpen) return "popover";
539
- return null;
540
- }
541
-
542
- function summaryButtonLabel(feature, displayMode = summaryDisplayMode()) {
543
- const definition = feature.pinnedSummary ?? {};
544
- return displayMode === "overlay"
545
- ? definition.overlayLabel ?? (locale === "zh-CN" ? "切换笔记" : "Toggle notes")
546
- : definition.pinnedLabel
547
- ?? feature.entry.ariaLabel
548
- ?? (locale === "zh-CN" ? "切换固定笔记" : "Toggle pinned notes");
549
- }
550
-
551
- function reducedMotion() {
552
- return window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false;
553
- }
554
-
555
- // ========================================================================
556
- // 通用图标工厂 — feature 入口与插件适配共享的 markup 图标
557
- // ========================================================================
558
-
559
- function createMarkupIcon(iconDefinition, className, size = 16) {
560
- if (!iconDefinition?.markup) return null;
561
- const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
562
- icon.setAttribute("width", String(size));
563
- icon.setAttribute("height", String(size));
564
- icon.setAttribute("viewBox", iconDefinition.viewBox ?? "0 0 24 24");
565
- icon.setAttribute("fill", "none");
566
- icon.setAttribute("stroke", "currentColor");
567
- icon.setAttribute("stroke-width", "1.5");
568
- icon.setAttribute("stroke-linecap", "round");
569
- icon.setAttribute("stroke-linejoin", "round");
570
- icon.setAttribute("aria-hidden", "true");
571
- if (className) icon.setAttribute("class", className);
572
- icon.innerHTML = iconDefinition.markup;
573
- return icon;
574
- }
575
-
576
- function createFeatureIcon(feature, className, size = 16) {
577
- return createMarkupIcon(feature.icon, className, size);
578
- }
579
-
580
-
581
-
582
- // ========================================================================
583
- // Surface 注册与主题桥 — surface 记录、侧栏状态、主题 token 与 Composer 文本
584
- // ========================================================================
585
-
586
- function surfaceKey(featureId, kind, detailId = "") {
587
- return `${featureId}:${kind}:${detailId}`;
588
- }
589
-
590
- function surfaceFrameName(featureId) {
591
- const nonce = crypto.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
592
- return `minecodex-surface-${config.sessionId}-${featureId}-${nonce}`;
593
- }
594
-
595
- function requestSurfaceLoad(record) {
596
- if (!record || record.loading || typeof globalThis[config.bindingName] !== "function") return false;
597
- const requestId = crypto.randomUUID?.()
598
- ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
599
- record.loading = true;
600
- record.ready = false;
601
- record.loadError = null;
602
- pendingHostActions.set(requestId, { recordKey: record.key, kind: "surface-load" });
603
- globalThis[config.bindingName](JSON.stringify({
604
- token: config.bindingToken,
605
- featureId: record.featureId,
606
- requestId,
607
- action: "load-surface",
608
- payload: {
609
- frameName: record.frame.name,
610
- surfaceUrl: record.surfaceUrl,
611
- },
612
- }));
613
- return true;
614
- }
615
-
616
174
  function updateHostBinding(bindingName, bindingToken) {
617
175
  if (config.bindingName === bindingName && config.bindingToken === bindingToken) return;
618
- // CDP 重连会轮换 token;保留已有页面和草稿,只重试未就绪的页面。
176
+ // CDP 重连会轮换 token;页面脚本保留原状态,只更新后续请求使用的凭据。
619
177
  config.bindingName = bindingName;
620
178
  config.bindingToken = bindingToken;
621
179
  for (const feature of pageScriptFeatures) {
622
180
  feature.pageScript.config.bindingName = bindingName;
623
181
  feature.pageScript.config.bindingToken = bindingToken;
624
182
  }
625
- for (const [requestId, pending] of pendingHostActions) {
626
- if (pending.kind === "surface-load") pendingHostActions.delete(requestId);
627
- }
628
- for (const record of surfaceRecords.values()) {
629
- if (record.ready) continue;
630
- record.loading = false;
631
- requestSurfaceLoad(record);
632
- }
633
- }
634
-
635
- function postSurfaceActive(record, active) {
636
- if (!record || record.kind !== "page") return;
637
- record.frame.contentWindow?.postMessage({
638
- type: "codex-personal:surface-active",
639
- active: Boolean(active),
640
- }, "/");
641
- }
642
-
643
- function nativeSidebarTrigger() {
644
- const triggers = Array.from(document.querySelectorAll(
645
- 'button[data-app-shell-sidebar-trigger="true"]',
646
- ));
647
- return triggers.find((button) => isVisibleToolbarControl(button)) ?? triggers[0] ?? null;
648
- }
649
-
650
- function sidebarState() {
651
- const trigger = nativeSidebarTrigger();
652
- const expanded = trigger?.getAttribute("aria-expanded");
653
- const triggerRect = trigger?.getBoundingClientRect();
654
- return {
655
- available: Boolean(trigger),
656
- open: expanded === null ? sidebarRight() > 0 : expanded === "true",
657
- label: trigger?.getAttribute("aria-label") ?? "Show sidebar",
658
- leading: triggerRect?.left ?? 16,
659
- };
660
- }
661
-
662
- function postSidebarState(record, state = sidebarState()) {
663
- if (!record || record.kind !== "page") return;
664
- record.frame.contentWindow?.postMessage({
665
- type: "codex-personal:sidebar-state",
666
- ...state,
667
- }, "/");
668
- }
669
-
670
- function registerSurface(key, feature, kind, surfaceUrl, element, frame, detailId = null) {
671
- const record = {
672
- key,
673
- featureId: feature.id,
674
- kind,
675
- detailId,
676
- origin: window.location.origin,
677
- surfaceUrl,
678
- element,
679
- frame,
680
- ready: false,
681
- loading: false,
682
- loadError: null,
683
- preferredHeight: null,
684
- };
685
- surfaceRecords.set(key, record);
686
- frame.addEventListener("load", () => {
687
- queueTheme();
688
- postSurfaceActive(record, activePageFeatureId === record.featureId && !record.element.hidden);
689
- postSidebarState(record);
690
- });
691
- requestSurfaceLoad(record);
692
- return record;
693
- }
694
-
695
- function reloadSurfaceIfUnready(key, surfaceUrl) {
696
- const record = surfaceRecords.get(key);
697
- if (!record || record.ready || record.surfaceUrl !== surfaceUrl) return false;
698
- return requestSurfaceLoad(record);
699
- }
700
-
701
- function removeSurfaceRecord(key) {
702
- if (activePromptPreview?.recordKey === key) closePromptPreview({ immediate: true });
703
- surfaceRecords.delete(key);
704
- pendingHostActions.forEach((pending, requestId) => {
705
- if (pending.recordKey !== key) return;
706
- pendingHostActions.delete(requestId);
707
- });
708
- }
709
-
710
- const resolvedTheme=featureUI.theme.read;
711
- resolvedTheme();
712
-
713
- function sendTheme() {
714
- themeQueued = false;
715
- const { style, tokens, mode } = resolvedTheme();
716
- const radiusToken = tokens["--radius-3xl-base"] ?? "20px";
717
- const radiusBase = Number.parseFloat(radiusToken) || 20;
718
- const radiusUnit = radiusToken.match(/[a-z%]+$/i)?.[0] ?? "px";
719
- const radiusScale = Number.parseFloat(tokens["--codex-corner-radius-scale"] ?? "1.25") || 1.25;
720
- const panelRadius = `${radiusBase * radiusScale}${radiusUnit}`;
721
- const panelCornerShape = tokens["--codex-corner-shape"] ?? "superellipse(1.5)";
722
- const dialogRadius = tokens["--radius-3xl"];
723
- const dialogShadow = `0 0 0 .5px ${tokens["--color-border"]},${tokens["--shadow-lg"]}`;
724
- const surfaceTokens = {
725
- ...tokens,
726
- // 部分 Codex 版本仅提供等价的正文前景 token。
727
- "--color-token-input-foreground": tokens["--color-token-input-foreground"] ?? tokens["--color-text-foreground"],
728
- "--codex-summary-section-title-font-size": "14px",
729
- "--codex-summary-section-title-line-height": "21px",
730
- "--codex-summary-section-title-font-weight": tokens["--font-weight-normal"] ?? "400",
731
- "--codex-summary-row-font-size": "16px",
732
- "--codex-summary-row-line-height": "24px",
733
- "--codex-summary-label-font-size": tokens["--text-base"] ?? "14px",
734
- "--codex-summary-label-line-height": tokens["--text-base--line-height"] ?? "1.5",
735
- "--codex-summary-font-weight": style.fontWeight || "445",
736
- "--codex-summary-icon-size": "18px",
737
- "--codex-summary-icon-stroke-width": "1.5px",
738
- "--codex-summary-panel-radius": panelRadius,
739
- "--codex-summary-panel-corner-shape": panelCornerShape,
740
- "--codex-dialog-radius": dialogRadius,
741
- "--codex-dialog-corner-shape": "round",
742
- "--codex-dialog-shadow": dialogShadow,
743
- "--codex-summary-row-radius": tokens["--radius-sm"] ?? "7.5px",
744
- "--codex-summary-control-radius": tokens["--radius-md"] ?? "10px",
745
- "--codex-summary-section-title-color": tokens["--color-token-text-tertiary"] ?? "rgba(13,13,13,.484)",
746
- "--codex-summary-icon-primary": tokens["--color-icon-primary"] ?? tokens["--color-token-foreground"] ?? style.color,
747
- "--codex-summary-icon-secondary": tokens["--color-icon-secondary"] ?? tokens["--color-token-text-secondary"] ?? style.color,
748
- "--codex-summary-icon-tertiary": tokens["--color-icon-tertiary"] ?? tokens["--color-token-text-tertiary"] ?? style.color,
749
- };
750
- for (const record of surfaceRecords.values()) {
751
- if (!record.element.isConnected) continue;
752
- if (record.kind === "pinned") {
753
- record.element.style.borderRadius = panelRadius;
754
- record.element.style.setProperty("corner-shape", panelCornerShape);
755
- record.element.style.boxShadow = tokens["--elevation-prominent"]
756
- ?? "0 0 0 .5px rgba(13,13,13,.11),0 3px 7.5px rgba(0,0,0,.04),0 0 20px rgba(0,0,0,.05)";
757
- }
758
- if (record.kind !== "modal") record.element.style.background = record.kind === "pinned"
759
- ? tokens["--color-token-dropdown-background"]
760
- ?? tokens["--color-token-main-surface-primary"]
761
- ?? style.backgroundColor
762
- : tokens["--color-token-main-surface-primary"] ?? style.backgroundColor;
763
- record.frame.contentWindow?.postMessage(
764
- { type: "codex-personal:theme", theme: mode, mode, locale, tokens: surfaceTokens },
765
- "/",
766
- );
767
- }
768
- }
769
-
770
- function queueTheme() {
771
- if (themeQueued) return;
772
- themeQueued = true;
773
- queueMicrotask(sendTheme);
774
- }
775
-
776
- function sidebarRight() {
777
- const sidebar = document.querySelector("[data-app-action-sidebar-scroll]");
778
- return sidebar?.getBoundingClientRect().right ?? 0;
779
183
  }
780
184
 
781
- // Windows 版 Codex 在 renderer 顶部渲染横贯全宽的应用菜单条
782
- // (_ApplicationMenuTopBar),page surface 必须避开该条并以内嵌卡片
783
- // 形式呈现(左上圆角)。macOS 无此元素,inset 为 0,布局保持不变。
784
- function applicationMenuTopInset() {
785
- const menuBar = document.querySelector('[class*="_ApplicationMenuTopBar_"]');
786
- if (!menuBar?.isConnected) return 0;
787
- const bottom = menuBar.getBoundingClientRect().bottom;
788
- return Number.isFinite(bottom) && bottom > 0 ? Math.round(bottom) : 0;
789
- }
790
-
791
- function updatePageSurfacePositions() {
792
- const state = sidebarState();
793
- const left = `${state.open ? Math.max(0, sidebarRight()) : 0}px`;
794
- const topInset = applicationMenuTopInset();
795
- const embeddedCard = topInset > 0;
796
- for (const [featureId, surface] of pageSurfaces) {
797
- surface.style.left = left;
798
- surface.style.top = `${topInset}px`;
799
- surface.style.borderTopLeftRadius = embeddedCard ? "12px" : "";
800
- surface.style.overflow = embeddedCard ? "hidden" : "";
801
- postSidebarState(surfaceRecords.get(surfaceKey(featureId, "page")), state);
802
- }
803
- }
804
-
805
- function syncAfterNativeSidebarToggle() {
806
- queueMicrotask(updatePageSurfacePositions);
807
- }
185
+ // ========================================================================
186
+ // Composer 文本插入 — 供 Notes 快捷提示词等页面脚本复用
187
+ // ========================================================================
808
188
 
809
189
  function threadIdentity() {
810
190
  const selected = document.querySelector('[data-app-action-sidebar-thread-selected="true"]');
@@ -814,21 +194,6 @@ export function createInjectionSource(features, {
814
194
  return `${project ?? ""}:${id ?? ""}:${location.pathname}:${location.search}`;
815
195
  }
816
196
 
817
- function checkThreadChange() {
818
- const next = threadIdentity();
819
- if (currentThread === null) {
820
- currentThread = next;
821
- return;
822
- }
823
- if (next === currentThread) return;
824
- currentThread = next;
825
- stopToolbarReadiness();
826
- savedComposerRange = null;
827
- savedComposerThread = null;
828
- hidePinnedSurfaces();
829
- queueEnsure();
830
- }
831
-
832
197
  function currentComposer() {
833
198
  return codex.composer(null, { purpose: "insertion" })?.editable ?? null;
834
199
  }
@@ -875,1179 +240,15 @@ export function createInjectionSource(features, {
875
240
  return { mode: "inserted", fallback };
876
241
  }
877
242
 
878
- function suppressNativeSelections() {
879
- const sidebar = document.querySelector("[data-app-action-sidebar-scroll]");
880
- if (!sidebar) return;
881
- const alreadySuppressed = new Set(suppressedSelections.map(({ element }) => element));
882
- const selected = new Set([
883
- ...sidebar.querySelectorAll('[aria-current="page"]'),
884
- ...sidebar.querySelectorAll('[data-app-action-sidebar-thread-selected="true"]'),
885
- ]);
886
- for (const element of selected) {
887
- if (element.hasAttribute(entryMarker) || alreadySuppressed.has(element)) continue;
888
- const selection = {
889
- element,
890
- ariaCurrent: element.getAttribute("aria-current"),
891
- threadSelected: element.getAttribute("data-app-action-sidebar-thread-selected"),
892
- hadPrimaryGhost: element.classList.contains("bg-primary-ghost-hover"),
893
- hadHoverPrimaryGhost: element.classList.contains("hover:bg-primary-ghost-hover"),
894
- hadActiveClass: element.classList.contains("bg-token-list-hover-background"),
895
- hadHoverClass: element.classList.contains("hover:bg-token-list-hover-background"),
896
- };
897
- suppressedSelections.push(selection);
898
- element.removeAttribute("aria-current");
899
- element.removeAttribute("data-app-action-sidebar-thread-selected");
900
- // 原生选中态(灰底)现在由 bg-primary-ghost-hover 承载,需一并移除;
901
- // 旧 token 类仍保留处理,以便兼容旧主题。
902
- element.classList.remove("bg-primary-ghost-hover");
903
- element.classList.remove("bg-token-list-hover-background");
904
- if (!selection.hadHoverPrimaryGhost) element.classList.add("hover:bg-primary-ghost-hover");
905
- element.classList.add("hover:bg-token-list-hover-background");
906
- }
907
- }
908
-
909
- function restoreNativeSelections() {
910
- for (const selection of suppressedSelections) {
911
- const { element } = selection;
912
- if (!element.isConnected) continue;
913
- if (selection.ariaCurrent !== null) element.setAttribute("aria-current", selection.ariaCurrent);
914
- if (selection.threadSelected !== null) {
915
- element.setAttribute("data-app-action-sidebar-thread-selected", selection.threadSelected);
916
- }
917
- element.classList.toggle("bg-primary-ghost-hover", Boolean(selection.hadPrimaryGhost));
918
- element.classList.toggle("hover:bg-primary-ghost-hover", Boolean(selection.hadHoverPrimaryGhost));
919
- element.classList.toggle("bg-token-list-hover-background", selection.hadActiveClass);
920
- element.classList.toggle("hover:bg-token-list-hover-background", selection.hadHoverClass);
921
- }
922
- suppressedSelections = [];
923
- }
924
-
925
- function updateSidebarSelectedState() {
926
- for (const feature of sidebarFeatures) {
927
- const entry = findEntry(feature.id);
928
- if (!entry) continue;
929
- const selected = feature.id === activePageFeatureId;
930
- entry.classList.toggle("bg-primary-ghost-hover", selected);
931
- entry.classList.toggle("bg-token-list-hover-background", selected);
932
- entry.classList.toggle("hover:bg-token-list-hover-background", !selected);
933
- if (selected) entry.setAttribute("aria-current", "page");
934
- else entry.removeAttribute("aria-current");
935
- }
936
- if (activePageFeatureId) suppressNativeSelections();
937
- else restoreNativeSelections();
938
- }
939
-
940
- function updateToolbarState() {
941
- const displayMode = summaryDisplayMode();
942
- for (const feature of toolbarFeatures) {
943
- const entry = findEntry(feature.id);
944
- if (!entry) continue;
945
- const open = visibleSummarySurface(feature.id, displayMode) !== null;
946
- const label = summaryButtonLabel(feature, displayMode);
947
- const pressed = String(open);
948
- const state = open ? "open" : "closed";
949
- if (entry.getAttribute("aria-label") !== label) entry.setAttribute("aria-label", label);
950
- if (entry.getAttribute("data-minecodex-tooltip") !== label) entry.setAttribute("data-minecodex-tooltip", label);
951
- if (entry.getAttribute("aria-pressed") !== pressed) entry.setAttribute("aria-pressed", pressed);
952
- if (entry.getAttribute("aria-expanded") !== pressed) entry.setAttribute("aria-expanded", pressed);
953
- if (entry.getAttribute("data-state") !== state) entry.setAttribute("data-state", state);
954
- entry.setAttribute("data-codex-personal-summary-mode", displayMode);
955
- }
956
- }
957
-
958
243
  // ========================================================================
959
- // Page/Pinned/Modal surface 与摘要呈现 — 页面/固定/模态 surface 创建、展示与响应式摘要
244
+ // 刷新调度 — DOM 变化后合并到下一帧刷新 Model Selector
960
245
  // ========================================================================
961
246
 
962
- function createFrame(featureId, title) {
963
- const frame = document.createElement("iframe");
964
- frame.name = surfaceFrameName(featureId);
965
- frame.src = "about:blank";
966
- frame.title = title;
967
- frame.allow = "clipboard-write";
968
- frame.style.cssText = "width:100%;height:100%;border:0;display:block;background:transparent";
969
- // 图片页覆盖原生标题栏,避免控件点击被窗口拖拽区域吞掉。
970
- if (featureId === "images") frame.style.setProperty("-webkit-app-region", "no-drag");
971
- return frame;
972
- }
973
-
974
- function createPageSurface(feature) {
975
- const surface = document.createElement("section");
976
- surface.setAttribute(pageSurfaceMarker, feature.id);
977
- featureUI.page(surface, { transparent:true });
978
- surface.style.cssText = [
979
- "top:0",
980
- "right:0",
981
- "bottom:0",
982
- "background:transparent",
983
- ].join(";");
984
- const frame = createFrame(feature.id, feature.label);
985
- surface.append(frame);
986
- document.body.append(surface);
987
- pageSurfaces.set(feature.id, surface);
988
- registerSurface(surfaceKey(feature.id, "page"), feature, "page", feature.surfaceUrl, surface, frame);
989
- return surface;
990
- }
991
-
992
- function showPageSurface(featureId) {
993
- const feature = featureById.get(featureId);
994
- if (!feature) return;
995
- hidePinnedSurfaces();
996
- for (const [otherFeatureId, surface] of pageSurfaces) {
997
- if (otherFeatureId === featureId) continue;
998
- surface.hidden = true;
999
- const record = surfaceRecords.get(surfaceKey(otherFeatureId, "page"));
1000
- postSurfaceActive(record, false);
1001
- }
1002
- const existing = pageSurfaces.get(featureId);
1003
- const reusable = existing?.isConnected ? existing : null;
1004
- const surface = reusable ?? createPageSurface(feature);
1005
- if (reusable) reloadSurfaceIfUnready(surfaceKey(feature.id, "page"), feature.surfaceUrl);
1006
- surface.hidden = false;
1007
- activePageFeatureId = featureId;
1008
- const record = surfaceRecords.get(surfaceKey(feature.id, "page"));
1009
- postSurfaceActive(record, true);
1010
- updatePageSurfacePositions();
1011
- updateSidebarSelectedState();
1012
- queueTheme();
1013
- }
1014
-
1015
- function hidePageSurfaces() {
1016
- for (const [featureId, surface] of pageSurfaces) {
1017
- surface.hidden = true;
1018
- const record = surfaceRecords.get(surfaceKey(featureId, "page"));
1019
- postSurfaceActive(record, false);
1020
- }
1021
- activePageFeatureId = null;
1022
- updateSidebarSelectedState();
1023
- }
1024
-
1025
- function closeNativeSummary() {
1026
- const toggle = nativeSummaryButton();
1027
- if (!toggle) return;
1028
- if (toggle.getAttribute("aria-expanded") === "true" || toggle.getAttribute("aria-pressed") === "true") {
1029
- toggle.click();
1030
- }
1031
- }
1032
-
1033
- function restoreThreadContentShift() {
1034
- for (const { element, shift, translate } of shiftedConversationOwners) {
1035
- if (!element.isConnected) continue;
1036
- if (shift.value) {
1037
- element.style.setProperty(
1038
- "--thread-wide-block-inline-shift",
1039
- shift.value,
1040
- shift.priority,
1041
- );
1042
- } else {
1043
- element.style.removeProperty("--thread-wide-block-inline-shift");
1044
- }
1045
- if (translate.value) element.style.setProperty("translate", translate.value, translate.priority);
1046
- else element.style.removeProperty("translate");
1047
- element.removeAttribute("data-codex-personal-summary-shift");
1048
- }
1049
- shiftedConversationOwners = [];
1050
- }
1051
-
1052
- function applyThreadContentShift(value) {
1053
- if (value === 0) {
1054
- restoreThreadContentShift();
1055
- return;
1056
- }
1057
- const viewport = mainContentViewport();
1058
- const owners = viewport
1059
- ? Array.from(viewport.querySelectorAll('[style*="--thread-wide-block-inline-shift"]'))
1060
- : [];
1061
- if (!owners.length) {
1062
- restoreThreadContentShift();
1063
- return;
1064
- }
1065
- if (
1066
- shiftedConversationOwners.length !== owners.length
1067
- || owners.some((owner, index) => shiftedConversationOwners[index]?.element !== owner)
1068
- ) {
1069
- restoreThreadContentShift();
1070
- shiftedConversationOwners = owners.map((owner) => ({
1071
- element: owner,
1072
- shift: {
1073
- value: owner.style.getPropertyValue("--thread-wide-block-inline-shift"),
1074
- priority: owner.style.getPropertyPriority("--thread-wide-block-inline-shift"),
1075
- },
1076
- translate: {
1077
- value: owner.style.getPropertyValue("translate"),
1078
- priority: owner.style.getPropertyPriority("translate"),
1079
- },
1080
- }));
1081
- }
1082
- for (const owner of owners) {
1083
- owner.style.setProperty(
1084
- "--thread-wide-block-inline-shift",
1085
- `${Number(value)}px`,
1086
- "important",
1087
- );
1088
- owner.style.setProperty("translate", `${Number(value)}px 0`, "important");
1089
- owner.setAttribute("data-codex-personal-summary-shift", String(value));
1090
- }
1091
- }
1092
-
1093
- function setSummarySurfaceVisible(surface, visible, mode) {
1094
- const wasVisible = surface.dataset.codexPersonalVisible === "true";
1095
- const previousMode = surface.dataset.codexPersonalMode;
1096
- const duration = reducedMotion() ? 0 : 300;
1097
- surface.dataset.codexPersonalVisible = String(visible);
1098
- surface.dataset.codexPersonalMode = mode;
1099
- surface.style.transition = duration
1100
- ? "opacity 180ms ease, transform 300ms cubic-bezier(.2,.8,.2,1)"
1101
- : "none";
1102
- if (!visible) {
1103
- closePromptPreview({ immediate: true });
1104
- surface.style.pointerEvents = "none";
1105
- surface.style.opacity = "0";
1106
- surface.style.transform = mode === "overlay"
1107
- ? "translateY(-4px) scale(.98)"
1108
- : "translateX(100%) scale(.8)";
1109
- const transitionId = `${performance.now()}-${Math.random()}`;
1110
- surface.dataset.codexPersonalTransition = transitionId;
1111
- if (duration === 0) surface.hidden = true;
1112
- else window.setTimeout(() => {
1113
- if (
1114
- surface.dataset.codexPersonalTransition === transitionId
1115
- && surface.dataset.codexPersonalVisible !== "true"
1116
- ) surface.hidden = true;
1117
- }, duration);
1118
- return;
1119
- }
1120
-
1121
- surface.dataset.codexPersonalTransition = "visible";
1122
- surface.hidden = false;
1123
- surface.style.pointerEvents = "auto";
1124
- if (!wasVisible || previousMode !== mode) {
1125
- surface.style.opacity = "0";
1126
- surface.style.transform = mode === "overlay"
1127
- ? "translateY(-4px) scale(.98)"
1128
- : "translateX(100%) scale(.8)";
1129
- const reveal = () => {
1130
- if (surface.dataset.codexPersonalVisible !== "true") return;
1131
- surface.style.opacity = "1";
1132
- surface.style.transform = "translate(0, 0) scale(1)";
1133
- };
1134
- requestAnimationFrame(reveal);
1135
- window.setTimeout(reveal, 50);
1136
- if (duration) window.setTimeout(() => {
1137
- if (surface.dataset.codexPersonalVisible !== "true") return;
1138
- surface.style.transition = "none";
1139
- surface.style.opacity = "1";
1140
- surface.style.transform = "translate(0, 0) scale(1)";
1141
- }, duration + 50);
1142
- } else {
1143
- surface.style.opacity = "1";
1144
- surface.style.transform = "translate(0, 0) scale(1)";
1145
- }
1146
- }
1147
-
1148
- function updatePinnedPosition(featureId) {
1149
- const surface = pinnedSurfaces.get(featureId);
1150
- const entry = findEntry(featureId);
1151
- const feature = featureById.get(featureId);
1152
- if (!surface || !entry || !feature) return;
1153
- const layout = config.summaryLayout;
1154
- const displayMode = summaryDisplayMode();
1155
- const visibleSurface = visibleSummarySurface(featureId, displayMode);
1156
- if (!visibleSurface) {
1157
- setSummarySurfaceVisible(surface, false, displayMode);
1158
- if (activePinnedFeatureId === featureId) applyThreadContentShift(0);
1159
- return;
1160
- }
1161
-
1162
- const target = mainContentTargetGeometry();
1163
- const entryRect = entry.getBoundingClientRect();
1164
- const scrollRect = mainContentViewport()?.querySelector(".thread-scroll-container")
1165
- ?.getBoundingClientRect();
1166
- // The native conversation header is a fixed overlay that starts above the
1167
- // thread scroll container (scroll top is often 0 while the header still
1168
- // occupies the top toolbar strip). Anchor below the header so the pinned
1169
- // surface never covers the toolbar entry used to close it again.
1170
- const header = entry.closest("header");
1171
- const headerBottom = header?.getBoundingClientRect().bottom ?? 0;
1172
- const contentTop = Math.max(scrollRect?.top ?? 0, headerBottom);
1173
- const top = displayMode === "overlay"
1174
- ? entryRect.bottom + layout.popoverOffset
1175
- : (contentTop || 46) + layout.verticalInset;
1176
- const right = displayMode === "overlay"
1177
- ? Math.max(layout.popoverOffset, innerWidth - entryRect.right)
1178
- : Math.max(layout.panelInset, innerWidth - target.right + layout.panelInset);
1179
- const bottomInset = displayMode === "overlay"
1180
- ? layout.popoverBottomInset
1181
- : layout.verticalInset;
1182
- const available = Math.max(120, innerHeight - top - bottomInset);
1183
- const record = surfaceRecords.get(surfaceKey(featureId, "pinned"));
1184
- const height = Math.min(record?.preferredHeight ?? 620, available);
1185
- Object.assign(surface.style, {
1186
- top: `${top}px`,
1187
- right: `${right}px`,
1188
- width: `${layout.panelWidth}px`,
1189
- height: `${height}px`,
1190
- maxHeight: `${available}px`,
1191
- });
1192
- surface.setAttribute("data-codex-personal-summary-presentation", displayMode);
1193
- setSummarySurfaceVisible(surface, true, displayMode);
1194
-
1195
- const shift = displayMode !== "overlay"
1196
- ? -(layout.panelWidth + layout.panelInset) / 2
1197
- : 0;
1198
- applyThreadContentShift(shift);
1199
- }
1200
-
1201
- function createPinnedSurface(feature) {
1202
- const definition = feature.pinnedSummary;
1203
- const surface = document.createElement("section");
1204
- surface.setAttribute(pinnedSurfaceMarker, feature.id);
1205
- surface.setAttribute("role", "dialog");
1206
- surface.setAttribute("aria-label", definition.label ?? feature.label);
1207
- surface.hidden = true;
1208
- featureUI.page(surface, { transparent:true });
1209
- surface.style.cssText = [
1210
- "overflow:hidden",
1211
- "border-radius:25px",
1212
- "corner-shape:var(--codex-corner-shape,superellipse(1.5))",
1213
- "background:var(--color-token-dropdown-background,var(--color-token-main-surface-primary))",
1214
- "box-shadow:var(--elevation-prominent,0 0 0 .5px rgba(13,13,13,.11),0 3px 7.5px rgba(0,0,0,.04),0 0 20px rgba(0,0,0,.05))",
1215
- "transform-origin:top right",
1216
- "opacity:0",
1217
- "pointer-events:none",
1218
- "will-change:transform,opacity",
1219
- ].join(";");
1220
- const frame = createFrame(feature.id, definition.label ?? feature.label);
1221
- surface.append(frame);
1222
- document.body.append(surface);
1223
- pinnedSurfaces.set(feature.id, surface);
1224
- registerSurface(
1225
- surfaceKey(feature.id, "pinned"),
1226
- feature,
1227
- "pinned",
1228
- definition.surfaceUrl,
1229
- surface,
1230
- frame,
1231
- );
1232
- return surface;
1233
- }
1234
-
1235
- function closePromptPreview({ immediate = false } = {}) {
1236
- clearTimeout(promptPreviewDismissTimer);
1237
- promptPreviewDismissTimer = null;
1238
- const current = activePromptPreview;
1239
- if (!current) return false;
1240
- activePromptPreview = null;
1241
- const remove = () => current.element.remove();
1242
- if (immediate || reducedMotion()) remove();
1243
- else {
1244
- current.element.style.pointerEvents = "none";
1245
- current.element.style.opacity = "0";
1246
- window.setTimeout(remove, 80);
1247
- }
1248
- return true;
1249
- }
1250
-
1251
- function requestPromptPreviewClose(record) {
1252
- if (!activePromptPreview || activePromptPreview.recordKey !== record.key) return false;
1253
- clearTimeout(promptPreviewDismissTimer);
1254
- promptPreviewDismissTimer = window.setTimeout(() => {
1255
- if (!activePromptPreview || activePromptPreview.recordKey !== record.key) return;
1256
- if (activePromptPreview.element.matches(":hover")) return;
1257
- closePromptPreview();
1258
- }, 120);
1259
- return true;
1260
- }
1261
-
1262
- function showPromptPreview(record, payload = {}) {
1263
- if (record.kind !== "pinned" || record.element.hidden) return false;
1264
- const body = String(payload.prompt?.body ?? "");
1265
- if (!body.trim()) return false;
1266
- const title = String(payload.prompt?.title ?? "").trim();
1267
- const surfaceLeft = record.element.offsetLeft;
1268
- const surfaceTop = record.element.offsetTop;
1269
- const anchorTop = Number(payload.anchor?.top);
1270
- if (!Number.isFinite(anchorTop) || record.element.offsetWidth <= 0 || record.element.offsetHeight <= 0) return false;
1271
-
1272
- closePromptPreview({ immediate: true });
1273
- const preview = document.createElement("aside");
1274
- preview.setAttribute(promptPreviewMarker, record.featureId);
1275
- preview.setAttribute("role", "tooltip");
1276
- preview.setAttribute("aria-label", locale === "zh-CN" ? "提示词预览" : "Prompt preview");
1277
- if (payload.prompt?.id) preview.setAttribute("data-codex-personal-prompt-id", String(payload.prompt.id));
1278
- preview.style.cssText = [
1279
- "position:fixed",
1280
- "z-index:41",
1281
- "box-sizing:border-box",
1282
- "display:flex",
1283
- "flex-direction:column",
1284
- "gap:8px",
1285
- "max-height:min(420px,calc(100vh - 24px))",
1286
- "overflow:hidden",
1287
- "padding:14px 16px 15px",
1288
- "border:.5px solid var(--color-token-border,var(--color-border,rgba(13,13,13,.11)))",
1289
- "border-radius:16px",
1290
- "corner-shape:var(--codex-corner-shape,superellipse(1.5))",
1291
- "color:var(--color-token-foreground,var(--color-text-foreground,#0d0d0d))",
1292
- "background:var(--color-token-dropdown-background,var(--color-token-main-surface-primary,#fff))",
1293
- "box-shadow:var(--elevation-prominent,0 0 0 .5px rgba(13,13,13,.08),0 8px 24px rgba(0,0,0,.10))",
1294
- "font-family:var(--font-sans-default,ui-sans-serif,system-ui,sans-serif)",
1295
- "pointer-events:auto",
1296
- "transition:opacity 80ms ease",
1297
- ].join(";");
1298
-
1299
- if (title) {
1300
- const heading = document.createElement("strong");
1301
- heading.textContent = title.slice(0, 500);
1302
- heading.style.cssText = [
1303
- "display:-webkit-box",
1304
- "overflow:hidden",
1305
- "-webkit-box-orient:vertical",
1306
- "-webkit-line-clamp:2",
1307
- "font-size:15px",
1308
- "font-weight:550",
1309
- "line-height:21px",
1310
- "overflow-wrap:anywhere",
1311
- ].join(";");
1312
- preview.append(heading);
1313
- }
1314
- const content = document.createElement("div");
1315
- content.textContent = body;
1316
- content.style.cssText = [
1317
- "min-height:0",
1318
- "overflow:auto",
1319
- "padding-right:2px",
1320
- "white-space:pre-wrap",
1321
- "overflow-wrap:anywhere",
1322
- "font-size:14px",
1323
- "font-weight:400",
1324
- "line-height:20px",
1325
- ].join(";");
1326
- preview.append(content);
1327
-
1328
- const inset = 8;
1329
- const gap = 10;
1330
- const availableLeft = Math.max(160, surfaceLeft - gap - inset);
1331
- const width = Math.min(360, availableLeft);
1332
- preview.style.width = `${width}px`;
1333
- document.body.append(preview);
1334
- const previewRect = preview.getBoundingClientRect();
1335
- const left = Math.max(inset, surfaceLeft - gap - width);
1336
- const desiredTop = surfaceTop + anchorTop - 8;
1337
- const top = Math.min(
1338
- Math.max(12, desiredTop),
1339
- Math.max(12, innerHeight - previewRect.height - 12),
1340
- );
1341
- preview.style.left = `${left}px`;
1342
- preview.style.top = `${top}px`;
1343
- activePromptPreview = { element: preview, recordKey: record.key };
1344
-
1345
- preview.addEventListener("pointerenter", () => {
1346
- clearTimeout(promptPreviewDismissTimer);
1347
- promptPreviewDismissTimer = null;
1348
- }, { signal: lifetime.signal });
1349
- preview.addEventListener("pointerleave", () => closePromptPreview(), { signal: lifetime.signal });
1350
- return true;
1351
- }
1352
-
1353
- function closeModalSurface() {
1354
- if (!activeModalRecordKey) return false;
1355
- const record = surfaceRecords.get(activeModalRecordKey);
1356
- record?.overlay?.remove();
1357
- record?.element?.remove();
1358
- if (record) removeSurfaceRecord(record.key);
1359
- activeModalRecordKey = null;
1360
- return true;
1361
- }
1362
-
1363
- function openEditorModal(sourceRecord, payload = {}) {
1364
- const feature = featureById.get(sourceRecord.featureId);
1365
- const kind = payload.kind === "todo"
1366
- ? "todo"
1367
- : new Set(["prompt", "note"]).has(payload.kind)
1368
- ? "prompt"
1369
- : null;
1370
- if (!feature || !kind) throw new Error("Editor kind must be prompt or todo");
1371
-
1372
- closePromptPreview({ immediate: true });
1373
- closeModalSurface();
1374
- const editorUrl = new URL(sourceRecord.surfaceUrl);
1375
- editorUrl.searchParams.set("surface", "editor");
1376
- editorUrl.searchParams.set("kind", kind);
1377
- if (typeof payload.id === "string" && payload.id) editorUrl.searchParams.set("id", payload.id);
1378
- else editorUrl.searchParams.delete("id");
1379
-
1380
- const overlay = document.createElement("div");
1381
- overlay.setAttribute("data-codex-personal-modal-overlay", feature.id);
1382
- featureUI.overlay(overlay);
1383
- overlay.setAttribute("aria-hidden", "true");
1384
- overlay.style.cssText = [
1385
- "position:fixed",
1386
- "inset:0",
1387
- "z-index:50",
1388
- ].join(";");
1389
-
1390
- const dialog = document.createElement("section");
1391
- dialog.setAttribute(modalSurfaceMarker, feature.id);
1392
- featureUI.surface(dialog, "dialog");
1393
- dialog.setAttribute("role", "dialog");
1394
- dialog.setAttribute("aria-modal", "true");
1395
- dialog.setAttribute("aria-label", `${payload.id ? "Edit" : "Add"} ${kind}`);
1396
- dialog.setAttribute("tabindex", "-1");
1397
- dialog.style.cssText = [
1398
- "position:fixed",
1399
- "left:50%",
1400
- "top:50%",
1401
- "z-index:51",
1402
- "width:min(420px,92vw)",
1403
- `height:${kind === "prompt" ? "250px" : "198px"}`,
1404
- "max-height:88vh",
1405
- "transform:translate(-50%,-50%)",
1406
- "overflow:hidden",
1407
- "padding:0",
1408
- "outline:none",
1409
- ].join(";");
1410
-
1411
- const frame = createFrame(feature.id, `${payload.id ? "Edit" : "Add"} ${kind}`);
1412
- dialog.append(frame);
1413
- document.body.append(overlay, dialog);
1414
- const key = surfaceKey(feature.id, "modal");
1415
- const record = registerSurface(key, feature, "modal", editorUrl.href, dialog, frame);
1416
- record.overlay = overlay;
1417
- activeModalRecordKey = key;
1418
- overlay.addEventListener("click", closeModalSurface, { signal: lifetime.signal });
1419
- queueTheme();
1420
- return { opened: true };
1421
- }
1422
-
1423
- function showPinnedSurface(featureId) {
1424
- const feature = featureById.get(featureId);
1425
- if (!feature?.pinnedSummary) return;
1426
- hidePageSurfaces();
1427
- closeNativeSummary();
1428
- for (const [id, state] of summaryStates) {
1429
- if (id === featureId) continue;
1430
- state.isPinned = false;
1431
- state.isPopoverOpen = false;
1432
- const other = pinnedSurfaces.get(id);
1433
- if (other) setSummarySurfaceVisible(other, false, summaryDisplayMode());
1434
- }
1435
- const existing = pinnedSurfaces.get(featureId);
1436
- const surface = existing ?? createPinnedSurface(feature);
1437
- if (existing) {
1438
- reloadSurfaceIfUnready(
1439
- surfaceKey(feature.id, "pinned"),
1440
- feature.pinnedSummary.surfaceUrl,
1441
- );
1442
- }
1443
- const displayMode = summaryDisplayMode();
1444
- const state = summaryState(featureId);
1445
- if (displayMode === "overlay") state.isPopoverOpen = true;
1446
- else state.isPinned = true;
1447
- activePinnedFeatureId = featureId;
1448
- updateToolbarState();
1449
- updatePinnedPosition(featureId);
1450
- queueTheme();
1451
- }
1452
-
1453
- function hidePinnedSurfaces() {
1454
- const displayMode = summaryDisplayMode();
1455
- for (const [featureId, state] of summaryStates) {
1456
- state.isPinned = false;
1457
- state.isPopoverOpen = false;
1458
- const surface = pinnedSurfaces.get(featureId);
1459
- if (surface) setSummarySurfaceVisible(surface, false, displayMode);
1460
- }
1461
- activePinnedFeatureId = null;
1462
- restoreThreadContentShift();
1463
- updateToolbarState();
1464
- }
1465
-
1466
- function togglePinnedSurface(featureId) {
1467
- const feature = featureById.get(featureId);
1468
- if (!feature?.pinnedSummary) return;
1469
- const displayMode = summaryDisplayMode();
1470
- const state = summaryState(featureId);
1471
- const isOpen = visibleSummarySurface(featureId, displayMode) !== null;
1472
- if (!isOpen) {
1473
- showPinnedSurface(featureId);
1474
- return;
1475
- }
1476
- if (displayMode === "overlay") state.isPopoverOpen = false;
1477
- else state.isPinned = false;
1478
- if (!state.isPinned && !state.isPopoverOpen) activePinnedFeatureId = null;
1479
- updatePinnedPosition(featureId);
1480
- if (!activePinnedFeatureId) restoreThreadContentShift();
1481
- updateToolbarState();
1482
- }
1483
-
1484
- function updateResponsiveSummaryPresentation() {
1485
- const nextMode = summaryDisplayMode();
1486
- if (currentSummaryDisplayMode === "overlay" && nextMode !== "overlay") {
1487
- for (const state of summaryStates.values()) state.isPopoverOpen = false;
1488
- }
1489
- currentSummaryDisplayMode = nextMode;
1490
- for (const feature of toolbarFeatures) updatePinnedPosition(feature.id);
1491
- if (!toolbarFeatures.some((feature) => (
1492
- visibleSummarySurface(feature.id, nextMode) === "inline"
1493
- ))) applyThreadContentShift(0);
1494
- updateToolbarState();
1495
- }
1496
-
1497
- function ensureMainContentObserver() {
1498
- const viewport = mainContentViewport();
1499
- if (!viewport || mainContentObserver?.target === viewport) return;
1500
- mainContentObserver?.observer.disconnect();
1501
- const observer = new ResizeObserver(() => updateResponsiveSummaryPresentation());
1502
- observer.observe(viewport);
1503
- mainContentObserver = { target: viewport, observer };
1504
- }
1505
-
1506
- // ========================================================================
1507
- // 原生 Detail Tab capability — React fiber/右侧面板探测、原生详情页签打开与线程跳转
1508
- // ========================================================================
1509
-
1510
-
1511
- function reactFiberFor(element) {
1512
- if (!(element instanceof Element)) return null;
1513
- const key = Object.getOwnPropertyNames(element).find((name) => (
1514
- name.startsWith("__reactFiber$")
1515
- || name.startsWith("__reactInternalInstance$")
1516
- ));
1517
- return key ? element[key] : null;
1518
- }
1519
-
1520
- function currentRightPanelScope() {
1521
- let anchor = document.querySelector('[data-app-shell-tabs="true"]');
1522
- let anchorFiber = reactFiberFor(anchor);
1523
- if (!anchorFiber) {
1524
- for (const element of document.querySelectorAll("body *")) {
1525
- const fiber = reactFiberFor(element);
1526
- if (!fiber) continue;
1527
- anchor = element;
1528
- anchorFiber = fiber;
1529
- break;
1530
- }
1531
- }
1532
- if (!anchorFiber) return null;
1533
-
1534
- let rootFiber = anchorFiber;
1535
- while (rootFiber.return) rootFiber = rootFiber.return;
1536
-
1537
- const isRightPanelScope = (value) => (
1538
- value
1539
- && typeof value === "object"
1540
- && typeof value.get === "function"
1541
- && typeof value.set === "function"
1542
- && rightPanelRouteKinds.has(value.value?.routeKind)
1543
- && typeof value.query?.getOrFetch === "function"
1544
- );
1545
- const inspected = new Set();
1546
- const inspect = (value) => {
1547
- if (
1548
- value == null
1549
- || (typeof value !== "object" && typeof value !== "function")
1550
- || inspected.has(value)
1551
- ) return null;
1552
- inspected.add(value);
1553
- if (isRightPanelScope(value)) return value;
1554
- if (isRightPanelScope(value.current)) return value.current;
1555
- return null;
1556
- };
1557
-
1558
- const stack = [rootFiber];
1559
- let visitedFibers = 0;
1560
- while (stack.length > 0 && visitedFibers < 50_000) {
1561
- const fiber = stack.pop();
1562
- visitedFibers += 1;
1563
- let scope = inspect(fiber.memoizedProps);
1564
- if (!scope && fiber.memoizedProps && typeof fiber.memoizedProps === "object") {
1565
- for (const value of Object.values(fiber.memoizedProps)) {
1566
- scope = inspect(value);
1567
- if (scope) break;
1568
- }
1569
- }
1570
-
1571
- let hook = fiber.memoizedState;
1572
- let hookIndex = 0;
1573
- while (!scope && hook && typeof hook === "object" && hookIndex < 140) {
1574
- scope = inspect(hook.memoizedState);
1575
- if (!scope && hook.memoizedState && typeof hook.memoizedState === "object") {
1576
- scope = inspect(hook.memoizedState.current);
1577
- }
1578
- hook = hook.next;
1579
- hookIndex += 1;
1580
- }
1581
- if (scope) return scope;
1582
- if (fiber.sibling) stack.push(fiber.sibling);
1583
- if (fiber.child) stack.push(fiber.child);
1584
- }
1585
- return null;
1586
- }
1587
-
1588
- async function waitForCurrentRightPanelScope() {
1589
- const deadline = performance.now() + 2_000;
1590
- do {
1591
- const scope = currentRightPanelScope();
1592
- if (scope) return scope;
1593
- await new Promise((resolve) => setTimeout(resolve, 50));
1594
- } while (performance.now() < deadline);
1595
- return null;
1596
- }
1597
-
1598
- async function waitForNativeTabModuleUrl() {
1599
- return findCodexModule('app-initial',{signal:lifetime.signal,waitForEntry:true});
1600
- }
1601
-
1602
- async function loadNativeTabCapability() {
1603
- if (nativeTabCapability) return nativeTabCapability;
1604
- if (nativeTabCapabilityPromise) return nativeTabCapabilityPromise;
1605
- nativeTabCapabilityPromise = (async () => {
1606
- try {
1607
- const moduleUrl = await waitForNativeTabModuleUrl();
1608
- if (!moduleUrl) throw new Error("Codex app-initial module was not found");
1609
- const appModule = await import(moduleUrl);
1610
- const controller = Object.values(appModule).find((value) => (
1611
- value
1612
- && typeof value === "object"
1613
- && typeof value.openTab === "function"
1614
- && typeof value.closeTab === "function"
1615
- && typeof value.activateTab === "function"
1616
- && value.panelId === "right"
1617
- ));
1618
- if (!controller) throw new Error("Codex right-panel controller is unavailable");
1619
- if (!controller?.tabById$) throw new Error("Codex native tab capability is incomplete");
1620
- nativeTabCapability = {
1621
- controller,
1622
- moduleAsset: moduleUrl.split("/").pop(),
1623
- };
1624
- nativeTabCapabilityError = null;
1625
- return nativeTabCapability;
1626
- } catch (error) {
1627
- nativeTabCapabilityError = error;
1628
- return null;
1629
- }
1630
- })();
1631
- const capability = await nativeTabCapabilityPromise;
1632
- if (!capability) nativeTabCapabilityPromise = null;
1633
- return capability;
1634
- }
1635
-
1636
- function nativeReactElement(type, props) {
1637
- return {
1638
- $$typeof: Symbol.for("react.transitional.element"),
1639
- type,
1640
- key: null,
1641
- ref: null,
1642
- props: props ?? {},
1643
- };
1644
- }
1645
-
1646
- function nativeDetailIcon(feature, detail) {
1647
- const icon = detail.icon ?? feature.icon;
1648
- if (!icon?.markup) return undefined;
1649
- return nativeReactElement("svg", {
1650
- width: 16,
1651
- height: 16,
1652
- viewBox: icon.viewBox ?? "0 0 24 24",
1653
- fill: "none",
1654
- stroke: "currentColor",
1655
- strokeWidth: 1.5,
1656
- strokeLinecap: "round",
1657
- strokeLinejoin: "round",
1658
- "aria-hidden": true,
1659
- className: "icon-xs shrink-0",
1660
- dangerouslySetInnerHTML: { __html: icon.markup },
1661
- });
1662
- }
1663
-
1664
- function nativeDetailComponent(feature, detail) {
1665
- const detailKey = `${feature.id}:${detail.id}`;
1666
- let Component = nativeDetailComponents.get(detailKey);
1667
- if (Component) return Component;
1668
- const frameName = surfaceFrameName(feature.id);
1669
- Component = function CodexPersonalDetailTab() {
1670
- return nativeReactElement("div", {
1671
- "data-codex-personal-native-detail": detailKey,
1672
- style: {
1673
- height: "100%",
1674
- minHeight: 0,
1675
- overflow: "hidden",
1676
- background: "var(--color-token-main-surface-primary)",
1677
- },
1678
- children: nativeReactElement("iframe", {
1679
- name: frameName,
1680
- src: "about:blank",
1681
- title: detail.label,
1682
- allow: "clipboard-write",
1683
- style: {
1684
- display: "block",
1685
- width: "100%",
1686
- height: "100%",
1687
- border: 0,
1688
- background: "transparent",
1689
- },
1690
- }),
1691
- });
1692
- };
1693
- nativeDetailComponents.set(detailKey, Component);
1694
- return Component;
1695
- }
1696
-
1697
- function reloadNativeDetailIfUnready(feature, detail) {
1698
- const keys = nativeDetailSurfaceKeys.get(`${feature.id}:${detail.id}`);
1699
- if (!keys) return false;
1700
- let reloaded = false;
1701
- for (const key of keys) {
1702
- if (reloadSurfaceIfUnready(key, detail.surfaceUrl)) reloaded = true;
1703
- }
1704
- return reloaded;
1705
- }
1706
-
1707
- async function waitForNativeDetailTabElement(detailKey) {
1708
- const deadline = performance.now() + 2_000;
1709
- do {
1710
- const element = document.querySelector(
1711
- `[data-codex-personal-native-detail="${detailKey}"]`,
1712
- );
1713
- const frame = element?.querySelector("iframe");
1714
- if (element && frame) return { element, frame };
1715
- await new Promise((resolve) => setTimeout(resolve, 25));
1716
- } while (performance.now() < deadline);
1717
- return null;
1718
- }
1719
-
1720
- async function openNativeDetailTab(feature, detail) {
1721
- const capability = await loadNativeTabCapability();
1722
- if (!capability) return null;
1723
- const scope = await waitForCurrentRightPanelScope();
1724
- if (!scope) {
1725
- nativeTabCapabilityError = new Error("The current Codex right-panel scope was not found");
1726
- return null;
1727
- }
1728
-
1729
- const { controller } = capability;
1730
- const tabId = `${feature.id}-${detail.id}`;
1731
- const detailKey = `${feature.id}:${detail.id}`;
1732
- let session = Array.from(nativeOpenTabSessions).find((candidate) => (
1733
- candidate.scope === scope && candidate.tabId === tabId
1734
- ));
1735
- let focusedExisting = Boolean(session);
1736
- if (!focusedExisting && controller.tabById$) {
1737
- try {
1738
- focusedExisting = scope.get(controller.tabById$, tabId) != null;
1739
- } catch {}
1740
- }
1741
- const isNewSession = !session;
1742
- if (!session) {
1743
- session = { scope, controller, tabId };
1744
- nativeOpenTabSessions.add(session);
1745
- }
1746
-
1747
- try {
1748
- // 原生 openTab 接收标签描述对象,并在挂载前检查可用性。
1749
- controller.openTab(scope, {
1750
- Component: nativeDetailComponent(feature, detail),
1751
- kind: tabId,
1752
- dropDestinations: ["right"],
1753
- isAvailable: () => true,
1754
- }, {
1755
- id: tabId,
1756
- kind: tabId,
1757
- title: detail.label,
1758
- tooltip: detail.label,
1759
- icon: nativeDetailIcon(feature, detail),
1760
- isClosable: true,
1761
- props: {},
1762
- onClose: () => {
1763
- nativeOpenTabSessions.delete(session);
1764
- const closedKeys = nativeDetailSurfaceKeys.get(detailKey);
1765
- if (closedKeys) {
1766
- for (const key of closedKeys) removeSurfaceRecord(key);
1767
- nativeDetailSurfaceKeys.delete(detailKey);
1768
- }
1769
- },
1770
- });
1771
- } catch (error) {
1772
- if (isNewSession) nativeOpenTabSessions.delete(session);
1773
- nativeTabCapabilityError = error;
1774
- return null;
1775
- }
1776
-
1777
- if (!nativeDetailSurfaceKeys.has(detailKey)) {
1778
- const mounted = await waitForNativeDetailTabElement(detailKey);
1779
- if (!mounted) {
1780
- if (isNewSession) nativeOpenTabSessions.delete(session);
1781
- if (!focusedExisting) {
1782
- try {
1783
- controller.closeTab(scope, tabId);
1784
- } catch {}
1785
- }
1786
- nativeTabCapabilityError = new Error("Codex native right-panel Tab did not mount");
1787
- return null;
1788
- }
1789
- const instanceId = crypto.randomUUID?.()
1790
- ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
1791
- const recordKey = surfaceKey(feature.id, "native-detail", `${detail.id}:${instanceId}`);
1792
- registerSurface(recordKey, feature, "detail", detail.surfaceUrl, mounted.element, mounted.frame, detail.id);
1793
- let keys = nativeDetailSurfaceKeys.get(detailKey);
1794
- if (!keys) {
1795
- keys = new Set();
1796
- nativeDetailSurfaceKeys.set(detailKey, keys);
1797
- }
1798
- keys.add(recordKey);
1799
- queueTheme();
1800
- }
1801
-
1802
- nativeTabCapabilityError = null;
1803
- if (focusedExisting) reloadNativeDetailIfUnready(feature, detail);
1804
- return { tabId, focusedExisting, presentation: "native" };
1805
- }
1806
-
1807
- function detailTabCapability() {
1808
- return {
1809
- presentation: nativeTabCapability ? "native" : "unavailable",
1810
- moduleAsset: nativeTabCapability?.moduleAsset ?? null,
1811
- error: nativeTabCapabilityError?.message ?? null,
1812
- openNativeTabs: nativeOpenTabSessions.size,
1813
- };
1814
- }
1815
-
1816
- function closeNativeDetailTabs() {
1817
- for (const keys of Array.from(nativeDetailSurfaceKeys.values())) {
1818
- for (const key of keys) removeSurfaceRecord(key);
1819
- }
1820
- for (const session of Array.from(nativeOpenTabSessions)) {
1821
- try {
1822
- session.controller.closeTab(session.scope, session.tabId);
1823
- } catch {}
1824
- }
1825
- nativeOpenTabSessions.clear();
1826
- nativeDetailComponents.clear();
1827
- nativeDetailSurfaceKeys.clear();
1828
- }
1829
-
1830
- async function openDetailTab(featureId, detailId) {
1831
- const feature = featureById.get(featureId);
1832
- const detail = feature?.detailTabs?.find((candidate) => candidate.id === detailId);
1833
- if (!feature || !detail) throw new Error("The requested detail Tab is not registered");
1834
- const result = await openNativeDetailTab(feature, detail);
1835
- if (!result) {
1836
- const error = nativeTabCapabilityError
1837
- ?? new Error("Codex native right-panel tabs are unavailable");
1838
- error.code = "NATIVE_TAB_UNAVAILABLE";
1839
- throw error;
1840
- }
1841
- return result;
1842
- }
1843
-
1844
- function openThread(threadId) {
1845
- const nativeThread = Array.from(
1846
- document.querySelectorAll("[data-app-action-sidebar-thread-id]"),
1847
- ).find((element) => {
1848
- const nativeId = element.getAttribute("data-app-action-sidebar-thread-id");
1849
- return nativeId === threadId || nativeId?.endsWith(`:${threadId}`);
1850
- });
1851
-
1852
- hidePageSurfaces();
1853
- hidePinnedSurfaces();
1854
- if (nativeThread) {
1855
- nativeThread.click();
1856
- nativeThread.focus({ preventScroll: false });
1857
- return;
1858
- }
1859
-
1860
- const link = document.createElement("a");
1861
- link.href = `codex://threads/${threadId}`;
1862
- link.target = "_blank";
1863
- link.rel = "noreferrer";
1864
- link.hidden = true;
1865
- document.body.append(link);
1866
- link.click();
1867
- link.remove();
1868
- }
1869
-
1870
- // ========================================================================
1871
- // Sidebar/Toolbar 入口与 ensure 调度 — 入口克隆注入、ensure 批处理调度
1872
- // ========================================================================
1873
-
1874
- function createSidebarEntry(feature, reference) {
1875
- const entry = reference.cloneNode(true);
1876
- entry.setAttribute(entryMarker, feature.id);
1877
- entry.removeAttribute("href");
1878
- entry.removeAttribute("aria-current");
1879
- entry.querySelectorAll("[data-state]").forEach((node) => node.removeAttribute("data-state"));
1880
- const originalIcon = entry.querySelector("svg");
1881
- const featureIcon = createFeatureIcon(feature, originalIcon?.getAttribute("class"));
1882
- if (originalIcon && featureIcon) originalIcon.replaceWith(featureIcon);
1883
-
1884
- const referenceLabels = nativeLabels.plugins;
1885
- const textLeaf = Array.from(entry.querySelectorAll("*")).find((node) => (
1886
- node.children.length === 0
1887
- && referenceLabels.includes(node.textContent?.trim().toLowerCase())
1888
- ));
1889
- if (textLeaf) textLeaf.textContent = feature.label;
1890
- else entry.setAttribute("aria-label", feature.label);
1891
-
1892
- entry.addEventListener("click", (event) => {
1893
- event.preventDefault();
1894
- event.stopPropagation();
1895
- showPageSurface(feature.id);
1896
- });
1897
- return entry;
1898
- }
1899
-
1900
- function createToolbarEntry(feature, nativeButton) {
1901
- const nativeRoot = toolbarControlRoot(nativeButton);
1902
- const root = nativeRoot.cloneNode(true);
1903
- root.setAttribute("data-codex-personal-toolbar-entry", feature.id);
1904
- root.classList.remove("ms-auto");
1905
- root.querySelectorAll("[id]").forEach((node) => node.removeAttribute("id"));
1906
- root.querySelectorAll("[aria-describedby]").forEach((node) => node.removeAttribute("aria-describedby"));
1907
- const button = root.matches("button") ? root : root.querySelector("button");
1908
- if (!button) return null;
1909
- const label = summaryButtonLabel(feature);
1910
- button.setAttribute(entryMarker, feature.id);
1911
- button.removeAttribute("title");
1912
- button.removeAttribute("aria-describedby");
1913
- button.setAttribute("aria-label", label);
1914
- button.setAttribute("data-minecodex-tooltip", label);
1915
- button.setAttribute("aria-pressed", "false");
1916
- button.setAttribute("aria-expanded", "false");
1917
- button.setAttribute("data-state", "closed");
1918
- button.removeAttribute("disabled");
1919
- button.classList.remove(
1920
- "text-token-foreground",
1921
- "bg-token-foreground/5",
1922
- "bg-token-foreground/10",
1923
- "enabled:hover:bg-token-foreground/10",
1924
- "data-[state=open]:bg-token-foreground/10",
1925
- "bg-token-list-hover-background",
1926
- );
1927
- button.classList.add(
1928
- "text-token-text-tertiary",
1929
- "enabled:hover:bg-token-list-hover-background",
1930
- "data-[state=open]:bg-token-list-hover-background",
1931
- );
1932
- const originalIcon = button.querySelector("svg");
1933
- const featureIcon = createFeatureIcon(feature, originalIcon?.getAttribute("class"), 16);
1934
- if (originalIcon && featureIcon) originalIcon.replaceWith(featureIcon);
1935
- button.addEventListener("click", (event) => {
1936
- event.preventDefault();
1937
- event.stopPropagation();
1938
- togglePinnedSurface(feature.id);
1939
- });
1940
- return button;
1941
- }
1942
-
1943
- function ensureSidebarEntries() {
1944
- if (!sidebarFeatures.length) return;
1945
- const reference = findReferenceEntry();
1946
- if (!reference) return;
1947
- for (const feature of sidebarFeatures) {
1948
- if (!findEntry(feature.id)) reference.after(createSidebarEntry(feature, reference));
1949
- }
1950
- const cursors = new Map();
1951
- for (const feature of sidebarFeatures) {
1952
- const anchorKey = feature.placement?.after ?? "sites";
1953
- const anchor = cursors.get(anchorKey) ?? findNativeEntry(anchorKey) ?? reference;
1954
- const entry = findEntry(feature.id);
1955
- if (anchor && entry && anchor.nextElementSibling !== entry) anchor.after(entry);
1956
- if (entry) cursors.set(anchorKey, entry);
1957
- }
1958
- }
1959
-
1960
- function ensureToolbarEntries() {
1961
- if (!toolbarFeatures.length) {
1962
- stopToolbarReadiness();
1963
- return;
1964
- }
1965
- const temporaryChatButton = nativeTemporaryChatButton();
1966
- const summaryButton = nativeSummaryButton();
1967
- const bottomPanelButton = nativeBottomPanelButton();
1968
- const sidePanelButton = nativeSidePanelButton();
1969
- const anchorButton = temporaryChatButton ?? summaryButton ?? bottomPanelButton ?? sidePanelButton;
1970
- if (!anchorButton) {
1971
- const candidate = structuralToolbarAnchor();
1972
- if (candidate) ensureToolbarReadiness(candidate);
1973
- return;
1974
- }
1975
- const anchorRoot = toolbarControlRoot(anchorButton);
1976
- // The bottom/side-panel toggles sit in narrow fixed containers that
1977
- // travel with the native panels. Entries anchored to them must live in
1978
- // the fallback toolbar group, otherwise the Notes entry would be
1979
- // dragged into the panel or push the native toggle out of the header.
1980
- // The Notes entry is only grouped with the native summary button when
1981
- // that button is the anchor.
1982
- const useFallbackAnchor = anchorButton === sidePanelButton || anchorButton === bottomPanelButton;
1983
- const summaryGroup = useFallbackAnchor ? null : nativeSummaryToolbarGroup(anchorButton);
1984
- let targetGroup = summaryGroup ?? ensureFallbackSummaryToolbarGroup(anchorButton);
1985
- if (!targetGroup) {
1986
- ensureToolbarReadiness(anchorButton);
1987
- return;
1988
- }
1989
- let cursor = useFallbackAnchor ? null : anchorRoot;
1990
- for (const feature of toolbarFeatures) {
1991
- const entries = Array.from(document.querySelectorAll(`[${entryMarker}="${feature.id}"]`));
1992
- for (const duplicate of entries.slice(1)) {
1993
- (duplicate.closest("[data-codex-personal-toolbar-entry]") ?? duplicate).remove();
1994
- }
1995
- const entry = entries[0]?.isConnected
1996
- ? entries[0]
1997
- : createToolbarEntry(feature, anchorButton);
1998
- const entryRoot = entry?.closest("[data-codex-personal-toolbar-entry]") ?? entry;
1999
- if (!entryRoot) continue;
2000
- if (cursor) {
2001
- if (cursor.nextElementSibling !== entryRoot) cursor.after(entryRoot);
2002
- } else if (targetGroup.lastElementChild !== entryRoot) {
2003
- targetGroup.append(entryRoot);
2004
- }
2005
- // Narrow native anchor containers (e.g. the Work-mode side-panel
2006
- // button) can push the entry outside the header so it becomes
2007
- // unreachable. Move it into the fallback toolbar group instead.
2008
- if (summaryGroup && !toolbarEntryFitsHeader(entryRoot)) {
2009
- const fallback = ensureFallbackSummaryToolbarGroup(anchorButton);
2010
- if (fallback && fallback !== summaryGroup) {
2011
- fallback.append(entryRoot);
2012
- targetGroup = fallback;
2013
- cursor = null;
2014
- }
2015
- }
2016
- cursor = entryRoot;
2017
- }
2018
- if (summaryGroup) {
2019
- document.querySelectorAll("[data-codex-personal-summary-toolbar-group]").forEach((group) => {
2020
- const used = [...group.querySelectorAll(`[${entryMarker}]`)].some((entry) => entry.isConnected);
2021
- if (!used) group.remove();
2022
- });
2023
- }
2024
- if (toolbarFeatures.every((feature) => findEntry(feature.id)?.isConnected)) {
2025
- stopToolbarReadiness();
2026
- } else {
2027
- ensureToolbarReadiness(anchorButton);
2028
- }
2029
- }
2030
-
2031
247
  function ensureEntries() {
2032
248
  ensureQueued = false;
2033
249
  ensureFrame = null;
2034
250
  ensureTimer = null;
2035
- ensureSidebarEntries();
2036
- ensureToolbarEntries();
2037
251
  modelSlider.ensure();
2038
- checkThreadChange();
2039
- updateSidebarSelectedState();
2040
- updatePageSurfacePositions();
2041
- ensureMainContentObserver();
2042
- updateResponsiveSummaryPresentation();
2043
- const nativeToggle = nativeSummaryButton();
2044
- if (
2045
- activePinnedFeatureId
2046
- && (nativeToggle?.getAttribute("aria-expanded") === "true"
2047
- || nativeToggle?.getAttribute("aria-pressed") === "true")
2048
- ) {
2049
- hidePinnedSurfaces();
2050
- }
2051
252
  }
2052
253
 
2053
254
  function flushEnsure() {
@@ -2067,115 +268,9 @@ export function createInjectionSource(features, {
2067
268
  }
2068
269
 
2069
270
  // ========================================================================
2070
- // 消息与 host action 路由 — postMessage 路由、host action 分发与结果回送
271
+ // host action 通道 — page-script 经 CDP binding 请求 RuntimeHost 并接收结果
2071
272
  // ========================================================================
2072
273
 
2073
- function findMessageSurface(event) {
2074
- for (const record of surfaceRecords.values()) {
2075
- // 身份以 event.source(WindowProxy 不可伪造)为准;Windows 上
2076
- // setDocumentContent 的 iframe 消息可能报告 null origin,
2077
- // 因此用已注册 frame 的 WindowProxy 精确匹配来源。
2078
- if (event.source === record.frame.contentWindow) return record;
2079
- }
2080
- return null;
2081
- }
2082
-
2083
- function respondToSurface(record, requestId, { ok, result = null, error = null }) {
2084
- record.frame.contentWindow?.postMessage({
2085
- type: "codex-personal:host-result",
2086
- feature: record.featureId,
2087
- requestId,
2088
- ok,
2089
- result,
2090
- error,
2091
- }, "/");
2092
- }
2093
-
2094
- function hostActionFailure(error) {
2095
- return {
2096
- code: typeof error?.code === "string" ? error.code : "HOST_ACTION_FAILED",
2097
- message: error?.message ?? "Host action failed",
2098
- };
2099
- }
2100
-
2101
- async function handleSurfaceHostAction(record, message) {
2102
- const feature = featureById.get(record.featureId);
2103
- const { requestId, action, payload = {} } = message;
2104
- if (typeof requestId !== "string" || !feature?.hostActions.has(action)) {
2105
- respondToSurface(record, requestId, {
2106
- ok: false,
2107
- error: { code: "ACTION_NOT_ALLOWED", message: "This Host action is not registered" },
2108
- });
2109
- return;
2110
- }
2111
- try {
2112
- if (action === "insert-text") {
2113
- respondToSurface(record, requestId, { ok: true, result: insertComposerText(payload.text) });
2114
- return;
2115
- }
2116
- if (action === "open-detail-tab") {
2117
- respondToSurface(record, requestId, {
2118
- ok: true,
2119
- result: await openDetailTab(feature.id, payload.surface),
2120
- });
2121
- return;
2122
- }
2123
- if (action === "open-editor-modal") {
2124
- respondToSurface(record, requestId, {
2125
- ok: true,
2126
- result: openEditorModal(record, payload),
2127
- });
2128
- return;
2129
- }
2130
- if (action === "open-sidebar") {
2131
- const trigger = nativeSidebarTrigger();
2132
- if (!trigger) throw new Error("The native sidebar control was not found");
2133
- const state = sidebarState();
2134
- if (!state.open) trigger.click();
2135
- queueMicrotask(() => postSidebarState(record, { ...state, open: true }));
2136
- respondToSurface(record, requestId, { ok: true, result: { opened: true } });
2137
- return;
2138
- }
2139
- if (action === "resolve-file-paths") {
2140
- const getPathForFile = window.electronBridge?.getPathForFile;
2141
- if (typeof getPathForFile !== "function") {
2142
- throw new Error("Codex did not expose its File path bridge");
2143
- }
2144
- const files = Array.isArray(payload.files) ? payload.files : [];
2145
- if (!files.length || files.some((file) => !(file instanceof File))) {
2146
- throw new Error("Real dropped File objects are required");
2147
- }
2148
- // Windows 盘符路径(C:\...)不以 / 开头,需同时接受 POSIX 与 Windows 形态。
2149
- const paths = files.map((file) => getPathForFile(file)).filter((value) => (
2150
- typeof value === "string" && (/^[/\\]/.test(value) || /^[a-zA-Z]:[/\\]/.test(value))
2151
- ));
2152
- if (!paths.length) throw new Error("Codex could not resolve the dropped file paths");
2153
- respondToSurface(record, requestId, { ok: true, result: { paths } });
2154
- return;
2155
- }
2156
- if (["attach-file", "create-image-edit-thread", "list-cloud-images", "import-cloud-image"].includes(action)) {
2157
- if (typeof globalThis[config.bindingName] !== "function") {
2158
- throw new Error("The native file bridge is unavailable");
2159
- }
2160
- pendingHostActions.set(requestId, { recordKey: record.key });
2161
- globalThis[config.bindingName](JSON.stringify({
2162
- token: config.bindingToken,
2163
- featureId: feature.id,
2164
- requestId,
2165
- action,
2166
- payload,
2167
- }));
2168
- return;
2169
- }
2170
- throw new Error("Unsupported Host action");
2171
- } catch (error) {
2172
- respondToSurface(record, requestId, {
2173
- ok: false,
2174
- error: hostActionFailure(error),
2175
- });
2176
- }
2177
- }
2178
-
2179
274
  function requestPageHostAction(featureId, action, payload) {
2180
275
  if (!featureById.get(featureId)?.hostActions.has(action)) return Promise.reject(new Error("Host action is not registered"));
2181
276
  if (typeof globalThis[config.bindingName] !== "function") return Promise.reject(new Error("Host bridge is unavailable"));
@@ -2185,7 +280,7 @@ export function createInjectionSource(features, {
2185
280
  pendingHostActions.delete(requestId);
2186
281
  reject(new Error("Host request timed out"));
2187
282
  }, featureId === "minecodex" ? 30_000 : 10_000);
2188
- pendingHostActions.set(requestId, { kind: "page-script", resolve, reject, timer });
283
+ pendingHostActions.set(requestId, { resolve, reject, timer });
2189
284
  try {
2190
285
  globalThis[config.bindingName](JSON.stringify({ token: config.bindingToken, featureId, requestId, action, payload }));
2191
286
  } catch (error) {
@@ -2200,160 +295,33 @@ export function createInjectionSource(features, {
2200
295
  const pending = pendingHostActions.get(requestId);
2201
296
  if (!pending) return false;
2202
297
  pendingHostActions.delete(requestId);
2203
- if (pending.kind === "page-script") {
2204
- clearTimeout(pending.timer);
2205
- if (response.ok) pending.resolve(response.result);
2206
- else pending.reject(new Error(response.error?.message ?? "Host request failed"));
2207
- return true;
2208
- }
2209
- const record = surfaceRecords.get(pending.recordKey);
2210
- if (!record) return false;
2211
- if (pending.kind === "surface-load") {
2212
- record.loading = false;
2213
- record.loadError = response.ok ? null : response.error;
2214
- return response.ok;
2215
- }
2216
- respondToSurface(record, requestId, response);
298
+ clearTimeout(pending.timer);
299
+ if (response.ok) pending.resolve(response.result);
300
+ else pending.reject(new Error(response.error?.message ?? "Host request failed"));
2217
301
  return true;
2218
302
  }
2219
303
 
2220
304
  document.addEventListener("selectionchange", rememberComposerSelection, { signal: lifetime.signal });
2221
- document.addEventListener(
2222
- "click",
2223
- (event) => {
2224
- if (event.target.closest?.('[data-app-shell-sidebar-trigger="true"]')) {
2225
- syncAfterNativeSidebarToggle();
2226
- }
2227
- if (isNativeSummaryButton(event.target) && activePinnedFeatureId) hidePinnedSurfaces();
2228
- if (activePinnedFeatureId && summaryDisplayMode() === "overlay") {
2229
- const state = summaryState(activePinnedFeatureId);
2230
- const surface = pinnedSurfaces.get(activePinnedFeatureId);
2231
- const ownEntry = event.target.closest?.(`[${entryMarker}="${activePinnedFeatureId}"]`);
2232
- const rightWorkspaceInteraction = event.target.closest?.(
2233
- '[data-app-shell-tab-strip-controller="right"],'
2234
- + '[data-app-shell-tab-panel-controller="right"]',
2235
- );
2236
- const promptPreviewInteraction = event.target.closest?.(`[${promptPreviewMarker}]`);
2237
- if (
2238
- state.isPopoverOpen
2239
- && !surface?.contains(event.target)
2240
- && !ownEntry
2241
- && !rightWorkspaceInteraction
2242
- && !promptPreviewInteraction
2243
- ) {
2244
- const featureId = activePinnedFeatureId;
2245
- state.isPopoverOpen = false;
2246
- if (!state.isPinned) activePinnedFeatureId = null;
2247
- updatePinnedPosition(featureId);
2248
- updateToolbarState();
2249
- }
2250
- }
2251
- if (!activePageFeatureId) return;
2252
- if (event.target.closest?.(`[${entryMarker}]`)) return;
2253
- if (event.target.closest?.("[data-app-action-sidebar-scroll]")) hidePageSurfaces();
2254
- },
2255
- { capture: true, signal: lifetime.signal },
2256
- );
2257
-
2258
- document.addEventListener("keydown", (event) => {
2259
- if (event.key !== "Escape") return;
2260
- if (closePromptPreview({ immediate: true })) {
2261
- event.preventDefault();
2262
- event.stopPropagation();
2263
- return;
2264
- }
2265
- if (closeModalSurface()) {
2266
- event.preventDefault();
2267
- event.stopPropagation();
2268
- return;
2269
- }
2270
- if (!activePinnedFeatureId || summaryDisplayMode() !== "overlay") return;
2271
- const state = summaryState(activePinnedFeatureId);
2272
- if (!state.isPopoverOpen) return;
2273
- const featureId = activePinnedFeatureId;
2274
- state.isPopoverOpen = false;
2275
- if (!state.isPinned) activePinnedFeatureId = null;
2276
- updatePinnedPosition(featureId);
2277
- updateToolbarState();
2278
- }, { capture: true, signal: lifetime.signal });
2279
-
2280
- window.addEventListener("message", (event) => {
2281
- const record = findMessageSurface(event);
2282
- if (!record || !event.data || typeof event.data !== "object") return;
2283
- if (event.data.feature && event.data.feature !== record.featureId) return;
2284
- if (event.data.type === "codex-personal:ready") {
2285
- record.ready = true;
2286
- queueTheme();
2287
- postSidebarState(record);
2288
- if (record.kind === "modal") {
2289
- record.frame.focus();
2290
- record.frame.contentWindow?.postMessage({
2291
- type: "codex-personal:focus",
2292
- feature: record.featureId,
2293
- }, "/");
2294
- }
2295
- }
2296
- if (event.data.type === "codex-personal:surface-metrics" && record.kind === "pinned") {
2297
- const preferredHeight = Number(event.data.height);
2298
- if (Number.isFinite(preferredHeight) && preferredHeight > 0) {
2299
- record.preferredHeight = Math.min(2_000, Math.max(80, Math.ceil(preferredHeight)));
2300
- updatePinnedPosition(record.featureId);
2301
- }
2302
- }
2303
- if (event.data.type === "codex-personal:prompt-preview" && record.kind === "pinned") {
2304
- if (event.data.phase === "show") showPromptPreview(record, event.data);
2305
- if (event.data.phase === "hide") requestPromptPreviewClose(record);
2306
- }
2307
- if (event.data.type === "codex-personal:close") {
2308
- if (record.kind === "pinned") hidePinnedSurfaces();
2309
- if (record.kind === "page") hidePageSurfaces();
2310
- if (record.kind === "modal") closeModalSurface();
2311
- }
2312
- if (event.data.type === "codex-personal:open-thread" && event.data.threadId) {
2313
- openThread(event.data.threadId);
2314
- }
2315
- if (event.data.type === "codex-personal:host-action") {
2316
- handleSurfaceHostAction(record, event.data);
2317
- }
2318
- }, { signal: lifetime.signal });
2319
-
2320
- window.addEventListener("resize", () => {
2321
- closePromptPreview({ immediate: true });
2322
- updatePageSurfacePositions();
2323
- updateResponsiveSummaryPresentation();
2324
- }, { signal: lifetime.signal });
2325
- const ensureObserver = new MutationObserver((mutations) => {
2326
- queueEnsure();
2327
- const sidebarTriggerChanged = mutations.some((mutation) => {
2328
- if (
2329
- mutation.type === "attributes"
2330
- && mutation.target.matches?.('[data-app-shell-sidebar-trigger="true"]')
2331
- ) return true;
2332
- return mutation.type === "childList"
2333
- && [...mutation.addedNodes, ...mutation.removedNodes].some((node) => (
2334
- node instanceof Element
2335
- && (
2336
- node.matches?.('[data-app-shell-sidebar-trigger="true"]')
2337
- || node.querySelector?.('[data-app-shell-sidebar-trigger="true"]')
2338
- )
2339
- ));
2340
- });
2341
- if (sidebarTriggerChanged) updatePageSurfacePositions();
2342
- });
305
+ const ensureObserver = new MutationObserver(queueEnsure);
2343
306
  ensureObserver.observe(document.documentElement, {
2344
307
  childList: true,
2345
308
  subtree: true,
2346
309
  attributes: true,
2347
- attributeFilter: [
2348
- "aria-expanded",
2349
- "aria-pressed",
2350
- "data-state",
2351
- "data-app-action-sidebar-thread-selected",
2352
- "data-app-action-sidebar-thread-id",
2353
- ],
310
+ attributeFilter: ["aria-expanded", "aria-pressed", "data-state"],
2354
311
  });
2355
312
  domObservers.push(ensureObserver);
2356
- const themeObserver = new MutationObserver(queueTheme);
313
+ // theme.read 会把原生主题 token 同步到根节点的 --mcx-* 变量,页面脚本依赖它跟随主题。
314
+ let themeQueued = false;
315
+ const syncTheme = () => {
316
+ if (themeQueued) return;
317
+ themeQueued = true;
318
+ queueMicrotask(() => {
319
+ themeQueued = false;
320
+ featureUI.theme.read();
321
+ });
322
+ };
323
+ featureUI.theme.read();
324
+ const themeObserver = new MutationObserver(syncTheme);
2357
325
  themeObserver.observe(document.documentElement, {
2358
326
  attributes: true,
2359
327
  attributeFilter: ["class", "style", "data-theme"],
@@ -2361,7 +329,7 @@ export function createInjectionSource(features, {
2361
329
  domObservers.push(themeObserver);
2362
330
 
2363
331
  // ========================================================================
2364
- // 运行时 API 与清理 — 对外 runtimeApi、dispose 与全局事件观察器
332
+ // 运行时 API 与清理
2365
333
  // ========================================================================
2366
334
 
2367
335
  const runtimeApi = {
@@ -2371,22 +339,11 @@ export function createInjectionSource(features, {
2371
339
  nativeCapabilities: () => codex.diagnostics(),
2372
340
  selfCheck: () => ({...selfCheck.snapshot(),capabilities:codex.diagnostics()}),
2373
341
  ensure: queueEnsure,
2374
- show: showPageSurface,
2375
- hide: () => {
2376
- hidePageSurfaces();
2377
- hidePinnedSurfaces();
2378
- closeModalSurface();
2379
- },
2380
- showPinned: showPinnedSurface,
2381
- hidePinned: hidePinnedSurfaces,
2382
- openDetailTab,
2383
- detailTabCapability,
2384
342
  insertText: insertComposerText,
2385
343
  resolveHostAction,
2386
344
  dispose: () => {
2387
345
  lifetime.abort();
2388
346
  for (const [id, pending] of pendingHostActions) {
2389
- if (pending.kind !== "page-script") continue;
2390
347
  clearTimeout(pending.timer);
2391
348
  pending.reject(new Error("Runtime disposed"));
2392
349
  pendingHostActions.delete(id);
@@ -2399,26 +356,12 @@ export function createInjectionSource(features, {
2399
356
  if (ensureFrame != null) cancelAnimationFrame(ensureFrame);
2400
357
  if (ensureTimer != null) clearTimeout(ensureTimer);
2401
358
  for (const observer of domObservers) observer.disconnect();
2402
- stopToolbarReadiness();
2403
- mainContentObserver?.observer.disconnect();
2404
- restoreThreadContentShift();
2405
- for (const entry of document.querySelectorAll(`[${entryMarker}]`)) entry.remove();
2406
- for (const root of document.querySelectorAll("[data-codex-personal-toolbar-entry]")) root.remove();
2407
- for (const group of document.querySelectorAll("[data-codex-personal-summary-toolbar-group]")) group.remove();
2408
- for (const surface of pageSurfaces.values()) surface.remove();
2409
- for (const surface of pinnedSurfaces.values()) surface.remove();
2410
- closePromptPreview({ immediate: true });
2411
- closeModalSurface();
2412
- closeNativeDetailTabs();
2413
359
  modelSlider.dispose();
2414
- surfaceRecords.clear();
2415
360
  featureUI.theme.dispose();
2416
361
  if (window.__codexPersonalRuntime === runtimeApi) delete window.__codexPersonalRuntime;
2417
362
  },
2418
363
  };
2419
364
  window.__codexPersonalRuntime = runtimeApi;
2420
- currentThread = threadIdentity();
2421
- currentSummaryDisplayMode = summaryDisplayMode();
2422
365
  queueEnsure();
2423
366
  }
2424
367
 
@@ -2427,12 +370,7 @@ export function createInjectionSource(features, {
2427
370
  id: feature.id,
2428
371
  label: feature.label,
2429
372
  labels: feature.labels ?? null,
2430
- surfaceUrl: feature.surfaceUrl,
2431
- icon: feature.icon,
2432
- placement: feature.placement,
2433
- entry: feature.entry ?? { kind: "sidebar" },
2434
- pinnedSummary: feature.pinnedSummary ?? null,
2435
- detailTabs: feature.detailTabs ?? [],
373
+ entry: feature.entry,
2436
374
  hostActions: feature.hostActions ?? [],
2437
375
  modelSelector: feature.modelSelector ?? null,
2438
376
  pageScript: feature.pageScript ? {
@@ -2451,8 +389,6 @@ export function createInjectionSource(features, {
2451
389
  sessionId: runtimeSessionId,
2452
390
  bindingName,
2453
391
  bindingToken,
2454
- summaryLayout: RESPONSIVE_SUMMARY_LAYOUT,
2455
- rightPanelRouteKinds: RIGHT_PANEL_ROUTE_KINDS,
2456
392
  themeTokens: CODEX_THEME_TOKENS,
2457
393
  themeTokenAliases: CODEX_THEME_TOKEN_ALIASES,
2458
394
  themeBindings: MINECODEX_THEME_BINDINGS,