minecodex 1.0.8 → 1.0.9
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.
- package/features/model-slider/README.md +7 -4
- package/features/notes/src/http-server.mjs +3 -0
- package/features/notes/web/app.js +9 -303
- package/features/notes/web/file-icons.mjs +68 -0
- package/features/notes/web/i18n.mjs +237 -0
- package/features/notes/web/todo-drag.mjs +18 -0
- package/package.json +1 -1
- package/packages/runtime-host/src/codex-cdp.mjs +153 -0
- package/packages/runtime-host/src/codex-design-contract.mjs +116 -0
- package/packages/runtime-host/src/codex-injection.mjs +4545 -0
- package/packages/runtime-host/src/codex-runtime.mjs +63 -4930
- package/packages/runtime-host/src/host-actions.mjs +221 -0
|
@@ -0,0 +1,4545 @@
|
|
|
1
|
+
// 注入源生成:createInjectionSource 产出注入到 Codex Renderer 的自包含脚本字符串。
|
|
2
|
+
// 该字符串经 CDP 注入后独立执行,因此 install 函数体必须自包含;
|
|
3
|
+
// CodexRuntime 与 CDP 客户端在 codex-runtime.mjs / codex-cdp.mjs。
|
|
4
|
+
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { CODEX_THEME_TOKENS } from "./codex-design-contract.mjs";
|
|
7
|
+
|
|
8
|
+
export const RUNTIME_VERSION = 100;
|
|
9
|
+
export const HOST_BINDING_NAME = "__codexPersonalHostAction";
|
|
10
|
+
export const CSP_BOOTSTRAP_VERSION = 1;
|
|
11
|
+
export const CSP_BOOTSTRAP_KEY = "__mineCodexCspBootstrapVersion";
|
|
12
|
+
export const CODEX_APP_ORIGIN = "app://-";
|
|
13
|
+
export const SURFACE_LOAD_ACTION = "load-surface";
|
|
14
|
+
export const SURFACE_FRAME_PREFIX = "minecodex-surface-";
|
|
15
|
+
export const LOOPBACK_SURFACE_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
|
|
16
|
+
export const RIGHT_PANEL_ROUTE_KINDS = Object.freeze(["home", "local-thread"]);
|
|
17
|
+
|
|
18
|
+
export const RESPONSIVE_SUMMARY_LAYOUT = Object.freeze({
|
|
19
|
+
contentBaseWidth: 736,
|
|
20
|
+
overlayClearance: 180,
|
|
21
|
+
gutterClearance: 400,
|
|
22
|
+
panelWidth: 300,
|
|
23
|
+
panelInset: 16,
|
|
24
|
+
verticalInset: 12,
|
|
25
|
+
popoverOffset: 8,
|
|
26
|
+
popoverBottomInset: 6,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export function responsiveSummaryDisplayMode(mainContentTargetWidth, layout = RESPONSIVE_SUMMARY_LAYOUT) {
|
|
30
|
+
const clearance = (Number(mainContentTargetWidth) - layout.contentBaseWidth) / 2;
|
|
31
|
+
if (clearance < layout.overlayClearance) return "overlay";
|
|
32
|
+
if (clearance < layout.gutterClearance) return "shift";
|
|
33
|
+
return "gutter";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function responsiveSummaryVisibleSurface({ displayMode, isPinned, isPopoverOpen }) {
|
|
37
|
+
if (displayMode !== "overlay" && isPinned) return "inline";
|
|
38
|
+
if (displayMode === "overlay" && isPopoverOpen) return "popover";
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function responsiveSummaryContentShift({ displayMode, isPinned }, layout = RESPONSIVE_SUMMARY_LAYOUT) {
|
|
43
|
+
return displayMode !== "overlay" && isPinned
|
|
44
|
+
? -(layout.panelWidth + layout.panelInset) / 2
|
|
45
|
+
: 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function declaredSurfaceUrls(feature) {
|
|
49
|
+
return new Set([
|
|
50
|
+
feature.surfaceUrl,
|
|
51
|
+
feature.pinnedSummary?.surfaceUrl,
|
|
52
|
+
...(feature.detailTabs ?? []).map((detail) => detail.surfaceUrl),
|
|
53
|
+
].filter(Boolean));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function requireDeclaredLoopbackSurface(feature, value) {
|
|
57
|
+
if (typeof value !== "string") {
|
|
58
|
+
throw Object.assign(new Error("Surface URL is not declared by this feature"), {
|
|
59
|
+
code: "SURFACE_URL_NOT_ALLOWED",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const url = new URL(value);
|
|
63
|
+
const matchesDeclaredDocument = Array.from(declaredSurfaceUrls(feature), (declared) => new URL(declared))
|
|
64
|
+
.some((declared) => (
|
|
65
|
+
declared.origin === url.origin
|
|
66
|
+
&& declared.username === url.username
|
|
67
|
+
&& declared.password === url.password
|
|
68
|
+
&& declared.pathname === url.pathname
|
|
69
|
+
&& declared.hash === url.hash
|
|
70
|
+
));
|
|
71
|
+
if (!matchesDeclaredDocument) {
|
|
72
|
+
throw Object.assign(new Error("Surface URL is not declared by this feature"), {
|
|
73
|
+
code: "SURFACE_URL_NOT_ALLOWED",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (url.protocol !== "http:" || !LOOPBACK_SURFACE_HOSTS.has(url.hostname)) {
|
|
77
|
+
throw Object.assign(new Error("Surface URL must use an exact loopback HTTP origin"), {
|
|
78
|
+
code: "SURFACE_URL_NOT_ALLOWED",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return url;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function pageScriptServiceOrigin(feature) {
|
|
85
|
+
if (!feature.pageScript || !feature.surfaceUrl || !feature.healthUrl) return null;
|
|
86
|
+
const surfaceUrl = new URL(feature.surfaceUrl);
|
|
87
|
+
const healthUrl = new URL(feature.healthUrl);
|
|
88
|
+
if (
|
|
89
|
+
surfaceUrl.origin !== healthUrl.origin
|
|
90
|
+
|| surfaceUrl.protocol !== "http:"
|
|
91
|
+
|| !LOOPBACK_SURFACE_HOSTS.has(surfaceUrl.hostname)
|
|
92
|
+
) return null;
|
|
93
|
+
return surfaceUrl.origin;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function findFrameByName(frameTree, frameName) {
|
|
97
|
+
if (frameTree.frame?.name === frameName) return frameTree.frame;
|
|
98
|
+
for (const child of frameTree.childFrames ?? []) {
|
|
99
|
+
const match = findFrameByName(child, frameName);
|
|
100
|
+
if (match) return match;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function documentWithBase(html, surfaceUrl) {
|
|
106
|
+
const head = /<head(?:\s[^>]*)?>/i;
|
|
107
|
+
if (!head.test(html)) {
|
|
108
|
+
throw Object.assign(new Error("Surface document has no head element"), {
|
|
109
|
+
code: "SURFACE_DOCUMENT_INVALID",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return html.replace(head, (match) => `${match}<base href=${JSON.stringify(surfaceUrl)}>`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// createInjectionSource:产出注入 Codex Renderer 的自包含脚本字符串。
|
|
116
|
+
// 返回的脚本经 CDP 注入后独立执行(install 函数体必须自包含,不能引用模块外部标识符)。
|
|
117
|
+
export function createInjectionSource(features, {
|
|
118
|
+
bindingName = HOST_BINDING_NAME,
|
|
119
|
+
bindingToken = "test-binding-token",
|
|
120
|
+
runtimeSessionId = randomBytes(16).toString("hex"),
|
|
121
|
+
} = {}) {
|
|
122
|
+
function install(config) {
|
|
123
|
+
if (window.top !== window) return;
|
|
124
|
+
if (!document.documentElement) {
|
|
125
|
+
// The document bootstrap runs before the root element exists. Defer the
|
|
126
|
+
// whole install until the DOM is ready instead of crashing on a null
|
|
127
|
+
// documentElement (which previously left the runtime permanently absent
|
|
128
|
+
// after a renderer reload).
|
|
129
|
+
document.addEventListener("DOMContentLoaded", () => install(config), { once: true });
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const currentRuntime = window.__codexPersonalRuntime;
|
|
133
|
+
if (currentRuntime?.version === config.version && currentRuntime?.sessionId === config.sessionId) {
|
|
134
|
+
currentRuntime.ensure();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
currentRuntime?.dispose?.();
|
|
138
|
+
|
|
139
|
+
const lifetime = new AbortController();
|
|
140
|
+
const rightPanelRouteKinds = new Set(config.rightPanelRouteKinds);
|
|
141
|
+
|
|
142
|
+
const entryMarker = "data-codex-personal-feature";
|
|
143
|
+
const pageSurfaceMarker = "data-codex-personal-surface";
|
|
144
|
+
const pinnedSurfaceMarker = "data-codex-personal-pinned-summary";
|
|
145
|
+
const modalSurfaceMarker = "data-codex-personal-modal";
|
|
146
|
+
const promptPreviewMarker = "data-codex-personal-prompt-preview";
|
|
147
|
+
const nativeLabels = {
|
|
148
|
+
sites: ["sites", "站点"],
|
|
149
|
+
scheduled: ["scheduled", "计划任务"],
|
|
150
|
+
plugins: ["plugins", "插件"],
|
|
151
|
+
};
|
|
152
|
+
const nativeToolbarLabels = {
|
|
153
|
+
summary: ["Toggle summary", "切换摘要"],
|
|
154
|
+
pinnedSummary: ["Toggle pinned summary", "切换固定摘要"],
|
|
155
|
+
bottomPanel: ["Toggle bottom panel", "切换底部面板"],
|
|
156
|
+
sidePanel: ["Toggle side panel", "切换侧边面板"],
|
|
157
|
+
temporaryChat: ["temporary chat", "临时聊天", "临时对话"],
|
|
158
|
+
};
|
|
159
|
+
const themeTokenNames = config.themeTokens;
|
|
160
|
+
|
|
161
|
+
function normalizedLocale(value) {
|
|
162
|
+
return String(value ?? "").toLowerCase().startsWith("zh") ? "zh-CN" : "en";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const locale = normalizedLocale(
|
|
166
|
+
document.documentElement.lang || navigator.languages?.[0] || navigator.language,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
function localizedText(fallback, locales = {}) {
|
|
170
|
+
return locales[locale] ?? locales.en ?? fallback;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ========================================================================
|
|
174
|
+
// 配置派生与运行状态 — feature 分类、surface/卡片状态、观察器与计时器
|
|
175
|
+
// ========================================================================
|
|
176
|
+
|
|
177
|
+
const features = config.features.map((feature) => ({
|
|
178
|
+
...feature,
|
|
179
|
+
label: localizedText(feature.label, feature.labels),
|
|
180
|
+
entry: feature.entry ? {
|
|
181
|
+
...feature.entry,
|
|
182
|
+
ariaLabel: localizedText(feature.entry.ariaLabel, feature.entry.ariaLabels),
|
|
183
|
+
} : { kind: "sidebar" },
|
|
184
|
+
pinnedSummary: feature.pinnedSummary ? {
|
|
185
|
+
...feature.pinnedSummary,
|
|
186
|
+
label: localizedText(feature.pinnedSummary.label, feature.pinnedSummary.labels),
|
|
187
|
+
overlayLabel: localizedText(feature.pinnedSummary.overlayLabel, feature.pinnedSummary.overlayLabels),
|
|
188
|
+
pinnedLabel: localizedText(feature.pinnedSummary.pinnedLabel, feature.pinnedSummary.pinnedLabels),
|
|
189
|
+
} : null,
|
|
190
|
+
detailTabs: (feature.detailTabs ?? []).map((detail) => ({
|
|
191
|
+
...detail,
|
|
192
|
+
label: localizedText(detail.label, detail.labels),
|
|
193
|
+
})),
|
|
194
|
+
modelSelector: feature.modelSelector ? {
|
|
195
|
+
...feature.modelSelector,
|
|
196
|
+
strings: Object.fromEntries(Object.entries(feature.modelSelector.strings ?? {}).map(
|
|
197
|
+
([key, definition]) => [key, localizedText(definition.value, definition.locales)],
|
|
198
|
+
)),
|
|
199
|
+
} : null,
|
|
200
|
+
hostActions: new Set(feature.hostActions ?? []),
|
|
201
|
+
}));
|
|
202
|
+
const featureById = new Map(features.map((feature) => [feature.id, feature]));
|
|
203
|
+
const sidebarFeatures = features.filter((feature) => feature.entry?.kind === "sidebar");
|
|
204
|
+
const toolbarFeatures = features.filter((feature) => feature.entry?.kind === "conversation-toolbar");
|
|
205
|
+
const modelSelectorFeature = features.find(
|
|
206
|
+
(feature) => feature.entry?.kind === "composer-model-selector",
|
|
207
|
+
) ?? null;
|
|
208
|
+
const pageScriptFeatures = features.filter((feature) => feature.pageScript);
|
|
209
|
+
const pageSurfaces = new Map();
|
|
210
|
+
const pinnedSurfaces = new Map();
|
|
211
|
+
const surfaceRecords = new Map();
|
|
212
|
+
const nativeDetailComponents = new Map();
|
|
213
|
+
const nativeDetailSurfaceKeys = new Map();
|
|
214
|
+
const nativeOpenTabSessions = new Set();
|
|
215
|
+
const summaryStates = new Map();
|
|
216
|
+
let activePageFeatureId = null;
|
|
217
|
+
let activePinnedFeatureId = null;
|
|
218
|
+
let ensureQueued = false;
|
|
219
|
+
let ensureFrame = null;
|
|
220
|
+
let ensureTimer = null;
|
|
221
|
+
let themeQueued = false;
|
|
222
|
+
let suppressedSelections = [];
|
|
223
|
+
let savedComposerRange = null;
|
|
224
|
+
let savedComposerThread = null;
|
|
225
|
+
let currentThread = null;
|
|
226
|
+
let currentSummaryDisplayMode = null;
|
|
227
|
+
let shiftedConversationOwners = [];
|
|
228
|
+
let mainContentObserver = null;
|
|
229
|
+
let toolbarReadiness = null;
|
|
230
|
+
const domObservers = [];
|
|
231
|
+
const pageScriptCleanups = [];
|
|
232
|
+
const pendingHostActions = new Map();
|
|
233
|
+
let activeModalRecordKey = null;
|
|
234
|
+
let activePromptPreview = null;
|
|
235
|
+
let promptPreviewDismissTimer = null;
|
|
236
|
+
let nativeTabCapabilityPromise = null;
|
|
237
|
+
let nativeTabCapability = null;
|
|
238
|
+
let nativeTabCapabilityError = null;
|
|
239
|
+
const modelSelectorOriginalToggleLabels = new Map();
|
|
240
|
+
const modelSelectorGenericContainers = new Map();
|
|
241
|
+
const modelSelectorNativeToggleHandlers = new Map();
|
|
242
|
+
const modelSelectorPositionObservers = new Map();
|
|
243
|
+
const modelSelectorMenuSizeFrames = new Map();
|
|
244
|
+
let modelSelectorReturnTimer = null;
|
|
245
|
+
let modelSelectorPendingReturn = false;
|
|
246
|
+
let modelSelectorEffortTransactionTimer = null;
|
|
247
|
+
let modelSelectorPendingOutsideDismiss = false;
|
|
248
|
+
let modelSelectorSuppressOutsidePointerSequence = false;
|
|
249
|
+
let modelSelectorTriggerObserver = null;
|
|
250
|
+
let modelSelectorObservedTrigger = null;
|
|
251
|
+
let modelSelectorControllerMenu = null;
|
|
252
|
+
let modelSelectorNativeFastIcons = null;
|
|
253
|
+
// Fast 图标来源标记:bundle 静态提取 vs DOM 克隆,DOM 克隆优先以保持像素一致。
|
|
254
|
+
let modelSelectorNativeFastIconSources = null;
|
|
255
|
+
// DOM 克隆可见性门控:仅在 Fast 控件从无到有时扫描一次,常驻会话不逐帧扫描。
|
|
256
|
+
let modelSelectorFastControlVisible = false;
|
|
257
|
+
// 持久化脏标记:仅在图标实际变化时写一次 localStorage。
|
|
258
|
+
let modelSelectorNativeFastIconsDirty = false;
|
|
259
|
+
const modelSelectorFastIconStorageKey = "codex-model-slider:fast-icons:v1";
|
|
260
|
+
const modelSelectorStyle = document.createElement("style");
|
|
261
|
+
modelSelectorStyle.setAttribute("data-codex-model-slider-style", "");
|
|
262
|
+
modelSelectorStyle.textContent = `
|
|
263
|
+
[data-codex-model-slider-menu] { width: 264px !important; min-width: 264px; overflow-x: hidden; }
|
|
264
|
+
[data-codex-model-slider-controller-menu] {
|
|
265
|
+
position: absolute !important;
|
|
266
|
+
left: 50%;
|
|
267
|
+
bottom: 0;
|
|
268
|
+
transform: translateX(-50%) !important;
|
|
269
|
+
z-index: 10;
|
|
270
|
+
padding: 6px;
|
|
271
|
+
background-color: var(--color-surface-elevated-secondary);
|
|
272
|
+
-webkit-backdrop-filter: none;
|
|
273
|
+
backdrop-filter: none;
|
|
274
|
+
}
|
|
275
|
+
[data-codex-model-slider-menu][data-codex-model-slider-overflow] {
|
|
276
|
+
max-height: var(--codex-model-slider-menu-max-height) !important;
|
|
277
|
+
overflow-y: auto;
|
|
278
|
+
}
|
|
279
|
+
[data-codex-model-slider-trigger] [class*="_ModelPickerTriggerModelLabel_"] {
|
|
280
|
+
min-width: 0; max-width: 110px; flex-shrink: 1; overflow: hidden; text-overflow: ellipsis;
|
|
281
|
+
}
|
|
282
|
+
[data-codex-model-slider-trigger] [class*="_ModelPickerTriggerModelText_"] {
|
|
283
|
+
min-width: 0; max-width: 110px; overflow: hidden; text-overflow: ellipsis;
|
|
284
|
+
}
|
|
285
|
+
html[data-codex-model-slider-catalog-ready]
|
|
286
|
+
[data-codex-model-slider-trigger]
|
|
287
|
+
[class*="_ModelPickerTriggerModelText_"]:not([data-codex-model-slider-trigger-label]) {
|
|
288
|
+
display: none !important;
|
|
289
|
+
}
|
|
290
|
+
[data-codex-model-slider-trigger-label] {
|
|
291
|
+
display: inline-block; min-width: 0; max-width: 110px;
|
|
292
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
293
|
+
}
|
|
294
|
+
/* 展示真实模型名/思考强度时:芯片 HUG 内容,长名称不被截断;
|
|
295
|
+
仅 "Select model / Select effort" 占位态沿用原生宽度。 */
|
|
296
|
+
[data-codex-model-slider-trigger]:not([data-codex-model-slider-trigger-placeholder]) [data-codex-model-slider-trigger-label] {
|
|
297
|
+
max-width: none; overflow: visible; text-overflow: clip; flex-shrink: 0;
|
|
298
|
+
}
|
|
299
|
+
[data-codex-model-slider-trigger]:not([data-codex-model-slider-trigger-placeholder]) [class*="_ModelPickerTriggerModelLabel_"] {
|
|
300
|
+
max-width: none; overflow: visible; flex-shrink: 0;
|
|
301
|
+
}
|
|
302
|
+
[data-codex-model-slider-trigger]:not([data-codex-model-slider-trigger-placeholder]) [class*="_ModelPickerTriggerContent_"] {
|
|
303
|
+
max-width: none !important;
|
|
304
|
+
}
|
|
305
|
+
[data-codex-model-slider-trigger][data-codex-model-slider-trigger-placeholder] [class*="_ModelPickerTriggerContent_"] {
|
|
306
|
+
width: 160px; min-width: 160px !important; max-width: 160px;
|
|
307
|
+
}
|
|
308
|
+
[data-codex-model-slider-trigger] [class*="_ModelPickerTriggerEffortLabel_"] {
|
|
309
|
+
display: inline-flex; align-items: center; align-self: center;
|
|
310
|
+
}
|
|
311
|
+
html[data-codex-model-slider-selecting-effort] [role="menu"][data-state="open"]:not(:has([data-reasoning-slider])) {
|
|
312
|
+
visibility: hidden !important; opacity: 0 !important; animation: none !important; pointer-events: none !important;
|
|
313
|
+
}
|
|
314
|
+
[data-codex-model-slider-row] { display: flex; width: 100%; min-width: 0; align-items: center; gap: 10px; }
|
|
315
|
+
[data-codex-model-slider-brand] { display: grid; width: 18px; height: 18px; flex: 0 0 18px; place-items: center; }
|
|
316
|
+
[data-codex-model-slider-brand] svg { width: 16px; height: 16px; display: block; }
|
|
317
|
+
[data-codex-model-slider-copy] { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 1px; }
|
|
318
|
+
[data-codex-model-slider-name] { overflow: hidden; color: var(--color-text-foreground); text-overflow: ellipsis; white-space: nowrap; }
|
|
319
|
+
[data-codex-model-slider-provider] { overflow: hidden; color: var(--color-text-foreground-tertiary); font-size: 11px; line-height: 14px; text-overflow: ellipsis; white-space: nowrap; }
|
|
320
|
+
[data-codex-model-slider-item] { position: relative; }
|
|
321
|
+
[data-codex-model-slider-item][data-codex-model-slider-selected] { background: var(--color-token-list-hover-background); }
|
|
322
|
+
[data-codex-model-slider-star] {
|
|
323
|
+
display: grid; width: 24px; height: 24px; flex: 0 0 24px; place-items: center;
|
|
324
|
+
border: 0; border-radius: var(--radius-md, 6px); padding: 0; background: transparent;
|
|
325
|
+
color: var(--color-text-foreground-tertiary); cursor: pointer; opacity: 0; pointer-events: none;
|
|
326
|
+
}
|
|
327
|
+
[data-codex-model-slider-item]:hover [data-codex-model-slider-star],
|
|
328
|
+
[data-codex-model-slider-item]:focus-within [data-codex-model-slider-star] { opacity: 1; pointer-events: auto; }
|
|
329
|
+
[data-codex-model-slider-star]:hover, [data-codex-model-slider-star]:focus-visible {
|
|
330
|
+
background: var(--color-token-list-hover-background); color: var(--color-text-foreground); outline: none;
|
|
331
|
+
}
|
|
332
|
+
[data-codex-model-slider-star]::after {
|
|
333
|
+
content: ""; position: absolute; top: 0; right: 0; bottom: 0; width: 40px; z-index: 1;
|
|
334
|
+
}
|
|
335
|
+
[data-codex-model-slider-star] svg { position: relative; z-index: 2; width: 16px; height: 16px; pointer-events: none; }
|
|
336
|
+
[data-codex-model-slider-divider] {
|
|
337
|
+
height: 0; border-top: 1px solid var(--color-token-border, var(--color-border));
|
|
338
|
+
margin: 6px 8px;
|
|
339
|
+
}
|
|
340
|
+
[data-codex-model-slider-generic-menu] {
|
|
341
|
+
overflow-x: hidden;
|
|
342
|
+
transform: translateX(var(--codex-model-slider-menu-center-shift, 0px)) !important;
|
|
343
|
+
}
|
|
344
|
+
[data-codex-model-slider-generic] { box-sizing: border-box; width: 100%; }
|
|
345
|
+
[data-codex-model-slider-fast] { border: 0; background: transparent; cursor: pointer; }
|
|
346
|
+
[data-codex-model-slider-fast]:focus-visible { outline: 2px solid var(--color-border-focus); outline-offset: 1px; }
|
|
347
|
+
[data-codex-model-slider-rail][data-codex-model-slider-fallback] { position: relative; height: 38px; margin: 1px 8px 0; outline: none; cursor: pointer; }
|
|
348
|
+
[data-codex-model-slider-track] { position: absolute; top: 12px; right: 0; left: 0; height: 16px; border-radius: 999px; background: var(--color-background-control); }
|
|
349
|
+
[data-codex-model-slider-range] { position: absolute; top: 0; bottom: 0; left: 0; width: var(--codex-model-slider-progress); border-radius: inherit; background: var(--color-text-accent); }
|
|
350
|
+
[data-codex-model-slider-fallback] [data-codex-model-slider-tick] {
|
|
351
|
+
position: absolute; top: 16px; width: 6px; height: 6px; transform: translate(-50%, -50%); border: 0; border-radius: 50%;
|
|
352
|
+
padding: 0; background: var(--color-text-foreground-tertiary); pointer-events: none;
|
|
353
|
+
}
|
|
354
|
+
[data-codex-model-slider-fallback] [data-codex-model-slider-tick][data-selected="true"] { background: var(--color-text-on-accent); }
|
|
355
|
+
[data-codex-model-slider-thumb] {
|
|
356
|
+
position: absolute; top: 7px; left: var(--codex-model-slider-progress); width: 26px; height: 26px;
|
|
357
|
+
transform: translateX(-50%); border: 0.5px solid var(--color-token-border, var(--color-border)); border-radius: 50%;
|
|
358
|
+
background: var(--color-background-elevated-primary-opaque, var(--color-background-panel)); box-shadow: var(--shadow-md);
|
|
359
|
+
pointer-events: none;
|
|
360
|
+
}
|
|
361
|
+
[data-codex-model-slider-fallback]:focus-visible [data-codex-model-slider-thumb] { box-shadow: 0 0 0 2px var(--color-border-focus); }
|
|
362
|
+
@media (prefers-reduced-motion: reduce) {
|
|
363
|
+
[data-codex-model-slider-star] { transition: none; }
|
|
364
|
+
}
|
|
365
|
+
`;
|
|
366
|
+
|
|
367
|
+
if (modelSelectorFeature) {
|
|
368
|
+
// 启动阶段就解析 Fast 图标:先读持久化的原生克隆,再从 app bundle 静态提取兜底,
|
|
369
|
+
// 避免首次会话必须等用户点开弹窗后 DOM 里才出现图标。
|
|
370
|
+
const persistedIcons = loadPersistedNativeFastIcons(modelSelectorFastIconStorageKey);
|
|
371
|
+
if (persistedIcons) {
|
|
372
|
+
modelSelectorNativeFastIcons = persistedIcons;
|
|
373
|
+
modelSelectorNativeFastIconSources = {};
|
|
374
|
+
for (const state of Object.keys(persistedIcons)) {
|
|
375
|
+
modelSelectorNativeFastIconSources[state] = "persisted";
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
void loadNativeFastIconFromBundle();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ========================================================================
|
|
382
|
+
// page-script 特征安装 — 执行各 feature 声明的页面脚本并登记清理
|
|
383
|
+
// ========================================================================
|
|
384
|
+
|
|
385
|
+
for (const feature of pageScriptFeatures) {
|
|
386
|
+
const source = feature.pageScript?.source ?? "";
|
|
387
|
+
if (!source.trim()) continue;
|
|
388
|
+
try {
|
|
389
|
+
const installer = new Function(
|
|
390
|
+
"signal",
|
|
391
|
+
"window",
|
|
392
|
+
"document",
|
|
393
|
+
"MutationObserver",
|
|
394
|
+
"config",
|
|
395
|
+
`"use strict";\n${source}`,
|
|
396
|
+
);
|
|
397
|
+
const cleanup = installer(
|
|
398
|
+
lifetime.signal,
|
|
399
|
+
window,
|
|
400
|
+
document,
|
|
401
|
+
MutationObserver,
|
|
402
|
+
feature.pageScript.config ?? {},
|
|
403
|
+
);
|
|
404
|
+
if (typeof cleanup === "function") pageScriptCleanups.push(cleanup);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.warn(`[codex-personal] page-script feature failed: ${feature.id}`, error);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ========================================================================
|
|
411
|
+
// Toolbar 入口定位与就绪 — 识别原生 Toolbar/Summary 控件并等待其就绪
|
|
412
|
+
// ========================================================================
|
|
413
|
+
|
|
414
|
+
function findEntry(featureId) {
|
|
415
|
+
return Array.from(document.querySelectorAll(`[${entryMarker}]`)).find(
|
|
416
|
+
(entry) => entry.getAttribute(entryMarker) === featureId,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function findNativeEntry(key) {
|
|
421
|
+
const labels = nativeLabels[key] ?? [String(key).toLowerCase()];
|
|
422
|
+
return Array.from(document.querySelectorAll("a, button")).find((element) => (
|
|
423
|
+
labels.includes(element.textContent?.trim().toLowerCase())
|
|
424
|
+
));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function findReferenceEntry() {
|
|
428
|
+
return findNativeEntry("plugins");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function isVisibleToolbarControl(element) {
|
|
432
|
+
if (!(element instanceof HTMLElement)) return false;
|
|
433
|
+
const style = getComputedStyle(element);
|
|
434
|
+
const rect = element.getBoundingClientRect();
|
|
435
|
+
return style.display !== "none"
|
|
436
|
+
&& style.visibility !== "hidden"
|
|
437
|
+
&& rect.width > 0
|
|
438
|
+
&& rect.height > 0;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function hasNativeToolbarLabel(button, key) {
|
|
442
|
+
const label = String(button?.getAttribute("aria-label") ?? "").trim().toLowerCase();
|
|
443
|
+
return nativeToolbarLabels[key].some((candidate) => (
|
|
444
|
+
key === "temporaryChat"
|
|
445
|
+
? label.includes(candidate.toLowerCase())
|
|
446
|
+
: label === candidate.toLowerCase()
|
|
447
|
+
));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function nativeSummaryButton() {
|
|
451
|
+
return Array.from(document.querySelectorAll("button")).find((button) => (
|
|
452
|
+
(
|
|
453
|
+
hasNativeToolbarLabel(button, "summary")
|
|
454
|
+
|| hasNativeToolbarLabel(button, "pinnedSummary")
|
|
455
|
+
)
|
|
456
|
+
&& isVisibleToolbarControl(button)
|
|
457
|
+
)) ?? null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function nativeBottomPanelButton() {
|
|
461
|
+
return Array.from(document.querySelectorAll("button")).find((button) => {
|
|
462
|
+
if (!hasNativeToolbarLabel(button, "bottomPanel")) return false;
|
|
463
|
+
return isVisibleToolbarControl(button);
|
|
464
|
+
}) ?? null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function nativeSidePanelButton() {
|
|
468
|
+
return Array.from(document.querySelectorAll("button")).find((button) => {
|
|
469
|
+
if (!hasNativeToolbarLabel(button, "sidePanel")) return false;
|
|
470
|
+
return isVisibleToolbarControl(button);
|
|
471
|
+
}) ?? null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function nativeTemporaryChatButton() {
|
|
475
|
+
return Array.from(document.querySelectorAll("button")).find((button) => {
|
|
476
|
+
if (!hasNativeToolbarLabel(button, "temporaryChat")) return false;
|
|
477
|
+
return isVisibleToolbarControl(button);
|
|
478
|
+
}) ?? null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function structuralToolbarAnchor() {
|
|
482
|
+
const buttons = Array.from(document.querySelectorAll("button"));
|
|
483
|
+
return buttons.find((button) => hasNativeToolbarLabel(button, "temporaryChat"))
|
|
484
|
+
?? buttons.find((button) => (
|
|
485
|
+
hasNativeToolbarLabel(button, "summary")
|
|
486
|
+
|| hasNativeToolbarLabel(button, "pinnedSummary")
|
|
487
|
+
))
|
|
488
|
+
?? buttons.find((button) => hasNativeToolbarLabel(button, "bottomPanel"))
|
|
489
|
+
?? buttons.find((button) => hasNativeToolbarLabel(button, "sidePanel"))
|
|
490
|
+
?? null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function stopToolbarReadiness() {
|
|
494
|
+
if (!toolbarReadiness) return;
|
|
495
|
+
toolbarReadiness.observer.disconnect();
|
|
496
|
+
if (toolbarReadiness.retryTimer != null) clearTimeout(toolbarReadiness.retryTimer);
|
|
497
|
+
if (toolbarReadiness.deadlineTimer != null) clearTimeout(toolbarReadiness.deadlineTimer);
|
|
498
|
+
toolbarReadiness = null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function ensureToolbarReadiness(candidate) {
|
|
502
|
+
const thread = threadIdentity();
|
|
503
|
+
if (toolbarReadiness?.candidate === candidate && toolbarReadiness.thread === thread) return;
|
|
504
|
+
stopToolbarReadiness();
|
|
505
|
+
const record = {
|
|
506
|
+
candidate,
|
|
507
|
+
thread,
|
|
508
|
+
attempts: 0,
|
|
509
|
+
retryTimer: null,
|
|
510
|
+
deadlineTimer: null,
|
|
511
|
+
observer: null,
|
|
512
|
+
};
|
|
513
|
+
const wake = () => {
|
|
514
|
+
if (toolbarReadiness !== record) return;
|
|
515
|
+
queueEnsure();
|
|
516
|
+
};
|
|
517
|
+
record.observer = new ResizeObserver(wake);
|
|
518
|
+
record.observer.observe(candidate);
|
|
519
|
+
const header = candidate.closest("header");
|
|
520
|
+
if (header) record.observer.observe(header);
|
|
521
|
+
const retry = () => {
|
|
522
|
+
if (toolbarReadiness !== record || record.attempts >= 8) return;
|
|
523
|
+
record.attempts += 1;
|
|
524
|
+
wake();
|
|
525
|
+
record.retryTimer = setTimeout(retry, 75);
|
|
526
|
+
};
|
|
527
|
+
record.retryTimer = setTimeout(retry, 75);
|
|
528
|
+
record.deadlineTimer = setTimeout(() => {
|
|
529
|
+
if (toolbarReadiness === record) stopToolbarReadiness();
|
|
530
|
+
}, 1_500);
|
|
531
|
+
toolbarReadiness = record;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function toolbarControlRoot(button) {
|
|
535
|
+
return button?.parentElement?.parentElement ?? button?.parentElement ?? button ?? null;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function nativeSummaryToolbarGroup(nativeButton) {
|
|
539
|
+
return toolbarControlRoot(nativeButton)?.parentElement ?? null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function conversationToolbarHost(nativeButton) {
|
|
543
|
+
const header = nativeButton?.closest("header");
|
|
544
|
+
if (!header) return null;
|
|
545
|
+
return Array.from(header.children).find((child) => {
|
|
546
|
+
if (!(child instanceof HTMLElement) || !isVisibleToolbarControl(child)) return false;
|
|
547
|
+
return Number.parseFloat(getComputedStyle(child).flexGrow) > 0;
|
|
548
|
+
}) ?? null;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function ensureFallbackSummaryToolbarGroup(nativeButton) {
|
|
552
|
+
const toolbarHost = conversationToolbarHost(nativeButton);
|
|
553
|
+
if (!toolbarHost) return null;
|
|
554
|
+
let group = toolbarHost.querySelector(":scope > [data-codex-personal-summary-toolbar-group]");
|
|
555
|
+
if (!group) {
|
|
556
|
+
group = document.createElement("div");
|
|
557
|
+
group.setAttribute("data-codex-personal-summary-toolbar-group", "fallback");
|
|
558
|
+
// The host toolbar strip is pointer-events:none; without this the
|
|
559
|
+
// fallback entries would be visible but unclickable.
|
|
560
|
+
group.className = "ms-auto flex shrink-0 items-center gap-1.5 pointer-events-auto";
|
|
561
|
+
// Same structure as the native tool group (e.g. the temporary-chat
|
|
562
|
+
// group in Chat mode): an in-flow ms-auto strip inside the flexible
|
|
563
|
+
// header area. It sits at the right edge while the side panel is
|
|
564
|
+
// closed and, when the side panel widens its host container, moves
|
|
565
|
+
// together with the native layout exactly like Chat mode does.
|
|
566
|
+
toolbarHost.append(group);
|
|
567
|
+
}
|
|
568
|
+
return group;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function toolbarEntryFitsHeader(entryRoot) {
|
|
572
|
+
const header = entryRoot.closest("header");
|
|
573
|
+
if (!header) return true;
|
|
574
|
+
const headerRect = header.getBoundingClientRect();
|
|
575
|
+
const entryRect = entryRoot.getBoundingClientRect();
|
|
576
|
+
if (entryRect.width <= 0 || entryRect.height <= 0) return false;
|
|
577
|
+
return entryRect.right <= headerRect.right + 1 && entryRect.left >= headerRect.left - 1;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function isNativeSummaryButton(element) {
|
|
581
|
+
if (!(element instanceof Element)) return false;
|
|
582
|
+
const button = element.closest("button");
|
|
583
|
+
return hasNativeToolbarLabel(button, "summary")
|
|
584
|
+
|| hasNativeToolbarLabel(button, "pinnedSummary");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function mainContentViewport() {
|
|
588
|
+
const composer = currentComposer();
|
|
589
|
+
return composer?.closest("main[data-app-shell-main-surface]")
|
|
590
|
+
?? document.querySelector("main[data-app-shell-main-surface]")
|
|
591
|
+
?? document.querySelector("[data-app-shell-main-content-layout]")
|
|
592
|
+
?? document.querySelector("main");
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function visibleRightWorkspacePanels() {
|
|
596
|
+
return Array.from(document.querySelectorAll('[data-app-shell-tab-panel-controller="right"]'))
|
|
597
|
+
.filter((panel) => {
|
|
598
|
+
if (!(panel instanceof HTMLElement) || panel.hidden) return false;
|
|
599
|
+
const style = getComputedStyle(panel);
|
|
600
|
+
const rect = panel.getBoundingClientRect();
|
|
601
|
+
return style.display !== "none" && style.visibility !== "hidden" && rect.width > 0;
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function mainContentTargetGeometry() {
|
|
606
|
+
const viewport = mainContentViewport();
|
|
607
|
+
const rect = viewport?.getBoundingClientRect() ?? {
|
|
608
|
+
left: Math.max(0, sidebarRight()),
|
|
609
|
+
right: innerWidth,
|
|
610
|
+
top: 0,
|
|
611
|
+
bottom: innerHeight,
|
|
612
|
+
width: Math.max(0, innerWidth - sidebarRight()),
|
|
613
|
+
};
|
|
614
|
+
let right = rect.right;
|
|
615
|
+
for (const panel of visibleRightWorkspacePanels()) {
|
|
616
|
+
const panelRect = panel.getBoundingClientRect();
|
|
617
|
+
if (panelRect.left > rect.left && panelRect.left < right) right = panelRect.left;
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
viewport,
|
|
621
|
+
left: rect.left,
|
|
622
|
+
right,
|
|
623
|
+
top: rect.top,
|
|
624
|
+
bottom: rect.bottom,
|
|
625
|
+
width: Math.max(0, right - rect.left),
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function summaryDisplayMode() {
|
|
630
|
+
const layout = config.summaryLayout;
|
|
631
|
+
const clearance = (mainContentTargetGeometry().width - layout.contentBaseWidth) / 2;
|
|
632
|
+
if (clearance < layout.overlayClearance) return "overlay";
|
|
633
|
+
if (clearance < layout.gutterClearance) return "shift";
|
|
634
|
+
return "gutter";
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function summaryState(featureId) {
|
|
638
|
+
let state = summaryStates.get(featureId);
|
|
639
|
+
if (!state) {
|
|
640
|
+
state = { isPinned: false, isPopoverOpen: false };
|
|
641
|
+
summaryStates.set(featureId, state);
|
|
642
|
+
}
|
|
643
|
+
return state;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function visibleSummarySurface(featureId, displayMode = summaryDisplayMode()) {
|
|
647
|
+
const state = summaryState(featureId);
|
|
648
|
+
if (displayMode !== "overlay" && state.isPinned) return "inline";
|
|
649
|
+
if (displayMode === "overlay" && state.isPopoverOpen) return "popover";
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function summaryButtonLabel(feature, displayMode = summaryDisplayMode()) {
|
|
654
|
+
const definition = feature.pinnedSummary ?? {};
|
|
655
|
+
return displayMode === "overlay"
|
|
656
|
+
? definition.overlayLabel ?? (locale === "zh-CN" ? "切换笔记" : "Toggle notes")
|
|
657
|
+
: definition.pinnedLabel
|
|
658
|
+
?? feature.entry.ariaLabel
|
|
659
|
+
?? (locale === "zh-CN" ? "切换固定笔记" : "Toggle pinned notes");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function reducedMotion() {
|
|
663
|
+
return window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches ?? false;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ========================================================================
|
|
667
|
+
// 图标工厂与原生 Fast 图标 — markup 图标、CSS 模块类提取、Fast 图标克隆与持久化
|
|
668
|
+
// ========================================================================
|
|
669
|
+
|
|
670
|
+
function createMarkupIcon(iconDefinition, className, size = 16) {
|
|
671
|
+
if (!iconDefinition?.markup) return null;
|
|
672
|
+
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
673
|
+
icon.setAttribute("width", String(size));
|
|
674
|
+
icon.setAttribute("height", String(size));
|
|
675
|
+
icon.setAttribute("viewBox", iconDefinition.viewBox ?? "0 0 24 24");
|
|
676
|
+
icon.setAttribute("fill", "none");
|
|
677
|
+
icon.setAttribute("stroke", "currentColor");
|
|
678
|
+
icon.setAttribute("stroke-width", "1.5");
|
|
679
|
+
icon.setAttribute("stroke-linecap", "round");
|
|
680
|
+
icon.setAttribute("stroke-linejoin", "round");
|
|
681
|
+
icon.setAttribute("aria-hidden", "true");
|
|
682
|
+
if (className) icon.setAttribute("class", className);
|
|
683
|
+
icon.innerHTML = iconDefinition.markup;
|
|
684
|
+
return icon;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function createFeatureIcon(feature, className, size = 16) {
|
|
688
|
+
return createMarkupIcon(feature.icon, className, size);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function createModelSelectorIcon(definition, { brand = false } = {}) {
|
|
692
|
+
const icon = createMarkupIcon(definition, "", 16);
|
|
693
|
+
if (!icon) return null;
|
|
694
|
+
if (brand) {
|
|
695
|
+
icon.setAttribute("fill", "currentColor");
|
|
696
|
+
icon.removeAttribute("stroke");
|
|
697
|
+
icon.removeAttribute("stroke-width");
|
|
698
|
+
icon.removeAttribute("stroke-linecap");
|
|
699
|
+
icon.removeAttribute("stroke-linejoin");
|
|
700
|
+
}
|
|
701
|
+
return icon;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function nativeCssModuleClass(tokenName, root = document) {
|
|
705
|
+
const pattern = new RegExp(`^_${tokenName}_[A-Za-z0-9_-]+$`);
|
|
706
|
+
const live = Array.from(root.querySelectorAll(`[class*="_${tokenName}_"]`))
|
|
707
|
+
.flatMap((element) => Array.from(element.classList))
|
|
708
|
+
.find((name) => pattern.test(name));
|
|
709
|
+
if (live) return live;
|
|
710
|
+
const selectorPattern = new RegExp(`\\.(_${tokenName}_[A-Za-z0-9_-]+)`);
|
|
711
|
+
const find = (rules) => {
|
|
712
|
+
for (const rule of rules ?? []) {
|
|
713
|
+
const match = String(rule.selectorText ?? "").match(selectorPattern);
|
|
714
|
+
if (match?.[1]) return match[1];
|
|
715
|
+
try {
|
|
716
|
+
const nested = find(rule.cssRules);
|
|
717
|
+
if (nested) return nested;
|
|
718
|
+
} catch {
|
|
719
|
+
// Ignore inaccessible nested rules and continue with Codex's readable sheets.
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return null;
|
|
723
|
+
};
|
|
724
|
+
for (const sheet of document.styleSheets) {
|
|
725
|
+
try {
|
|
726
|
+
const match = find(sheet.cssRules);
|
|
727
|
+
if (match) return match;
|
|
728
|
+
} catch {
|
|
729
|
+
// Ignore cross-origin stylesheets.
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function persistNativeFastIcons() {
|
|
736
|
+
if (!modelSelectorNativeFastIcons || !modelSelectorNativeFastIconsDirty) return;
|
|
737
|
+
try {
|
|
738
|
+
const payload = {};
|
|
739
|
+
for (const [state, template] of Object.entries(modelSelectorNativeFastIcons)) {
|
|
740
|
+
payload[state] = template.outerHTML;
|
|
741
|
+
}
|
|
742
|
+
localStorage.setItem(modelSelectorFastIconStorageKey, JSON.stringify(payload));
|
|
743
|
+
modelSelectorNativeFastIconsDirty = false;
|
|
744
|
+
} catch {
|
|
745
|
+
// localStorage 不可用时静默降级,仅本次会话内生效。
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function loadPersistedNativeFastIcons(storageKey) {
|
|
750
|
+
let stored;
|
|
751
|
+
try {
|
|
752
|
+
stored = localStorage.getItem(storageKey);
|
|
753
|
+
} catch {
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
let payload;
|
|
757
|
+
try {
|
|
758
|
+
payload = JSON.parse(stored ?? "null");
|
|
759
|
+
} catch {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
if (!payload || typeof payload !== "object") return null;
|
|
763
|
+
const icons = {};
|
|
764
|
+
for (const [state, markup] of Object.entries(payload)) {
|
|
765
|
+
if (typeof markup !== "string") continue;
|
|
766
|
+
const container = document.createElement("template");
|
|
767
|
+
container.innerHTML = markup;
|
|
768
|
+
const svg = container.content.querySelector("svg");
|
|
769
|
+
if (svg) icons[state] = svg;
|
|
770
|
+
}
|
|
771
|
+
return Object.keys(icons).length ? icons : null;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function extractFastIconPath(source, prefix) {
|
|
775
|
+
const match = String(source).match(new RegExp(`d:\`(${prefix}[^\`]*)\``));
|
|
776
|
+
return match?.[1] ?? null;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async function loadNativeFastIconFromBundle() {
|
|
780
|
+
// 两个图标均已就绪(持久化或 DOM 克隆)时不再拉取整包 app-initial 资源。
|
|
781
|
+
if (modelSelectorNativeFastIcons?.active && modelSelectorNativeFastIcons?.inactive) return;
|
|
782
|
+
// Fast bolt 图标定义在新版随 app-primary 入口包加载(app-initial 里没有),
|
|
783
|
+
// 按 app-primary -> app-initial 顺序取第一个已加载的入口包。
|
|
784
|
+
const sourceLink = document.querySelector('link[href*="/assets/app-primary-"][href$=".js"]')
|
|
785
|
+
?? document.querySelector('link[href*="/assets/app-initial-"][href$=".js"]');
|
|
786
|
+
if (!sourceLink?.href) return;
|
|
787
|
+
try {
|
|
788
|
+
const source = await fetch(sourceLink.href).then((response) => response.text());
|
|
789
|
+
if (!modelSelectorNativeFastIcons) {
|
|
790
|
+
modelSelectorNativeFastIcons = {};
|
|
791
|
+
modelSelectorNativeFastIconSources = {};
|
|
792
|
+
}
|
|
793
|
+
let gained = false;
|
|
794
|
+
const define = (state, prefix, definition) => {
|
|
795
|
+
if (modelSelectorNativeFastIcons[state]) return;
|
|
796
|
+
const path = extractFastIconPath(source, prefix);
|
|
797
|
+
if (!path) return;
|
|
798
|
+
const svg = createModelSelectorIcon({
|
|
799
|
+
viewBox: definition.viewBox,
|
|
800
|
+
markup: definition.transform
|
|
801
|
+
? `<g transform="${definition.transform}"><path d="${path}" fill="currentColor" /></g>`
|
|
802
|
+
: `<path d="${path}" fill="currentColor" />`,
|
|
803
|
+
}, { brand: true });
|
|
804
|
+
if (!svg) return;
|
|
805
|
+
svg.setAttribute("width", "20");
|
|
806
|
+
svg.setAttribute("height", "20");
|
|
807
|
+
modelSelectorNativeFastIcons[state] = svg;
|
|
808
|
+
modelSelectorNativeFastIconSources[state] = "bundle";
|
|
809
|
+
gained = true;
|
|
810
|
+
};
|
|
811
|
+
define("active", "M11\\.9125 21\\.4125", { viewBox: "0 0 24 24" });
|
|
812
|
+
define("inactive", "M9\\.80999 17\\.8302", { viewBox: "0 0 20 20" });
|
|
813
|
+
if (gained) {
|
|
814
|
+
modelSelectorNativeFastIconsDirty = true;
|
|
815
|
+
persistNativeFastIcons();
|
|
816
|
+
applyNativeFastIconsToShells();
|
|
817
|
+
queueEnsure();
|
|
818
|
+
}
|
|
819
|
+
} catch {
|
|
820
|
+
// 私有资源移动时保持原生控件不变,等待 DOM 克隆兜底。
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function hasVisibleFastControl() {
|
|
825
|
+
return Boolean(
|
|
826
|
+
document.querySelector("[data-codex-model-slider-fast]")
|
|
827
|
+
|| document.querySelector('[class*="_FastModeToggle_"]')
|
|
828
|
+
|| document.querySelector('[class*="_ModelPickerTriggerFastIndicator_"] svg'),
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function captureNativeFastIcon() {
|
|
833
|
+
// Fast 图标只在 Fast 控件渲染后才出现在 DOM(打开菜单或 GPT 模型);
|
|
834
|
+
// 只在控件可见性从无到有时做一次全页扫描(升级为 DOM 克隆),
|
|
835
|
+
// 常驻会话不逐帧扫描整个文档;缺图标时由 bundle/持久化兜底。
|
|
836
|
+
const visible = hasVisibleFastControl();
|
|
837
|
+
const justBecameVisible = visible && !modelSelectorFastControlVisible;
|
|
838
|
+
modelSelectorFastControlVisible = visible;
|
|
839
|
+
if (visible && justBecameVisible) {
|
|
840
|
+
const capture = (state, pathPrefix) => {
|
|
841
|
+
const current = modelSelectorNativeFastIcons?.[state];
|
|
842
|
+
const currentSource = modelSelectorNativeFastIconSources?.[state];
|
|
843
|
+
if (current && currentSource === "dom") return;
|
|
844
|
+
const path = Array.from(document.querySelectorAll("svg path")).find(
|
|
845
|
+
(candidate) => candidate.getAttribute("d")?.startsWith(pathPrefix),
|
|
846
|
+
);
|
|
847
|
+
const svg = path?.closest("svg");
|
|
848
|
+
if (!svg) return;
|
|
849
|
+
const template = svg.cloneNode(true);
|
|
850
|
+
template.removeAttribute("id");
|
|
851
|
+
// 统一呈现尺寸:保留原生 viewBox,固定 20px。DOM 克隆来源是
|
|
852
|
+
// 原生 active(24px)/inactive(20px) 两套 SVG,若不归一化,
|
|
853
|
+
// active 24px 放进 26px 的 Fast content 容器会显得大一圈。
|
|
854
|
+
template.setAttribute("width", "20");
|
|
855
|
+
template.setAttribute("height", "20");
|
|
856
|
+
if (!modelSelectorNativeFastIcons) {
|
|
857
|
+
modelSelectorNativeFastIcons = {};
|
|
858
|
+
modelSelectorNativeFastIconSources = {};
|
|
859
|
+
}
|
|
860
|
+
modelSelectorNativeFastIcons[state] = template;
|
|
861
|
+
modelSelectorNativeFastIconSources[state] = "dom";
|
|
862
|
+
modelSelectorNativeFastIconsDirty = true;
|
|
863
|
+
};
|
|
864
|
+
capture("active", "M11.9125 21.4125");
|
|
865
|
+
// 现版 app 的 inactive bolt 为 20x20,路径以 M9.80999 开头;
|
|
866
|
+
// 旧的 M7.38 前缀已匹配不到任何图标,导致 inactive 缺失。
|
|
867
|
+
capture("inactive", "M9.80999 17.8302");
|
|
868
|
+
}
|
|
869
|
+
const ready = Boolean(
|
|
870
|
+
modelSelectorNativeFastIcons?.active && modelSelectorNativeFastIcons?.inactive,
|
|
871
|
+
);
|
|
872
|
+
if (ready) {
|
|
873
|
+
persistNativeFastIcons();
|
|
874
|
+
applyNativeFastIconsToShells();
|
|
875
|
+
}
|
|
876
|
+
return modelSelectorNativeFastIcons;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function applyNativeFastIconsToShells() {
|
|
880
|
+
document.querySelectorAll("[data-codex-model-slider-fast]").forEach((button) => {
|
|
881
|
+
const content = button.querySelector("[data-codex-model-slider-fast-content]");
|
|
882
|
+
// 幂等补装:只给还没有图标的按钮补上,避免每次 ensure 都重建节点。
|
|
883
|
+
if (!content || content.querySelector("svg")) return;
|
|
884
|
+
const icon = createNativeFastIcon(button.getAttribute("aria-pressed") === "true");
|
|
885
|
+
if (icon) content.replaceChildren(icon);
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function createNativeFastIcon(active) {
|
|
890
|
+
// 直接读取已就绪的图标存储(启动时由 bundle/持久化填充,运行期由
|
|
891
|
+
// captureNativeFastIcon 升级为 DOM 克隆),避免再次触发捕获造成递归。
|
|
892
|
+
// 优先克隆当前面板的图标,保留原生尺寸和路径,不使用旧版图标规格。
|
|
893
|
+
const nativeContent = Array.from(document.querySelectorAll('[class*="_FastModeToggleContent_"]'))
|
|
894
|
+
.find((content) => !content.closest("[data-codex-model-slider-generic]")
|
|
895
|
+
&& (content.getAttribute("data-fast-mode-enabled")
|
|
896
|
+
?? content.closest('[class*="_FastModeToggle_"]')?.getAttribute("data-fast-mode-enabled")) === String(active));
|
|
897
|
+
const template = nativeContent?.querySelector("svg")
|
|
898
|
+
?? modelSelectorNativeFastIcons?.[active ? "active" : "inactive"];
|
|
899
|
+
if (!template) return null;
|
|
900
|
+
const icon = template.cloneNode(true);
|
|
901
|
+
icon.setAttribute("aria-hidden", "true");
|
|
902
|
+
return icon;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// ========================================================================
|
|
906
|
+
// 模型 identity、目录与菜单增强 — 模型身份解析、品牌/别名、收藏与原生菜单增强
|
|
907
|
+
// ========================================================================
|
|
908
|
+
|
|
909
|
+
function modelIdentity(rawValue) {
|
|
910
|
+
const raw = String(rawValue ?? "").trim();
|
|
911
|
+
const slash = raw.indexOf("/");
|
|
912
|
+
return {
|
|
913
|
+
raw,
|
|
914
|
+
provider: slash > 0 ? raw.slice(0, slash).trim() : "",
|
|
915
|
+
backend: slash > 0 ? raw.slice(slash + 1).trim() : raw,
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function formatIdentifier(value) {
|
|
920
|
+
const raw = String(value ?? "").trim();
|
|
921
|
+
if (!raw) return "";
|
|
922
|
+
if (/\s/.test(raw) && !raw.includes("/") && !raw.includes("_") && !raw.includes("-")) return raw;
|
|
923
|
+
const canonical = new Map([
|
|
924
|
+
["ai", "AI"], ["api", "API"], ["chatglm", "ChatGLM"], ["claude", "Claude"],
|
|
925
|
+
["codex", "Codex"], ["deepseek", "DeepSeek"], ["gemini", "Gemini"], ["gemma", "Gemma"],
|
|
926
|
+
["glm", "GLM"], ["gpt", "GPT"], ["grok", "Grok"], ["kimi", "Kimi"],
|
|
927
|
+
["minimax", "MiniMax"], ["mimo", "MiMo"], ["moonshot", "Moonshot"], ["qwen", "Qwen"],
|
|
928
|
+
["qwq", "QwQ"], ["xai", "xAI"], ["xiaomi", "Xiaomi"], ["zhipu", "Zhipu"],
|
|
929
|
+
]);
|
|
930
|
+
return raw
|
|
931
|
+
.replace(/[_-]+/g, " ")
|
|
932
|
+
.split(/\s+/)
|
|
933
|
+
.filter(Boolean)
|
|
934
|
+
.map((token) => {
|
|
935
|
+
const lower = token.toLowerCase();
|
|
936
|
+
if (canonical.has(lower)) return canonical.get(lower);
|
|
937
|
+
if (/^v\d/i.test(token)) return `V${token.slice(1)}`;
|
|
938
|
+
if (/^o\d/i.test(token)) return token.toUpperCase();
|
|
939
|
+
return token.charAt(0).toUpperCase() + token.slice(1);
|
|
940
|
+
})
|
|
941
|
+
.join(" ");
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function providerLabel(provider, selector) {
|
|
945
|
+
if (!provider) return "";
|
|
946
|
+
return selector.providerAliases?.[provider.toLowerCase()] ?? formatIdentifier(provider);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function modelDisplayLabel(identity, selector) {
|
|
950
|
+
const model = catalogModelFor(identity, selector) ?? nativeCatalogModelFor(identity);
|
|
951
|
+
const source = model?.displayName || model?.slug || identity.backend;
|
|
952
|
+
return formatIdentifier(source.slice(source.lastIndexOf("/") + 1));
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
function compactModelLabel(rawValue) {
|
|
956
|
+
const raw = String(rawValue ?? "").trim();
|
|
957
|
+
return formatIdentifier(raw.slice(raw.lastIndexOf("/") + 1));
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function brandForModel(identity, selector) {
|
|
961
|
+
const haystack = `${identity.backend} ${identity.raw}`.toLowerCase();
|
|
962
|
+
return selector.brands?.find((brand) => (
|
|
963
|
+
brand.keywords.some((keyword) => haystack.includes(keyword))
|
|
964
|
+
)) ?? null;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function comparableModelLabel(value) {
|
|
968
|
+
return formatIdentifier(modelIdentity(value).backend)
|
|
969
|
+
.toLowerCase()
|
|
970
|
+
.replace(/^gpt\s+/, "")
|
|
971
|
+
.trim();
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function currentReactFiber(element) {
|
|
975
|
+
const fiberKey = Object.getOwnPropertyNames(element ?? {}).find((key) => key.startsWith("__reactFiber$"));
|
|
976
|
+
const propsKey = Object.getOwnPropertyNames(element ?? {}).find((key) => key.startsWith("__reactProps$"));
|
|
977
|
+
let fiber = fiberKey ? element[fiberKey] : null;
|
|
978
|
+
if (
|
|
979
|
+
propsKey
|
|
980
|
+
&& fiber?.alternate?.memoizedProps === element[propsKey]
|
|
981
|
+
&& fiber.memoizedProps !== element[propsKey]
|
|
982
|
+
) return fiber.alternate;
|
|
983
|
+
return fiber;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function nativeComposerTrigger() {
|
|
987
|
+
const triggers = Array.from(document.querySelectorAll("[data-codex-intelligence-trigger]"))
|
|
988
|
+
.filter((trigger) => trigger.checkVisibility({ checkVisibilityCSS: true, checkOpacity: true }));
|
|
989
|
+
return triggers.find((trigger) => trigger.getAttribute("data-state") === "open")
|
|
990
|
+
?? triggers[0] ?? null;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function nativeComposerModelController() {
|
|
994
|
+
const trigger = nativeComposerTrigger();
|
|
995
|
+
let fiber = currentReactFiber(trigger);
|
|
996
|
+
for (let depth = 0; fiber && depth < 40; depth += 1, fiber = fiber.return) {
|
|
997
|
+
const props = fiber.memoizedProps;
|
|
998
|
+
if (Array.isArray(props?.models) && typeof props.onSelectReasoningEffort === "function") return props;
|
|
999
|
+
// Chat/Temporary Chat 使用原生模型选择器,不进入任务模型适配。
|
|
1000
|
+
if (props?.selectedModel && typeof props.onModelChange === "function") return null;
|
|
1001
|
+
}
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function nativeCatalogModelFor(identity, selector = modelSelectorFeature?.modelSelector) {
|
|
1006
|
+
const controller = nativeComposerModelController();
|
|
1007
|
+
const models = controller?.models ?? [];
|
|
1008
|
+
const exact = models.find((model) => String(model.model ?? "").toLowerCase() === identity.raw.toLowerCase());
|
|
1009
|
+
const target = comparableModelLabel(identity.backend);
|
|
1010
|
+
const aliases = exact ? [] : models.filter((model) => (
|
|
1011
|
+
comparableModelLabel(model.model) === target || comparableModelLabel(model.displayName) === target
|
|
1012
|
+
));
|
|
1013
|
+
const model = exact ?? (aliases.length === 1 ? aliases[0] : null);
|
|
1014
|
+
if (!model) return null;
|
|
1015
|
+
const nativeReasoningLevels = (model.supportedReasoningEfforts ?? []).map((level) => ({
|
|
1016
|
+
effort: String(level?.reasoningEffort ?? level?.effort ?? level ?? "").toLowerCase(),
|
|
1017
|
+
})).filter((level) => level.effort);
|
|
1018
|
+
const supportedReasoningLevels = nativeReasoningLevels;
|
|
1019
|
+
const currentReasoningLevel = String(controller?.reasoningEffort ?? "").toLowerCase();
|
|
1020
|
+
return {
|
|
1021
|
+
slug: String(model.model ?? ""),
|
|
1022
|
+
displayName: String(model.displayName ?? model.model ?? ""),
|
|
1023
|
+
currentReasoningLevel: nativeReasoningLevels.some(
|
|
1024
|
+
(level) => level.effort === currentReasoningLevel,
|
|
1025
|
+
) ? currentReasoningLevel : null,
|
|
1026
|
+
currentReasoningLevelVisible: supportedReasoningLevels.some(
|
|
1027
|
+
(level) => level.effort === currentReasoningLevel,
|
|
1028
|
+
),
|
|
1029
|
+
defaultReasoningLevel: String(model.defaultReasoningEffort ?? "").toLowerCase(),
|
|
1030
|
+
supportedReasoningLevels,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function modelDefinitionFor(identity, selector) {
|
|
1035
|
+
return nativeCatalogModelFor(identity) ?? catalogModelFor(identity, selector);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// 用 OpenCodex 配置声明的真实档位对滑条做精确交集,过滤 catalog 为支持
|
|
1039
|
+
// mock 顶档而额外广告的 max/ultra。键匹配顺序:模型自带的
|
|
1040
|
+
// opencodex_capability_provenance → catalog 模型 → 菜单原始 identity;
|
|
1041
|
+
// 只做过滤不扩展,配置未声明的模型保持 catalog 原样。
|
|
1042
|
+
function modelWithOpenCodexEfforts(identity, selector, model) {
|
|
1043
|
+
if (!model || !selector?.openCodexRealEfforts) return model;
|
|
1044
|
+
const provenance = model.opencodex_capability_provenance;
|
|
1045
|
+
const catalogModel = provenance?.provider && provenance?.model_id
|
|
1046
|
+
? null
|
|
1047
|
+
: catalogModelFor(identity, selector);
|
|
1048
|
+
const source = catalogModel ?? model;
|
|
1049
|
+
const sourceProvenance = source.opencodex_capability_provenance;
|
|
1050
|
+
const key = sourceProvenance?.provider && sourceProvenance?.model_id
|
|
1051
|
+
? `${sourceProvenance.provider}/${sourceProvenance.model_id}`.toLowerCase()
|
|
1052
|
+
: identity.raw.toLowerCase();
|
|
1053
|
+
const realEfforts = selector.openCodexRealEfforts[key];
|
|
1054
|
+
if (!realEfforts) return model;
|
|
1055
|
+
const allowed = new Set(realEfforts);
|
|
1056
|
+
const levels = (model.supportedReasoningLevels ?? []).filter((level) => {
|
|
1057
|
+
const effort = String(level?.effort ?? level ?? "").toLowerCase();
|
|
1058
|
+
return allowed.has(effort);
|
|
1059
|
+
});
|
|
1060
|
+
return { ...model, supportedReasoningLevels: levels };
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function nativeModelDescriptorForItem(item, observedRaw) {
|
|
1064
|
+
const target = comparableModelLabel(observedRaw);
|
|
1065
|
+
if (!target) return null;
|
|
1066
|
+
const fiberKey = Object.getOwnPropertyNames(item).find((key) => key.startsWith("__reactFiber$"));
|
|
1067
|
+
let fiber = fiberKey ? item[fiberKey] : null;
|
|
1068
|
+
for (let depth = 0; fiber && depth < 35; depth += 1, fiber = fiber.return) {
|
|
1069
|
+
if (!Array.isArray(fiber.memoizedProps)) continue;
|
|
1070
|
+
const matches = fiber.memoizedProps.map((element) => {
|
|
1071
|
+
const props = element?.props?.children?.props;
|
|
1072
|
+
const slug = String(props?.model ?? element?.key ?? "").trim();
|
|
1073
|
+
const displayName = String(props?.displayName ?? "").trim();
|
|
1074
|
+
return slug && (
|
|
1075
|
+
comparableModelLabel(slug) === target || comparableModelLabel(displayName) === target
|
|
1076
|
+
) ? { slug, displayName } : null;
|
|
1077
|
+
}).filter(Boolean);
|
|
1078
|
+
if (matches.length === 1) return matches[0];
|
|
1079
|
+
}
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function catalogModelFor(identity, selector) {
|
|
1084
|
+
const raw = identity.raw.toLowerCase();
|
|
1085
|
+
const backend = identity.backend.toLowerCase();
|
|
1086
|
+
const exact = selector.models?.find((model) => (
|
|
1087
|
+
model.slug.toLowerCase() === raw
|
|
1088
|
+
|| model.slug.toLowerCase() === backend
|
|
1089
|
+
|| model.displayName.toLowerCase() === raw
|
|
1090
|
+
)) ?? null;
|
|
1091
|
+
if (exact) return exact;
|
|
1092
|
+
const target = comparableModelLabel(identity.backend);
|
|
1093
|
+
if (!target) return null;
|
|
1094
|
+
const aliases = (selector.models ?? []).filter((model) => (
|
|
1095
|
+
comparableModelLabel(model.slug) === target || comparableModelLabel(model.displayName) === target
|
|
1096
|
+
));
|
|
1097
|
+
return resolveModelAlias(aliases, identity);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function resolveModelAlias(aliases, identity) {
|
|
1101
|
+
if (aliases.length === 1) return aliases[0];
|
|
1102
|
+
if (aliases.length < 2 || identity.provider) return null;
|
|
1103
|
+
const native = aliases.find((model) => !model.slug.includes("/"));
|
|
1104
|
+
const thirdParty = aliases.find((model) => model.slug.includes("/"));
|
|
1105
|
+
if (!native || !thirdParty) return null;
|
|
1106
|
+
if (!modelIdentity(native.slug).backend.toLowerCase().startsWith("gpt")) return null;
|
|
1107
|
+
const nativeLabel = formatIdentifier(
|
|
1108
|
+
modelIdentity(String(native.displayName ?? native.slug)).backend,
|
|
1109
|
+
).toLowerCase();
|
|
1110
|
+
return formatIdentifier(identity.backend).toLowerCase() === nativeLabel ? native : thirdParty;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function loadModelFavorites(selector) {
|
|
1114
|
+
try {
|
|
1115
|
+
const value = JSON.parse(localStorage.getItem(selector.favoriteStorageKey) ?? "[]");
|
|
1116
|
+
return new Set(Array.isArray(value) ? value.map(String) : []);
|
|
1117
|
+
} catch {
|
|
1118
|
+
return new Set();
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function saveModelFavorites(selector, favorites) {
|
|
1123
|
+
try {
|
|
1124
|
+
localStorage.setItem(selector.favoriteStorageKey, JSON.stringify(Array.from(favorites)));
|
|
1125
|
+
} catch {
|
|
1126
|
+
// A disabled localStorage must not break the native picker.
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
function nativeModelPickerMenu() {
|
|
1131
|
+
return Array.from(document.querySelectorAll('[role="menu"][data-state="open"]')).find(
|
|
1132
|
+
(menu) => (
|
|
1133
|
+
menu.querySelector("[data-reasoning-slider]")
|
|
1134
|
+
&& (menu.querySelector("[data-model-picker-view-toggle]") || nativeModelPickerRow(menu))
|
|
1135
|
+
),
|
|
1136
|
+
) ?? null;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function nativeModelPickerRow(menu) {
|
|
1140
|
+
const rows = Array.from(menu?.querySelectorAll('[role="menuitem"]') ?? []);
|
|
1141
|
+
return rows.find((row) => (
|
|
1142
|
+
row.getAttribute("aria-haspopup") === "menu"
|
|
1143
|
+
&& !row.hasAttribute("data-model-picker-view-toggle")
|
|
1144
|
+
)) ?? rows.find((row) => {
|
|
1145
|
+
const aria = String(row.getAttribute("aria-label") ?? "").trim();
|
|
1146
|
+
const firstLine = String(row.innerText ?? "").split("\n", 1)[0].trim();
|
|
1147
|
+
return /^model(?:\s|$)/i.test(aria) || /^model$/i.test(firstLine);
|
|
1148
|
+
}) ?? null;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function nativeModelValue(menu) {
|
|
1152
|
+
const toggle = menu?.querySelector("[data-model-picker-view-toggle]");
|
|
1153
|
+
const toggleModelLabel = toggle?.querySelector('[class*="_ViewToggleModelLabel_"]');
|
|
1154
|
+
const toggleValues = String(
|
|
1155
|
+
toggle?.hasAttribute("data-model-picker-view-toggle")
|
|
1156
|
+
? (toggleModelLabel?.textContent ?? toggle.innerText)
|
|
1157
|
+
: "",
|
|
1158
|
+
).split("\n").map((value) => value.trim()).filter(Boolean);
|
|
1159
|
+
if (toggleValues.length) return toggleValues[0];
|
|
1160
|
+
const row = nativeModelPickerRow(menu);
|
|
1161
|
+
const values = String(row?.innerText ?? "").split("\n").map((value) => value.trim()).filter(Boolean);
|
|
1162
|
+
if (values.length > 1) return values.at(-1);
|
|
1163
|
+
const aria = String(row?.getAttribute("aria-label") ?? "").trim();
|
|
1164
|
+
return aria.replace(/^\S+\s+/, "").trim();
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function firstVisibleTextNode(element) {
|
|
1168
|
+
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
1169
|
+
let node = walker.nextNode();
|
|
1170
|
+
while (node) {
|
|
1171
|
+
if (node.textContent.trim() && !node.parentElement?.closest("svg")) return node;
|
|
1172
|
+
node = walker.nextNode();
|
|
1173
|
+
}
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function modelMenuItems(menu) {
|
|
1178
|
+
return Array.from(menu?.querySelectorAll(
|
|
1179
|
+
':scope [role="menuitemradio"], :scope [role="menuitemcheckbox"], :scope [role="menuitem"]',
|
|
1180
|
+
) ?? []).filter((item) => item.closest('[role="menu"]') === menu && !item.hasAttribute("aria-haspopup"));
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function nativeMenuItemLabel(item) {
|
|
1184
|
+
return Array.from(item?.querySelectorAll("span") ?? [])
|
|
1185
|
+
.map((span) => String(span.textContent ?? "").trim())
|
|
1186
|
+
.find(Boolean) ?? String(item?.textContent ?? "").trim();
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function nativeModelSubmenu(parentMenu) {
|
|
1190
|
+
if (nativeModelPickerRow(parentMenu)?.getAttribute("aria-expanded") !== "true") return null;
|
|
1191
|
+
return Array.from(document.querySelectorAll('[role="menu"][data-state="open"]')).find((menu) => (
|
|
1192
|
+
!menu.querySelector("[data-reasoning-slider]") && modelMenuItems(menu).length > 1
|
|
1193
|
+
)) ?? null;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function isNativeEffortSubmenu(menu) {
|
|
1197
|
+
const effortLabels = new Set([
|
|
1198
|
+
"none", "minimal", "light", "low", "medium", "high", "extra high", "max", "ultra",
|
|
1199
|
+
]);
|
|
1200
|
+
if (!menu || menu.querySelector("[data-reasoning-slider]")) return false;
|
|
1201
|
+
const labels = modelMenuItems(menu).map((item) => (
|
|
1202
|
+
nativeMenuItemLabel(item).toLowerCase()
|
|
1203
|
+
)).filter(Boolean);
|
|
1204
|
+
return labels.length > 1 && labels.every((label) => effortLabels.has(label));
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function nativeEffortSubmenu() {
|
|
1208
|
+
return Array.from(document.querySelectorAll('[role="menu"][data-state="open"]'))
|
|
1209
|
+
.find((menu) => isNativeEffortSubmenu(menu)) ?? null;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function nativeModelPickerSubmenus() {
|
|
1213
|
+
return Array.from(document.querySelectorAll('[role="menu"][data-state="open"]')).filter((menu) => {
|
|
1214
|
+
if (menu.querySelector("[data-reasoning-slider]")) return false;
|
|
1215
|
+
if (menu.hasAttribute("data-codex-model-slider-menu")) return true;
|
|
1216
|
+
return isNativeEffortSubmenu(menu);
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function setReactControlledMenuOpen(element, open) {
|
|
1221
|
+
if (!element) return false;
|
|
1222
|
+
const fiberKey = Object.getOwnPropertyNames(element).find((key) => key.startsWith("__reactFiber$"));
|
|
1223
|
+
let fiber = fiberKey ? element[fiberKey] : null;
|
|
1224
|
+
for (let depth = 0; fiber && depth < 40; depth += 1, fiber = fiber.return) {
|
|
1225
|
+
const props = fiber.memoizedProps;
|
|
1226
|
+
if (typeof props?.open === "boolean" && props.open !== open && typeof props.onOpenChange === "function") {
|
|
1227
|
+
props.onOpenChange(open);
|
|
1228
|
+
return true;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function closeReactControlledMenu(element) {
|
|
1235
|
+
return setReactControlledMenuOpen(element, false);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function invokeNativeMenuItemSelect(item) {
|
|
1239
|
+
if (!item) return false;
|
|
1240
|
+
const fiberKey = Object.getOwnPropertyNames(item).find((key) => key.startsWith("__reactFiber$"));
|
|
1241
|
+
let fiber = fiberKey ? item[fiberKey] : null;
|
|
1242
|
+
for (let depth = 0; fiber && depth < 24; depth += 1, fiber = fiber.return) {
|
|
1243
|
+
const onSelect = fiber.memoizedProps?.onSelect;
|
|
1244
|
+
if (typeof onSelect !== "function") continue;
|
|
1245
|
+
onSelect({ preventDefault() {}, defaultPrevented: false });
|
|
1246
|
+
return true;
|
|
1247
|
+
}
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function closeNativeModelPickerSubmenus() {
|
|
1252
|
+
const submenus = nativeModelPickerSubmenus();
|
|
1253
|
+
const parentMenu = nativeModelPickerMenu();
|
|
1254
|
+
const expandedRows = Array.from(
|
|
1255
|
+
parentMenu?.querySelectorAll('[role="menuitem"][aria-haspopup="menu"][aria-expanded="true"]') ?? [],
|
|
1256
|
+
);
|
|
1257
|
+
let controlledMenuClosed = false;
|
|
1258
|
+
for (const row of expandedRows) {
|
|
1259
|
+
controlledMenuClosed = closeReactControlledMenu(row) || controlledMenuClosed;
|
|
1260
|
+
}
|
|
1261
|
+
for (const submenu of submenus.reverse()) {
|
|
1262
|
+
if (controlledMenuClosed || closeReactControlledMenu(submenu)) continue;
|
|
1263
|
+
submenu.dispatchEvent(new KeyboardEvent("keydown", {
|
|
1264
|
+
key: "Escape",
|
|
1265
|
+
code: "Escape",
|
|
1266
|
+
bubbles: true,
|
|
1267
|
+
cancelable: true,
|
|
1268
|
+
}));
|
|
1269
|
+
}
|
|
1270
|
+
return submenus.length > 0;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function endNativeEffortTransaction() {
|
|
1274
|
+
clearTimeout(modelSelectorEffortTransactionTimer);
|
|
1275
|
+
modelSelectorEffortTransactionTimer = null;
|
|
1276
|
+
modelSelectorPendingOutsideDismiss = false;
|
|
1277
|
+
document.documentElement.removeAttribute("data-codex-model-slider-selecting-effort");
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function beginNativeEffortTransaction() {
|
|
1281
|
+
clearTimeout(modelSelectorEffortTransactionTimer);
|
|
1282
|
+
document.documentElement.setAttribute("data-codex-model-slider-selecting-effort", "");
|
|
1283
|
+
modelSelectorEffortTransactionTimer = setTimeout(() => {
|
|
1284
|
+
closeNativeModelPickerSubmenus();
|
|
1285
|
+
endNativeEffortTransaction();
|
|
1286
|
+
}, 2_000);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function deferNativeEffortOutsideDismiss(event) {
|
|
1290
|
+
const insideMenu = event.target.closest?.('[role="menu"]');
|
|
1291
|
+
if (insideMenu) return;
|
|
1292
|
+
if (event.type === "pointerdown" && modelSelectorEffortTransactionTimer != null) {
|
|
1293
|
+
modelSelectorPendingOutsideDismiss = true;
|
|
1294
|
+
modelSelectorSuppressOutsidePointerSequence = true;
|
|
1295
|
+
}
|
|
1296
|
+
if (!modelSelectorSuppressOutsidePointerSequence) return;
|
|
1297
|
+
event.preventDefault();
|
|
1298
|
+
event.stopPropagation();
|
|
1299
|
+
event.stopImmediatePropagation();
|
|
1300
|
+
if (event.type === "click") modelSelectorSuppressOutsidePointerSequence = false;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
for (const type of ["pointerdown", "mousedown", "pointerup", "mouseup", "click"]) {
|
|
1304
|
+
document.addEventListener(type, deferNativeEffortOutsideDismiss, {
|
|
1305
|
+
capture: true,
|
|
1306
|
+
signal: lifetime.signal,
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function modelItemRawValue(item) {
|
|
1311
|
+
if (item.dataset.codexModelSliderRaw) return item.dataset.codexModelSliderRaw;
|
|
1312
|
+
return String(item.innerText ?? "").split("\n").map((value) => value.trim()).filter(Boolean).join(" ");
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
function visibleBlockingDialog() {
|
|
1316
|
+
return Array.from(document.querySelectorAll('[role="dialog"]')).some((dialog) => {
|
|
1317
|
+
const rect = dialog.getBoundingClientRect();
|
|
1318
|
+
const style = getComputedStyle(dialog);
|
|
1319
|
+
return rect.width > 0 && rect.height > 0 && style.visibility !== "hidden" && style.display !== "none";
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function dispatchPointerClick(element) {
|
|
1324
|
+
if (!element) return;
|
|
1325
|
+
const rect = element.getBoundingClientRect();
|
|
1326
|
+
const init = {
|
|
1327
|
+
bubbles: true,
|
|
1328
|
+
cancelable: true,
|
|
1329
|
+
view: window,
|
|
1330
|
+
pointerType: "mouse",
|
|
1331
|
+
button: 0,
|
|
1332
|
+
clientX: rect.left + rect.width / 2,
|
|
1333
|
+
clientY: rect.top + rect.height / 2,
|
|
1334
|
+
};
|
|
1335
|
+
for (const type of [
|
|
1336
|
+
"pointermove", "pointerover", "mouseover", "mousemove",
|
|
1337
|
+
"pointerdown", "mousedown", "pointerup", "mouseup", "click",
|
|
1338
|
+
]) {
|
|
1339
|
+
const EventType = type.startsWith("pointer") ? PointerEvent : MouseEvent;
|
|
1340
|
+
const pressed = type === "pointerdown" || type === "mousedown";
|
|
1341
|
+
element.dispatchEvent(new EventType(type, {
|
|
1342
|
+
...init,
|
|
1343
|
+
buttons: pressed ? 1 : 0,
|
|
1344
|
+
detail: type === "click" ? 1 : 0,
|
|
1345
|
+
}));
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function returnToNativeModelSlider(attempt = 0) {
|
|
1350
|
+
if (visibleBlockingDialog()) {
|
|
1351
|
+
closeNativeModelPickerSubmenus();
|
|
1352
|
+
setTimeout(endNativeEffortTransaction, 120);
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
if (closeNativeModelPickerSubmenus()) {
|
|
1356
|
+
setTimeout(() => returnToNativeModelSlider(attempt), 100);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
let parentMenu = nativeModelPickerMenu();
|
|
1360
|
+
if (parentMenu) {
|
|
1361
|
+
const toggle = parentMenu.querySelector("[data-model-picker-view-toggle]");
|
|
1362
|
+
if (toggle?.getAttribute("aria-expanded") === "true") toggle.click();
|
|
1363
|
+
setTimeout(() => {
|
|
1364
|
+
if (!nativeModelPickerMenu() && attempt < 3) {
|
|
1365
|
+
returnToNativeModelSlider(attempt + 1);
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
modelSelectorPendingReturn = false;
|
|
1369
|
+
if (nativeModelPickerSubmenus().length === 0) endNativeEffortTransaction();
|
|
1370
|
+
queueEnsure();
|
|
1371
|
+
}, 140);
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
const trigger = nativeComposerTrigger();
|
|
1375
|
+
if (!trigger) return;
|
|
1376
|
+
dispatchPointerClick(trigger);
|
|
1377
|
+
setTimeout(() => {
|
|
1378
|
+
if (!nativeModelPickerMenu() && attempt < 3) {
|
|
1379
|
+
returnToNativeModelSlider(attempt + 1);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
modelSelectorPendingReturn = false;
|
|
1383
|
+
if (nativeModelPickerSubmenus().length === 0) endNativeEffortTransaction();
|
|
1384
|
+
queueEnsure();
|
|
1385
|
+
}, 140);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function scheduleModelSliderReturn() {
|
|
1389
|
+
modelSelectorPendingReturn = true;
|
|
1390
|
+
clearTimeout(modelSelectorReturnTimer);
|
|
1391
|
+
modelSelectorReturnTimer = setTimeout(() => {
|
|
1392
|
+
modelSelectorReturnTimer = null;
|
|
1393
|
+
returnToNativeModelSlider();
|
|
1394
|
+
}, 260);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function nativeGptSortRank(rawValue) {
|
|
1398
|
+
const known = new Map([
|
|
1399
|
+
["gpt-6-astra", 1], ["gpt-5.6-sol", 2], ["gpt-5.6-terra", 3], ["gpt-5.6-luna", 4],
|
|
1400
|
+
["gpt-5.5", 5], ["gpt-5.4", 6], ["gpt-5.4-mini", 7], ["gpt-5.3-codex-spark", 8],
|
|
1401
|
+
]);
|
|
1402
|
+
const raw = String(rawValue ?? "").trim();
|
|
1403
|
+
const knownRank = known.get(raw);
|
|
1404
|
+
if (knownRank != null) return { official: true, rank: knownRank };
|
|
1405
|
+
const identity = modelIdentity(raw);
|
|
1406
|
+
return identity.provider ? null : { official: true, rank: 100 };
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function compareEnhancedModelItems(a, b, favorites) {
|
|
1410
|
+
const favA = Number(favorites.has(a.dataset.codexModelSliderRaw));
|
|
1411
|
+
const favB = Number(favorites.has(b.dataset.codexModelSliderRaw));
|
|
1412
|
+
if (favA !== favB) return favB - favA;
|
|
1413
|
+
const rankA = nativeGptSortRank(a.dataset.codexModelSliderRaw);
|
|
1414
|
+
const rankB = nativeGptSortRank(b.dataset.codexModelSliderRaw);
|
|
1415
|
+
if (Boolean(rankA) !== Boolean(rankB)) return rankA ? -1 : 1;
|
|
1416
|
+
if (rankA && rankB) return rankA.rank - rankB.rank;
|
|
1417
|
+
return 0;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function renderEnhancedModelMenu(menu, selector) {
|
|
1421
|
+
const items = modelMenuItems(menu).filter((item) => item.dataset.codexModelSliderRaw);
|
|
1422
|
+
if (!items.length) return;
|
|
1423
|
+
const favorites = loadModelFavorites(selector);
|
|
1424
|
+
items.forEach((item) => { item.style.display = ""; });
|
|
1425
|
+
if (!menu.hasAttribute("data-codex-model-slider-controller-menu")) {
|
|
1426
|
+
if (!modelSelectorMenuSizeFrames.has(menu)) {
|
|
1427
|
+
const frame = requestAnimationFrame(() => {
|
|
1428
|
+
modelSelectorMenuSizeFrames.delete(menu);
|
|
1429
|
+
sizeModelMenuViewport(menu, selector.maxVisibleItems);
|
|
1430
|
+
});
|
|
1431
|
+
modelSelectorMenuSizeFrames.set(menu, frame);
|
|
1432
|
+
}
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
const starred = items.filter((item) => favorites.has(item.dataset.codexModelSliderRaw));
|
|
1436
|
+
const ordinary = items.filter((item) => !favorites.has(item.dataset.codexModelSliderRaw));
|
|
1437
|
+
const desired = items.slice().sort((a, b) => compareEnhancedModelItems(a, b, favorites));
|
|
1438
|
+
const parent = items[0].parentElement;
|
|
1439
|
+
if (!parent || !desired.every((item) => item.parentElement === parent)) return;
|
|
1440
|
+
const current = Array.from(parent.children).filter((child) => child.hasAttribute?.("data-codex-model-slider-item"));
|
|
1441
|
+
if (desired.some((item, index) => current[index] !== item)) desired.forEach((item) => parent.append(item));
|
|
1442
|
+
|
|
1443
|
+
let divider = parent.querySelector(":scope > [data-codex-model-slider-divider]");
|
|
1444
|
+
if (starred.length && ordinary.length) {
|
|
1445
|
+
if (!divider) {
|
|
1446
|
+
divider = document.createElement("div");
|
|
1447
|
+
divider.setAttribute("data-codex-model-slider-divider", "");
|
|
1448
|
+
divider.setAttribute("role", "separator");
|
|
1449
|
+
}
|
|
1450
|
+
const firstOrdinary = desired.find((item) => !favorites.has(item.dataset.codexModelSliderRaw));
|
|
1451
|
+
if (firstOrdinary && divider.nextElementSibling !== firstOrdinary) {
|
|
1452
|
+
parent.insertBefore(divider, firstOrdinary);
|
|
1453
|
+
}
|
|
1454
|
+
} else {
|
|
1455
|
+
divider?.remove();
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
if (!modelSelectorMenuSizeFrames.has(menu)) {
|
|
1459
|
+
const frame = requestAnimationFrame(() => {
|
|
1460
|
+
modelSelectorMenuSizeFrames.delete(menu);
|
|
1461
|
+
sizeModelMenuViewport(menu, selector.maxVisibleItems);
|
|
1462
|
+
});
|
|
1463
|
+
modelSelectorMenuSizeFrames.set(menu, frame);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function sizeModelMenuViewport(menu, maxVisibleItems) {
|
|
1468
|
+
if (!menu.isConnected) return;
|
|
1469
|
+
const items = modelMenuItems(menu).filter((item) => item.dataset.codexModelSliderRaw);
|
|
1470
|
+
if (items.length <= maxVisibleItems) {
|
|
1471
|
+
menu.removeAttribute("data-codex-model-slider-overflow");
|
|
1472
|
+
menu.style.removeProperty("--codex-model-slider-menu-max-height");
|
|
1473
|
+
menu.style.removeProperty("max-height");
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
const lastVisibleItem = items[maxVisibleItems - 1];
|
|
1477
|
+
const menuRect = menu.getBoundingClientRect();
|
|
1478
|
+
const itemRect = lastVisibleItem.getBoundingClientRect();
|
|
1479
|
+
const paddingBottom = Number.parseFloat(getComputedStyle(menu).paddingBottom) || 0;
|
|
1480
|
+
const maxHeight = Math.ceil(itemRect.bottom - menuRect.top + menu.scrollTop + paddingBottom);
|
|
1481
|
+
menu.setAttribute("data-codex-model-slider-overflow", "");
|
|
1482
|
+
menu.style.setProperty("--codex-model-slider-menu-max-height", `${maxHeight}px`);
|
|
1483
|
+
menu.style.overflowY = "auto";
|
|
1484
|
+
positionModelSubmenu(menu);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function toggleModelFavorite(event, menu, selector, raw, label) {
|
|
1488
|
+
event.preventDefault();
|
|
1489
|
+
event.stopPropagation();
|
|
1490
|
+
event.stopImmediatePropagation();
|
|
1491
|
+
const favorites = loadModelFavorites(selector);
|
|
1492
|
+
if (favorites.has(raw)) favorites.delete(raw);
|
|
1493
|
+
else favorites.add(raw);
|
|
1494
|
+
saveModelFavorites(selector, favorites);
|
|
1495
|
+
for (const item of modelMenuItems(menu)) {
|
|
1496
|
+
if (!item.dataset.codexModelSliderRaw) continue;
|
|
1497
|
+
const starred = favorites.has(item.dataset.codexModelSliderRaw);
|
|
1498
|
+
const button = item.querySelector("[data-codex-model-slider-star]");
|
|
1499
|
+
if (!button) continue;
|
|
1500
|
+
button.replaceChildren(createModelSelectorIcon(starred ? selector.icons.unstar : selector.icons.star));
|
|
1501
|
+
const itemLabel = item.querySelector("[data-codex-model-slider-name]")?.textContent ?? label;
|
|
1502
|
+
button.setAttribute("aria-label", selector.strings[starred ? "unstar" : "star"].replace("{model}", itemLabel));
|
|
1503
|
+
button.setAttribute("title", selector.strings[starred ? "unstar" : "star"].replace("{model}", itemLabel));
|
|
1504
|
+
}
|
|
1505
|
+
renderEnhancedModelMenu(menu, selector);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
function enhanceModelItem(item, menu, selector, index, {
|
|
1509
|
+
rawValue = null,
|
|
1510
|
+
selected = null,
|
|
1511
|
+
scheduleReturn = true,
|
|
1512
|
+
owned = false,
|
|
1513
|
+
} = {}) {
|
|
1514
|
+
const observedRaw = rawValue ?? modelItemRawValue(item);
|
|
1515
|
+
if (!observedRaw) return;
|
|
1516
|
+
const observedIdentity = modelIdentity(observedRaw);
|
|
1517
|
+
const nativeDescriptor = nativeModelDescriptorForItem(item, observedRaw);
|
|
1518
|
+
const catalogModel = catalogModelFor(observedIdentity, selector);
|
|
1519
|
+
const raw = catalogModel?.slug ?? nativeDescriptor?.slug ?? observedRaw;
|
|
1520
|
+
// 先刷新 dataset:React 原地复用菜单节点时,raw/排序/收藏键不能基于过期值。
|
|
1521
|
+
item.dataset.codexModelSliderRaw = raw;
|
|
1522
|
+
item.dataset.codexModelSliderItem = "";
|
|
1523
|
+
item.dataset.codexModelSliderOrder = String(index);
|
|
1524
|
+
item.dataset.codexModelSliderIdentitySource = catalogModel ? "catalog" : nativeDescriptor ? "native" : "visible";
|
|
1525
|
+
if (item.querySelector("[data-codex-model-slider-row]")) return;
|
|
1526
|
+
item.toggleAttribute(
|
|
1527
|
+
"data-codex-model-slider-selected",
|
|
1528
|
+
selected ?? Boolean(item.querySelector("svg")),
|
|
1529
|
+
);
|
|
1530
|
+
if (!owned) {
|
|
1531
|
+
if (scheduleReturn && !item.hasAttribute("data-codex-model-slider-return-listener")) {
|
|
1532
|
+
item.setAttribute("data-codex-model-slider-return-listener", "");
|
|
1533
|
+
item.addEventListener("click", scheduleModelSliderReturn);
|
|
1534
|
+
}
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
const identity = modelIdentity(raw);
|
|
1538
|
+
const display = modelDisplayLabel(identity, selector);
|
|
1539
|
+
const provider = providerLabel(identity.provider, selector);
|
|
1540
|
+
const brand = brandForModel(identity, selector);
|
|
1541
|
+
const favorites = loadModelFavorites(selector);
|
|
1542
|
+
const starred = favorites.has(raw);
|
|
1543
|
+
const row = document.createElement("span");
|
|
1544
|
+
row.setAttribute("data-codex-model-slider-row", "");
|
|
1545
|
+
const brandRoot = document.createElement("span");
|
|
1546
|
+
brandRoot.setAttribute("data-codex-model-slider-brand", brand?.id ?? "fallback");
|
|
1547
|
+
brandRoot.append(createModelSelectorIcon(brand?.icon ?? selector.icons.fallback, { brand: Boolean(brand) }));
|
|
1548
|
+
const copy = document.createElement("span");
|
|
1549
|
+
copy.setAttribute("data-codex-model-slider-copy", "");
|
|
1550
|
+
const name = document.createElement("span");
|
|
1551
|
+
name.setAttribute("data-codex-model-slider-name", "");
|
|
1552
|
+
name.textContent = display;
|
|
1553
|
+
copy.append(name);
|
|
1554
|
+
if (provider) {
|
|
1555
|
+
const secondary = document.createElement("span");
|
|
1556
|
+
secondary.setAttribute("data-codex-model-slider-provider", "");
|
|
1557
|
+
secondary.textContent = provider;
|
|
1558
|
+
copy.append(secondary);
|
|
1559
|
+
}
|
|
1560
|
+
row.append(brandRoot, copy);
|
|
1561
|
+
const star = document.createElement("button");
|
|
1562
|
+
star.type = "button";
|
|
1563
|
+
star.setAttribute("data-codex-model-slider-star", starred ? "unstar" : "star");
|
|
1564
|
+
star.setAttribute("aria-label", selector.strings[starred ? "unstar" : "star"].replace("{model}", display));
|
|
1565
|
+
star.setAttribute("title", selector.strings[starred ? "unstar" : "star"].replace("{model}", display));
|
|
1566
|
+
star.append(createModelSelectorIcon(starred ? selector.icons.unstar : selector.icons.star));
|
|
1567
|
+
row.append(star);
|
|
1568
|
+
item.replaceChildren(row);
|
|
1569
|
+
if (scheduleReturn && !item.hasAttribute("data-codex-model-slider-return-listener")) {
|
|
1570
|
+
item.setAttribute("data-codex-model-slider-return-listener", "");
|
|
1571
|
+
item.addEventListener("click", (event) => {
|
|
1572
|
+
if (!event.target.closest?.("[data-codex-model-slider-star]")) scheduleModelSliderReturn();
|
|
1573
|
+
});
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function enhanceNativeModelSubmenu(menu, selector) {
|
|
1578
|
+
menu.setAttribute("data-codex-model-slider-menu", "");
|
|
1579
|
+
observeModelSubmenuPosition(menu);
|
|
1580
|
+
positionModelSubmenu(menu);
|
|
1581
|
+
const items = modelMenuItems(menu);
|
|
1582
|
+
items.forEach((item, index) => enhanceModelItem(item, menu, selector, index));
|
|
1583
|
+
if (!menu.hasAttribute("data-codex-model-slider-events")) {
|
|
1584
|
+
menu.setAttribute("data-codex-model-slider-events", "");
|
|
1585
|
+
menu.addEventListener("click", (event) => {
|
|
1586
|
+
const star = event.target.closest?.("[data-codex-model-slider-star]");
|
|
1587
|
+
if (star) {
|
|
1588
|
+
const item = star.closest("[data-codex-model-slider-item]");
|
|
1589
|
+
const raw = item?.dataset.codexModelSliderRaw;
|
|
1590
|
+
if (!raw) return;
|
|
1591
|
+
const label = item.querySelector("[data-codex-model-slider-name]")?.textContent ?? raw;
|
|
1592
|
+
toggleModelFavorite(event, menu, selector, raw, label);
|
|
1593
|
+
event.stopImmediatePropagation();
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
if (event.target.closest?.("[data-codex-model-slider-item]")) scheduleModelSliderReturn();
|
|
1597
|
+
}, { capture: true });
|
|
1598
|
+
menu.addEventListener("pointerdown", (event) => {
|
|
1599
|
+
const star = event.target.closest?.("[data-codex-model-slider-star]");
|
|
1600
|
+
if (star) {
|
|
1601
|
+
event.stopPropagation();
|
|
1602
|
+
event.stopImmediatePropagation();
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
if (event.target.closest?.("[data-codex-model-slider-item]")) scheduleModelSliderReturn();
|
|
1606
|
+
}, { capture: true });
|
|
1607
|
+
for (const type of ["mousedown", "pointerup", "mouseup"]) {
|
|
1608
|
+
menu.addEventListener(type, (event) => {
|
|
1609
|
+
if (!event.target.closest?.("[data-codex-model-slider-star]")) return;
|
|
1610
|
+
event.stopPropagation();
|
|
1611
|
+
event.stopImmediatePropagation();
|
|
1612
|
+
}, { capture: true });
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
renderEnhancedModelMenu(menu, selector);
|
|
1616
|
+
if (!menu.hasAttribute("data-codex-model-slider-positioned")) menu.style.visibility = "hidden";
|
|
1617
|
+
positionModelSubmenu(menu);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
function positionModelSubmenu(menu) {
|
|
1621
|
+
const trigger = document.getElementById(menu?.getAttribute("aria-labelledby") ?? "");
|
|
1622
|
+
const parentMenu = trigger?.closest('[role="menu"]') ?? nativeModelPickerMenu();
|
|
1623
|
+
const wrapper = menu?.parentElement;
|
|
1624
|
+
if (
|
|
1625
|
+
!parentMenu?.hasAttribute("data-codex-model-slider-generic-menu")
|
|
1626
|
+
|| !wrapper?.hasAttribute("data-radix-popper-content-wrapper")
|
|
1627
|
+
) return;
|
|
1628
|
+
const parentRect = parentMenu.getBoundingClientRect();
|
|
1629
|
+
const wrapperRect = wrapper.getBoundingClientRect();
|
|
1630
|
+
if (!(parentRect.width > 0 && wrapperRect.width > 0 && wrapperRect.height > 0)) return;
|
|
1631
|
+
const inset = 8;
|
|
1632
|
+
const left = Math.min(
|
|
1633
|
+
innerWidth - wrapperRect.width - inset,
|
|
1634
|
+
Math.max(inset, parentRect.right - wrapperRect.width),
|
|
1635
|
+
);
|
|
1636
|
+
const top = Math.min(
|
|
1637
|
+
innerHeight - wrapperRect.height - inset,
|
|
1638
|
+
Math.max(inset, parentRect.bottom - wrapperRect.height),
|
|
1639
|
+
);
|
|
1640
|
+
const transform = `translate(${Math.round(left)}px, ${Math.round(top)}px)`;
|
|
1641
|
+
if (wrapper.style.transform !== transform) wrapper.style.transform = transform;
|
|
1642
|
+
if (wrapper.style.getPropertyValue("--radix-popper-transform-origin") !== "100% 100%") {
|
|
1643
|
+
wrapper.style.setProperty("--radix-popper-transform-origin", "100% 100%");
|
|
1644
|
+
}
|
|
1645
|
+
const record = modelSelectorPositionObservers.get(wrapper);
|
|
1646
|
+
if (record) record.lastAppliedStyle = wrapper.getAttribute("style") ?? "";
|
|
1647
|
+
requestAnimationFrame(() => {
|
|
1648
|
+
if (!menu.isConnected) return;
|
|
1649
|
+
const positionedRect = wrapper.getBoundingClientRect();
|
|
1650
|
+
if (
|
|
1651
|
+
Math.abs(positionedRect.right - parentRect.right) <= 2
|
|
1652
|
+
&& Math.abs(positionedRect.bottom - parentRect.bottom) <= 2
|
|
1653
|
+
) {
|
|
1654
|
+
menu.setAttribute("data-codex-model-slider-positioned", "");
|
|
1655
|
+
menu.style.removeProperty("visibility");
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
function observeModelSubmenuPosition(menu) {
|
|
1661
|
+
const wrapper = menu?.parentElement;
|
|
1662
|
+
if (!wrapper?.hasAttribute("data-radix-popper-content-wrapper")) return;
|
|
1663
|
+
const existing = modelSelectorPositionObservers.get(wrapper);
|
|
1664
|
+
if (existing) {
|
|
1665
|
+
existing.menu = menu;
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
const record = {
|
|
1669
|
+
menu,
|
|
1670
|
+
queued: false,
|
|
1671
|
+
observer: null,
|
|
1672
|
+
resizeObserver: null,
|
|
1673
|
+
lastAppliedStyle: null,
|
|
1674
|
+
};
|
|
1675
|
+
const schedule = (mutations = []) => {
|
|
1676
|
+
if (
|
|
1677
|
+
mutations.length
|
|
1678
|
+
&& wrapper.getAttribute("style") === record.lastAppliedStyle
|
|
1679
|
+
) return;
|
|
1680
|
+
if (record.queued) return;
|
|
1681
|
+
record.queued = true;
|
|
1682
|
+
requestAnimationFrame(() => {
|
|
1683
|
+
record.queued = false;
|
|
1684
|
+
if (record.menu.isConnected) positionModelSubmenu(record.menu);
|
|
1685
|
+
});
|
|
1686
|
+
};
|
|
1687
|
+
record.observer = new MutationObserver(schedule);
|
|
1688
|
+
record.observer.observe(wrapper, { attributes: true, attributeFilter: ["style"] });
|
|
1689
|
+
record.resizeObserver = new ResizeObserver(schedule);
|
|
1690
|
+
record.resizeObserver.observe(menu);
|
|
1691
|
+
const trigger = document.getElementById(menu.getAttribute("aria-labelledby") ?? "");
|
|
1692
|
+
const parentMenu = trigger?.closest('[role="menu"]');
|
|
1693
|
+
if (parentMenu) record.resizeObserver.observe(parentMenu);
|
|
1694
|
+
modelSelectorPositionObservers.set(wrapper, record);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
function pruneModelSubmenuPositionObservers() {
|
|
1698
|
+
for (const [wrapper, record] of modelSelectorPositionObservers) {
|
|
1699
|
+
if (wrapper.isConnected) continue;
|
|
1700
|
+
record.observer.disconnect();
|
|
1701
|
+
record.resizeObserver.disconnect();
|
|
1702
|
+
modelSelectorPositionObservers.delete(wrapper);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
function pruneNativeModelToggleHandlers() {
|
|
1707
|
+
for (const [toggle, handlers] of modelSelectorNativeToggleHandlers) {
|
|
1708
|
+
if (toggle.isConnected) continue;
|
|
1709
|
+
toggle.removeEventListener("click", handlers.openModels);
|
|
1710
|
+
toggle.removeEventListener("keydown", handlers.onKeyDown);
|
|
1711
|
+
modelSelectorNativeToggleHandlers.delete(toggle);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
function closeControllerModelMenu() {
|
|
1716
|
+
const record = modelSelectorControllerMenu;
|
|
1717
|
+
if (!record) return false;
|
|
1718
|
+
const placeholder = document.querySelector(
|
|
1719
|
+
'[data-codex-intelligence-trigger] [class*="_ModelPickerTriggerPlaceholder_"]',
|
|
1720
|
+
);
|
|
1721
|
+
if (placeholder?.firstChild && record.placeholderText != null) {
|
|
1722
|
+
placeholder.firstChild.nodeValue = record.placeholderText;
|
|
1723
|
+
}
|
|
1724
|
+
record.button.setAttribute("aria-expanded", "false");
|
|
1725
|
+
record.parentMenu.style.position = record.position;
|
|
1726
|
+
record.parentMenu.style.overflow = record.overflow;
|
|
1727
|
+
record.menu.remove();
|
|
1728
|
+
modelSelectorControllerMenu = null;
|
|
1729
|
+
return true;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function controllerModelReasoningEffort(controller, model) {
|
|
1733
|
+
const current = String(controller?.reasoningEffort ?? "").toLowerCase();
|
|
1734
|
+
const supported = (model?.supportedReasoningEfforts ?? []).map((entry) => (
|
|
1735
|
+
String(entry?.reasoningEffort ?? entry ?? "").toLowerCase()
|
|
1736
|
+
)).filter(Boolean);
|
|
1737
|
+
return supported.includes(current)
|
|
1738
|
+
? current
|
|
1739
|
+
: String(model?.defaultReasoningEffort ?? supported[0] ?? "none").toLowerCase();
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function openControllerModelMenu(parentMenu, modelButton, selector) {
|
|
1743
|
+
if (modelSelectorControllerMenu) {
|
|
1744
|
+
closeControllerModelMenu();
|
|
1745
|
+
return;
|
|
1746
|
+
}
|
|
1747
|
+
const controller = nativeComposerModelController();
|
|
1748
|
+
if (!Array.isArray(controller?.models) || typeof controller.onSelectModel !== "function") return;
|
|
1749
|
+
const models = controller.models.filter((model) => !model.hidden && String(model.model ?? "").trim());
|
|
1750
|
+
if (!models.length) return;
|
|
1751
|
+
|
|
1752
|
+
const menu = document.createElement("div");
|
|
1753
|
+
menu.className = parentMenu.className;
|
|
1754
|
+
menu.setAttribute("role", "menu");
|
|
1755
|
+
menu.setAttribute("data-state", "open");
|
|
1756
|
+
menu.setAttribute("data-codex-model-slider-menu", "");
|
|
1757
|
+
menu.setAttribute("data-codex-model-slider-controller-menu", "");
|
|
1758
|
+
models.forEach((model, index) => {
|
|
1759
|
+
const item = document.createElement("div");
|
|
1760
|
+
item.className = "no-drag outline-hidden rounded-lg px-[var(--padding-row-x)] py-[var(--padding-row-y)] text-sm text-default group hover:bg-primary-ghost-hover focus:bg-primary-ghost-hover cursor-interaction flex flex-col";
|
|
1761
|
+
item.setAttribute("role", "menuitem");
|
|
1762
|
+
item.setAttribute("tabindex", "-1");
|
|
1763
|
+
enhanceModelItem(item, menu, selector, index, {
|
|
1764
|
+
rawValue: String(model.model),
|
|
1765
|
+
selected: String(model.model) === String(controller.model),
|
|
1766
|
+
scheduleReturn: false,
|
|
1767
|
+
owned: true,
|
|
1768
|
+
});
|
|
1769
|
+
menu.append(item);
|
|
1770
|
+
});
|
|
1771
|
+
menu.addEventListener("click", (event) => {
|
|
1772
|
+
const star = event.target.closest?.("[data-codex-model-slider-star]");
|
|
1773
|
+
const item = event.target.closest?.("[data-codex-model-slider-item]");
|
|
1774
|
+
if (!item) return;
|
|
1775
|
+
if (star) {
|
|
1776
|
+
const raw = item.dataset.codexModelSliderRaw;
|
|
1777
|
+
const label = item.querySelector("[data-codex-model-slider-name]")?.textContent ?? raw;
|
|
1778
|
+
toggleModelFavorite(event, menu, selector, raw, label);
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
const currentController = nativeComposerModelController();
|
|
1782
|
+
const raw = item.dataset.codexModelSliderRaw;
|
|
1783
|
+
const model = currentController?.models?.find((candidate) => String(candidate.model) === raw);
|
|
1784
|
+
if (!model || typeof currentController.onSelectModel !== "function") return;
|
|
1785
|
+
const reasoningEffort = controllerModelReasoningEffort(currentController, model);
|
|
1786
|
+
currentController.onSelectModel(raw, reasoningEffort);
|
|
1787
|
+
closeControllerModelMenu();
|
|
1788
|
+
queueEnsure();
|
|
1789
|
+
});
|
|
1790
|
+
menu.addEventListener("keydown", (event) => {
|
|
1791
|
+
if (event.key === "Escape") {
|
|
1792
|
+
event.preventDefault();
|
|
1793
|
+
closeControllerModelMenu();
|
|
1794
|
+
modelButton.focus();
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
1798
|
+
const item = event.target.closest?.("[data-codex-model-slider-item]");
|
|
1799
|
+
if (!item) return;
|
|
1800
|
+
event.preventDefault();
|
|
1801
|
+
item.click();
|
|
1802
|
+
});
|
|
1803
|
+
renderEnhancedModelMenu(menu, selector);
|
|
1804
|
+
|
|
1805
|
+
const placeholder = document.querySelector(
|
|
1806
|
+
'[data-codex-intelligence-trigger] [class*="_ModelPickerTriggerPlaceholder_"]',
|
|
1807
|
+
);
|
|
1808
|
+
const placeholderText = placeholder?.firstChild?.nodeValue ?? null;
|
|
1809
|
+
// 保留产品行为:模型列表打开时 chip 显示 Select model。只能原地更新
|
|
1810
|
+
// 同一个文本节点(nodeValue),不能 textContent 替换整个节点,否则
|
|
1811
|
+
// React 卸载菜单时 removeChild 找不到旧文本节点会触发错误边界。
|
|
1812
|
+
if (placeholder?.firstChild) {
|
|
1813
|
+
placeholder.firstChild.nodeValue = locale === "zh-CN" ? "选择模型" : "Select model";
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
modelSelectorControllerMenu = {
|
|
1817
|
+
menu,
|
|
1818
|
+
parentMenu,
|
|
1819
|
+
button: modelButton,
|
|
1820
|
+
placeholderText,
|
|
1821
|
+
position: parentMenu.style.position,
|
|
1822
|
+
overflow: parentMenu.style.overflow,
|
|
1823
|
+
};
|
|
1824
|
+
parentMenu.style.position = "relative";
|
|
1825
|
+
parentMenu.style.overflow = "visible";
|
|
1826
|
+
parentMenu.append(menu);
|
|
1827
|
+
sizeModelMenuViewport(menu, selector.maxVisibleItems);
|
|
1828
|
+
modelButton.setAttribute("aria-expanded", "true");
|
|
1829
|
+
(menu.querySelector('[data-codex-model-slider-selected]')
|
|
1830
|
+
?? menu.querySelector('[role="menuitem"]'))?.focus();
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
// ========================================================================
|
|
1834
|
+
// 原生 effort 子菜单与滑条视觉 — effort 事务、Fast/通用滑条视觉与状态同步
|
|
1835
|
+
// ========================================================================
|
|
1836
|
+
|
|
1837
|
+
function openNativeModelSubmenu(parentMenu) {
|
|
1838
|
+
let attempts = 0;
|
|
1839
|
+
const open = () => {
|
|
1840
|
+
attempts += 1;
|
|
1841
|
+
const currentMenu = nativeModelPickerMenu() ?? parentMenu;
|
|
1842
|
+
const row = nativeModelPickerRow(currentMenu);
|
|
1843
|
+
if (row) {
|
|
1844
|
+
setReactControlledMenuOpen(row, true);
|
|
1845
|
+
setTimeout(queueEnsure, 0);
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
if (attempts < 12) requestAnimationFrame(open);
|
|
1849
|
+
};
|
|
1850
|
+
setTimeout(open, 80);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function nativeEffortRow(parentMenu) {
|
|
1854
|
+
return Array.from(parentMenu?.querySelectorAll('[role="menuitem"][aria-haspopup="menu"]') ?? [])
|
|
1855
|
+
.filter((row) => !row.hasAttribute("data-model-picker-view-toggle"))[1] ?? null;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
function effortLabel(effort) {
|
|
1859
|
+
return new Map([
|
|
1860
|
+
["none", "None"], ["minimal", "Minimal"], ["low", "Light"], ["medium", "Medium"],
|
|
1861
|
+
["high", "High"], ["xhigh", "Extra High"], ["max", "Max"], ["ultra", "Ultra"],
|
|
1862
|
+
]).get(String(effort).toLowerCase()) ?? formatIdentifier(effort);
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
function selectNativeEffort(parentMenu, effort) {
|
|
1866
|
+
const identity = modelIdentity(nativeModelValue(parentMenu));
|
|
1867
|
+
const model = modelDefinitionFor(identity, modelSelectorFeature?.modelSelector);
|
|
1868
|
+
if (!model?.supportedReasoningLevels?.some((level) => level.effort === String(effort).toLowerCase())) return;
|
|
1869
|
+
const controller = nativeComposerModelController();
|
|
1870
|
+
if (typeof controller?.onSelectReasoningEffort === "function") {
|
|
1871
|
+
controller.onSelectReasoningEffort(effort);
|
|
1872
|
+
if (modelSelectorPendingOutsideDismiss) {
|
|
1873
|
+
closeReactControlledMenu(nativeComposerTrigger());
|
|
1874
|
+
modelSelectorPendingOutsideDismiss = false;
|
|
1875
|
+
}
|
|
1876
|
+
queueEnsure();
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
const row = nativeEffortRow(parentMenu);
|
|
1880
|
+
if (!row) return;
|
|
1881
|
+
beginNativeEffortTransaction();
|
|
1882
|
+
if (!setReactControlledMenuOpen(row, true)) {
|
|
1883
|
+
endNativeEffortTransaction();
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
let observer = null;
|
|
1887
|
+
let selectionTimer = null;
|
|
1888
|
+
const select = () => {
|
|
1889
|
+
const submenu = nativeEffortSubmenu();
|
|
1890
|
+
const desired = effortLabel(effort).toLowerCase();
|
|
1891
|
+
const item = modelMenuItems(submenu).find((candidate) => (
|
|
1892
|
+
nativeMenuItemLabel(candidate).toLowerCase() === desired
|
|
1893
|
+
));
|
|
1894
|
+
if (!item) return;
|
|
1895
|
+
observer?.disconnect();
|
|
1896
|
+
clearTimeout(selectionTimer);
|
|
1897
|
+
const dismissAfterCommit = modelSelectorPendingOutsideDismiss;
|
|
1898
|
+
if (!invokeNativeMenuItemSelect(item)) {
|
|
1899
|
+
endNativeEffortTransaction();
|
|
1900
|
+
queueEnsure();
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
if (dismissAfterCommit) {
|
|
1904
|
+
closeReactControlledMenu(item);
|
|
1905
|
+
closeReactControlledMenu(nativeComposerTrigger());
|
|
1906
|
+
modelSelectorPendingReturn = false;
|
|
1907
|
+
endNativeEffortTransaction();
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
scheduleModelSliderReturn();
|
|
1911
|
+
};
|
|
1912
|
+
observer = new MutationObserver(select);
|
|
1913
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
1914
|
+
selectionTimer = setTimeout(() => {
|
|
1915
|
+
observer.disconnect();
|
|
1916
|
+
endNativeEffortTransaction();
|
|
1917
|
+
queueEnsure();
|
|
1918
|
+
}, 1_500);
|
|
1919
|
+
select();
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
function setGenericSliderVisual(rail, index, count) {
|
|
1923
|
+
const progress = count <= 1 ? 0 : (index / (count - 1)) * 100;
|
|
1924
|
+
const nativeRange = rail.querySelector("[data-codex-model-slider-native-range]");
|
|
1925
|
+
const nativeThumb = rail.querySelector("[data-codex-model-slider-native-thumb]");
|
|
1926
|
+
if (nativeRange && nativeThumb) {
|
|
1927
|
+
const thumbRadius = 13;
|
|
1928
|
+
const correction = thumbRadius - (progress / 50) * thumbRadius;
|
|
1929
|
+
const segment = count <= 1 ? 100 : 100 / (count - 1);
|
|
1930
|
+
const edgeScale = Math.min(progress / segment, (100 - progress) / segment, 1);
|
|
1931
|
+
const edgeCorrection = correction * Math.max(0, edgeScale);
|
|
1932
|
+
nativeRange.style.transform = `translateX(calc(${progress - 100}% + ${edgeCorrection}px))`;
|
|
1933
|
+
nativeThumb.style.left = `calc(${progress}% + ${correction}px)`;
|
|
1934
|
+
const particleClip = rail.querySelector('[class*="_FastParticleClip_"]');
|
|
1935
|
+
if (particleClip) particleClip.style.clipPath = `inset(0 calc(${100 - progress}% - ${edgeCorrection}px) 0 0)`;
|
|
1936
|
+
}
|
|
1937
|
+
rail.style.setProperty("--codex-model-slider-progress", `${progress}%`);
|
|
1938
|
+
rail.querySelectorAll("[data-codex-model-slider-tick]").forEach((tick, tickIndex) => {
|
|
1939
|
+
tick.setAttribute("data-selected", String(tickIndex <= index));
|
|
1940
|
+
});
|
|
1941
|
+
rail.setAttribute("aria-valuenow", String(index));
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
function neutralizeStaleSliderVisual(rail) {
|
|
1945
|
+
for (const selector of [
|
|
1946
|
+
"[data-codex-model-slider-native-range]",
|
|
1947
|
+
"[data-codex-model-slider-native-thumb]",
|
|
1948
|
+
"[data-codex-model-slider-range]",
|
|
1949
|
+
"[data-codex-model-slider-thumb]",
|
|
1950
|
+
]) {
|
|
1951
|
+
rail.querySelectorAll(selector).forEach((element) => {
|
|
1952
|
+
element.style.setProperty("visibility", "hidden");
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
rail.querySelectorAll("[data-codex-model-slider-tick]").forEach((tick) => {
|
|
1956
|
+
tick.removeAttribute("data-selected");
|
|
1957
|
+
});
|
|
1958
|
+
rail.removeAttribute("aria-valuenow");
|
|
1959
|
+
rail.style.removeProperty("--codex-model-slider-progress");
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
function nativeParticleSeed(index, salt) {
|
|
1963
|
+
const value = Math.sin((index + 1) * 12.9898 + salt * 78.233) * 43758.5453;
|
|
1964
|
+
return value - Math.floor(value);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
function syncNativeFastParticles(rail, enabled) {
|
|
1968
|
+
const track = Array.from(rail.children).find((element) => String(element.className).includes("_Track_"));
|
|
1969
|
+
if (!track) return;
|
|
1970
|
+
track.querySelectorAll('[class*="_FastParticleClip_"]').forEach((element) => element.remove());
|
|
1971
|
+
if (!enabled || matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
1972
|
+
const clipClass = nativeCssModuleClass("FastParticleClip");
|
|
1973
|
+
const particlesClass = nativeCssModuleClass("FastTrackParticles");
|
|
1974
|
+
const pathClass = nativeCssModuleClass("FastTrackParticlePath");
|
|
1975
|
+
const particleClass = nativeCssModuleClass("TrackParticle");
|
|
1976
|
+
if (!clipClass || !particlesClass || !pathClass || !particleClass) return;
|
|
1977
|
+
const clip = document.createElement("span");
|
|
1978
|
+
clip.className = clipClass;
|
|
1979
|
+
clip.setAttribute("aria-hidden", "true");
|
|
1980
|
+
clip.style.opacity = "1";
|
|
1981
|
+
const particles = document.createElement("span");
|
|
1982
|
+
particles.className = particlesClass;
|
|
1983
|
+
particles.setAttribute("aria-hidden", "true");
|
|
1984
|
+
particles.setAttribute("data-animation-active", "true");
|
|
1985
|
+
const travelDuration = 1.9;
|
|
1986
|
+
const stagger = travelDuration / 14;
|
|
1987
|
+
const valueMax = Number(rail.getAttribute("aria-valuemax") ?? 0);
|
|
1988
|
+
const valueNow = Number(rail.getAttribute("aria-valuenow") ?? 0);
|
|
1989
|
+
const initialStartPercent = valueMax > 0 ? (valueNow / valueMax) * 100 : 0;
|
|
1990
|
+
const remainingTravel = 1 - Math.min(Math.max(initialStartPercent, 0), 100) / 100;
|
|
1991
|
+
particles.append(...Array.from({ length: 14 }, (_, index) => {
|
|
1992
|
+
const speed = 1 + (nativeParticleSeed(index, 21) - 0.5) * 0.4;
|
|
1993
|
+
const duration = travelDuration / speed;
|
|
1994
|
+
const path = document.createElement("span");
|
|
1995
|
+
path.className = pathClass;
|
|
1996
|
+
path.style.animationDelay = `${index * stagger - duration * remainingTravel}s`;
|
|
1997
|
+
path.style.animationDuration = `${duration}s`;
|
|
1998
|
+
path.style.top = `${12 + nativeParticleSeed(index, 23) * 76}%`;
|
|
1999
|
+
const particle = document.createElement("span");
|
|
2000
|
+
particle.className = particleClass;
|
|
2001
|
+
particle.style.opacity = String(0.4 + nativeParticleSeed(index, 11) * 0.6);
|
|
2002
|
+
particle.style.transform = `translate(-50%, -50%) scale(${0.5 + nativeParticleSeed(index, 12) * 0.45})`;
|
|
2003
|
+
path.append(particle);
|
|
2004
|
+
return path;
|
|
2005
|
+
}));
|
|
2006
|
+
clip.append(particles);
|
|
2007
|
+
const tickRail = Array.from(track.children).find((element) => String(element.className).includes("_TickRail_"));
|
|
2008
|
+
track.insertBefore(clip, tickRail ?? null);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
function syncGenericFastVisual(shell, enabled, maxPowerSelection, animate = false) {
|
|
2012
|
+
const button = shell.querySelector("[data-codex-model-slider-fast]");
|
|
2013
|
+
const rail = shell.querySelector("[data-codex-model-slider-rail]");
|
|
2014
|
+
if (!button || !rail) return;
|
|
2015
|
+
button.setAttribute("aria-pressed", String(enabled));
|
|
2016
|
+
button.setAttribute("data-fast-mode-enabled", String(enabled));
|
|
2017
|
+
button.setAttribute("data-max-power-selection", String(maxPowerSelection));
|
|
2018
|
+
button.setAttribute("aria-label", enabled
|
|
2019
|
+
? (locale === "zh-CN" ? "关闭 Fast 模式" : "Disable Fast mode")
|
|
2020
|
+
: (locale === "zh-CN" ? "开启 Fast 模式" : "Enable Fast mode"));
|
|
2021
|
+
const icon = createNativeFastIcon(enabled);
|
|
2022
|
+
const content = button.querySelector("[data-codex-model-slider-fast-content]");
|
|
2023
|
+
if (icon && content) content.replaceChildren(icon);
|
|
2024
|
+
rail.setAttribute("data-fast-mode", String(enabled));
|
|
2025
|
+
rail.setAttribute("data-fast-mode-dot-transition", animate ? (enabled ? "entering" : "exiting") : (enabled ? "active" : "inactive"));
|
|
2026
|
+
syncNativeFastParticles(rail, enabled);
|
|
2027
|
+
setGenericSliderVisual(rail, Number(rail.getAttribute("aria-valuenow") ?? 0), Number(rail.getAttribute("aria-valuemax") ?? 0) + 1);
|
|
2028
|
+
if (animate) {
|
|
2029
|
+
setTimeout(() => {
|
|
2030
|
+
if (rail.isConnected && rail.getAttribute("data-fast-mode") === String(enabled)) {
|
|
2031
|
+
rail.setAttribute("data-fast-mode-dot-transition", enabled ? "active" : "inactive");
|
|
2032
|
+
}
|
|
2033
|
+
}, enabled ? 1_200 : 350);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
function cloneNativePowerSlider(parentMenu, count, fastEnabled) {
|
|
2038
|
+
const template = parentMenu.firstElementChild?.querySelector("[data-model-picker-power-slider]");
|
|
2039
|
+
if (!template) return null;
|
|
2040
|
+
const clone = template.cloneNode(true);
|
|
2041
|
+
clone.setAttribute("data-codex-model-slider-native-slider", "");
|
|
2042
|
+
clone.removeAttribute("data-keyboard-focused");
|
|
2043
|
+
const root = clone.firstElementChild;
|
|
2044
|
+
if (!root) return null;
|
|
2045
|
+
const nativeInput = clone.querySelector('[role="slider"]');
|
|
2046
|
+
nativeInput?.removeAttribute("role");
|
|
2047
|
+
nativeInput?.removeAttribute("tabindex");
|
|
2048
|
+
nativeInput?.removeAttribute("aria-valuemin");
|
|
2049
|
+
nativeInput?.removeAttribute("aria-valuemax");
|
|
2050
|
+
nativeInput?.removeAttribute("aria-valuenow");
|
|
2051
|
+
root.setAttribute("role", "slider");
|
|
2052
|
+
root.removeAttribute("aria-hidden");
|
|
2053
|
+
root.removeAttribute("data-disabled");
|
|
2054
|
+
root.removeAttribute("disabled");
|
|
2055
|
+
root.setAttribute("tabindex", "0");
|
|
2056
|
+
root.setAttribute("aria-valuemax", String(Math.max(0, count - 1)));
|
|
2057
|
+
root.setAttribute("data-fast-mode", String(fastEnabled));
|
|
2058
|
+
root.setAttribute("data-fast-mode-dot-transition", fastEnabled ? "active" : "inactive");
|
|
2059
|
+
root.setAttribute("data-max", "false");
|
|
2060
|
+
root.setAttribute("data-endpoint-labels-visible", "false");
|
|
2061
|
+
const range = Array.from(root.querySelectorAll("span")).find((element) => (
|
|
2062
|
+
String(element.className).includes("_Range_")
|
|
2063
|
+
));
|
|
2064
|
+
const thumbScale = Array.from(root.querySelectorAll("span")).find((element) => (
|
|
2065
|
+
String(element.className).includes("_ThumbScale_")
|
|
2066
|
+
));
|
|
2067
|
+
const tickRail = Array.from(root.querySelectorAll("div")).find((element) => (
|
|
2068
|
+
String(element.className).includes("_TickRail_")
|
|
2069
|
+
));
|
|
2070
|
+
const tickTemplate = tickRail?.querySelector("span");
|
|
2071
|
+
if (!range || !thumbScale || !tickRail || !tickTemplate) return null;
|
|
2072
|
+
range.setAttribute("data-codex-model-slider-native-range", "");
|
|
2073
|
+
thumbScale.setAttribute("data-codex-model-slider-native-thumb", "");
|
|
2074
|
+
tickRail.replaceChildren(...Array.from({ length: count }, (_, index) => {
|
|
2075
|
+
const tick = tickTemplate.cloneNode(true);
|
|
2076
|
+
tick.setAttribute("data-codex-model-slider-tick", "");
|
|
2077
|
+
const progress = count <= 1 ? 0 : (index / (count - 1)) * 100;
|
|
2078
|
+
const correction = 13 - (progress / 50) * 13;
|
|
2079
|
+
tick.style.left = `calc(${progress}% + ${correction}px)`;
|
|
2080
|
+
return tick;
|
|
2081
|
+
}));
|
|
2082
|
+
syncNativeFastParticles(root, fastEnabled);
|
|
2083
|
+
return clone;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
function createGenericModelSlider(parentMenu, selector, identity, model, selectedEffort) {
|
|
2087
|
+
const efforts = model.supportedReasoningLevels.map((level) => level.effort);
|
|
2088
|
+
const selectedIndex = efforts.indexOf(selectedEffort);
|
|
2089
|
+
const selectedEffortVisible = selectedIndex >= 0;
|
|
2090
|
+
const classes = Object.fromEntries([
|
|
2091
|
+
"Menu",
|
|
2092
|
+
"ViewToggle",
|
|
2093
|
+
"ViewToggleContent",
|
|
2094
|
+
"ViewToggleIcon",
|
|
2095
|
+
"ViewToggleModelLabel",
|
|
2096
|
+
"ViewToggleEffortLabel",
|
|
2097
|
+
"ViewControls",
|
|
2098
|
+
"ViewPanel",
|
|
2099
|
+
"FastModeToggle",
|
|
2100
|
+
"FastModeToggleContent",
|
|
2101
|
+
"SimpleView",
|
|
2102
|
+
].map((tokenName) => [tokenName, nativeCssModuleClass(tokenName, parentMenu)]));
|
|
2103
|
+
if (Object.values(classes).some((name) => !name)) return null;
|
|
2104
|
+
const shell = document.createElement("div");
|
|
2105
|
+
shell.setAttribute("data-codex-model-slider-generic", `${identity.raw}:${selectedEffort}`);
|
|
2106
|
+
shell.className = classes.Menu;
|
|
2107
|
+
const panel = document.createElement("div");
|
|
2108
|
+
panel.className = classes.ViewPanel;
|
|
2109
|
+
shell.append(panel);
|
|
2110
|
+
shell.setAttribute("data-model-picker-view", "simple");
|
|
2111
|
+
shell.setAttribute("data-reduced-motion", String(matchMedia("(prefers-reduced-motion: reduce)").matches));
|
|
2112
|
+
shell.setAttribute("data-transitions-ready", "false");
|
|
2113
|
+
const nativeRow = nativeModelPickerRow(parentMenu);
|
|
2114
|
+
const controller = nativeComposerModelController();
|
|
2115
|
+
const fastOption = controller?.serviceTierOptions?.find((option) => option?.value === "priority") ?? null;
|
|
2116
|
+
const nativeModel = nativeCatalogModelFor(identity);
|
|
2117
|
+
const supportsFast = Boolean(fastOption && typeof controller.onSelectServiceTier === "function");
|
|
2118
|
+
shell.setAttribute("data-codex-model-slider-has-fast", String(supportsFast));
|
|
2119
|
+
const modelButton = document.createElement("div");
|
|
2120
|
+
modelButton.className = classes.ViewToggle;
|
|
2121
|
+
modelButton.setAttribute("role", "button");
|
|
2122
|
+
modelButton.setAttribute("tabindex", "0");
|
|
2123
|
+
modelButton.setAttribute("data-codex-model-slider-model-button", "");
|
|
2124
|
+
modelButton.setAttribute("aria-label", `Model ${modelDisplayLabel(identity, selector)}`);
|
|
2125
|
+
modelButton.setAttribute("aria-haspopup", "menu");
|
|
2126
|
+
modelButton.setAttribute("aria-expanded", "false");
|
|
2127
|
+
const buttonContent = document.createElement("span");
|
|
2128
|
+
buttonContent.className = classes.ViewToggleContent;
|
|
2129
|
+
// 上下两行独立计算宽度,长模型名不能撑开强度与箭头。
|
|
2130
|
+
buttonContent.style.display = "flex";
|
|
2131
|
+
buttonContent.style.flexDirection = "column";
|
|
2132
|
+
buttonContent.style.gap = "0";
|
|
2133
|
+
const effortRow = document.createElement("span");
|
|
2134
|
+
effortRow.setAttribute("data-codex-model-slider-effort-row", "");
|
|
2135
|
+
effortRow.style.display = "grid";
|
|
2136
|
+
effortRow.style.gridTemplateColumns = "calc(var(--spacing) * 4) auto calc(var(--spacing) * 4)";
|
|
2137
|
+
effortRow.style.alignItems = "center";
|
|
2138
|
+
const buttonLabel = document.createElement("span");
|
|
2139
|
+
buttonLabel.className = classes.ViewToggleModelLabel;
|
|
2140
|
+
buttonLabel.setAttribute("data-codex-model-slider-model-label", "");
|
|
2141
|
+
buttonLabel.textContent = modelDisplayLabel(identity, selector);
|
|
2142
|
+
const effortText = selectedEffortVisible ? document.createElement("span") : null;
|
|
2143
|
+
if (effortText) {
|
|
2144
|
+
effortText.className = classes.ViewToggleEffortLabel;
|
|
2145
|
+
effortText.textContent = effortLabel(selectedEffort);
|
|
2146
|
+
effortText.setAttribute("data-accent", "true");
|
|
2147
|
+
effortText.setAttribute("data-effort-only", "false");
|
|
2148
|
+
effortText.setAttribute("data-maximum", String(selectedEffort === "ultra"));
|
|
2149
|
+
}
|
|
2150
|
+
// 0.152.1 起原生头行 aria-label 为 "Select model",nativeModelPickerRow
|
|
2151
|
+
// 不再命中;优先从 view-toggle 行取 chevron,旧结构走原路径兜底。
|
|
2152
|
+
const nativeChevron = (
|
|
2153
|
+
parentMenu?.querySelector("[data-model-picker-view-toggle] svg")
|
|
2154
|
+
?? nativeRow?.querySelector("svg")
|
|
2155
|
+
)?.cloneNode(true);
|
|
2156
|
+
if (nativeChevron) {
|
|
2157
|
+
nativeChevron.classList.add(classes.ViewToggleIcon);
|
|
2158
|
+
nativeChevron.setAttribute("aria-hidden", "true");
|
|
2159
|
+
if (effortText) effortRow.append(effortText);
|
|
2160
|
+
effortRow.append(nativeChevron);
|
|
2161
|
+
} else {
|
|
2162
|
+
if (effortText) effortRow.append(effortText);
|
|
2163
|
+
}
|
|
2164
|
+
buttonContent.append(effortRow, buttonLabel);
|
|
2165
|
+
modelButton.append(buttonContent);
|
|
2166
|
+
const openModels = () => openControllerModelMenu(parentMenu, modelButton, selector);
|
|
2167
|
+
modelButton.addEventListener("click", openModels);
|
|
2168
|
+
modelButton.addEventListener("keydown", (event) => {
|
|
2169
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
2170
|
+
event.preventDefault();
|
|
2171
|
+
openModels();
|
|
2172
|
+
});
|
|
2173
|
+
|
|
2174
|
+
const viewControls = document.createElement("div");
|
|
2175
|
+
viewControls.className = classes.ViewControls;
|
|
2176
|
+
// 沿用原生显式模型布局:上方档位,下方模型,两侧放置能力控件。
|
|
2177
|
+
viewControls.setAttribute("data-explicit-model", "true");
|
|
2178
|
+
viewControls.setAttribute("data-ultra-warning-visible", "false");
|
|
2179
|
+
const fastEnabled = Boolean(
|
|
2180
|
+
supportsFast && (
|
|
2181
|
+
controller.selectedServiceTier?.id === (fastOption.tier?.id ?? "priority")
|
|
2182
|
+
|| controller.selectedServiceTier === fastOption.value
|
|
2183
|
+
),
|
|
2184
|
+
);
|
|
2185
|
+
if (supportsFast) {
|
|
2186
|
+
const fastButton = document.createElement("button");
|
|
2187
|
+
fastButton.type = "button";
|
|
2188
|
+
fastButton.className = classes.FastModeToggle;
|
|
2189
|
+
fastButton.setAttribute("data-codex-model-slider-fast", "");
|
|
2190
|
+
fastButton.setAttribute("aria-pressed", String(fastEnabled));
|
|
2191
|
+
fastButton.setAttribute("data-fast-mode-enabled", String(fastEnabled));
|
|
2192
|
+
fastButton.setAttribute("data-max-power-selection", String(selectedEffort === "ultra"));
|
|
2193
|
+
fastButton.setAttribute("data-visible", "true");
|
|
2194
|
+
fastButton.setAttribute("aria-label", fastEnabled
|
|
2195
|
+
? (locale === "zh-CN" ? "关闭 Fast 模式" : "Disable Fast mode")
|
|
2196
|
+
: (locale === "zh-CN" ? "开启 Fast 模式" : "Enable Fast mode"));
|
|
2197
|
+
const fastIcon = createNativeFastIcon(fastEnabled);
|
|
2198
|
+
const fastContent = document.createElement("span");
|
|
2199
|
+
fastContent.className = classes.FastModeToggleContent;
|
|
2200
|
+
fastContent.setAttribute("data-codex-model-slider-fast-content", "");
|
|
2201
|
+
if (fastIcon) fastContent.append(fastIcon);
|
|
2202
|
+
fastButton.append(fastContent);
|
|
2203
|
+
fastButton.addEventListener("click", () => {
|
|
2204
|
+
const nextEnabled = fastButton.getAttribute("aria-pressed") !== "true";
|
|
2205
|
+
syncGenericFastVisual(shell, nextEnabled, selectedEffort === "ultra", true);
|
|
2206
|
+
controller.onSelectServiceTier?.(nextEnabled ? fastOption.value : null);
|
|
2207
|
+
queueEnsure();
|
|
2208
|
+
});
|
|
2209
|
+
viewControls.append(modelButton, fastButton);
|
|
2210
|
+
} else {
|
|
2211
|
+
viewControls.append(modelButton);
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
const nativeReset = parentMenu.firstElementChild?.querySelector('[class*="_ResetToDefault_"]');
|
|
2215
|
+
if (nativeReset) {
|
|
2216
|
+
const reset = nativeReset.cloneNode(true);
|
|
2217
|
+
reset.setAttribute("role", "button");
|
|
2218
|
+
reset.setAttribute("tabindex", "0");
|
|
2219
|
+
reset.setAttribute("data-codex-model-slider-reset", "");
|
|
2220
|
+
reset.removeAttribute("data-radix-collection-item");
|
|
2221
|
+
reset.addEventListener("click", () => {
|
|
2222
|
+
const currentReset = parentMenu.firstElementChild?.querySelector('[class*="_ResetToDefault_"]');
|
|
2223
|
+
invokeNativeMenuItemSelect(currentReset);
|
|
2224
|
+
queueEnsure();
|
|
2225
|
+
});
|
|
2226
|
+
reset.addEventListener("keydown", (event) => {
|
|
2227
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
2228
|
+
event.preventDefault();
|
|
2229
|
+
reset.click();
|
|
2230
|
+
});
|
|
2231
|
+
viewControls.append(reset);
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
if (efforts.length < 2) {
|
|
2235
|
+
panel.append(viewControls);
|
|
2236
|
+
return shell;
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
const nativeSlider = cloneNativePowerSlider(parentMenu, efforts.length, fastEnabled);
|
|
2240
|
+
const rail = nativeSlider?.querySelector('[role="slider"]') ?? document.createElement("div");
|
|
2241
|
+
rail.setAttribute("data-codex-model-slider-rail", "");
|
|
2242
|
+
if (!nativeSlider) rail.setAttribute("data-codex-model-slider-fallback", "");
|
|
2243
|
+
rail.setAttribute("role", "slider");
|
|
2244
|
+
rail.setAttribute("tabindex", "0");
|
|
2245
|
+
rail.setAttribute("aria-label", locale === "zh-CN" ? "推理强度" : "Reasoning effort");
|
|
2246
|
+
rail.setAttribute("aria-valuemin", "0");
|
|
2247
|
+
rail.setAttribute("aria-valuemax", String(efforts.length - 1));
|
|
2248
|
+
rail.setAttribute("aria-valuetext", selectedEffortVisible
|
|
2249
|
+
? effortLabel(selectedEffort)
|
|
2250
|
+
: (locale === "zh-CN" ? "当前档位不可用" : "Current effort unavailable"));
|
|
2251
|
+
rail.toggleAttribute("data-stale-reasoning-effort", !selectedEffortVisible);
|
|
2252
|
+
if (!selectedEffortVisible) {
|
|
2253
|
+
rail.setAttribute("aria-disabled", "true");
|
|
2254
|
+
rail.removeAttribute("tabindex");
|
|
2255
|
+
neutralizeStaleSliderVisual(rail);
|
|
2256
|
+
const staleView = document.createElement("div");
|
|
2257
|
+
staleView.className = classes.SimpleView;
|
|
2258
|
+
staleView.append(nativeSlider ?? rail);
|
|
2259
|
+
panel.append(viewControls, staleView);
|
|
2260
|
+
return shell;
|
|
2261
|
+
}
|
|
2262
|
+
if (!nativeSlider) {
|
|
2263
|
+
const track = document.createElement("div");
|
|
2264
|
+
track.setAttribute("data-codex-model-slider-track", "");
|
|
2265
|
+
const range = document.createElement("div");
|
|
2266
|
+
range.setAttribute("data-codex-model-slider-range", "");
|
|
2267
|
+
track.append(range);
|
|
2268
|
+
efforts.forEach((_effort, index) => {
|
|
2269
|
+
const tick = document.createElement("span");
|
|
2270
|
+
tick.setAttribute("data-codex-model-slider-tick", "");
|
|
2271
|
+
tick.style.left = `${efforts.length <= 1 ? 0 : (index / (efforts.length - 1)) * 100}%`;
|
|
2272
|
+
track.append(tick);
|
|
2273
|
+
});
|
|
2274
|
+
const thumb = document.createElement("span");
|
|
2275
|
+
thumb.setAttribute("data-codex-model-slider-thumb", "");
|
|
2276
|
+
rail.append(track, thumb);
|
|
2277
|
+
}
|
|
2278
|
+
setGenericSliderVisual(rail, selectedIndex, efforts.length);
|
|
2279
|
+
syncNativeFastParticles(rail, fastEnabled);
|
|
2280
|
+
setGenericSliderVisual(rail, selectedIndex, efforts.length);
|
|
2281
|
+
|
|
2282
|
+
let pendingIndex = selectedIndex;
|
|
2283
|
+
let dragging = false;
|
|
2284
|
+
let dragRect = null;
|
|
2285
|
+
let previewFrame = null;
|
|
2286
|
+
let pendingClientX = null;
|
|
2287
|
+
const indexAt = (clientX) => {
|
|
2288
|
+
const rect = dragRect ?? rail.getBoundingClientRect();
|
|
2289
|
+
const ratio = Math.min(1, Math.max(0, (clientX - rect.left) / Math.max(1, rect.width)));
|
|
2290
|
+
return Math.round(ratio * (efforts.length - 1));
|
|
2291
|
+
};
|
|
2292
|
+
const previewIndex = (index) => {
|
|
2293
|
+
pendingIndex = index;
|
|
2294
|
+
const effort = efforts[index];
|
|
2295
|
+
setGenericSliderVisual(rail, index, efforts.length);
|
|
2296
|
+
rail.setAttribute("aria-valuetext", effortLabel(effort));
|
|
2297
|
+
if (effortText) {
|
|
2298
|
+
effortText.textContent = effortLabel(effort);
|
|
2299
|
+
effortText.setAttribute("data-maximum", String(effort === "ultra"));
|
|
2300
|
+
}
|
|
2301
|
+
};
|
|
2302
|
+
const preview = (clientX) => previewIndex(indexAt(clientX));
|
|
2303
|
+
rail.addEventListener("pointerdown", (event) => {
|
|
2304
|
+
dragging = true;
|
|
2305
|
+
dragRect = rail.getBoundingClientRect();
|
|
2306
|
+
rail.setPointerCapture?.(event.pointerId);
|
|
2307
|
+
preview(event.clientX);
|
|
2308
|
+
});
|
|
2309
|
+
rail.addEventListener("pointermove", (event) => {
|
|
2310
|
+
if (!dragging) return;
|
|
2311
|
+
pendingClientX = event.clientX;
|
|
2312
|
+
if (previewFrame != null) return;
|
|
2313
|
+
previewFrame = requestAnimationFrame(() => {
|
|
2314
|
+
previewFrame = null;
|
|
2315
|
+
if (dragging && pendingClientX != null) preview(pendingClientX);
|
|
2316
|
+
});
|
|
2317
|
+
});
|
|
2318
|
+
rail.addEventListener("pointerup", (event) => {
|
|
2319
|
+
if (!dragging) return;
|
|
2320
|
+
dragging = false;
|
|
2321
|
+
if (previewFrame != null) cancelAnimationFrame(previewFrame);
|
|
2322
|
+
previewFrame = null;
|
|
2323
|
+
pendingClientX = null;
|
|
2324
|
+
preview(event.clientX);
|
|
2325
|
+
dragRect = null;
|
|
2326
|
+
selectNativeEffort(parentMenu, efforts[pendingIndex]);
|
|
2327
|
+
});
|
|
2328
|
+
rail.addEventListener("pointercancel", () => {
|
|
2329
|
+
dragging = false;
|
|
2330
|
+
if (previewFrame != null) cancelAnimationFrame(previewFrame);
|
|
2331
|
+
previewFrame = null;
|
|
2332
|
+
pendingClientX = null;
|
|
2333
|
+
dragRect = null;
|
|
2334
|
+
previewIndex(selectedIndex);
|
|
2335
|
+
});
|
|
2336
|
+
rail.addEventListener("keydown", (event) => {
|
|
2337
|
+
if (!new Set(["ArrowLeft", "ArrowRight", "Home", "End"]).has(event.key)) return;
|
|
2338
|
+
event.preventDefault();
|
|
2339
|
+
if (event.key === "Home") pendingIndex = 0;
|
|
2340
|
+
else if (event.key === "End") pendingIndex = efforts.length - 1;
|
|
2341
|
+
else pendingIndex = Math.min(
|
|
2342
|
+
efforts.length - 1,
|
|
2343
|
+
Math.max(0, pendingIndex + (event.key === "ArrowRight" ? 1 : -1)),
|
|
2344
|
+
);
|
|
2345
|
+
previewIndex(pendingIndex);
|
|
2346
|
+
selectNativeEffort(parentMenu, efforts[pendingIndex]);
|
|
2347
|
+
});
|
|
2348
|
+
const simpleView = document.createElement("div");
|
|
2349
|
+
simpleView.className = classes.SimpleView;
|
|
2350
|
+
simpleView.append(nativeSlider ?? rail);
|
|
2351
|
+
panel.append(viewControls, simpleView);
|
|
2352
|
+
return shell;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
function centerGenericModelPicker(parentMenu) {
|
|
2356
|
+
const trigger = nativeComposerTrigger();
|
|
2357
|
+
if (!trigger) return;
|
|
2358
|
+
const menuRect = parentMenu.getBoundingClientRect();
|
|
2359
|
+
const triggerRect = trigger.getBoundingClientRect();
|
|
2360
|
+
if (!(menuRect.width > 0 && triggerRect.width > 0)) return;
|
|
2361
|
+
const previousShift = Number.parseFloat(
|
|
2362
|
+
parentMenu.style.getPropertyValue("--codex-model-slider-menu-center-shift"),
|
|
2363
|
+
) || 0;
|
|
2364
|
+
const unshiftedCenter = menuRect.left + (menuRect.width / 2) - previousShift;
|
|
2365
|
+
const nextShift = triggerRect.left + (triggerRect.width / 2) - unshiftedCenter;
|
|
2366
|
+
parentMenu.style.setProperty("--codex-model-slider-menu-center-shift", `${nextShift}px`);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
// ========================================================================
|
|
2370
|
+
// 通用 picker 增强与恢复 — 菜单/触发器增强、布局居中与恢复降级
|
|
2371
|
+
// ========================================================================
|
|
2372
|
+
|
|
2373
|
+
function enhanceGenericModelPicker(parentMenu, selector) {
|
|
2374
|
+
const identity = modelIdentity(nativeModelValue(parentMenu));
|
|
2375
|
+
const model = modelDefinitionFor(identity, selector);
|
|
2376
|
+
const displayModel = modelWithOpenCodexEfforts(identity, selector, model);
|
|
2377
|
+
const efforts = displayModel?.supportedReasoningLevels ?? [];
|
|
2378
|
+
if (!model) return false;
|
|
2379
|
+
// 控制器推导的任务内实际 effort 优先,避免子任务/主任务切换后触发器属性滞后;
|
|
2380
|
+
// 触发器属性仅在控制器无法解析时作为实时回退,静态目录默认值最后兜底。
|
|
2381
|
+
const nativeModel = nativeCatalogModelFor(identity);
|
|
2382
|
+
const selectedEffort = nativeModel?.currentReasoningLevel
|
|
2383
|
+
?? nativeComposerTrigger()
|
|
2384
|
+
?.getAttribute("data-selected-reasoning-effort")
|
|
2385
|
+
?? model.defaultReasoningLevel
|
|
2386
|
+
?? efforts[0]?.effort
|
|
2387
|
+
?? "none";
|
|
2388
|
+
const effortRank = (value) => {
|
|
2389
|
+
const order = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
2390
|
+
const index = order.indexOf(String(value ?? "").toLowerCase());
|
|
2391
|
+
return index < 0 ? -1 : index;
|
|
2392
|
+
};
|
|
2393
|
+
const visibleEffort = efforts.find((level) => level.effort === selectedEffort)?.effort
|
|
2394
|
+
?? efforts.filter((level) => effortRank(level.effort) <= effortRank(selectedEffort))
|
|
2395
|
+
.sort((left, right) => effortRank(right.effort) - effortRank(left.effort))[0]?.effort
|
|
2396
|
+
?? efforts[0]?.effort
|
|
2397
|
+
?? "none";
|
|
2398
|
+
const selectedEffortValue = visibleEffort;
|
|
2399
|
+
const selectedServiceTier = nativeComposerModelController()?.selectedServiceTier;
|
|
2400
|
+
const serviceTierKey = selectedServiceTier?.id ?? selectedServiceTier ?? "standard";
|
|
2401
|
+
const selectionMode = nativeComposerModelController()?.selectionMode ?? "";
|
|
2402
|
+
const key = `${identity.raw}:${selectedEffortValue}:${serviceTierKey}:${selectionMode}:${efforts.map((level) => level.effort).join(",")}`;
|
|
2403
|
+
const existing = parentMenu.querySelector(":scope > [data-codex-model-slider-generic]");
|
|
2404
|
+
const nativeTemplateReady = Boolean(
|
|
2405
|
+
parentMenu.firstElementChild
|
|
2406
|
+
?.querySelector("[data-model-picker-power-slider] [class*='_TickRail_'] > span"),
|
|
2407
|
+
);
|
|
2408
|
+
const templateWaitCount = Number(parentMenu.getAttribute("data-codex-model-slider-template-wait") ?? 0);
|
|
2409
|
+
if (nativeModel?.slug.toLowerCase().startsWith("gpt-") && !nativeTemplateReady && templateWaitCount < 3) {
|
|
2410
|
+
parentMenu.setAttribute("data-codex-model-slider-template-wait", String(templateWaitCount + 1));
|
|
2411
|
+
parentMenu.style.visibility = "hidden";
|
|
2412
|
+
requestAnimationFrame(queueEnsure);
|
|
2413
|
+
return true;
|
|
2414
|
+
}
|
|
2415
|
+
parentMenu.removeAttribute("data-codex-model-slider-template-wait");
|
|
2416
|
+
parentMenu.style.removeProperty("visibility");
|
|
2417
|
+
const canUpgradeFallback = Boolean(
|
|
2418
|
+
existing?.querySelector("[data-codex-model-slider-fallback]") && nativeTemplateReady,
|
|
2419
|
+
);
|
|
2420
|
+
if (existing?.getAttribute("data-codex-model-slider-generic") === key && !canUpgradeFallback) {
|
|
2421
|
+
centerGenericModelPicker(parentMenu);
|
|
2422
|
+
return true;
|
|
2423
|
+
}
|
|
2424
|
+
existing?.remove();
|
|
2425
|
+
const nativeContainer = parentMenu.firstElementChild;
|
|
2426
|
+
if (!nativeContainer) return false;
|
|
2427
|
+
const shell = createGenericModelSlider(
|
|
2428
|
+
parentMenu,
|
|
2429
|
+
selector,
|
|
2430
|
+
identity,
|
|
2431
|
+
displayModel,
|
|
2432
|
+
selectedEffortValue,
|
|
2433
|
+
);
|
|
2434
|
+
if (!shell) return false;
|
|
2435
|
+
if (!modelSelectorGenericContainers.has(nativeContainer)) {
|
|
2436
|
+
modelSelectorGenericContainers.set(nativeContainer, nativeContainer.style.display);
|
|
2437
|
+
}
|
|
2438
|
+
parentMenu.setAttribute("data-codex-model-slider-generic-menu", "");
|
|
2439
|
+
nativeContainer.style.display = "none";
|
|
2440
|
+
shell.setAttribute("data-codex-model-slider-generic", key);
|
|
2441
|
+
parentMenu.append(shell);
|
|
2442
|
+
centerGenericModelPicker(parentMenu);
|
|
2443
|
+
return true;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
function restoreGenericModelPicker(parentMenu) {
|
|
2447
|
+
parentMenu.querySelector(":scope > [data-codex-model-slider-generic]")?.remove();
|
|
2448
|
+
parentMenu.removeAttribute("data-codex-model-slider-generic-menu");
|
|
2449
|
+
parentMenu.style.removeProperty("--codex-model-slider-menu-center-shift");
|
|
2450
|
+
const nativeContainer = parentMenu.firstElementChild;
|
|
2451
|
+
if (nativeContainer && modelSelectorGenericContainers.has(nativeContainer)) {
|
|
2452
|
+
nativeContainer.style.display = modelSelectorGenericContainers.get(nativeContainer);
|
|
2453
|
+
modelSelectorGenericContainers.delete(nativeContainer);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
function enhanceNativeModelPicker(parentMenu, selector) {
|
|
2458
|
+
if (enhanceGenericModelPicker(parentMenu, selector)) return;
|
|
2459
|
+
const toggle = parentMenu.querySelector("[data-model-picker-view-toggle]");
|
|
2460
|
+
if (toggle && typeof nativeComposerModelController()?.onSelectModel === "function") {
|
|
2461
|
+
// 私有 DOM 无法满足 MCX shell 契约时,保留原生菜单作为降级路径。
|
|
2462
|
+
const raw = nativeModelValue(parentMenu);
|
|
2463
|
+
const textNode = firstVisibleTextNode(toggle);
|
|
2464
|
+
if (raw && textNode) {
|
|
2465
|
+
if (!modelSelectorOriginalToggleLabels.has(textNode)) {
|
|
2466
|
+
modelSelectorOriginalToggleLabels.set(textNode, textNode.textContent);
|
|
2467
|
+
}
|
|
2468
|
+
const label = compactModelLabel(raw);
|
|
2469
|
+
if (textNode.textContent !== label) textNode.textContent = label;
|
|
2470
|
+
}
|
|
2471
|
+
if (!toggle.hasAttribute("data-codex-model-slider-toggle")) {
|
|
2472
|
+
const originalAriaExpanded = toggle.getAttribute("aria-expanded");
|
|
2473
|
+
toggle.setAttribute("data-codex-model-slider-toggle", "");
|
|
2474
|
+
const openModels = (event) => {
|
|
2475
|
+
event.preventDefault();
|
|
2476
|
+
event.stopPropagation();
|
|
2477
|
+
event.stopImmediatePropagation();
|
|
2478
|
+
openControllerModelMenu(parentMenu, toggle, selector);
|
|
2479
|
+
};
|
|
2480
|
+
const onKeyDown = (event) => {
|
|
2481
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
2482
|
+
openModels(event);
|
|
2483
|
+
};
|
|
2484
|
+
toggle.addEventListener("click", openModels);
|
|
2485
|
+
toggle.addEventListener("keydown", onKeyDown);
|
|
2486
|
+
modelSelectorNativeToggleHandlers.set(toggle, { openModels, onKeyDown, originalAriaExpanded });
|
|
2487
|
+
}
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
// 原生 ModelText 重建前的空窗期:立即插回 MCX 标签,避免关盒后芯片
|
|
2493
|
+
// 长时间失去模型名。
|
|
2494
|
+
function restoreTriggerLabelWithoutNative(trigger, selector) {
|
|
2495
|
+
if (trigger.getAttribute("data-state") !== "closed") return;
|
|
2496
|
+
if (trigger.querySelector("[data-codex-model-slider-trigger-label]")) return;
|
|
2497
|
+
const raw = trigger.getAttribute("data-codex-model-slider-trigger-raw");
|
|
2498
|
+
if (!raw) return;
|
|
2499
|
+
const anchor = trigger.querySelector('[class*="_ModelPickerTriggerLabel_"]')
|
|
2500
|
+
?? trigger.querySelector('[class*="_ModelPickerTriggerContent_"]');
|
|
2501
|
+
if (!anchor) return;
|
|
2502
|
+
const displayLabel = document.createElement("span");
|
|
2503
|
+
displayLabel.setAttribute("data-codex-model-slider-trigger-label", "");
|
|
2504
|
+
displayLabel.textContent = modelDisplayLabel(modelIdentity(raw), selector);
|
|
2505
|
+
anchor.prepend(displayLabel);
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
function isComposerModelPlaceholder(value) {
|
|
2509
|
+
return /^(select\s+(model|effort)|选择\s*(模型|思考强度))$/i.test(String(value ?? "").trim());
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
function enhanceComposerModelTrigger(selector) {
|
|
2513
|
+
const trigger = nativeComposerTrigger();
|
|
2514
|
+
if (!trigger) return;
|
|
2515
|
+
if (modelSelectorObservedTrigger !== trigger) {
|
|
2516
|
+
modelSelectorTriggerObserver?.disconnect();
|
|
2517
|
+
modelSelectorObservedTrigger = trigger;
|
|
2518
|
+
modelSelectorTriggerObserver = new MutationObserver(queueEnsure);
|
|
2519
|
+
modelSelectorTriggerObserver.observe(trigger, { childList: true, subtree: true, characterData: true });
|
|
2520
|
+
}
|
|
2521
|
+
const nativeLabels = Array.from(trigger.querySelectorAll(
|
|
2522
|
+
'[class*="_ModelPickerTriggerModelText_"]:not([data-codex-model-slider-trigger-label])',
|
|
2523
|
+
));
|
|
2524
|
+
if (!nativeLabels.length) {
|
|
2525
|
+
trigger.setAttribute("data-codex-model-slider-trigger", "");
|
|
2526
|
+
trigger.toggleAttribute(
|
|
2527
|
+
"data-codex-model-slider-trigger-placeholder",
|
|
2528
|
+
isComposerModelPlaceholder(trigger.innerText),
|
|
2529
|
+
);
|
|
2530
|
+
restoreTriggerLabelWithoutNative(trigger, selector);
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
const visibleValue = String(nativeLabels.at(-1)?.textContent ?? "").trim();
|
|
2534
|
+
const previousRaw = trigger.getAttribute("data-codex-model-slider-trigger-raw") ?? "";
|
|
2535
|
+
const previousLabel = previousRaw
|
|
2536
|
+
? modelDisplayLabel(modelIdentity(previousRaw), selector)
|
|
2537
|
+
: "";
|
|
2538
|
+
const raw = !previousRaw || visibleValue !== previousLabel ? visibleValue : previousRaw;
|
|
2539
|
+
if (!raw) return;
|
|
2540
|
+
const label = modelDisplayLabel(modelIdentity(raw), selector);
|
|
2541
|
+
trigger.setAttribute("data-codex-model-slider-trigger", "");
|
|
2542
|
+
trigger.setAttribute("data-codex-model-slider-trigger-raw", raw);
|
|
2543
|
+
// 区分占位态(Select model / Select effort)与真实模型态:占位态沿用
|
|
2544
|
+
// 原生宽度契约,真实模型态让芯片 HUG 完整名称,避免第三方长名被截断。
|
|
2545
|
+
const placeholder = isComposerModelPlaceholder(label);
|
|
2546
|
+
trigger.toggleAttribute("data-codex-model-slider-trigger-placeholder", placeholder);
|
|
2547
|
+
for (const nativeLabel of nativeLabels) {
|
|
2548
|
+
let displayLabel = nativeLabel.nextElementSibling;
|
|
2549
|
+
if (!displayLabel?.hasAttribute("data-codex-model-slider-trigger-label")) {
|
|
2550
|
+
// 空窗期回插等场景会在别处留下现成的 MCX 标签,优先移动复用,
|
|
2551
|
+
// 避免恢复后出现两个可见名称把芯片撑宽。
|
|
2552
|
+
displayLabel = trigger.querySelector("[data-codex-model-slider-trigger-label]");
|
|
2553
|
+
if (!displayLabel) {
|
|
2554
|
+
displayLabel = document.createElement("span");
|
|
2555
|
+
displayLabel.className = nativeLabel.className;
|
|
2556
|
+
displayLabel.setAttribute("data-codex-model-slider-trigger-label", "");
|
|
2557
|
+
}
|
|
2558
|
+
nativeLabel.after(displayLabel);
|
|
2559
|
+
}
|
|
2560
|
+
if (displayLabel.textContent !== label) displayLabel.textContent = label;
|
|
2561
|
+
}
|
|
2562
|
+
// 原生开合的临时 min-width 钉定与弹层水平居中都基于对 Measurement
|
|
2563
|
+
// 探针的测量;把探针文本同步为 MCX 显示标签,原生机制即可测得
|
|
2564
|
+
// 真实显示宽度(含 110px 名称上限与 160px Content 上限)。
|
|
2565
|
+
const measurement = trigger.querySelector('[class*="_ModelPickerTriggerMeasurement_"]');
|
|
2566
|
+
if (measurement && measurement.textContent !== label) measurement.textContent = label;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
function ensureModelSelector() {
|
|
2570
|
+
const selector = modelSelectorFeature?.modelSelector;
|
|
2571
|
+
if (!selector) return;
|
|
2572
|
+
if (!nativeComposerModelController()) {
|
|
2573
|
+
if (modelSelectorStyle.isConnected) restoreModelSelectorEnhancements();
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
pruneModelSubmenuPositionObservers();
|
|
2577
|
+
pruneNativeModelToggleHandlers();
|
|
2578
|
+
document.documentElement.setAttribute("data-codex-model-slider-catalog-ready", "");
|
|
2579
|
+
if (!modelSelectorStyle.isConnected) document.head?.append(modelSelectorStyle);
|
|
2580
|
+
enhanceComposerModelTrigger(selector);
|
|
2581
|
+
const parentMenu = nativeModelPickerMenu();
|
|
2582
|
+
if (modelSelectorControllerMenu && modelSelectorControllerMenu.parentMenu !== parentMenu) {
|
|
2583
|
+
closeControllerModelMenu();
|
|
2584
|
+
}
|
|
2585
|
+
if (!parentMenu) {
|
|
2586
|
+
if (
|
|
2587
|
+
modelSelectorPendingReturn
|
|
2588
|
+
&& !visibleBlockingDialog()
|
|
2589
|
+
&& nativeModelPickerSubmenus().length === 0
|
|
2590
|
+
) {
|
|
2591
|
+
clearTimeout(modelSelectorReturnTimer);
|
|
2592
|
+
modelSelectorReturnTimer = null;
|
|
2593
|
+
returnToNativeModelSlider();
|
|
2594
|
+
}
|
|
2595
|
+
} else {
|
|
2596
|
+
enhanceNativeModelPicker(parentMenu, selector);
|
|
2597
|
+
const submenu = nativeModelSubmenu(parentMenu);
|
|
2598
|
+
if (submenu) enhanceNativeModelSubmenu(submenu, selector);
|
|
2599
|
+
}
|
|
2600
|
+
try {
|
|
2601
|
+
// 图标工作与菜单增强解耦:放在最后并隔离异常,任何失败都不影响标签与菜单。
|
|
2602
|
+
captureNativeFastIcon();
|
|
2603
|
+
} catch (error) {
|
|
2604
|
+
console.warn("[codex-personal] fast icon capture failed", error);
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
function restoreModelSelectorEnhancements() {
|
|
2609
|
+
closeControllerModelMenu();
|
|
2610
|
+
clearTimeout(modelSelectorReturnTimer);
|
|
2611
|
+
clearTimeout(modelSelectorEffortTransactionTimer);
|
|
2612
|
+
modelSelectorTriggerObserver?.disconnect();
|
|
2613
|
+
for (const record of modelSelectorPositionObservers.values()) {
|
|
2614
|
+
record.observer.disconnect();
|
|
2615
|
+
record.resizeObserver.disconnect();
|
|
2616
|
+
}
|
|
2617
|
+
modelSelectorPositionObservers.clear();
|
|
2618
|
+
for (const frame of modelSelectorMenuSizeFrames.values()) cancelAnimationFrame(frame);
|
|
2619
|
+
modelSelectorMenuSizeFrames.clear();
|
|
2620
|
+
modelSelectorTriggerObserver = null;
|
|
2621
|
+
modelSelectorObservedTrigger = null;
|
|
2622
|
+
modelSelectorReturnTimer = null;
|
|
2623
|
+
modelSelectorEffortTransactionTimer = null;
|
|
2624
|
+
modelSelectorPendingOutsideDismiss = false;
|
|
2625
|
+
modelSelectorSuppressOutsidePointerSequence = false;
|
|
2626
|
+
modelSelectorPendingReturn = false;
|
|
2627
|
+
document.documentElement.removeAttribute("data-codex-model-slider-selecting-effort");
|
|
2628
|
+
for (const item of document.querySelectorAll("[data-codex-model-slider-item]")) {
|
|
2629
|
+
item.style.removeProperty("display");
|
|
2630
|
+
for (const name of [
|
|
2631
|
+
"data-codex-model-slider-raw",
|
|
2632
|
+
"data-codex-model-slider-item",
|
|
2633
|
+
"data-codex-model-slider-order",
|
|
2634
|
+
"data-codex-model-slider-identity-source",
|
|
2635
|
+
"data-codex-model-slider-return-listener",
|
|
2636
|
+
]) item.removeAttribute(name);
|
|
2637
|
+
}
|
|
2638
|
+
for (const [node, value] of modelSelectorOriginalToggleLabels) {
|
|
2639
|
+
if (node.isConnected) node.textContent = value;
|
|
2640
|
+
}
|
|
2641
|
+
for (const [toggle, handlers] of modelSelectorNativeToggleHandlers) {
|
|
2642
|
+
toggle.removeEventListener("click", handlers.openModels);
|
|
2643
|
+
toggle.removeEventListener("keydown", handlers.onKeyDown);
|
|
2644
|
+
toggle.removeAttribute("data-codex-model-slider-toggle");
|
|
2645
|
+
if (handlers.originalAriaExpanded == null) toggle.removeAttribute("aria-expanded");
|
|
2646
|
+
else toggle.setAttribute("aria-expanded", handlers.originalAriaExpanded);
|
|
2647
|
+
}
|
|
2648
|
+
for (const [container, display] of modelSelectorGenericContainers) {
|
|
2649
|
+
if (container.isConnected) container.style.display = display;
|
|
2650
|
+
}
|
|
2651
|
+
document.querySelectorAll("[data-codex-model-slider-divider]")
|
|
2652
|
+
.forEach((element) => element.remove());
|
|
2653
|
+
document.querySelectorAll("[data-codex-model-slider-generic]").forEach((element) => element.remove());
|
|
2654
|
+
document.querySelectorAll("[data-codex-model-slider-generic-menu]")
|
|
2655
|
+
.forEach((element) => element.removeAttribute("data-codex-model-slider-generic-menu"));
|
|
2656
|
+
document.querySelectorAll("[data-codex-model-slider-menu]")
|
|
2657
|
+
.forEach((element) => element.removeAttribute("data-codex-model-slider-menu"));
|
|
2658
|
+
document.querySelectorAll("[data-codex-model-slider-trigger]").forEach((element) => {
|
|
2659
|
+
element.removeAttribute("data-codex-model-slider-trigger");
|
|
2660
|
+
element.removeAttribute("data-codex-model-slider-trigger-raw");
|
|
2661
|
+
});
|
|
2662
|
+
document.querySelectorAll("[data-codex-model-slider-trigger-label]").forEach((element) => element.remove());
|
|
2663
|
+
document.documentElement.removeAttribute("data-codex-model-slider-catalog-ready");
|
|
2664
|
+
modelSelectorStyle.remove();
|
|
2665
|
+
modelSelectorOriginalToggleLabels.clear();
|
|
2666
|
+
modelSelectorGenericContainers.clear();
|
|
2667
|
+
modelSelectorNativeToggleHandlers.clear();
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
// ========================================================================
|
|
2671
|
+
// Surface 注册与主题桥 — surface 记录、侧栏状态、主题 token 与 Composer 文本
|
|
2672
|
+
// ========================================================================
|
|
2673
|
+
|
|
2674
|
+
function surfaceKey(featureId, kind, detailId = "") {
|
|
2675
|
+
return `${featureId}:${kind}:${detailId}`;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
function surfaceFrameName(featureId) {
|
|
2679
|
+
const nonce = crypto.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2680
|
+
return `minecodex-surface-${config.sessionId}-${featureId}-${nonce}`;
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
function requestSurfaceLoad(record) {
|
|
2684
|
+
if (!record || record.loading || typeof globalThis[config.bindingName] !== "function") return false;
|
|
2685
|
+
const requestId = crypto.randomUUID?.()
|
|
2686
|
+
?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2687
|
+
record.loading = true;
|
|
2688
|
+
record.ready = false;
|
|
2689
|
+
record.loadError = null;
|
|
2690
|
+
pendingHostActions.set(requestId, { recordKey: record.key, kind: "surface-load" });
|
|
2691
|
+
globalThis[config.bindingName](JSON.stringify({
|
|
2692
|
+
token: config.bindingToken,
|
|
2693
|
+
featureId: record.featureId,
|
|
2694
|
+
requestId,
|
|
2695
|
+
action: "load-surface",
|
|
2696
|
+
payload: {
|
|
2697
|
+
frameName: record.frame.name,
|
|
2698
|
+
surfaceUrl: record.surfaceUrl,
|
|
2699
|
+
},
|
|
2700
|
+
}));
|
|
2701
|
+
return true;
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
function postSurfaceActive(record, active) {
|
|
2705
|
+
if (!record || record.kind !== "page") return;
|
|
2706
|
+
record.frame.contentWindow?.postMessage({
|
|
2707
|
+
type: "codex-personal:surface-active",
|
|
2708
|
+
active: Boolean(active),
|
|
2709
|
+
}, record.origin);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
function nativeSidebarTrigger() {
|
|
2713
|
+
const triggers = Array.from(document.querySelectorAll(
|
|
2714
|
+
'button[data-app-shell-sidebar-trigger="true"]',
|
|
2715
|
+
));
|
|
2716
|
+
return triggers.find((button) => isVisibleToolbarControl(button)) ?? triggers[0] ?? null;
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
function sidebarState() {
|
|
2720
|
+
const trigger = nativeSidebarTrigger();
|
|
2721
|
+
const expanded = trigger?.getAttribute("aria-expanded");
|
|
2722
|
+
const triggerRect = trigger?.getBoundingClientRect();
|
|
2723
|
+
return {
|
|
2724
|
+
available: Boolean(trigger),
|
|
2725
|
+
open: expanded === null ? sidebarRight() > 0 : expanded === "true",
|
|
2726
|
+
label: trigger?.getAttribute("aria-label") ?? "Show sidebar",
|
|
2727
|
+
leading: triggerRect?.left ?? 16,
|
|
2728
|
+
};
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
function postSidebarState(record, state = sidebarState()) {
|
|
2732
|
+
if (!record || record.kind !== "page") return;
|
|
2733
|
+
record.frame.contentWindow?.postMessage({
|
|
2734
|
+
type: "codex-personal:sidebar-state",
|
|
2735
|
+
...state,
|
|
2736
|
+
}, record.origin);
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
function registerSurface(key, feature, kind, surfaceUrl, element, frame, detailId = null) {
|
|
2740
|
+
const record = {
|
|
2741
|
+
key,
|
|
2742
|
+
featureId: feature.id,
|
|
2743
|
+
kind,
|
|
2744
|
+
detailId,
|
|
2745
|
+
origin: window.location.origin,
|
|
2746
|
+
surfaceUrl,
|
|
2747
|
+
element,
|
|
2748
|
+
frame,
|
|
2749
|
+
ready: false,
|
|
2750
|
+
loading: false,
|
|
2751
|
+
loadError: null,
|
|
2752
|
+
preferredHeight: null,
|
|
2753
|
+
};
|
|
2754
|
+
surfaceRecords.set(key, record);
|
|
2755
|
+
frame.addEventListener("load", () => {
|
|
2756
|
+
queueTheme();
|
|
2757
|
+
postSurfaceActive(record, activePageFeatureId === record.featureId && !record.element.hidden);
|
|
2758
|
+
postSidebarState(record);
|
|
2759
|
+
});
|
|
2760
|
+
requestSurfaceLoad(record);
|
|
2761
|
+
return record;
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
function reloadSurfaceIfUnready(key, surfaceUrl) {
|
|
2765
|
+
const record = surfaceRecords.get(key);
|
|
2766
|
+
if (!record || record.ready || record.surfaceUrl !== surfaceUrl) return false;
|
|
2767
|
+
return requestSurfaceLoad(record);
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
function removeSurfaceRecord(key) {
|
|
2771
|
+
if (activePromptPreview?.recordKey === key) closePromptPreview({ immediate: true });
|
|
2772
|
+
surfaceRecords.delete(key);
|
|
2773
|
+
pendingHostActions.forEach((pending, requestId) => {
|
|
2774
|
+
if (pending.recordKey !== key) return;
|
|
2775
|
+
pendingHostActions.delete(requestId);
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
function resolvedTheme() {
|
|
2780
|
+
const root = document.documentElement;
|
|
2781
|
+
const style = getComputedStyle(root);
|
|
2782
|
+
const tokens = Object.fromEntries(
|
|
2783
|
+
themeTokenNames
|
|
2784
|
+
.map((name) => [name, style.getPropertyValue(name).trim()])
|
|
2785
|
+
.filter(([, value]) => value),
|
|
2786
|
+
);
|
|
2787
|
+
const mode = root.classList.contains("electron-dark") || style.colorScheme === "dark"
|
|
2788
|
+
? "dark"
|
|
2789
|
+
: "light";
|
|
2790
|
+
return { root, style, tokens, mode };
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
function sendTheme() {
|
|
2794
|
+
themeQueued = false;
|
|
2795
|
+
const { style, tokens, mode } = resolvedTheme();
|
|
2796
|
+
const radiusToken = tokens["--radius-3xl-base"] ?? "20px";
|
|
2797
|
+
const radiusBase = Number.parseFloat(radiusToken) || 20;
|
|
2798
|
+
const radiusUnit = radiusToken.match(/[a-z%]+$/i)?.[0] ?? "px";
|
|
2799
|
+
const radiusScale = Number.parseFloat(tokens["--codex-corner-radius-scale"] ?? "1.25") || 1.25;
|
|
2800
|
+
const panelRadius = `${radiusBase * radiusScale}${radiusUnit}`;
|
|
2801
|
+
const panelCornerShape = tokens["--codex-corner-shape"] ?? "superellipse(1.5)";
|
|
2802
|
+
const dialogRadius = tokens["--radius-2xl"] ?? "20px";
|
|
2803
|
+
const dialogShadow = "0 16px 32px -8px rgba(0,0,0,.19)";
|
|
2804
|
+
const surfaceTokens = {
|
|
2805
|
+
...tokens,
|
|
2806
|
+
// 部分 Codex 版本仅提供等价的正文前景 token。
|
|
2807
|
+
"--color-token-input-foreground": tokens["--color-token-input-foreground"] ?? tokens["--color-text-foreground"],
|
|
2808
|
+
"--codex-summary-section-title-font-size": "14px",
|
|
2809
|
+
"--codex-summary-section-title-line-height": "21px",
|
|
2810
|
+
"--codex-summary-section-title-font-weight": tokens["--font-weight-normal"] ?? "400",
|
|
2811
|
+
"--codex-summary-row-font-size": "16px",
|
|
2812
|
+
"--codex-summary-row-line-height": "24px",
|
|
2813
|
+
"--codex-summary-label-font-size": tokens["--text-base"] ?? "14px",
|
|
2814
|
+
"--codex-summary-label-line-height": tokens["--text-base--line-height"] ?? "1.5",
|
|
2815
|
+
"--codex-summary-font-weight": tokens["--vscode-font-weight"] ?? (style.fontWeight || "445"),
|
|
2816
|
+
"--codex-summary-icon-size": "18px",
|
|
2817
|
+
"--codex-summary-icon-stroke-width": "1.5px",
|
|
2818
|
+
"--codex-summary-panel-radius": panelRadius,
|
|
2819
|
+
"--codex-summary-panel-corner-shape": panelCornerShape,
|
|
2820
|
+
"--codex-dialog-radius": dialogRadius,
|
|
2821
|
+
"--codex-dialog-corner-shape": "round",
|
|
2822
|
+
"--codex-dialog-shadow": dialogShadow,
|
|
2823
|
+
"--codex-summary-row-radius": tokens["--radius-sm"] ?? "7.5px",
|
|
2824
|
+
"--codex-summary-control-radius": tokens["--radius-md"] ?? "10px",
|
|
2825
|
+
"--codex-summary-section-title-color": tokens["--color-token-text-tertiary"] ?? "rgba(13,13,13,.484)",
|
|
2826
|
+
"--codex-summary-icon-primary": tokens["--color-icon-primary"] ?? tokens["--color-token-foreground"] ?? style.color,
|
|
2827
|
+
"--codex-summary-icon-secondary": tokens["--color-icon-secondary"] ?? tokens["--color-token-text-secondary"] ?? style.color,
|
|
2828
|
+
"--codex-summary-icon-tertiary": tokens["--color-icon-tertiary"] ?? tokens["--color-token-text-tertiary"] ?? style.color,
|
|
2829
|
+
};
|
|
2830
|
+
for (const record of surfaceRecords.values()) {
|
|
2831
|
+
if (!record.element.isConnected) continue;
|
|
2832
|
+
if (record.kind === "pinned") {
|
|
2833
|
+
record.element.style.borderRadius = panelRadius;
|
|
2834
|
+
record.element.style.setProperty("corner-shape", panelCornerShape);
|
|
2835
|
+
record.element.style.boxShadow = tokens["--elevation-prominent"]
|
|
2836
|
+
?? "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)";
|
|
2837
|
+
}
|
|
2838
|
+
if (record.kind === "modal") {
|
|
2839
|
+
record.element.style.borderRadius = dialogRadius;
|
|
2840
|
+
record.element.style.setProperty("corner-shape", "round");
|
|
2841
|
+
record.element.style.boxShadow = dialogShadow;
|
|
2842
|
+
}
|
|
2843
|
+
record.element.style.background = new Set(["pinned", "modal"]).has(record.kind)
|
|
2844
|
+
? tokens["--color-token-dropdown-background"]
|
|
2845
|
+
?? tokens["--color-token-main-surface-primary"]
|
|
2846
|
+
?? style.backgroundColor
|
|
2847
|
+
: tokens["--color-token-main-surface-primary"] ?? style.backgroundColor;
|
|
2848
|
+
record.frame.contentWindow?.postMessage(
|
|
2849
|
+
{ type: "codex-personal:theme", theme: mode, mode, locale, tokens: surfaceTokens },
|
|
2850
|
+
record.origin,
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
function queueTheme() {
|
|
2856
|
+
if (themeQueued) return;
|
|
2857
|
+
themeQueued = true;
|
|
2858
|
+
queueMicrotask(sendTheme);
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
function sidebarRight() {
|
|
2862
|
+
const sidebar = document.querySelector("[data-app-action-sidebar-scroll]");
|
|
2863
|
+
return sidebar?.getBoundingClientRect().right ?? 0;
|
|
2864
|
+
}
|
|
2865
|
+
|
|
2866
|
+
function updatePageSurfacePositions() {
|
|
2867
|
+
const state = sidebarState();
|
|
2868
|
+
const left = `${state.open ? Math.max(0, sidebarRight()) : 0}px`;
|
|
2869
|
+
for (const [featureId, surface] of pageSurfaces) {
|
|
2870
|
+
surface.style.left = left;
|
|
2871
|
+
postSidebarState(surfaceRecords.get(surfaceKey(featureId, "page")), state);
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
function syncAfterNativeSidebarToggle() {
|
|
2876
|
+
queueMicrotask(updatePageSurfacePositions);
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
function threadIdentity() {
|
|
2880
|
+
const selected = document.querySelector('[data-app-action-sidebar-thread-selected="true"]');
|
|
2881
|
+
const id = selected?.getAttribute("data-app-action-sidebar-thread-id");
|
|
2882
|
+
const project = selected?.closest?.("[data-app-action-sidebar-project-id]")
|
|
2883
|
+
?.getAttribute("data-app-action-sidebar-project-id");
|
|
2884
|
+
return `${project ?? ""}:${id ?? ""}:${location.pathname}:${location.search}`;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
function checkThreadChange() {
|
|
2888
|
+
const next = threadIdentity();
|
|
2889
|
+
if (currentThread === null) {
|
|
2890
|
+
currentThread = next;
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
if (next === currentThread) return;
|
|
2894
|
+
currentThread = next;
|
|
2895
|
+
stopToolbarReadiness();
|
|
2896
|
+
savedComposerRange = null;
|
|
2897
|
+
savedComposerThread = null;
|
|
2898
|
+
hidePinnedSurfaces();
|
|
2899
|
+
queueEnsure();
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
function currentComposer() {
|
|
2903
|
+
const marked = document.querySelector('[data-codex-composer="true"][contenteditable="true"]');
|
|
2904
|
+
if (marked) return marked;
|
|
2905
|
+
return Array.from(document.querySelectorAll('[contenteditable="true"][role="textbox"]'))
|
|
2906
|
+
.find((element) => isVisibleToolbarControl(element)) ?? null;
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
function rememberComposerSelection() {
|
|
2910
|
+
const composer = currentComposer();
|
|
2911
|
+
const selection = document.getSelection();
|
|
2912
|
+
if (!composer || !selection?.rangeCount) return;
|
|
2913
|
+
const range = selection.getRangeAt(0);
|
|
2914
|
+
if (!composer.contains(range.startContainer) || !composer.contains(range.endContainer)) return;
|
|
2915
|
+
savedComposerRange = range.cloneRange();
|
|
2916
|
+
savedComposerThread = threadIdentity();
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
function insertComposerText(text) {
|
|
2920
|
+
if (typeof text !== "string" || !text) throw new Error("Text is required");
|
|
2921
|
+
const composer = currentComposer();
|
|
2922
|
+
if (!composer) throw new Error("The active Composer was not found");
|
|
2923
|
+
const selection = document.getSelection();
|
|
2924
|
+
let fallback = true;
|
|
2925
|
+
if (
|
|
2926
|
+
savedComposerRange
|
|
2927
|
+
&& savedComposerThread === threadIdentity()
|
|
2928
|
+
&& savedComposerRange.startContainer?.isConnected
|
|
2929
|
+
&& composer.contains(savedComposerRange.startContainer)
|
|
2930
|
+
&& composer.contains(savedComposerRange.endContainer)
|
|
2931
|
+
) {
|
|
2932
|
+
selection.removeAllRanges();
|
|
2933
|
+
selection.addRange(savedComposerRange);
|
|
2934
|
+
fallback = false;
|
|
2935
|
+
} else {
|
|
2936
|
+
const range = document.createRange();
|
|
2937
|
+
range.selectNodeContents(composer);
|
|
2938
|
+
range.collapse(false);
|
|
2939
|
+
selection.removeAllRanges();
|
|
2940
|
+
selection.addRange(range);
|
|
2941
|
+
}
|
|
2942
|
+
composer.focus({ preventScroll: true });
|
|
2943
|
+
const existing = composer.innerText.replace(/\n+$/, "");
|
|
2944
|
+
const insertedText = fallback && existing ? `\n${text}` : text;
|
|
2945
|
+
const inserted = document.execCommand("insertText", false, insertedText);
|
|
2946
|
+
if (!inserted) throw new Error("Codex rejected Composer insertion");
|
|
2947
|
+
rememberComposerSelection();
|
|
2948
|
+
return { mode: "inserted", fallback };
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
function suppressNativeSelections() {
|
|
2952
|
+
const sidebar = document.querySelector("[data-app-action-sidebar-scroll]");
|
|
2953
|
+
if (!sidebar) return;
|
|
2954
|
+
const alreadySuppressed = new Set(suppressedSelections.map(({ element }) => element));
|
|
2955
|
+
const selected = new Set([
|
|
2956
|
+
...sidebar.querySelectorAll('[aria-current="page"]'),
|
|
2957
|
+
...sidebar.querySelectorAll('[data-app-action-sidebar-thread-selected="true"]'),
|
|
2958
|
+
]);
|
|
2959
|
+
for (const element of selected) {
|
|
2960
|
+
if (element.hasAttribute(entryMarker) || alreadySuppressed.has(element)) continue;
|
|
2961
|
+
const selection = {
|
|
2962
|
+
element,
|
|
2963
|
+
ariaCurrent: element.getAttribute("aria-current"),
|
|
2964
|
+
threadSelected: element.getAttribute("data-app-action-sidebar-thread-selected"),
|
|
2965
|
+
hadPrimaryGhost: element.classList.contains("bg-primary-ghost-hover"),
|
|
2966
|
+
hadHoverPrimaryGhost: element.classList.contains("hover:bg-primary-ghost-hover"),
|
|
2967
|
+
hadActiveClass: element.classList.contains("bg-token-list-hover-background"),
|
|
2968
|
+
hadHoverClass: element.classList.contains("hover:bg-token-list-hover-background"),
|
|
2969
|
+
};
|
|
2970
|
+
suppressedSelections.push(selection);
|
|
2971
|
+
element.removeAttribute("aria-current");
|
|
2972
|
+
element.removeAttribute("data-app-action-sidebar-thread-selected");
|
|
2973
|
+
// 原生选中态(灰底)现在由 bg-primary-ghost-hover 承载,需一并移除;
|
|
2974
|
+
// 旧 token 类仍保留处理,以便兼容旧主题。
|
|
2975
|
+
element.classList.remove("bg-primary-ghost-hover");
|
|
2976
|
+
element.classList.remove("bg-token-list-hover-background");
|
|
2977
|
+
if (!selection.hadHoverPrimaryGhost) element.classList.add("hover:bg-primary-ghost-hover");
|
|
2978
|
+
element.classList.add("hover:bg-token-list-hover-background");
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
function restoreNativeSelections() {
|
|
2983
|
+
for (const selection of suppressedSelections) {
|
|
2984
|
+
const { element } = selection;
|
|
2985
|
+
if (!element.isConnected) continue;
|
|
2986
|
+
if (selection.ariaCurrent !== null) element.setAttribute("aria-current", selection.ariaCurrent);
|
|
2987
|
+
if (selection.threadSelected !== null) {
|
|
2988
|
+
element.setAttribute("data-app-action-sidebar-thread-selected", selection.threadSelected);
|
|
2989
|
+
}
|
|
2990
|
+
element.classList.toggle("bg-primary-ghost-hover", Boolean(selection.hadPrimaryGhost));
|
|
2991
|
+
element.classList.toggle("hover:bg-primary-ghost-hover", Boolean(selection.hadHoverPrimaryGhost));
|
|
2992
|
+
element.classList.toggle("bg-token-list-hover-background", selection.hadActiveClass);
|
|
2993
|
+
element.classList.toggle("hover:bg-token-list-hover-background", selection.hadHoverClass);
|
|
2994
|
+
}
|
|
2995
|
+
suppressedSelections = [];
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2998
|
+
function updateSidebarSelectedState() {
|
|
2999
|
+
for (const feature of sidebarFeatures) {
|
|
3000
|
+
const entry = findEntry(feature.id);
|
|
3001
|
+
if (!entry) continue;
|
|
3002
|
+
const selected = feature.id === activePageFeatureId;
|
|
3003
|
+
entry.classList.toggle("bg-primary-ghost-hover", selected);
|
|
3004
|
+
entry.classList.toggle("bg-token-list-hover-background", selected);
|
|
3005
|
+
entry.classList.toggle("hover:bg-token-list-hover-background", !selected);
|
|
3006
|
+
if (selected) entry.setAttribute("aria-current", "page");
|
|
3007
|
+
else entry.removeAttribute("aria-current");
|
|
3008
|
+
}
|
|
3009
|
+
if (activePageFeatureId) suppressNativeSelections();
|
|
3010
|
+
else restoreNativeSelections();
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
function updateToolbarState() {
|
|
3014
|
+
const displayMode = summaryDisplayMode();
|
|
3015
|
+
for (const feature of toolbarFeatures) {
|
|
3016
|
+
const entry = findEntry(feature.id);
|
|
3017
|
+
if (!entry) continue;
|
|
3018
|
+
const open = visibleSummarySurface(feature.id, displayMode) !== null;
|
|
3019
|
+
const label = summaryButtonLabel(feature, displayMode);
|
|
3020
|
+
const pressed = String(open);
|
|
3021
|
+
const state = open ? "open" : "closed";
|
|
3022
|
+
if (entry.getAttribute("aria-label") !== label) entry.setAttribute("aria-label", label);
|
|
3023
|
+
if (entry.getAttribute("title") !== label) entry.setAttribute("title", label);
|
|
3024
|
+
if (entry.getAttribute("aria-pressed") !== pressed) entry.setAttribute("aria-pressed", pressed);
|
|
3025
|
+
if (entry.getAttribute("aria-expanded") !== pressed) entry.setAttribute("aria-expanded", pressed);
|
|
3026
|
+
if (entry.getAttribute("data-state") !== state) entry.setAttribute("data-state", state);
|
|
3027
|
+
entry.setAttribute("data-codex-personal-summary-mode", displayMode);
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
// ========================================================================
|
|
3032
|
+
// Page/Pinned/Modal surface 与摘要呈现 — 页面/固定/模态 surface 创建、展示与响应式摘要
|
|
3033
|
+
// ========================================================================
|
|
3034
|
+
|
|
3035
|
+
function createFrame(featureId, title) {
|
|
3036
|
+
const frame = document.createElement("iframe");
|
|
3037
|
+
frame.name = surfaceFrameName(featureId);
|
|
3038
|
+
frame.src = "about:blank";
|
|
3039
|
+
frame.title = title;
|
|
3040
|
+
frame.allow = "clipboard-write";
|
|
3041
|
+
frame.style.cssText = "width:100%;height:100%;border:0;display:block;background:transparent";
|
|
3042
|
+
return frame;
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
function createPageSurface(feature) {
|
|
3046
|
+
const surface = document.createElement("section");
|
|
3047
|
+
surface.setAttribute(pageSurfaceMarker, feature.id);
|
|
3048
|
+
surface.style.cssText = [
|
|
3049
|
+
"position:fixed",
|
|
3050
|
+
"top:0",
|
|
3051
|
+
"right:0",
|
|
3052
|
+
"bottom:0",
|
|
3053
|
+
"z-index:40",
|
|
3054
|
+
"background:transparent",
|
|
3055
|
+
].join(";");
|
|
3056
|
+
const frame = createFrame(feature.id, feature.label);
|
|
3057
|
+
surface.append(frame);
|
|
3058
|
+
document.body.append(surface);
|
|
3059
|
+
pageSurfaces.set(feature.id, surface);
|
|
3060
|
+
registerSurface(surfaceKey(feature.id, "page"), feature, "page", feature.surfaceUrl, surface, frame);
|
|
3061
|
+
return surface;
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
function showPageSurface(featureId) {
|
|
3065
|
+
const feature = featureById.get(featureId);
|
|
3066
|
+
if (!feature) return;
|
|
3067
|
+
hidePinnedSurfaces();
|
|
3068
|
+
for (const [otherFeatureId, surface] of pageSurfaces) {
|
|
3069
|
+
if (otherFeatureId === featureId) continue;
|
|
3070
|
+
surface.hidden = true;
|
|
3071
|
+
const record = surfaceRecords.get(surfaceKey(otherFeatureId, "page"));
|
|
3072
|
+
postSurfaceActive(record, false);
|
|
3073
|
+
}
|
|
3074
|
+
const existing = pageSurfaces.get(featureId);
|
|
3075
|
+
const reusable = existing?.isConnected ? existing : null;
|
|
3076
|
+
const surface = reusable ?? createPageSurface(feature);
|
|
3077
|
+
if (reusable) reloadSurfaceIfUnready(surfaceKey(feature.id, "page"), feature.surfaceUrl);
|
|
3078
|
+
surface.hidden = false;
|
|
3079
|
+
activePageFeatureId = featureId;
|
|
3080
|
+
const record = surfaceRecords.get(surfaceKey(feature.id, "page"));
|
|
3081
|
+
postSurfaceActive(record, true);
|
|
3082
|
+
updatePageSurfacePositions();
|
|
3083
|
+
updateSidebarSelectedState();
|
|
3084
|
+
queueTheme();
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
function hidePageSurfaces() {
|
|
3088
|
+
for (const [featureId, surface] of pageSurfaces) {
|
|
3089
|
+
surface.hidden = true;
|
|
3090
|
+
const record = surfaceRecords.get(surfaceKey(featureId, "page"));
|
|
3091
|
+
postSurfaceActive(record, false);
|
|
3092
|
+
}
|
|
3093
|
+
activePageFeatureId = null;
|
|
3094
|
+
updateSidebarSelectedState();
|
|
3095
|
+
}
|
|
3096
|
+
|
|
3097
|
+
function closeNativeSummary() {
|
|
3098
|
+
const toggle = nativeSummaryButton();
|
|
3099
|
+
if (!toggle) return;
|
|
3100
|
+
if (toggle.getAttribute("aria-expanded") === "true" || toggle.getAttribute("aria-pressed") === "true") {
|
|
3101
|
+
toggle.click();
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
|
|
3105
|
+
function restoreThreadContentShift() {
|
|
3106
|
+
for (const { element, shift, translate } of shiftedConversationOwners) {
|
|
3107
|
+
if (!element.isConnected) continue;
|
|
3108
|
+
if (shift.value) {
|
|
3109
|
+
element.style.setProperty(
|
|
3110
|
+
"--thread-wide-block-inline-shift",
|
|
3111
|
+
shift.value,
|
|
3112
|
+
shift.priority,
|
|
3113
|
+
);
|
|
3114
|
+
} else {
|
|
3115
|
+
element.style.removeProperty("--thread-wide-block-inline-shift");
|
|
3116
|
+
}
|
|
3117
|
+
if (translate.value) element.style.setProperty("translate", translate.value, translate.priority);
|
|
3118
|
+
else element.style.removeProperty("translate");
|
|
3119
|
+
element.removeAttribute("data-codex-personal-summary-shift");
|
|
3120
|
+
}
|
|
3121
|
+
shiftedConversationOwners = [];
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
function applyThreadContentShift(value) {
|
|
3125
|
+
if (value === 0) {
|
|
3126
|
+
restoreThreadContentShift();
|
|
3127
|
+
return;
|
|
3128
|
+
}
|
|
3129
|
+
const viewport = mainContentViewport();
|
|
3130
|
+
const owners = viewport
|
|
3131
|
+
? Array.from(viewport.querySelectorAll('[style*="--thread-wide-block-inline-shift"]'))
|
|
3132
|
+
: [];
|
|
3133
|
+
if (!owners.length) {
|
|
3134
|
+
restoreThreadContentShift();
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
if (
|
|
3138
|
+
shiftedConversationOwners.length !== owners.length
|
|
3139
|
+
|| owners.some((owner, index) => shiftedConversationOwners[index]?.element !== owner)
|
|
3140
|
+
) {
|
|
3141
|
+
restoreThreadContentShift();
|
|
3142
|
+
shiftedConversationOwners = owners.map((owner) => ({
|
|
3143
|
+
element: owner,
|
|
3144
|
+
shift: {
|
|
3145
|
+
value: owner.style.getPropertyValue("--thread-wide-block-inline-shift"),
|
|
3146
|
+
priority: owner.style.getPropertyPriority("--thread-wide-block-inline-shift"),
|
|
3147
|
+
},
|
|
3148
|
+
translate: {
|
|
3149
|
+
value: owner.style.getPropertyValue("translate"),
|
|
3150
|
+
priority: owner.style.getPropertyPriority("translate"),
|
|
3151
|
+
},
|
|
3152
|
+
}));
|
|
3153
|
+
}
|
|
3154
|
+
for (const owner of owners) {
|
|
3155
|
+
owner.style.setProperty(
|
|
3156
|
+
"--thread-wide-block-inline-shift",
|
|
3157
|
+
`${Number(value)}px`,
|
|
3158
|
+
"important",
|
|
3159
|
+
);
|
|
3160
|
+
owner.style.setProperty("translate", `${Number(value)}px 0`, "important");
|
|
3161
|
+
owner.setAttribute("data-codex-personal-summary-shift", String(value));
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
function setSummarySurfaceVisible(surface, visible, mode) {
|
|
3166
|
+
const wasVisible = surface.dataset.codexPersonalVisible === "true";
|
|
3167
|
+
const previousMode = surface.dataset.codexPersonalMode;
|
|
3168
|
+
const duration = reducedMotion() ? 0 : 300;
|
|
3169
|
+
surface.dataset.codexPersonalVisible = String(visible);
|
|
3170
|
+
surface.dataset.codexPersonalMode = mode;
|
|
3171
|
+
surface.style.transition = duration
|
|
3172
|
+
? "opacity 180ms ease, transform 300ms cubic-bezier(.2,.8,.2,1)"
|
|
3173
|
+
: "none";
|
|
3174
|
+
if (!visible) {
|
|
3175
|
+
closePromptPreview({ immediate: true });
|
|
3176
|
+
surface.style.pointerEvents = "none";
|
|
3177
|
+
surface.style.opacity = "0";
|
|
3178
|
+
surface.style.transform = mode === "overlay"
|
|
3179
|
+
? "translateY(-4px) scale(.98)"
|
|
3180
|
+
: "translateX(100%) scale(.8)";
|
|
3181
|
+
const transitionId = `${performance.now()}-${Math.random()}`;
|
|
3182
|
+
surface.dataset.codexPersonalTransition = transitionId;
|
|
3183
|
+
if (duration === 0) surface.hidden = true;
|
|
3184
|
+
else window.setTimeout(() => {
|
|
3185
|
+
if (
|
|
3186
|
+
surface.dataset.codexPersonalTransition === transitionId
|
|
3187
|
+
&& surface.dataset.codexPersonalVisible !== "true"
|
|
3188
|
+
) surface.hidden = true;
|
|
3189
|
+
}, duration);
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
surface.dataset.codexPersonalTransition = "visible";
|
|
3194
|
+
surface.hidden = false;
|
|
3195
|
+
surface.style.pointerEvents = "auto";
|
|
3196
|
+
if (!wasVisible || previousMode !== mode) {
|
|
3197
|
+
surface.style.opacity = "0";
|
|
3198
|
+
surface.style.transform = mode === "overlay"
|
|
3199
|
+
? "translateY(-4px) scale(.98)"
|
|
3200
|
+
: "translateX(100%) scale(.8)";
|
|
3201
|
+
const reveal = () => {
|
|
3202
|
+
if (surface.dataset.codexPersonalVisible !== "true") return;
|
|
3203
|
+
surface.style.opacity = "1";
|
|
3204
|
+
surface.style.transform = "translate(0, 0) scale(1)";
|
|
3205
|
+
};
|
|
3206
|
+
requestAnimationFrame(reveal);
|
|
3207
|
+
window.setTimeout(reveal, 50);
|
|
3208
|
+
if (duration) window.setTimeout(() => {
|
|
3209
|
+
if (surface.dataset.codexPersonalVisible !== "true") return;
|
|
3210
|
+
surface.style.transition = "none";
|
|
3211
|
+
surface.style.opacity = "1";
|
|
3212
|
+
surface.style.transform = "translate(0, 0) scale(1)";
|
|
3213
|
+
}, duration + 50);
|
|
3214
|
+
} else {
|
|
3215
|
+
surface.style.opacity = "1";
|
|
3216
|
+
surface.style.transform = "translate(0, 0) scale(1)";
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
function updatePinnedPosition(featureId) {
|
|
3221
|
+
const surface = pinnedSurfaces.get(featureId);
|
|
3222
|
+
const entry = findEntry(featureId);
|
|
3223
|
+
const feature = featureById.get(featureId);
|
|
3224
|
+
if (!surface || !entry || !feature) return;
|
|
3225
|
+
const layout = config.summaryLayout;
|
|
3226
|
+
const displayMode = summaryDisplayMode();
|
|
3227
|
+
const visibleSurface = visibleSummarySurface(featureId, displayMode);
|
|
3228
|
+
if (!visibleSurface) {
|
|
3229
|
+
setSummarySurfaceVisible(surface, false, displayMode);
|
|
3230
|
+
if (activePinnedFeatureId === featureId) applyThreadContentShift(0);
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
const target = mainContentTargetGeometry();
|
|
3235
|
+
const entryRect = entry.getBoundingClientRect();
|
|
3236
|
+
const scrollRect = mainContentViewport()?.querySelector(".thread-scroll-container")
|
|
3237
|
+
?.getBoundingClientRect();
|
|
3238
|
+
// The native conversation header is a fixed overlay that starts above the
|
|
3239
|
+
// thread scroll container (scroll top is often 0 while the header still
|
|
3240
|
+
// occupies the top toolbar strip). Anchor below the header so the pinned
|
|
3241
|
+
// surface never covers the toolbar entry used to close it again.
|
|
3242
|
+
const header = entry.closest("header");
|
|
3243
|
+
const headerBottom = header?.getBoundingClientRect().bottom ?? 0;
|
|
3244
|
+
const contentTop = Math.max(scrollRect?.top ?? 0, headerBottom);
|
|
3245
|
+
const top = displayMode === "overlay"
|
|
3246
|
+
? entryRect.bottom + layout.popoverOffset
|
|
3247
|
+
: (contentTop || 46) + layout.verticalInset;
|
|
3248
|
+
const right = displayMode === "overlay"
|
|
3249
|
+
? Math.max(layout.popoverOffset, innerWidth - entryRect.right)
|
|
3250
|
+
: Math.max(layout.panelInset, innerWidth - target.right + layout.panelInset);
|
|
3251
|
+
const bottomInset = displayMode === "overlay"
|
|
3252
|
+
? layout.popoverBottomInset
|
|
3253
|
+
: layout.verticalInset;
|
|
3254
|
+
const available = Math.max(120, innerHeight - top - bottomInset);
|
|
3255
|
+
const record = surfaceRecords.get(surfaceKey(featureId, "pinned"));
|
|
3256
|
+
const height = Math.min(record?.preferredHeight ?? 620, available);
|
|
3257
|
+
Object.assign(surface.style, {
|
|
3258
|
+
top: `${top}px`,
|
|
3259
|
+
right: `${right}px`,
|
|
3260
|
+
width: `${layout.panelWidth}px`,
|
|
3261
|
+
height: `${height}px`,
|
|
3262
|
+
maxHeight: `${available}px`,
|
|
3263
|
+
});
|
|
3264
|
+
surface.setAttribute("data-codex-personal-summary-presentation", displayMode);
|
|
3265
|
+
setSummarySurfaceVisible(surface, true, displayMode);
|
|
3266
|
+
|
|
3267
|
+
const shift = displayMode !== "overlay"
|
|
3268
|
+
? -(layout.panelWidth + layout.panelInset) / 2
|
|
3269
|
+
: 0;
|
|
3270
|
+
applyThreadContentShift(shift);
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
function createPinnedSurface(feature) {
|
|
3274
|
+
const definition = feature.pinnedSummary;
|
|
3275
|
+
const surface = document.createElement("section");
|
|
3276
|
+
surface.setAttribute(pinnedSurfaceMarker, feature.id);
|
|
3277
|
+
surface.setAttribute("role", "dialog");
|
|
3278
|
+
surface.setAttribute("aria-label", definition.label ?? feature.label);
|
|
3279
|
+
surface.hidden = true;
|
|
3280
|
+
surface.style.cssText = [
|
|
3281
|
+
"position:fixed",
|
|
3282
|
+
"z-index:40",
|
|
3283
|
+
"overflow:hidden",
|
|
3284
|
+
"border-radius:25px",
|
|
3285
|
+
"corner-shape:var(--codex-corner-shape,superellipse(1.5))",
|
|
3286
|
+
"background:var(--color-token-dropdown-background,var(--color-token-main-surface-primary))",
|
|
3287
|
+
"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))",
|
|
3288
|
+
"transform-origin:top right",
|
|
3289
|
+
"opacity:0",
|
|
3290
|
+
"pointer-events:none",
|
|
3291
|
+
"will-change:transform,opacity",
|
|
3292
|
+
].join(";");
|
|
3293
|
+
const frame = createFrame(feature.id, definition.label ?? feature.label);
|
|
3294
|
+
surface.append(frame);
|
|
3295
|
+
document.body.append(surface);
|
|
3296
|
+
pinnedSurfaces.set(feature.id, surface);
|
|
3297
|
+
registerSurface(
|
|
3298
|
+
surfaceKey(feature.id, "pinned"),
|
|
3299
|
+
feature,
|
|
3300
|
+
"pinned",
|
|
3301
|
+
definition.surfaceUrl,
|
|
3302
|
+
surface,
|
|
3303
|
+
frame,
|
|
3304
|
+
);
|
|
3305
|
+
return surface;
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
function closePromptPreview({ immediate = false } = {}) {
|
|
3309
|
+
clearTimeout(promptPreviewDismissTimer);
|
|
3310
|
+
promptPreviewDismissTimer = null;
|
|
3311
|
+
const current = activePromptPreview;
|
|
3312
|
+
if (!current) return false;
|
|
3313
|
+
activePromptPreview = null;
|
|
3314
|
+
const remove = () => current.element.remove();
|
|
3315
|
+
if (immediate || reducedMotion()) remove();
|
|
3316
|
+
else {
|
|
3317
|
+
current.element.style.pointerEvents = "none";
|
|
3318
|
+
current.element.style.opacity = "0";
|
|
3319
|
+
window.setTimeout(remove, 80);
|
|
3320
|
+
}
|
|
3321
|
+
return true;
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
function requestPromptPreviewClose(record) {
|
|
3325
|
+
if (!activePromptPreview || activePromptPreview.recordKey !== record.key) return false;
|
|
3326
|
+
clearTimeout(promptPreviewDismissTimer);
|
|
3327
|
+
promptPreviewDismissTimer = window.setTimeout(() => {
|
|
3328
|
+
if (!activePromptPreview || activePromptPreview.recordKey !== record.key) return;
|
|
3329
|
+
if (activePromptPreview.element.matches(":hover")) return;
|
|
3330
|
+
closePromptPreview();
|
|
3331
|
+
}, 120);
|
|
3332
|
+
return true;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
function showPromptPreview(record, payload = {}) {
|
|
3336
|
+
if (record.kind !== "pinned" || record.element.hidden) return false;
|
|
3337
|
+
const body = String(payload.prompt?.body ?? "");
|
|
3338
|
+
if (!body.trim()) return false;
|
|
3339
|
+
const title = String(payload.prompt?.title ?? "").trim();
|
|
3340
|
+
const surfaceLeft = record.element.offsetLeft;
|
|
3341
|
+
const surfaceTop = record.element.offsetTop;
|
|
3342
|
+
const anchorTop = Number(payload.anchor?.top);
|
|
3343
|
+
if (!Number.isFinite(anchorTop) || record.element.offsetWidth <= 0 || record.element.offsetHeight <= 0) return false;
|
|
3344
|
+
|
|
3345
|
+
closePromptPreview({ immediate: true });
|
|
3346
|
+
const preview = document.createElement("aside");
|
|
3347
|
+
preview.setAttribute(promptPreviewMarker, record.featureId);
|
|
3348
|
+
preview.setAttribute("role", "tooltip");
|
|
3349
|
+
preview.setAttribute("aria-label", locale === "zh-CN" ? "提示词预览" : "Prompt preview");
|
|
3350
|
+
if (payload.prompt?.id) preview.setAttribute("data-codex-personal-prompt-id", String(payload.prompt.id));
|
|
3351
|
+
preview.style.cssText = [
|
|
3352
|
+
"position:fixed",
|
|
3353
|
+
"z-index:41",
|
|
3354
|
+
"box-sizing:border-box",
|
|
3355
|
+
"display:flex",
|
|
3356
|
+
"flex-direction:column",
|
|
3357
|
+
"gap:8px",
|
|
3358
|
+
"max-height:min(420px,calc(100vh - 24px))",
|
|
3359
|
+
"overflow:hidden",
|
|
3360
|
+
"padding:14px 16px 15px",
|
|
3361
|
+
"border:.5px solid var(--color-token-border,var(--color-border,rgba(13,13,13,.11)))",
|
|
3362
|
+
"border-radius:16px",
|
|
3363
|
+
"corner-shape:var(--codex-corner-shape,superellipse(1.5))",
|
|
3364
|
+
"color:var(--color-token-foreground,var(--color-text-foreground,#0d0d0d))",
|
|
3365
|
+
"background:var(--color-token-dropdown-background,var(--color-token-main-surface-primary,#fff))",
|
|
3366
|
+
"box-shadow:var(--elevation-prominent,0 0 0 .5px rgba(13,13,13,.08),0 8px 24px rgba(0,0,0,.10))",
|
|
3367
|
+
"font-family:var(--font-sans-default,ui-sans-serif,system-ui,sans-serif)",
|
|
3368
|
+
"pointer-events:auto",
|
|
3369
|
+
"transition:opacity 80ms ease",
|
|
3370
|
+
].join(";");
|
|
3371
|
+
|
|
3372
|
+
if (title) {
|
|
3373
|
+
const heading = document.createElement("strong");
|
|
3374
|
+
heading.textContent = title.slice(0, 500);
|
|
3375
|
+
heading.style.cssText = [
|
|
3376
|
+
"display:-webkit-box",
|
|
3377
|
+
"overflow:hidden",
|
|
3378
|
+
"-webkit-box-orient:vertical",
|
|
3379
|
+
"-webkit-line-clamp:2",
|
|
3380
|
+
"font-size:15px",
|
|
3381
|
+
"font-weight:550",
|
|
3382
|
+
"line-height:21px",
|
|
3383
|
+
"overflow-wrap:anywhere",
|
|
3384
|
+
].join(";");
|
|
3385
|
+
preview.append(heading);
|
|
3386
|
+
}
|
|
3387
|
+
const content = document.createElement("div");
|
|
3388
|
+
content.textContent = body;
|
|
3389
|
+
content.style.cssText = [
|
|
3390
|
+
"min-height:0",
|
|
3391
|
+
"overflow:auto",
|
|
3392
|
+
"padding-right:2px",
|
|
3393
|
+
"white-space:pre-wrap",
|
|
3394
|
+
"overflow-wrap:anywhere",
|
|
3395
|
+
"font-size:14px",
|
|
3396
|
+
"font-weight:400",
|
|
3397
|
+
"line-height:20px",
|
|
3398
|
+
].join(";");
|
|
3399
|
+
preview.append(content);
|
|
3400
|
+
|
|
3401
|
+
const inset = 8;
|
|
3402
|
+
const gap = 10;
|
|
3403
|
+
const availableLeft = Math.max(160, surfaceLeft - gap - inset);
|
|
3404
|
+
const width = Math.min(360, availableLeft);
|
|
3405
|
+
preview.style.width = `${width}px`;
|
|
3406
|
+
document.body.append(preview);
|
|
3407
|
+
const previewRect = preview.getBoundingClientRect();
|
|
3408
|
+
const left = Math.max(inset, surfaceLeft - gap - width);
|
|
3409
|
+
const desiredTop = surfaceTop + anchorTop - 8;
|
|
3410
|
+
const top = Math.min(
|
|
3411
|
+
Math.max(12, desiredTop),
|
|
3412
|
+
Math.max(12, innerHeight - previewRect.height - 12),
|
|
3413
|
+
);
|
|
3414
|
+
preview.style.left = `${left}px`;
|
|
3415
|
+
preview.style.top = `${top}px`;
|
|
3416
|
+
activePromptPreview = { element: preview, recordKey: record.key };
|
|
3417
|
+
|
|
3418
|
+
preview.addEventListener("pointerenter", () => {
|
|
3419
|
+
clearTimeout(promptPreviewDismissTimer);
|
|
3420
|
+
promptPreviewDismissTimer = null;
|
|
3421
|
+
}, { signal: lifetime.signal });
|
|
3422
|
+
preview.addEventListener("pointerleave", () => closePromptPreview(), { signal: lifetime.signal });
|
|
3423
|
+
return true;
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
function closeModalSurface() {
|
|
3427
|
+
if (!activeModalRecordKey) return false;
|
|
3428
|
+
const record = surfaceRecords.get(activeModalRecordKey);
|
|
3429
|
+
record?.overlay?.remove();
|
|
3430
|
+
record?.element?.remove();
|
|
3431
|
+
if (record) removeSurfaceRecord(record.key);
|
|
3432
|
+
activeModalRecordKey = null;
|
|
3433
|
+
return true;
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
function openEditorModal(sourceRecord, payload = {}) {
|
|
3437
|
+
const feature = featureById.get(sourceRecord.featureId);
|
|
3438
|
+
const kind = payload.kind === "todo"
|
|
3439
|
+
? "todo"
|
|
3440
|
+
: new Set(["prompt", "note"]).has(payload.kind)
|
|
3441
|
+
? "prompt"
|
|
3442
|
+
: null;
|
|
3443
|
+
if (!feature || !kind) throw new Error("Editor kind must be prompt or todo");
|
|
3444
|
+
|
|
3445
|
+
closePromptPreview({ immediate: true });
|
|
3446
|
+
closeModalSurface();
|
|
3447
|
+
const editorUrl = new URL(sourceRecord.surfaceUrl);
|
|
3448
|
+
editorUrl.searchParams.set("surface", "editor");
|
|
3449
|
+
editorUrl.searchParams.set("kind", kind);
|
|
3450
|
+
if (typeof payload.id === "string" && payload.id) editorUrl.searchParams.set("id", payload.id);
|
|
3451
|
+
else editorUrl.searchParams.delete("id");
|
|
3452
|
+
|
|
3453
|
+
const overlay = document.createElement("div");
|
|
3454
|
+
overlay.setAttribute("data-codex-personal-modal-overlay", feature.id);
|
|
3455
|
+
overlay.setAttribute("aria-hidden", "true");
|
|
3456
|
+
overlay.style.cssText = [
|
|
3457
|
+
"position:fixed",
|
|
3458
|
+
"inset:0",
|
|
3459
|
+
"z-index:50",
|
|
3460
|
+
"background:rgba(0,0,0,.133)",
|
|
3461
|
+
].join(";");
|
|
3462
|
+
|
|
3463
|
+
const dialog = document.createElement("section");
|
|
3464
|
+
dialog.setAttribute(modalSurfaceMarker, feature.id);
|
|
3465
|
+
dialog.setAttribute("role", "dialog");
|
|
3466
|
+
dialog.setAttribute("aria-modal", "true");
|
|
3467
|
+
dialog.setAttribute("aria-label", `${payload.id ? "Edit" : "Add"} ${kind}`);
|
|
3468
|
+
dialog.setAttribute("tabindex", "-1");
|
|
3469
|
+
dialog.style.cssText = [
|
|
3470
|
+
"position:fixed",
|
|
3471
|
+
"left:50%",
|
|
3472
|
+
"top:50%",
|
|
3473
|
+
"z-index:51",
|
|
3474
|
+
"width:min(420px,92vw)",
|
|
3475
|
+
`height:${kind === "prompt" ? "250px" : "198px"}`,
|
|
3476
|
+
"max-height:88vh",
|
|
3477
|
+
"transform:translate(-50%,-50%)",
|
|
3478
|
+
"overflow:hidden",
|
|
3479
|
+
"border:1px solid transparent",
|
|
3480
|
+
"border-radius:20px",
|
|
3481
|
+
"corner-shape:round",
|
|
3482
|
+
"background:var(--color-token-dropdown-background,var(--color-token-main-surface-primary))",
|
|
3483
|
+
"box-shadow:0 16px 32px -8px rgba(0,0,0,.19)",
|
|
3484
|
+
"outline:none",
|
|
3485
|
+
].join(";");
|
|
3486
|
+
|
|
3487
|
+
const frame = createFrame(feature.id, `${payload.id ? "Edit" : "Add"} ${kind}`);
|
|
3488
|
+
dialog.append(frame);
|
|
3489
|
+
document.body.append(overlay, dialog);
|
|
3490
|
+
const key = surfaceKey(feature.id, "modal");
|
|
3491
|
+
const record = registerSurface(key, feature, "modal", editorUrl.href, dialog, frame);
|
|
3492
|
+
record.overlay = overlay;
|
|
3493
|
+
activeModalRecordKey = key;
|
|
3494
|
+
overlay.addEventListener("click", closeModalSurface, { signal: lifetime.signal });
|
|
3495
|
+
queueTheme();
|
|
3496
|
+
return { opened: true };
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
function showPinnedSurface(featureId) {
|
|
3500
|
+
const feature = featureById.get(featureId);
|
|
3501
|
+
if (!feature?.pinnedSummary) return;
|
|
3502
|
+
hidePageSurfaces();
|
|
3503
|
+
closeNativeSummary();
|
|
3504
|
+
for (const [id, state] of summaryStates) {
|
|
3505
|
+
if (id === featureId) continue;
|
|
3506
|
+
state.isPinned = false;
|
|
3507
|
+
state.isPopoverOpen = false;
|
|
3508
|
+
const other = pinnedSurfaces.get(id);
|
|
3509
|
+
if (other) setSummarySurfaceVisible(other, false, summaryDisplayMode());
|
|
3510
|
+
}
|
|
3511
|
+
const existing = pinnedSurfaces.get(featureId);
|
|
3512
|
+
const surface = existing ?? createPinnedSurface(feature);
|
|
3513
|
+
if (existing) {
|
|
3514
|
+
reloadSurfaceIfUnready(
|
|
3515
|
+
surfaceKey(feature.id, "pinned"),
|
|
3516
|
+
feature.pinnedSummary.surfaceUrl,
|
|
3517
|
+
);
|
|
3518
|
+
}
|
|
3519
|
+
const displayMode = summaryDisplayMode();
|
|
3520
|
+
const state = summaryState(featureId);
|
|
3521
|
+
if (displayMode === "overlay") state.isPopoverOpen = true;
|
|
3522
|
+
else state.isPinned = true;
|
|
3523
|
+
activePinnedFeatureId = featureId;
|
|
3524
|
+
updateToolbarState();
|
|
3525
|
+
updatePinnedPosition(featureId);
|
|
3526
|
+
queueTheme();
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
function hidePinnedSurfaces() {
|
|
3530
|
+
const displayMode = summaryDisplayMode();
|
|
3531
|
+
for (const [featureId, state] of summaryStates) {
|
|
3532
|
+
state.isPinned = false;
|
|
3533
|
+
state.isPopoverOpen = false;
|
|
3534
|
+
const surface = pinnedSurfaces.get(featureId);
|
|
3535
|
+
if (surface) setSummarySurfaceVisible(surface, false, displayMode);
|
|
3536
|
+
}
|
|
3537
|
+
activePinnedFeatureId = null;
|
|
3538
|
+
restoreThreadContentShift();
|
|
3539
|
+
updateToolbarState();
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
function togglePinnedSurface(featureId) {
|
|
3543
|
+
const feature = featureById.get(featureId);
|
|
3544
|
+
if (!feature?.pinnedSummary) return;
|
|
3545
|
+
const displayMode = summaryDisplayMode();
|
|
3546
|
+
const state = summaryState(featureId);
|
|
3547
|
+
const isOpen = visibleSummarySurface(featureId, displayMode) !== null;
|
|
3548
|
+
if (!isOpen) {
|
|
3549
|
+
showPinnedSurface(featureId);
|
|
3550
|
+
return;
|
|
3551
|
+
}
|
|
3552
|
+
if (displayMode === "overlay") state.isPopoverOpen = false;
|
|
3553
|
+
else state.isPinned = false;
|
|
3554
|
+
if (!state.isPinned && !state.isPopoverOpen) activePinnedFeatureId = null;
|
|
3555
|
+
updatePinnedPosition(featureId);
|
|
3556
|
+
if (!activePinnedFeatureId) restoreThreadContentShift();
|
|
3557
|
+
updateToolbarState();
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
function updateResponsiveSummaryPresentation() {
|
|
3561
|
+
const nextMode = summaryDisplayMode();
|
|
3562
|
+
if (currentSummaryDisplayMode === "overlay" && nextMode !== "overlay") {
|
|
3563
|
+
for (const state of summaryStates.values()) state.isPopoverOpen = false;
|
|
3564
|
+
}
|
|
3565
|
+
currentSummaryDisplayMode = nextMode;
|
|
3566
|
+
for (const feature of toolbarFeatures) updatePinnedPosition(feature.id);
|
|
3567
|
+
if (!toolbarFeatures.some((feature) => (
|
|
3568
|
+
visibleSummarySurface(feature.id, nextMode) === "inline"
|
|
3569
|
+
))) applyThreadContentShift(0);
|
|
3570
|
+
updateToolbarState();
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
function ensureMainContentObserver() {
|
|
3574
|
+
const viewport = mainContentViewport();
|
|
3575
|
+
if (!viewport || mainContentObserver?.target === viewport) return;
|
|
3576
|
+
mainContentObserver?.observer.disconnect();
|
|
3577
|
+
const observer = new ResizeObserver(() => updateResponsiveSummaryPresentation());
|
|
3578
|
+
observer.observe(viewport);
|
|
3579
|
+
mainContentObserver = { target: viewport, observer };
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
// ========================================================================
|
|
3583
|
+
// 原生 Detail Tab capability — React fiber/右侧面板探测、原生详情页签打开与线程跳转
|
|
3584
|
+
// ========================================================================
|
|
3585
|
+
|
|
3586
|
+
function nativeTabModuleUrl() {
|
|
3587
|
+
return Array.from(document.querySelectorAll('link[rel="modulepreload"]'))
|
|
3588
|
+
.map((link) => link.href)
|
|
3589
|
+
.find((url) => /\/assets\/app-initial-[^/]+\.js(?:\?|$)/.test(url)) ?? null;
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3592
|
+
function reactFiberFor(element) {
|
|
3593
|
+
if (!(element instanceof Element)) return null;
|
|
3594
|
+
const key = Object.getOwnPropertyNames(element).find((name) => (
|
|
3595
|
+
name.startsWith("__reactFiber$")
|
|
3596
|
+
|| name.startsWith("__reactInternalInstance$")
|
|
3597
|
+
));
|
|
3598
|
+
return key ? element[key] : null;
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
function currentRightPanelScope() {
|
|
3602
|
+
let anchor = document.querySelector('[data-app-shell-tabs="true"]');
|
|
3603
|
+
let anchorFiber = reactFiberFor(anchor);
|
|
3604
|
+
if (!anchorFiber) {
|
|
3605
|
+
for (const element of document.querySelectorAll("body *")) {
|
|
3606
|
+
const fiber = reactFiberFor(element);
|
|
3607
|
+
if (!fiber) continue;
|
|
3608
|
+
anchor = element;
|
|
3609
|
+
anchorFiber = fiber;
|
|
3610
|
+
break;
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
if (!anchorFiber) return null;
|
|
3614
|
+
|
|
3615
|
+
let rootFiber = anchorFiber;
|
|
3616
|
+
while (rootFiber.return) rootFiber = rootFiber.return;
|
|
3617
|
+
|
|
3618
|
+
const isRightPanelScope = (value) => (
|
|
3619
|
+
value
|
|
3620
|
+
&& typeof value === "object"
|
|
3621
|
+
&& typeof value.get === "function"
|
|
3622
|
+
&& typeof value.set === "function"
|
|
3623
|
+
&& rightPanelRouteKinds.has(value.value?.routeKind)
|
|
3624
|
+
&& typeof value.query?.getOrFetch === "function"
|
|
3625
|
+
);
|
|
3626
|
+
const inspected = new Set();
|
|
3627
|
+
const inspect = (value) => {
|
|
3628
|
+
if (
|
|
3629
|
+
value == null
|
|
3630
|
+
|| (typeof value !== "object" && typeof value !== "function")
|
|
3631
|
+
|| inspected.has(value)
|
|
3632
|
+
) return null;
|
|
3633
|
+
inspected.add(value);
|
|
3634
|
+
if (isRightPanelScope(value)) return value;
|
|
3635
|
+
if (isRightPanelScope(value.current)) return value.current;
|
|
3636
|
+
return null;
|
|
3637
|
+
};
|
|
3638
|
+
|
|
3639
|
+
const stack = [rootFiber];
|
|
3640
|
+
let visitedFibers = 0;
|
|
3641
|
+
while (stack.length > 0 && visitedFibers < 50_000) {
|
|
3642
|
+
const fiber = stack.pop();
|
|
3643
|
+
visitedFibers += 1;
|
|
3644
|
+
let scope = inspect(fiber.memoizedProps);
|
|
3645
|
+
if (!scope && fiber.memoizedProps && typeof fiber.memoizedProps === "object") {
|
|
3646
|
+
for (const value of Object.values(fiber.memoizedProps)) {
|
|
3647
|
+
scope = inspect(value);
|
|
3648
|
+
if (scope) break;
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
let hook = fiber.memoizedState;
|
|
3653
|
+
let hookIndex = 0;
|
|
3654
|
+
while (!scope && hook && typeof hook === "object" && hookIndex < 140) {
|
|
3655
|
+
scope = inspect(hook.memoizedState);
|
|
3656
|
+
if (!scope && hook.memoizedState && typeof hook.memoizedState === "object") {
|
|
3657
|
+
scope = inspect(hook.memoizedState.current);
|
|
3658
|
+
}
|
|
3659
|
+
hook = hook.next;
|
|
3660
|
+
hookIndex += 1;
|
|
3661
|
+
}
|
|
3662
|
+
if (scope) return scope;
|
|
3663
|
+
if (fiber.sibling) stack.push(fiber.sibling);
|
|
3664
|
+
if (fiber.child) stack.push(fiber.child);
|
|
3665
|
+
}
|
|
3666
|
+
return null;
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
async function waitForCurrentRightPanelScope() {
|
|
3670
|
+
const deadline = performance.now() + 2_000;
|
|
3671
|
+
do {
|
|
3672
|
+
const scope = currentRightPanelScope();
|
|
3673
|
+
if (scope) return scope;
|
|
3674
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
3675
|
+
} while (performance.now() < deadline);
|
|
3676
|
+
return null;
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
async function waitForNativeTabModuleUrl() {
|
|
3680
|
+
const deadline = performance.now() + 2_000;
|
|
3681
|
+
do {
|
|
3682
|
+
const moduleUrl = nativeTabModuleUrl();
|
|
3683
|
+
if (moduleUrl) return moduleUrl;
|
|
3684
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
3685
|
+
} while (performance.now() < deadline);
|
|
3686
|
+
return null;
|
|
3687
|
+
}
|
|
3688
|
+
|
|
3689
|
+
async function loadNativeTabCapability() {
|
|
3690
|
+
if (nativeTabCapability) return nativeTabCapability;
|
|
3691
|
+
if (nativeTabCapabilityPromise) return nativeTabCapabilityPromise;
|
|
3692
|
+
nativeTabCapabilityPromise = (async () => {
|
|
3693
|
+
try {
|
|
3694
|
+
const moduleUrl = await waitForNativeTabModuleUrl();
|
|
3695
|
+
if (!moduleUrl) throw new Error("Codex app-initial modulepreload was not found");
|
|
3696
|
+
const appModule = await import(moduleUrl);
|
|
3697
|
+
const controller = Object.values(appModule).find((value) => (
|
|
3698
|
+
value
|
|
3699
|
+
&& typeof value === "object"
|
|
3700
|
+
&& typeof value.openTab === "function"
|
|
3701
|
+
&& typeof value.closeTab === "function"
|
|
3702
|
+
&& typeof value.activateTab === "function"
|
|
3703
|
+
&& value.panelId === "right"
|
|
3704
|
+
));
|
|
3705
|
+
if (!controller) throw new Error("Codex right-panel controller is unavailable");
|
|
3706
|
+
if (!controller?.tabById$) throw new Error("Codex native tab capability is incomplete");
|
|
3707
|
+
nativeTabCapability = {
|
|
3708
|
+
controller,
|
|
3709
|
+
moduleAsset: moduleUrl.split("/").pop(),
|
|
3710
|
+
};
|
|
3711
|
+
nativeTabCapabilityError = null;
|
|
3712
|
+
return nativeTabCapability;
|
|
3713
|
+
} catch (error) {
|
|
3714
|
+
nativeTabCapabilityError = error;
|
|
3715
|
+
return null;
|
|
3716
|
+
}
|
|
3717
|
+
})();
|
|
3718
|
+
const capability = await nativeTabCapabilityPromise;
|
|
3719
|
+
if (!capability) nativeTabCapabilityPromise = null;
|
|
3720
|
+
return capability;
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
function nativeReactElement(type, props) {
|
|
3724
|
+
return {
|
|
3725
|
+
$$typeof: Symbol.for("react.transitional.element"),
|
|
3726
|
+
type,
|
|
3727
|
+
key: null,
|
|
3728
|
+
ref: null,
|
|
3729
|
+
props: props ?? {},
|
|
3730
|
+
};
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
function nativeDetailIcon(feature, detail) {
|
|
3734
|
+
const icon = detail.icon ?? feature.icon;
|
|
3735
|
+
if (!icon?.markup) return undefined;
|
|
3736
|
+
return nativeReactElement("svg", {
|
|
3737
|
+
width: 16,
|
|
3738
|
+
height: 16,
|
|
3739
|
+
viewBox: icon.viewBox ?? "0 0 24 24",
|
|
3740
|
+
fill: "none",
|
|
3741
|
+
stroke: "currentColor",
|
|
3742
|
+
strokeWidth: 1.5,
|
|
3743
|
+
strokeLinecap: "round",
|
|
3744
|
+
strokeLinejoin: "round",
|
|
3745
|
+
"aria-hidden": true,
|
|
3746
|
+
className: "icon-xs shrink-0",
|
|
3747
|
+
dangerouslySetInnerHTML: { __html: icon.markup },
|
|
3748
|
+
});
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
function nativeDetailComponent(feature, detail) {
|
|
3752
|
+
const detailKey = `${feature.id}:${detail.id}`;
|
|
3753
|
+
let Component = nativeDetailComponents.get(detailKey);
|
|
3754
|
+
if (Component) return Component;
|
|
3755
|
+
const frameName = surfaceFrameName(feature.id);
|
|
3756
|
+
Component = function CodexPersonalDetailTab() {
|
|
3757
|
+
return nativeReactElement("div", {
|
|
3758
|
+
"data-codex-personal-native-detail": detailKey,
|
|
3759
|
+
style: {
|
|
3760
|
+
height: "100%",
|
|
3761
|
+
minHeight: 0,
|
|
3762
|
+
overflow: "hidden",
|
|
3763
|
+
background: "var(--color-token-main-surface-primary)",
|
|
3764
|
+
},
|
|
3765
|
+
children: nativeReactElement("iframe", {
|
|
3766
|
+
name: frameName,
|
|
3767
|
+
src: "about:blank",
|
|
3768
|
+
title: detail.label,
|
|
3769
|
+
allow: "clipboard-write",
|
|
3770
|
+
style: {
|
|
3771
|
+
display: "block",
|
|
3772
|
+
width: "100%",
|
|
3773
|
+
height: "100%",
|
|
3774
|
+
border: 0,
|
|
3775
|
+
background: "transparent",
|
|
3776
|
+
},
|
|
3777
|
+
}),
|
|
3778
|
+
});
|
|
3779
|
+
};
|
|
3780
|
+
nativeDetailComponents.set(detailKey, Component);
|
|
3781
|
+
return Component;
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
function reloadNativeDetailIfUnready(feature, detail) {
|
|
3785
|
+
const keys = nativeDetailSurfaceKeys.get(`${feature.id}:${detail.id}`);
|
|
3786
|
+
if (!keys) return false;
|
|
3787
|
+
let reloaded = false;
|
|
3788
|
+
for (const key of keys) {
|
|
3789
|
+
if (reloadSurfaceIfUnready(key, detail.surfaceUrl)) reloaded = true;
|
|
3790
|
+
}
|
|
3791
|
+
return reloaded;
|
|
3792
|
+
}
|
|
3793
|
+
|
|
3794
|
+
async function waitForNativeDetailTabElement(detailKey) {
|
|
3795
|
+
const deadline = performance.now() + 2_000;
|
|
3796
|
+
do {
|
|
3797
|
+
const element = document.querySelector(
|
|
3798
|
+
`[data-codex-personal-native-detail="${detailKey}"]`,
|
|
3799
|
+
);
|
|
3800
|
+
const frame = element?.querySelector("iframe");
|
|
3801
|
+
if (element && frame) return { element, frame };
|
|
3802
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
3803
|
+
} while (performance.now() < deadline);
|
|
3804
|
+
return null;
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3807
|
+
async function openNativeDetailTab(feature, detail) {
|
|
3808
|
+
const capability = await loadNativeTabCapability();
|
|
3809
|
+
if (!capability) return null;
|
|
3810
|
+
const scope = await waitForCurrentRightPanelScope();
|
|
3811
|
+
if (!scope) {
|
|
3812
|
+
nativeTabCapabilityError = new Error("The current Codex right-panel scope was not found");
|
|
3813
|
+
return null;
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
const { controller } = capability;
|
|
3817
|
+
const tabId = `${feature.id}-${detail.id}`;
|
|
3818
|
+
const detailKey = `${feature.id}:${detail.id}`;
|
|
3819
|
+
let session = Array.from(nativeOpenTabSessions).find((candidate) => (
|
|
3820
|
+
candidate.scope === scope && candidate.tabId === tabId
|
|
3821
|
+
));
|
|
3822
|
+
let focusedExisting = Boolean(session);
|
|
3823
|
+
if (!focusedExisting && controller.tabById$) {
|
|
3824
|
+
try {
|
|
3825
|
+
focusedExisting = scope.get(controller.tabById$, tabId) != null;
|
|
3826
|
+
} catch {}
|
|
3827
|
+
}
|
|
3828
|
+
const isNewSession = !session;
|
|
3829
|
+
if (!session) {
|
|
3830
|
+
session = { scope, controller, tabId };
|
|
3831
|
+
nativeOpenTabSessions.add(session);
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
try {
|
|
3835
|
+
controller.openTab(scope, nativeDetailComponent(feature, detail), {
|
|
3836
|
+
id: tabId,
|
|
3837
|
+
kind: tabId,
|
|
3838
|
+
title: detail.label,
|
|
3839
|
+
tooltip: detail.label,
|
|
3840
|
+
icon: nativeDetailIcon(feature, detail),
|
|
3841
|
+
isClosable: true,
|
|
3842
|
+
props: {},
|
|
3843
|
+
onClose: () => {
|
|
3844
|
+
nativeOpenTabSessions.delete(session);
|
|
3845
|
+
const closedKeys = nativeDetailSurfaceKeys.get(detailKey);
|
|
3846
|
+
if (closedKeys) {
|
|
3847
|
+
for (const key of closedKeys) removeSurfaceRecord(key);
|
|
3848
|
+
nativeDetailSurfaceKeys.delete(detailKey);
|
|
3849
|
+
}
|
|
3850
|
+
},
|
|
3851
|
+
});
|
|
3852
|
+
} catch (error) {
|
|
3853
|
+
if (isNewSession) nativeOpenTabSessions.delete(session);
|
|
3854
|
+
nativeTabCapabilityError = error;
|
|
3855
|
+
return null;
|
|
3856
|
+
}
|
|
3857
|
+
|
|
3858
|
+
if (!nativeDetailSurfaceKeys.has(detailKey)) {
|
|
3859
|
+
const mounted = await waitForNativeDetailTabElement(detailKey);
|
|
3860
|
+
if (!mounted) {
|
|
3861
|
+
if (isNewSession) nativeOpenTabSessions.delete(session);
|
|
3862
|
+
if (!focusedExisting) {
|
|
3863
|
+
try {
|
|
3864
|
+
controller.closeTab(scope, tabId);
|
|
3865
|
+
} catch {}
|
|
3866
|
+
}
|
|
3867
|
+
nativeTabCapabilityError = new Error("Codex native right-panel Tab did not mount");
|
|
3868
|
+
return null;
|
|
3869
|
+
}
|
|
3870
|
+
const instanceId = crypto.randomUUID?.()
|
|
3871
|
+
?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3872
|
+
const recordKey = surfaceKey(feature.id, "native-detail", `${detail.id}:${instanceId}`);
|
|
3873
|
+
registerSurface(recordKey, feature, "detail", detail.surfaceUrl, mounted.element, mounted.frame, detail.id);
|
|
3874
|
+
let keys = nativeDetailSurfaceKeys.get(detailKey);
|
|
3875
|
+
if (!keys) {
|
|
3876
|
+
keys = new Set();
|
|
3877
|
+
nativeDetailSurfaceKeys.set(detailKey, keys);
|
|
3878
|
+
}
|
|
3879
|
+
keys.add(recordKey);
|
|
3880
|
+
queueTheme();
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
nativeTabCapabilityError = null;
|
|
3884
|
+
if (focusedExisting) reloadNativeDetailIfUnready(feature, detail);
|
|
3885
|
+
return { tabId, focusedExisting, presentation: "native" };
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
function detailTabCapability() {
|
|
3889
|
+
return {
|
|
3890
|
+
presentation: nativeTabCapability ? "native" : "unavailable",
|
|
3891
|
+
moduleAsset: nativeTabCapability?.moduleAsset ?? null,
|
|
3892
|
+
error: nativeTabCapabilityError?.message ?? null,
|
|
3893
|
+
openNativeTabs: nativeOpenTabSessions.size,
|
|
3894
|
+
};
|
|
3895
|
+
}
|
|
3896
|
+
|
|
3897
|
+
function closeNativeDetailTabs() {
|
|
3898
|
+
for (const keys of Array.from(nativeDetailSurfaceKeys.values())) {
|
|
3899
|
+
for (const key of keys) removeSurfaceRecord(key);
|
|
3900
|
+
}
|
|
3901
|
+
for (const session of Array.from(nativeOpenTabSessions)) {
|
|
3902
|
+
try {
|
|
3903
|
+
session.controller.closeTab(session.scope, session.tabId);
|
|
3904
|
+
} catch {}
|
|
3905
|
+
}
|
|
3906
|
+
nativeOpenTabSessions.clear();
|
|
3907
|
+
nativeDetailComponents.clear();
|
|
3908
|
+
nativeDetailSurfaceKeys.clear();
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
async function openDetailTab(featureId, detailId) {
|
|
3912
|
+
const feature = featureById.get(featureId);
|
|
3913
|
+
const detail = feature?.detailTabs?.find((candidate) => candidate.id === detailId);
|
|
3914
|
+
if (!feature || !detail) throw new Error("The requested detail Tab is not registered");
|
|
3915
|
+
const result = await openNativeDetailTab(feature, detail);
|
|
3916
|
+
if (!result) {
|
|
3917
|
+
const error = nativeTabCapabilityError
|
|
3918
|
+
?? new Error("Codex native right-panel tabs are unavailable");
|
|
3919
|
+
error.code = "NATIVE_TAB_UNAVAILABLE";
|
|
3920
|
+
throw error;
|
|
3921
|
+
}
|
|
3922
|
+
return result;
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
function openThread(threadId) {
|
|
3926
|
+
const nativeThread = Array.from(
|
|
3927
|
+
document.querySelectorAll("[data-app-action-sidebar-thread-id]"),
|
|
3928
|
+
).find((element) => {
|
|
3929
|
+
const nativeId = element.getAttribute("data-app-action-sidebar-thread-id");
|
|
3930
|
+
return nativeId === threadId || nativeId?.endsWith(`:${threadId}`);
|
|
3931
|
+
});
|
|
3932
|
+
|
|
3933
|
+
hidePageSurfaces();
|
|
3934
|
+
hidePinnedSurfaces();
|
|
3935
|
+
if (nativeThread) {
|
|
3936
|
+
nativeThread.click();
|
|
3937
|
+
nativeThread.focus({ preventScroll: false });
|
|
3938
|
+
return;
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
const link = document.createElement("a");
|
|
3942
|
+
link.href = `codex://threads/${threadId}`;
|
|
3943
|
+
link.target = "_blank";
|
|
3944
|
+
link.rel = "noreferrer";
|
|
3945
|
+
link.hidden = true;
|
|
3946
|
+
document.body.append(link);
|
|
3947
|
+
link.click();
|
|
3948
|
+
link.remove();
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
// ========================================================================
|
|
3952
|
+
// Sidebar/Toolbar 入口与 ensure 调度 — 入口克隆注入、ensure 批处理调度
|
|
3953
|
+
// ========================================================================
|
|
3954
|
+
|
|
3955
|
+
function createSidebarEntry(feature, reference) {
|
|
3956
|
+
const entry = reference.cloneNode(true);
|
|
3957
|
+
entry.setAttribute(entryMarker, feature.id);
|
|
3958
|
+
entry.removeAttribute("href");
|
|
3959
|
+
entry.removeAttribute("aria-current");
|
|
3960
|
+
entry.querySelectorAll("[data-state]").forEach((node) => node.removeAttribute("data-state"));
|
|
3961
|
+
const originalIcon = entry.querySelector("svg");
|
|
3962
|
+
const featureIcon = createFeatureIcon(feature, originalIcon?.getAttribute("class"));
|
|
3963
|
+
if (originalIcon && featureIcon) originalIcon.replaceWith(featureIcon);
|
|
3964
|
+
|
|
3965
|
+
const referenceLabels = nativeLabels.plugins;
|
|
3966
|
+
const textLeaf = Array.from(entry.querySelectorAll("*")).find((node) => (
|
|
3967
|
+
node.children.length === 0
|
|
3968
|
+
&& referenceLabels.includes(node.textContent?.trim().toLowerCase())
|
|
3969
|
+
));
|
|
3970
|
+
if (textLeaf) textLeaf.textContent = feature.label;
|
|
3971
|
+
else entry.setAttribute("aria-label", feature.label);
|
|
3972
|
+
|
|
3973
|
+
entry.addEventListener("click", (event) => {
|
|
3974
|
+
event.preventDefault();
|
|
3975
|
+
event.stopPropagation();
|
|
3976
|
+
showPageSurface(feature.id);
|
|
3977
|
+
});
|
|
3978
|
+
return entry;
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
function createToolbarEntry(feature, nativeButton) {
|
|
3982
|
+
const nativeRoot = toolbarControlRoot(nativeButton);
|
|
3983
|
+
const root = nativeRoot.cloneNode(true);
|
|
3984
|
+
root.setAttribute("data-codex-personal-toolbar-entry", feature.id);
|
|
3985
|
+
root.classList.remove("ms-auto");
|
|
3986
|
+
root.querySelectorAll("[id]").forEach((node) => node.removeAttribute("id"));
|
|
3987
|
+
root.querySelectorAll("[aria-describedby]").forEach((node) => node.removeAttribute("aria-describedby"));
|
|
3988
|
+
const button = root.matches("button") ? root : root.querySelector("button");
|
|
3989
|
+
if (!button) return null;
|
|
3990
|
+
const label = summaryButtonLabel(feature);
|
|
3991
|
+
button.setAttribute(entryMarker, feature.id);
|
|
3992
|
+
button.setAttribute("aria-label", label);
|
|
3993
|
+
button.setAttribute("title", label);
|
|
3994
|
+
button.setAttribute("aria-pressed", "false");
|
|
3995
|
+
button.setAttribute("aria-expanded", "false");
|
|
3996
|
+
button.setAttribute("data-state", "closed");
|
|
3997
|
+
button.removeAttribute("disabled");
|
|
3998
|
+
button.classList.remove(
|
|
3999
|
+
"text-token-foreground",
|
|
4000
|
+
"bg-token-foreground/5",
|
|
4001
|
+
"bg-token-foreground/10",
|
|
4002
|
+
"enabled:hover:bg-token-foreground/10",
|
|
4003
|
+
"data-[state=open]:bg-token-foreground/10",
|
|
4004
|
+
"bg-token-list-hover-background",
|
|
4005
|
+
);
|
|
4006
|
+
button.classList.add(
|
|
4007
|
+
"text-token-text-tertiary",
|
|
4008
|
+
"enabled:hover:bg-token-list-hover-background",
|
|
4009
|
+
"data-[state=open]:bg-token-list-hover-background",
|
|
4010
|
+
);
|
|
4011
|
+
const originalIcon = button.querySelector("svg");
|
|
4012
|
+
const featureIcon = createFeatureIcon(feature, originalIcon?.getAttribute("class"), 16);
|
|
4013
|
+
if (originalIcon && featureIcon) originalIcon.replaceWith(featureIcon);
|
|
4014
|
+
button.addEventListener("click", (event) => {
|
|
4015
|
+
event.preventDefault();
|
|
4016
|
+
event.stopPropagation();
|
|
4017
|
+
togglePinnedSurface(feature.id);
|
|
4018
|
+
});
|
|
4019
|
+
return button;
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
function ensureSidebarEntries() {
|
|
4023
|
+
if (!sidebarFeatures.length) return;
|
|
4024
|
+
const reference = findReferenceEntry();
|
|
4025
|
+
if (!reference) return;
|
|
4026
|
+
for (const feature of sidebarFeatures) {
|
|
4027
|
+
if (!findEntry(feature.id)) reference.after(createSidebarEntry(feature, reference));
|
|
4028
|
+
}
|
|
4029
|
+
const cursors = new Map();
|
|
4030
|
+
for (const feature of sidebarFeatures) {
|
|
4031
|
+
const anchorKey = feature.placement?.after ?? "sites";
|
|
4032
|
+
const anchor = cursors.get(anchorKey) ?? findNativeEntry(anchorKey) ?? reference;
|
|
4033
|
+
const entry = findEntry(feature.id);
|
|
4034
|
+
if (anchor && entry && anchor.nextElementSibling !== entry) anchor.after(entry);
|
|
4035
|
+
if (entry) cursors.set(anchorKey, entry);
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
function ensureToolbarEntries() {
|
|
4040
|
+
if (!toolbarFeatures.length) {
|
|
4041
|
+
stopToolbarReadiness();
|
|
4042
|
+
return;
|
|
4043
|
+
}
|
|
4044
|
+
const temporaryChatButton = nativeTemporaryChatButton();
|
|
4045
|
+
const summaryButton = nativeSummaryButton();
|
|
4046
|
+
const bottomPanelButton = nativeBottomPanelButton();
|
|
4047
|
+
const sidePanelButton = nativeSidePanelButton();
|
|
4048
|
+
const anchorButton = temporaryChatButton ?? summaryButton ?? bottomPanelButton ?? sidePanelButton;
|
|
4049
|
+
if (!anchorButton) {
|
|
4050
|
+
const candidate = structuralToolbarAnchor();
|
|
4051
|
+
if (candidate) ensureToolbarReadiness(candidate);
|
|
4052
|
+
return;
|
|
4053
|
+
}
|
|
4054
|
+
const anchorRoot = toolbarControlRoot(anchorButton);
|
|
4055
|
+
// The bottom/side-panel toggles sit in narrow fixed containers that
|
|
4056
|
+
// travel with the native panels. Entries anchored to them must live in
|
|
4057
|
+
// the fallback toolbar group, otherwise the Notes entry would be
|
|
4058
|
+
// dragged into the panel or push the native toggle out of the header.
|
|
4059
|
+
// The Notes entry is only grouped with the native summary button when
|
|
4060
|
+
// that button is the anchor.
|
|
4061
|
+
const useFallbackAnchor = anchorButton === sidePanelButton || anchorButton === bottomPanelButton;
|
|
4062
|
+
const summaryGroup = useFallbackAnchor ? null : nativeSummaryToolbarGroup(anchorButton);
|
|
4063
|
+
let targetGroup = summaryGroup ?? ensureFallbackSummaryToolbarGroup(anchorButton);
|
|
4064
|
+
if (!targetGroup) {
|
|
4065
|
+
ensureToolbarReadiness(anchorButton);
|
|
4066
|
+
return;
|
|
4067
|
+
}
|
|
4068
|
+
let cursor = useFallbackAnchor ? null : anchorRoot;
|
|
4069
|
+
for (const feature of toolbarFeatures) {
|
|
4070
|
+
const entries = Array.from(document.querySelectorAll(`[${entryMarker}="${feature.id}"]`));
|
|
4071
|
+
for (const duplicate of entries.slice(1)) {
|
|
4072
|
+
(duplicate.closest("[data-codex-personal-toolbar-entry]") ?? duplicate).remove();
|
|
4073
|
+
}
|
|
4074
|
+
const entry = entries[0]?.isConnected
|
|
4075
|
+
? entries[0]
|
|
4076
|
+
: createToolbarEntry(feature, anchorButton);
|
|
4077
|
+
const entryRoot = entry?.closest("[data-codex-personal-toolbar-entry]") ?? entry;
|
|
4078
|
+
if (!entryRoot) continue;
|
|
4079
|
+
if (cursor) {
|
|
4080
|
+
if (cursor.nextElementSibling !== entryRoot) cursor.after(entryRoot);
|
|
4081
|
+
} else if (targetGroup.lastElementChild !== entryRoot) {
|
|
4082
|
+
targetGroup.append(entryRoot);
|
|
4083
|
+
}
|
|
4084
|
+
// Narrow native anchor containers (e.g. the Work-mode side-panel
|
|
4085
|
+
// button) can push the entry outside the header so it becomes
|
|
4086
|
+
// unreachable. Move it into the fallback toolbar group instead.
|
|
4087
|
+
if (summaryGroup && !toolbarEntryFitsHeader(entryRoot)) {
|
|
4088
|
+
const fallback = ensureFallbackSummaryToolbarGroup(anchorButton);
|
|
4089
|
+
if (fallback && fallback !== summaryGroup) {
|
|
4090
|
+
fallback.append(entryRoot);
|
|
4091
|
+
targetGroup = fallback;
|
|
4092
|
+
cursor = null;
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
cursor = entryRoot;
|
|
4096
|
+
}
|
|
4097
|
+
if (summaryGroup) {
|
|
4098
|
+
document.querySelectorAll("[data-codex-personal-summary-toolbar-group]").forEach((group) => {
|
|
4099
|
+
const used = [...group.querySelectorAll(`[${entryMarker}]`)].some((entry) => entry.isConnected);
|
|
4100
|
+
if (!used) group.remove();
|
|
4101
|
+
});
|
|
4102
|
+
}
|
|
4103
|
+
if (toolbarFeatures.every((feature) => findEntry(feature.id)?.isConnected)) {
|
|
4104
|
+
stopToolbarReadiness();
|
|
4105
|
+
} else {
|
|
4106
|
+
ensureToolbarReadiness(anchorButton);
|
|
4107
|
+
}
|
|
4108
|
+
}
|
|
4109
|
+
|
|
4110
|
+
function ensureEntries() {
|
|
4111
|
+
ensureQueued = false;
|
|
4112
|
+
ensureFrame = null;
|
|
4113
|
+
ensureTimer = null;
|
|
4114
|
+
ensureSidebarEntries();
|
|
4115
|
+
ensureToolbarEntries();
|
|
4116
|
+
ensureModelSelector();
|
|
4117
|
+
checkThreadChange();
|
|
4118
|
+
updateSidebarSelectedState();
|
|
4119
|
+
updatePageSurfacePositions();
|
|
4120
|
+
ensureMainContentObserver();
|
|
4121
|
+
updateResponsiveSummaryPresentation();
|
|
4122
|
+
const nativeToggle = nativeSummaryButton();
|
|
4123
|
+
if (
|
|
4124
|
+
activePinnedFeatureId
|
|
4125
|
+
&& (nativeToggle?.getAttribute("aria-expanded") === "true"
|
|
4126
|
+
|| nativeToggle?.getAttribute("aria-pressed") === "true")
|
|
4127
|
+
) {
|
|
4128
|
+
hidePinnedSurfaces();
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
|
|
4132
|
+
function flushEnsure() {
|
|
4133
|
+
if (!ensureQueued) return;
|
|
4134
|
+
if (ensureFrame != null) cancelAnimationFrame(ensureFrame);
|
|
4135
|
+
if (ensureTimer != null) clearTimeout(ensureTimer);
|
|
4136
|
+
ensureFrame = null;
|
|
4137
|
+
ensureTimer = null;
|
|
4138
|
+
ensureEntries();
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
function queueEnsure() {
|
|
4142
|
+
if (ensureQueued) return;
|
|
4143
|
+
ensureQueued = true;
|
|
4144
|
+
ensureFrame = requestAnimationFrame(flushEnsure);
|
|
4145
|
+
ensureTimer = setTimeout(flushEnsure, 50);
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
// ========================================================================
|
|
4149
|
+
// 消息与 host action 路由 — postMessage 路由、host action 分发与结果回送
|
|
4150
|
+
// ========================================================================
|
|
4151
|
+
|
|
4152
|
+
function findMessageSurface(event) {
|
|
4153
|
+
for (const record of surfaceRecords.values()) {
|
|
4154
|
+
if (event.origin === record.origin && event.source === record.frame.contentWindow) return record;
|
|
4155
|
+
}
|
|
4156
|
+
return null;
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4159
|
+
function respondToSurface(record, requestId, { ok, result = null, error = null }) {
|
|
4160
|
+
record.frame.contentWindow?.postMessage({
|
|
4161
|
+
type: "codex-personal:host-result",
|
|
4162
|
+
feature: record.featureId,
|
|
4163
|
+
requestId,
|
|
4164
|
+
ok,
|
|
4165
|
+
result,
|
|
4166
|
+
error,
|
|
4167
|
+
}, record.origin);
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
function hostActionFailure(error) {
|
|
4171
|
+
return {
|
|
4172
|
+
code: typeof error?.code === "string" ? error.code : "HOST_ACTION_FAILED",
|
|
4173
|
+
message: error?.message ?? "Host action failed",
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
|
|
4177
|
+
async function handleSurfaceHostAction(record, message) {
|
|
4178
|
+
const feature = featureById.get(record.featureId);
|
|
4179
|
+
const { requestId, action, payload = {} } = message;
|
|
4180
|
+
if (typeof requestId !== "string" || !feature?.hostActions.has(action)) {
|
|
4181
|
+
respondToSurface(record, requestId, {
|
|
4182
|
+
ok: false,
|
|
4183
|
+
error: { code: "ACTION_NOT_ALLOWED", message: "This Host action is not registered" },
|
|
4184
|
+
});
|
|
4185
|
+
return;
|
|
4186
|
+
}
|
|
4187
|
+
try {
|
|
4188
|
+
if (action === "insert-text") {
|
|
4189
|
+
respondToSurface(record, requestId, { ok: true, result: insertComposerText(payload.text) });
|
|
4190
|
+
return;
|
|
4191
|
+
}
|
|
4192
|
+
if (action === "open-detail-tab") {
|
|
4193
|
+
respondToSurface(record, requestId, {
|
|
4194
|
+
ok: true,
|
|
4195
|
+
result: await openDetailTab(feature.id, payload.surface),
|
|
4196
|
+
});
|
|
4197
|
+
return;
|
|
4198
|
+
}
|
|
4199
|
+
if (action === "open-editor-modal") {
|
|
4200
|
+
respondToSurface(record, requestId, {
|
|
4201
|
+
ok: true,
|
|
4202
|
+
result: openEditorModal(record, payload),
|
|
4203
|
+
});
|
|
4204
|
+
return;
|
|
4205
|
+
}
|
|
4206
|
+
if (action === "open-sidebar") {
|
|
4207
|
+
const trigger = nativeSidebarTrigger();
|
|
4208
|
+
if (!trigger) throw new Error("The native sidebar control was not found");
|
|
4209
|
+
const state = sidebarState();
|
|
4210
|
+
if (!state.open) trigger.click();
|
|
4211
|
+
queueMicrotask(() => postSidebarState(record, { ...state, open: true }));
|
|
4212
|
+
respondToSurface(record, requestId, { ok: true, result: { opened: true } });
|
|
4213
|
+
return;
|
|
4214
|
+
}
|
|
4215
|
+
if (action === "resolve-file-paths") {
|
|
4216
|
+
const getPathForFile = window.electronBridge?.getPathForFile;
|
|
4217
|
+
if (typeof getPathForFile !== "function") {
|
|
4218
|
+
throw new Error("Codex did not expose its File path bridge");
|
|
4219
|
+
}
|
|
4220
|
+
const files = Array.isArray(payload.files) ? payload.files : [];
|
|
4221
|
+
if (!files.length || files.some((file) => !(file instanceof File))) {
|
|
4222
|
+
throw new Error("Real dropped File objects are required");
|
|
4223
|
+
}
|
|
4224
|
+
const paths = files.map((file) => getPathForFile(file)).filter((value) => (
|
|
4225
|
+
typeof value === "string" && value.startsWith("/")
|
|
4226
|
+
));
|
|
4227
|
+
if (!paths.length) throw new Error("Codex could not resolve the dropped file paths");
|
|
4228
|
+
respondToSurface(record, requestId, { ok: true, result: { paths } });
|
|
4229
|
+
return;
|
|
4230
|
+
}
|
|
4231
|
+
if (["attach-file", "create-image-edit-thread"].includes(action)) {
|
|
4232
|
+
if (typeof globalThis[config.bindingName] !== "function") {
|
|
4233
|
+
throw new Error("The native file bridge is unavailable");
|
|
4234
|
+
}
|
|
4235
|
+
pendingHostActions.set(requestId, { recordKey: record.key });
|
|
4236
|
+
globalThis[config.bindingName](JSON.stringify({
|
|
4237
|
+
token: config.bindingToken,
|
|
4238
|
+
featureId: feature.id,
|
|
4239
|
+
requestId,
|
|
4240
|
+
action,
|
|
4241
|
+
payload,
|
|
4242
|
+
}));
|
|
4243
|
+
return;
|
|
4244
|
+
}
|
|
4245
|
+
throw new Error("Unsupported Host action");
|
|
4246
|
+
} catch (error) {
|
|
4247
|
+
respondToSurface(record, requestId, {
|
|
4248
|
+
ok: false,
|
|
4249
|
+
error: hostActionFailure(error),
|
|
4250
|
+
});
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
|
|
4254
|
+
function resolveHostAction(requestId, response) {
|
|
4255
|
+
const pending = pendingHostActions.get(requestId);
|
|
4256
|
+
if (!pending) return false;
|
|
4257
|
+
pendingHostActions.delete(requestId);
|
|
4258
|
+
const record = surfaceRecords.get(pending.recordKey);
|
|
4259
|
+
if (!record) return false;
|
|
4260
|
+
if (pending.kind === "surface-load") {
|
|
4261
|
+
record.loading = false;
|
|
4262
|
+
record.loadError = response.ok ? null : response.error;
|
|
4263
|
+
return response.ok;
|
|
4264
|
+
}
|
|
4265
|
+
respondToSurface(record, requestId, response);
|
|
4266
|
+
return true;
|
|
4267
|
+
}
|
|
4268
|
+
|
|
4269
|
+
document.addEventListener("selectionchange", rememberComposerSelection, { signal: lifetime.signal });
|
|
4270
|
+
document.addEventListener(
|
|
4271
|
+
"click",
|
|
4272
|
+
(event) => {
|
|
4273
|
+
if (event.target.closest?.('[data-app-shell-sidebar-trigger="true"]')) {
|
|
4274
|
+
syncAfterNativeSidebarToggle();
|
|
4275
|
+
}
|
|
4276
|
+
if (
|
|
4277
|
+
modelSelectorControllerMenu
|
|
4278
|
+
&& !modelSelectorControllerMenu.menu.contains(event.target)
|
|
4279
|
+
&& !modelSelectorControllerMenu.button.contains(event.target)
|
|
4280
|
+
) closeControllerModelMenu();
|
|
4281
|
+
if (isNativeSummaryButton(event.target) && activePinnedFeatureId) hidePinnedSurfaces();
|
|
4282
|
+
if (activePinnedFeatureId && summaryDisplayMode() === "overlay") {
|
|
4283
|
+
const state = summaryState(activePinnedFeatureId);
|
|
4284
|
+
const surface = pinnedSurfaces.get(activePinnedFeatureId);
|
|
4285
|
+
const ownEntry = event.target.closest?.(`[${entryMarker}="${activePinnedFeatureId}"]`);
|
|
4286
|
+
const rightWorkspaceInteraction = event.target.closest?.(
|
|
4287
|
+
'[data-app-shell-tab-strip-controller="right"],'
|
|
4288
|
+
+ '[data-app-shell-tab-panel-controller="right"]',
|
|
4289
|
+
);
|
|
4290
|
+
const promptPreviewInteraction = event.target.closest?.(`[${promptPreviewMarker}]`);
|
|
4291
|
+
if (
|
|
4292
|
+
state.isPopoverOpen
|
|
4293
|
+
&& !surface?.contains(event.target)
|
|
4294
|
+
&& !ownEntry
|
|
4295
|
+
&& !rightWorkspaceInteraction
|
|
4296
|
+
&& !promptPreviewInteraction
|
|
4297
|
+
) {
|
|
4298
|
+
const featureId = activePinnedFeatureId;
|
|
4299
|
+
state.isPopoverOpen = false;
|
|
4300
|
+
if (!state.isPinned) activePinnedFeatureId = null;
|
|
4301
|
+
updatePinnedPosition(featureId);
|
|
4302
|
+
updateToolbarState();
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
if (!activePageFeatureId) return;
|
|
4306
|
+
if (event.target.closest?.(`[${entryMarker}]`)) return;
|
|
4307
|
+
if (event.target.closest?.("[data-app-action-sidebar-scroll]")) hidePageSurfaces();
|
|
4308
|
+
},
|
|
4309
|
+
{ capture: true, signal: lifetime.signal },
|
|
4310
|
+
);
|
|
4311
|
+
|
|
4312
|
+
document.addEventListener("keydown", (event) => {
|
|
4313
|
+
if (event.key !== "Escape") return;
|
|
4314
|
+
if (closePromptPreview({ immediate: true })) {
|
|
4315
|
+
event.preventDefault();
|
|
4316
|
+
event.stopPropagation();
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
if (closeModalSurface()) {
|
|
4320
|
+
event.preventDefault();
|
|
4321
|
+
event.stopPropagation();
|
|
4322
|
+
return;
|
|
4323
|
+
}
|
|
4324
|
+
if (!activePinnedFeatureId || summaryDisplayMode() !== "overlay") return;
|
|
4325
|
+
const state = summaryState(activePinnedFeatureId);
|
|
4326
|
+
if (!state.isPopoverOpen) return;
|
|
4327
|
+
const featureId = activePinnedFeatureId;
|
|
4328
|
+
state.isPopoverOpen = false;
|
|
4329
|
+
if (!state.isPinned) activePinnedFeatureId = null;
|
|
4330
|
+
updatePinnedPosition(featureId);
|
|
4331
|
+
updateToolbarState();
|
|
4332
|
+
}, { capture: true, signal: lifetime.signal });
|
|
4333
|
+
|
|
4334
|
+
window.addEventListener("message", (event) => {
|
|
4335
|
+
const record = findMessageSurface(event);
|
|
4336
|
+
if (!record || !event.data || typeof event.data !== "object") return;
|
|
4337
|
+
if (event.data.feature && event.data.feature !== record.featureId) return;
|
|
4338
|
+
if (event.data.type === "codex-personal:ready") {
|
|
4339
|
+
record.ready = true;
|
|
4340
|
+
queueTheme();
|
|
4341
|
+
postSidebarState(record);
|
|
4342
|
+
if (record.kind === "modal") {
|
|
4343
|
+
record.frame.focus();
|
|
4344
|
+
record.frame.contentWindow?.postMessage({
|
|
4345
|
+
type: "codex-personal:focus",
|
|
4346
|
+
feature: record.featureId,
|
|
4347
|
+
}, record.origin);
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
if (event.data.type === "codex-personal:surface-metrics" && record.kind === "pinned") {
|
|
4351
|
+
const preferredHeight = Number(event.data.height);
|
|
4352
|
+
if (Number.isFinite(preferredHeight) && preferredHeight > 0) {
|
|
4353
|
+
record.preferredHeight = Math.min(2_000, Math.max(80, Math.ceil(preferredHeight)));
|
|
4354
|
+
updatePinnedPosition(record.featureId);
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
if (event.data.type === "codex-personal:prompt-preview" && record.kind === "pinned") {
|
|
4358
|
+
if (event.data.phase === "show") showPromptPreview(record, event.data);
|
|
4359
|
+
if (event.data.phase === "hide") requestPromptPreviewClose(record);
|
|
4360
|
+
}
|
|
4361
|
+
if (event.data.type === "codex-personal:close") {
|
|
4362
|
+
if (record.kind === "pinned") hidePinnedSurfaces();
|
|
4363
|
+
if (record.kind === "page") hidePageSurfaces();
|
|
4364
|
+
if (record.kind === "modal") closeModalSurface();
|
|
4365
|
+
}
|
|
4366
|
+
if (event.data.type === "codex-personal:open-thread" && event.data.threadId) {
|
|
4367
|
+
openThread(event.data.threadId);
|
|
4368
|
+
}
|
|
4369
|
+
if (event.data.type === "codex-personal:host-action") {
|
|
4370
|
+
handleSurfaceHostAction(record, event.data);
|
|
4371
|
+
}
|
|
4372
|
+
}, { signal: lifetime.signal });
|
|
4373
|
+
|
|
4374
|
+
window.addEventListener("resize", () => {
|
|
4375
|
+
closePromptPreview({ immediate: true });
|
|
4376
|
+
updatePageSurfacePositions();
|
|
4377
|
+
updateResponsiveSummaryPresentation();
|
|
4378
|
+
}, { signal: lifetime.signal });
|
|
4379
|
+
const ensureObserver = new MutationObserver((mutations) => {
|
|
4380
|
+
queueEnsure();
|
|
4381
|
+
const sidebarTriggerChanged = mutations.some((mutation) => {
|
|
4382
|
+
if (
|
|
4383
|
+
mutation.type === "attributes"
|
|
4384
|
+
&& mutation.target.matches?.('[data-app-shell-sidebar-trigger="true"]')
|
|
4385
|
+
) return true;
|
|
4386
|
+
return mutation.type === "childList"
|
|
4387
|
+
&& [...mutation.addedNodes, ...mutation.removedNodes].some((node) => (
|
|
4388
|
+
node instanceof Element
|
|
4389
|
+
&& (
|
|
4390
|
+
node.matches?.('[data-app-shell-sidebar-trigger="true"]')
|
|
4391
|
+
|| node.querySelector?.('[data-app-shell-sidebar-trigger="true"]')
|
|
4392
|
+
)
|
|
4393
|
+
));
|
|
4394
|
+
});
|
|
4395
|
+
if (sidebarTriggerChanged) updatePageSurfacePositions();
|
|
4396
|
+
});
|
|
4397
|
+
ensureObserver.observe(document.documentElement, {
|
|
4398
|
+
childList: true,
|
|
4399
|
+
subtree: true,
|
|
4400
|
+
attributes: true,
|
|
4401
|
+
attributeFilter: [
|
|
4402
|
+
"aria-expanded",
|
|
4403
|
+
"aria-pressed",
|
|
4404
|
+
"data-state",
|
|
4405
|
+
"data-app-action-sidebar-thread-selected",
|
|
4406
|
+
"data-app-action-sidebar-thread-id",
|
|
4407
|
+
],
|
|
4408
|
+
});
|
|
4409
|
+
domObservers.push(ensureObserver);
|
|
4410
|
+
const themeObserver = new MutationObserver(queueTheme);
|
|
4411
|
+
themeObserver.observe(document.documentElement, {
|
|
4412
|
+
attributes: true,
|
|
4413
|
+
attributeFilter: ["class", "style"],
|
|
4414
|
+
});
|
|
4415
|
+
domObservers.push(themeObserver);
|
|
4416
|
+
|
|
4417
|
+
// ========================================================================
|
|
4418
|
+
// 运行时 API 与清理 — 对外 runtimeApi、dispose 与全局事件观察器
|
|
4419
|
+
// ========================================================================
|
|
4420
|
+
|
|
4421
|
+
const runtimeApi = {
|
|
4422
|
+
version: config.version,
|
|
4423
|
+
sessionId: config.sessionId,
|
|
4424
|
+
ensure: queueEnsure,
|
|
4425
|
+
show: showPageSurface,
|
|
4426
|
+
hide: () => {
|
|
4427
|
+
hidePageSurfaces();
|
|
4428
|
+
hidePinnedSurfaces();
|
|
4429
|
+
closeModalSurface();
|
|
4430
|
+
},
|
|
4431
|
+
showPinned: showPinnedSurface,
|
|
4432
|
+
hidePinned: hidePinnedSurfaces,
|
|
4433
|
+
openDetailTab,
|
|
4434
|
+
detailTabCapability,
|
|
4435
|
+
insertText: insertComposerText,
|
|
4436
|
+
resolveHostAction,
|
|
4437
|
+
dispose: () => {
|
|
4438
|
+
lifetime.abort();
|
|
4439
|
+
for (const cleanup of pageScriptCleanups.splice(0)) {
|
|
4440
|
+
try { cleanup(); } catch (error) {
|
|
4441
|
+
console.warn("[codex-personal] page-script cleanup failed", error);
|
|
4442
|
+
}
|
|
4443
|
+
}
|
|
4444
|
+
if (ensureFrame != null) cancelAnimationFrame(ensureFrame);
|
|
4445
|
+
if (ensureTimer != null) clearTimeout(ensureTimer);
|
|
4446
|
+
for (const observer of domObservers) observer.disconnect();
|
|
4447
|
+
stopToolbarReadiness();
|
|
4448
|
+
mainContentObserver?.observer.disconnect();
|
|
4449
|
+
restoreThreadContentShift();
|
|
4450
|
+
for (const entry of document.querySelectorAll(`[${entryMarker}]`)) entry.remove();
|
|
4451
|
+
for (const root of document.querySelectorAll("[data-codex-personal-toolbar-entry]")) root.remove();
|
|
4452
|
+
for (const group of document.querySelectorAll("[data-codex-personal-summary-toolbar-group]")) group.remove();
|
|
4453
|
+
for (const surface of pageSurfaces.values()) surface.remove();
|
|
4454
|
+
for (const surface of pinnedSurfaces.values()) surface.remove();
|
|
4455
|
+
closePromptPreview({ immediate: true });
|
|
4456
|
+
closeModalSurface();
|
|
4457
|
+
closeNativeDetailTabs();
|
|
4458
|
+
restoreModelSelectorEnhancements();
|
|
4459
|
+
surfaceRecords.clear();
|
|
4460
|
+
if (window.__codexPersonalRuntime === runtimeApi) delete window.__codexPersonalRuntime;
|
|
4461
|
+
},
|
|
4462
|
+
};
|
|
4463
|
+
window.__codexPersonalRuntime = runtimeApi;
|
|
4464
|
+
currentThread = threadIdentity();
|
|
4465
|
+
currentSummaryDisplayMode = summaryDisplayMode();
|
|
4466
|
+
queueEnsure();
|
|
4467
|
+
}
|
|
4468
|
+
|
|
4469
|
+
const runtimeFeatures = features.map((feature) => ({
|
|
4470
|
+
id: feature.id,
|
|
4471
|
+
label: feature.label,
|
|
4472
|
+
labels: feature.labels ?? null,
|
|
4473
|
+
surfaceUrl: feature.surfaceUrl,
|
|
4474
|
+
icon: feature.icon,
|
|
4475
|
+
placement: feature.placement,
|
|
4476
|
+
entry: feature.entry ?? { kind: "sidebar" },
|
|
4477
|
+
pinnedSummary: feature.pinnedSummary ?? null,
|
|
4478
|
+
detailTabs: feature.detailTabs ?? [],
|
|
4479
|
+
hostActions: feature.hostActions ?? [],
|
|
4480
|
+
modelSelector: feature.modelSelector ?? null,
|
|
4481
|
+
pageScript: feature.pageScript ? {
|
|
4482
|
+
...feature.pageScript,
|
|
4483
|
+
config: {
|
|
4484
|
+
serviceOrigin: pageScriptServiceOrigin(feature),
|
|
4485
|
+
bindingName,
|
|
4486
|
+
bindingToken,
|
|
4487
|
+
featureId: feature.id,
|
|
4488
|
+
},
|
|
4489
|
+
} : null,
|
|
4490
|
+
}));
|
|
4491
|
+
return `(${install.toString()})(${JSON.stringify({
|
|
4492
|
+
version: RUNTIME_VERSION,
|
|
4493
|
+
sessionId: runtimeSessionId,
|
|
4494
|
+
bindingName,
|
|
4495
|
+
bindingToken,
|
|
4496
|
+
summaryLayout: RESPONSIVE_SUMMARY_LAYOUT,
|
|
4497
|
+
rightPanelRouteKinds: RIGHT_PANEL_ROUTE_KINDS,
|
|
4498
|
+
themeTokens: CODEX_THEME_TOKENS,
|
|
4499
|
+
features: runtimeFeatures,
|
|
4500
|
+
})})`;
|
|
4501
|
+
}
|
|
4502
|
+
|
|
4503
|
+
export function createRuntimeReadyContract({
|
|
4504
|
+
pid,
|
|
4505
|
+
features = [],
|
|
4506
|
+
plugins = [],
|
|
4507
|
+
failures = [],
|
|
4508
|
+
renderer = {},
|
|
4509
|
+
runtimeSessionId,
|
|
4510
|
+
}) {
|
|
4511
|
+
const discovered = Boolean(renderer.discovered);
|
|
4512
|
+
const discoveredTargets = Math.max(0, Number(renderer.discoveredTargets) || 0);
|
|
4513
|
+
const injectedTargets = Math.max(0, Number(renderer.injectedTargets) || 0);
|
|
4514
|
+
const targetFailures = Array.isArray(renderer.targetFailures) ? renderer.targetFailures : [];
|
|
4515
|
+
const active = Boolean(renderer.active) && discovered && injectedTargets > 0;
|
|
4516
|
+
return {
|
|
4517
|
+
pid,
|
|
4518
|
+
features: features.map((feature) => typeof feature === "string" ? feature : feature.id),
|
|
4519
|
+
failures,
|
|
4520
|
+
runtimeSessionId,
|
|
4521
|
+
plugins: plugins.map((plugin) => ({
|
|
4522
|
+
featureId: plugin.featureId,
|
|
4523
|
+
state: active && (plugin.state === "configured" || plugin.state === "ready")
|
|
4524
|
+
? "injected"
|
|
4525
|
+
: plugin.state,
|
|
4526
|
+
owned: Boolean(plugin.owned),
|
|
4527
|
+
health: plugin.health ?? null,
|
|
4528
|
+
failure: plugin.failure ?? null,
|
|
4529
|
+
})),
|
|
4530
|
+
renderer: {
|
|
4531
|
+
discovered,
|
|
4532
|
+
discoveredTargets,
|
|
4533
|
+
injectedTargets,
|
|
4534
|
+
active,
|
|
4535
|
+
healthy: active && targetFailures.length === 0 && injectedTargets === discoveredTargets,
|
|
4536
|
+
targetFailures,
|
|
4537
|
+
runtimeSessionId: renderer.runtimeSessionId ?? runtimeSessionId,
|
|
4538
|
+
},
|
|
4539
|
+
};
|
|
4540
|
+
}
|
|
4541
|
+
|
|
4542
|
+
export function createDocumentBootstrapSource(source) {
|
|
4543
|
+
return `window[${JSON.stringify(CSP_BOOTSTRAP_KEY)}] = ${CSP_BOOTSTRAP_VERSION};\n${source}`;
|
|
4544
|
+
}
|
|
4545
|
+
|