minecodex 0.2.2 → 0.2.3
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/README.md +2 -0
- package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +2 -1
- package/package.json +1 -1
- package/packages/cli/src/commands.mjs +117 -73
- package/packages/cli/src/{install-progress.mjs → progress.mjs} +2 -2
- package/packages/runtime-host/src/codex-runtime.mjs +156 -15
- package/packages/runtime-host/src/feature-registry.mjs +6 -0
- package/packages/runtime-host/src/main.mjs +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,8 @@ mcx install
|
|
|
26
26
|
|
|
27
27
|
运行 `mcx install` 不会强制关闭或启动 Codex,也不会影响当前打开的窗口。安装完成后,终端会说明更改将在下一次重启 Codex 时生效,并询问是否立即重启;默认选项为 **No**。只有在您明确确认,或后续手动运行 `mcx restart` 时,才会重新打开 Codex。安装完成后,macOS 的“登录项与扩展”中会添加名为 **MineCodex** 的后台项目。
|
|
28
28
|
|
|
29
|
+
`mcx install`、`mcx update`、`mcx restart` 和 `mcx uninstall` 会在终端持续显示当前阶段;`mcx open` 仅在需要启动后台服务时显示进度。查询类命令保持简洁,便于直接阅读或用于脚本。
|
|
30
|
+
|
|
29
31
|
MineCodex 只以后台辅助服务运行,前台应用始终是 OpenAI 签名的官方 `ChatGPT.app`,并继续使用官方默认用户数据。通过 Dock、Finder、Spotlight、登录项或会话恢复打开 ChatGPT 时,后台服务会验证官方应用与对应进程;若该进程尚未开放兼容的本机调试端口,则确认其完整退出后再通过 LaunchServices 重新打开官方应用并注入功能。MineCodex 不修改 `ChatGPT.app`、`app.asar`、Dock 项目或默认打开方式。
|
|
30
32
|
|
|
31
33
|
打开本地控制台:
|
|
@@ -15,7 +15,8 @@ function isUpsellBanner(element) {
|
|
|
15
15
|
if (!text) return false;
|
|
16
16
|
const isCard = element.classList.contains("rounded-2xl")
|
|
17
17
|
&& element.classList.contains("border")
|
|
18
|
-
&& element.classList.contains("bg-token-main-surface-primary")
|
|
18
|
+
&& (element.classList.contains("bg-token-main-surface-primary")
|
|
19
|
+
|| element.classList.contains("bg-surface"));
|
|
19
20
|
if (!isCard) return false;
|
|
20
21
|
const titleMatch = /out of Codex and Work usage/i.test(text)
|
|
21
22
|
|| /out of Codex messages/i.test(text)
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
4
4
|
import { assertNodeSupported, MacPlatformAdapter } from "./platform.mjs";
|
|
5
5
|
import { ensureConfig, ensureControlToken, readConfig } from "./config.mjs";
|
|
6
6
|
import { createControlServer } from "./control-server.mjs";
|
|
7
|
-
import {
|
|
7
|
+
import { createProgressReporter } from "./progress.mjs";
|
|
8
8
|
import { createNpmAdapter } from "./npm-adapter.mjs";
|
|
9
9
|
import { resolvePaths } from "./paths.mjs";
|
|
10
10
|
import { RuntimeManager, writeServicePid } from "./runtime-manager.mjs";
|
|
@@ -119,7 +119,7 @@ export async function confirmRestartNow({ input = process.stdin, terminalOutput
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
async function installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, confirmRestart, nodeVersion = process.versions.node }) {
|
|
122
|
-
const reporter =
|
|
122
|
+
const reporter = createProgressReporter({ output });
|
|
123
123
|
reporter.heading("Installing MineCodex");
|
|
124
124
|
let previousService = null;
|
|
125
125
|
try {
|
|
@@ -167,21 +167,30 @@ async function installCommand({ paths, platform, cliPath, output, serviceProbe,
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
async function openCommand({ paths, platform, serviceProbe, fetchImpl }) {
|
|
170
|
+
async function openCommand({ paths, platform, output, serviceProbe, fetchImpl }) {
|
|
171
171
|
const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
|
|
172
|
+
let reporter = null;
|
|
172
173
|
if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl, timeoutMs: 750 }))) {
|
|
173
174
|
if (!(await serviceInstalled(platform, paths))) throw new Error("MineCodex is not installed. Run `mcx install` first.");
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
reporter = createProgressReporter({ output });
|
|
176
|
+
reporter.heading("Opening MineCodex");
|
|
177
|
+
await reporter.step(1, 2, "Starting the background helper", async () => {
|
|
178
|
+
await platform.startService(paths);
|
|
179
|
+
if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))) throw new Error("MineCodex service could not be started.");
|
|
180
|
+
});
|
|
176
181
|
}
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
const openControlPanel = async () => {
|
|
183
|
+
const exchangeResponse = await authorizedFetch(paths, "/api/auth/exchange", {
|
|
184
|
+
method: "POST",
|
|
185
|
+
body: "{}",
|
|
186
|
+
}, { fetchImpl });
|
|
187
|
+
if (!exchangeResponse.ok) throw new Error(`MineCodex browser exchange failed: HTTP ${exchangeResponse.status}`);
|
|
188
|
+
const { exchange } = await exchangeResponse.json();
|
|
189
|
+
if (!exchange) throw new Error("MineCodex browser exchange did not return a short-lived value.");
|
|
190
|
+
await platform.openBrowser(`${controlOrigin()}/auth?exchange=${encodeURIComponent(exchange)}`);
|
|
191
|
+
};
|
|
192
|
+
if (reporter) await reporter.step(2, 2, "Opening the control panel", openControlPanel);
|
|
193
|
+
else await openControlPanel();
|
|
185
194
|
}
|
|
186
195
|
|
|
187
196
|
function featureStatusText(feature) {
|
|
@@ -239,95 +248,130 @@ async function statusCommand({ paths, platform, jsonOutput, output, fetchImpl })
|
|
|
239
248
|
}
|
|
240
249
|
|
|
241
250
|
async function restartCommand({ paths, output, fetchImpl }) {
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
251
|
+
const reporter = createProgressReporter({ output });
|
|
252
|
+
reporter.heading("Restarting MineCodex");
|
|
253
|
+
await reporter.step(1, 1, "Restarting Codex and applying plugin changes", async () => {
|
|
254
|
+
const response = await authorizedFetch(paths, "/api/restart", { method: "POST", body: "{}" }, { fetchImpl });
|
|
255
|
+
if (!response.ok) throw new Error(`MineCodex restart failed: HTTP ${response.status}`);
|
|
256
|
+
});
|
|
257
|
+
reporter.heading("MineCodex restarted and saved plugin switches are now applied.");
|
|
245
258
|
}
|
|
246
259
|
|
|
247
260
|
async function uninstallCommand({ paths, platform, purge, output, cliPath }) {
|
|
248
|
-
|
|
249
|
-
|
|
261
|
+
const reporter = createProgressReporter({ output });
|
|
262
|
+
reporter.heading("Uninstalling MineCodex");
|
|
263
|
+
const previousService = await reporter.step(1, 2, "Checking the current installation", async () => {
|
|
264
|
+
platform.assertInstallSupported();
|
|
265
|
+
return serviceSnapshot(platform, paths);
|
|
266
|
+
});
|
|
250
267
|
let wasManaged = false;
|
|
251
268
|
try {
|
|
252
|
-
await
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
269
|
+
await reporter.step(2, 2, purge ? "Removing MineCodex and its data" : "Removing the MineCodex integration", async () => {
|
|
270
|
+
await platform.removeService(paths);
|
|
271
|
+
wasManaged = await platform.terminateOwnedCodex(paths);
|
|
272
|
+
await platform.assertServiceStopped?.();
|
|
273
|
+
await platform.assertOwnedCodexStopped?.(paths);
|
|
274
|
+
if (purge) {
|
|
275
|
+
const targets = [paths.supportDir, paths.notesDataDir, paths.imagesDataDir];
|
|
276
|
+
for (const target of targets) {
|
|
277
|
+
if (!path.isAbsolute(target) || target === paths.homeDir || target.length <= paths.homeDir.length + 2) {
|
|
278
|
+
throw new Error(`Refusing unsafe purge target: ${target}`);
|
|
279
|
+
}
|
|
261
280
|
}
|
|
281
|
+
for (const target of targets) await rm(target, { recursive: true, force: true });
|
|
262
282
|
}
|
|
263
|
-
|
|
264
|
-
output("MineCodex integration and MineCodex-owned data were removed.");
|
|
265
|
-
} else {
|
|
266
|
-
output("MineCodex integration was removed. Plugin data was preserved.");
|
|
267
|
-
}
|
|
283
|
+
});
|
|
268
284
|
} catch (error) {
|
|
269
285
|
let rollbackError = null;
|
|
286
|
+
reporter.heading("Uninstall failed. Restoring the background helper.");
|
|
270
287
|
try {
|
|
271
|
-
await
|
|
288
|
+
await reporter.step(1, 1, "Restoring the background helper", async () => {
|
|
289
|
+
await restoreService(platform, previousService, { cliPath, paths });
|
|
290
|
+
});
|
|
272
291
|
} catch (failure) {
|
|
273
292
|
rollbackError = failure;
|
|
274
293
|
}
|
|
275
294
|
throw appendRollbackError(error, rollbackError);
|
|
276
295
|
}
|
|
277
|
-
|
|
278
|
-
|
|
296
|
+
reporter.heading(purge
|
|
297
|
+
? "MineCodex integration and MineCodex-owned data were removed."
|
|
298
|
+
: "MineCodex integration was removed. Plugin data was preserved.");
|
|
299
|
+
if (wasManaged) {
|
|
300
|
+
await reporter.step(1, 1, "Reopening Codex", async () => platform.openNativeCodex());
|
|
301
|
+
}
|
|
302
|
+
reporter.heading("Codex generated images and files referenced by Notes were not modified.");
|
|
279
303
|
}
|
|
280
304
|
|
|
281
305
|
async function updateCommand({ paths, platform, cliPath, output, npm = createNpmAdapter(), serviceProbe, fetchImpl }) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
306
|
+
const reporter = createProgressReporter({ output });
|
|
307
|
+
reporter.heading("Updating MineCodex");
|
|
308
|
+
const latest = await reporter.step(1, 5, "Checking for updates", async () => {
|
|
309
|
+
platform.assertInstallSupported();
|
|
310
|
+
const version = await npm.viewLatestVersion();
|
|
311
|
+
if (!version) throw new Error("Could not resolve the latest MineCodex version.");
|
|
312
|
+
return version;
|
|
313
|
+
});
|
|
285
314
|
if (latest === packageMetadata.version) {
|
|
286
|
-
|
|
315
|
+
reporter.heading(`MineCodex ${packageMetadata.version} is already up to date.`);
|
|
287
316
|
return;
|
|
288
317
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
318
|
+
let wasInstalled;
|
|
319
|
+
let previousService;
|
|
320
|
+
const rollback = await reporter.step(2, 5, "Backing up the current version", async () => {
|
|
321
|
+
wasInstalled = await serviceInstalled(platform, paths);
|
|
322
|
+
previousService = wasInstalled
|
|
323
|
+
? (await serviceSnapshot(platform, paths) ?? { running: true, plist: { exists: true } })
|
|
324
|
+
: null;
|
|
325
|
+
return npm.createRollbackArchive(paths.packageRoot);
|
|
326
|
+
});
|
|
294
327
|
const rollbackPath = typeof rollback === "string" ? rollback : rollback.archivePath;
|
|
295
328
|
if (!rollbackPath) throw new Error("Could not create a rollback archive for the installed MineCodex version.");
|
|
296
329
|
try {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
await npm.installGlobal("minecodex@latest");
|
|
302
|
-
if (wasInstalled) {
|
|
303
|
-
await platform.installService({ cliPath, paths, start: previousService.running });
|
|
304
|
-
if (previousService.running) {
|
|
305
|
-
const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
|
|
306
|
-
const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
|
|
307
|
-
if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))
|
|
308
|
-
|| !(await waitForManagedReadyImpl(paths, { fetchImpl }))) {
|
|
309
|
-
throw new Error("MineCodex service did not become ready after update.");
|
|
310
|
-
}
|
|
311
|
-
} else {
|
|
330
|
+
await reporter.step(3, 5, "Pausing the background helper", async () => {
|
|
331
|
+
if (wasInstalled && previousService?.running) {
|
|
332
|
+
await platform.stopService(paths, { ignoreErrors: false });
|
|
312
333
|
await platform.assertServiceStopped?.();
|
|
313
334
|
}
|
|
314
|
-
}
|
|
315
|
-
|
|
335
|
+
});
|
|
336
|
+
await reporter.step(4, 5, `Installing MineCodex ${latest}`, async () => {
|
|
337
|
+
await npm.installGlobal("minecodex@latest");
|
|
338
|
+
});
|
|
339
|
+
await reporter.step(5, 5, "Restoring the background helper", async () => {
|
|
340
|
+
if (wasInstalled) {
|
|
341
|
+
await platform.installService({ cliPath, paths, start: previousService.running });
|
|
342
|
+
if (previousService.running) {
|
|
343
|
+
const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
|
|
344
|
+
const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
|
|
345
|
+
if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))
|
|
346
|
+
|| !(await waitForManagedReadyImpl(paths, { fetchImpl }))) {
|
|
347
|
+
throw new Error("MineCodex service did not become ready after update.");
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
await platform.assertServiceStopped?.();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
reporter.heading(`MineCodex updated from ${packageMetadata.version} to ${latest}.`);
|
|
316
355
|
} catch (error) {
|
|
317
356
|
let rollbackError = null;
|
|
357
|
+
reporter.heading("Update failed. Restoring the previous version.");
|
|
318
358
|
try {
|
|
319
|
-
await
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (
|
|
325
|
-
|
|
326
|
-
|
|
359
|
+
await reporter.step(1, 2, "Restoring the previous version", async () => {
|
|
360
|
+
await npm.installGlobal(rollbackPath);
|
|
361
|
+
});
|
|
362
|
+
await reporter.step(2, 2, "Restoring the background helper", async () => {
|
|
363
|
+
await restoreService(platform, previousService, { cliPath, paths });
|
|
364
|
+
if (wasInstalled && previousService?.running) {
|
|
365
|
+
const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
|
|
366
|
+
const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
|
|
367
|
+
if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))
|
|
368
|
+
|| !(await waitForManagedReadyImpl(paths, { fetchImpl }))) {
|
|
369
|
+
throw new Error("Rolled-back MineCodex service did not become ready.");
|
|
370
|
+
}
|
|
371
|
+
} else if (wasInstalled && !previousService?.running) {
|
|
372
|
+
await platform.assertServiceStopped?.();
|
|
327
373
|
}
|
|
328
|
-
}
|
|
329
|
-
await platform.assertServiceStopped?.();
|
|
330
|
-
}
|
|
374
|
+
});
|
|
331
375
|
} catch (failure) {
|
|
332
376
|
rollbackError = failure;
|
|
333
377
|
}
|
|
@@ -397,7 +441,7 @@ export async function runCli(argv, {
|
|
|
397
441
|
if (["--help", "-h", "help"].includes(command)) return printHelp(output);
|
|
398
442
|
if (["--version", "-v"].includes(command)) return output(packageMetadata.version);
|
|
399
443
|
if (command === "install") return installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, confirmRestart, nodeVersion });
|
|
400
|
-
if (command === "open" || command === "gui") return openCommand({ paths, platform, serviceProbe, fetchImpl });
|
|
444
|
+
if (command === "open" || command === "gui") return openCommand({ paths, platform, output, serviceProbe, fetchImpl });
|
|
401
445
|
if (command === "status") return statusCommand({ paths, platform, jsonOutput: args.includes("--json"), output, fetchImpl });
|
|
402
446
|
if (command === "restart") return restartCommand({ paths, output, fetchImpl });
|
|
403
447
|
if (command === "uninstall" || command === "remove") {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
2
2
|
|
|
3
|
-
function
|
|
3
|
+
function createProgressReporter({ output = console.log, terminalOutput = process.stdout } = {}) {
|
|
4
4
|
// 只有真实终端才显示 spinner;测试或脚本环境保持普通逐行文本。
|
|
5
5
|
const interactive = output === console.log && Boolean(terminalOutput?.isTTY);
|
|
6
6
|
let timer = null;
|
|
@@ -53,4 +53,4 @@ function createInstallReporter({ output = console.log, terminalOutput = process.
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
export {
|
|
56
|
+
export { createProgressReporter };
|
|
@@ -306,6 +306,7 @@ export function createInjectionSource(features, {
|
|
|
306
306
|
let modelSelectorSuppressOutsidePointerSequence = false;
|
|
307
307
|
let modelSelectorTriggerObserver = null;
|
|
308
308
|
let modelSelectorObservedTrigger = null;
|
|
309
|
+
let modelSelectorControllerMenu = null;
|
|
309
310
|
let modelSelectorNativeFastIcons = null;
|
|
310
311
|
// Fast 图标来源标记:bundle 静态提取 vs DOM 克隆,DOM 克隆优先以保持像素一致。
|
|
311
312
|
let modelSelectorNativeFastIconSources = null;
|
|
@@ -318,6 +319,7 @@ export function createInjectionSource(features, {
|
|
|
318
319
|
modelSelectorStyle.setAttribute("data-codex-model-slider-style", "");
|
|
319
320
|
modelSelectorStyle.textContent = `
|
|
320
321
|
[data-codex-model-slider-menu] { width: 264px !important; min-width: 264px; overflow-x: hidden; }
|
|
322
|
+
[data-codex-model-slider-controller-menu] { position: absolute !important; right: 0; bottom: 0; z-index: 1; }
|
|
321
323
|
[data-codex-model-slider-menu][data-codex-model-slider-overflow] {
|
|
322
324
|
max-height: var(--codex-model-slider-menu-max-height) !important;
|
|
323
325
|
overflow-y: auto;
|
|
@@ -952,10 +954,21 @@ export function createInjectionSource(features, {
|
|
|
952
954
|
.trim();
|
|
953
955
|
}
|
|
954
956
|
|
|
957
|
+
function currentReactFiber(element) {
|
|
958
|
+
const fiberKey = Object.getOwnPropertyNames(element ?? {}).find((key) => key.startsWith("__reactFiber$"));
|
|
959
|
+
const propsKey = Object.getOwnPropertyNames(element ?? {}).find((key) => key.startsWith("__reactProps$"));
|
|
960
|
+
let fiber = fiberKey ? element[fiberKey] : null;
|
|
961
|
+
if (
|
|
962
|
+
propsKey
|
|
963
|
+
&& fiber?.alternate?.memoizedProps === element[propsKey]
|
|
964
|
+
&& fiber.memoizedProps !== element[propsKey]
|
|
965
|
+
) return fiber.alternate;
|
|
966
|
+
return fiber;
|
|
967
|
+
}
|
|
968
|
+
|
|
955
969
|
function nativeComposerModelController() {
|
|
956
970
|
const trigger = document.querySelector("[data-codex-intelligence-trigger]");
|
|
957
|
-
|
|
958
|
-
let fiber = fiberKey ? trigger[fiberKey] : null;
|
|
971
|
+
let fiber = currentReactFiber(trigger);
|
|
959
972
|
for (let depth = 0; fiber && depth < 40; depth += 1, fiber = fiber.return) {
|
|
960
973
|
const props = fiber.memoizedProps;
|
|
961
974
|
if (Array.isArray(props?.models) && typeof props.onSelectReasoningEffort === "function") return props;
|
|
@@ -1480,8 +1493,12 @@ export function createInjectionSource(features, {
|
|
|
1480
1493
|
renderEnhancedModelMenu(menu, selector);
|
|
1481
1494
|
}
|
|
1482
1495
|
|
|
1483
|
-
function enhanceModelItem(item, menu, selector, index
|
|
1484
|
-
|
|
1496
|
+
function enhanceModelItem(item, menu, selector, index, {
|
|
1497
|
+
rawValue = null,
|
|
1498
|
+
selected = null,
|
|
1499
|
+
scheduleReturn = true,
|
|
1500
|
+
} = {}) {
|
|
1501
|
+
const observedRaw = rawValue ?? modelItemRawValue(item);
|
|
1485
1502
|
if (!observedRaw) return;
|
|
1486
1503
|
const observedIdentity = modelIdentity(observedRaw);
|
|
1487
1504
|
const nativeDescriptor = nativeModelDescriptorForItem(item, observedRaw);
|
|
@@ -1499,7 +1516,10 @@ export function createInjectionSource(features, {
|
|
|
1499
1516
|
const display = modelDisplayLabel(identity, selector);
|
|
1500
1517
|
const provider = providerLabel(identity.provider, selector);
|
|
1501
1518
|
const brand = brandForModel(identity, selector);
|
|
1502
|
-
item.toggleAttribute(
|
|
1519
|
+
item.toggleAttribute(
|
|
1520
|
+
"data-codex-model-slider-selected",
|
|
1521
|
+
selected ?? Boolean(item.querySelector("svg")),
|
|
1522
|
+
);
|
|
1503
1523
|
const favorites = loadModelFavorites(selector);
|
|
1504
1524
|
const starred = favorites.has(raw);
|
|
1505
1525
|
const row = document.createElement("span");
|
|
@@ -1528,7 +1548,7 @@ export function createInjectionSource(features, {
|
|
|
1528
1548
|
star.append(createModelSelectorIcon(starred ? selector.icons.unstar : selector.icons.star));
|
|
1529
1549
|
row.append(star);
|
|
1530
1550
|
item.replaceChildren(row);
|
|
1531
|
-
if (!item.hasAttribute("data-codex-model-slider-return-listener")) {
|
|
1551
|
+
if (scheduleReturn && !item.hasAttribute("data-codex-model-slider-return-listener")) {
|
|
1532
1552
|
item.setAttribute("data-codex-model-slider-return-listener", "");
|
|
1533
1553
|
item.addEventListener("click", (event) => {
|
|
1534
1554
|
if (!event.target.closest?.("[data-codex-model-slider-star]")) scheduleModelSliderReturn();
|
|
@@ -1665,6 +1685,106 @@ export function createInjectionSource(features, {
|
|
|
1665
1685
|
}
|
|
1666
1686
|
}
|
|
1667
1687
|
|
|
1688
|
+
function closeControllerModelMenu() {
|
|
1689
|
+
const record = modelSelectorControllerMenu;
|
|
1690
|
+
if (!record) return false;
|
|
1691
|
+
record.button.setAttribute("aria-expanded", "false");
|
|
1692
|
+
record.parentMenu.style.position = record.position;
|
|
1693
|
+
record.parentMenu.style.overflow = record.overflow;
|
|
1694
|
+
for (const item of modelMenuItems(record.menu)) modelSelectorOriginalItems.delete(item);
|
|
1695
|
+
record.menu.remove();
|
|
1696
|
+
modelSelectorControllerMenu = null;
|
|
1697
|
+
return true;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function controllerModelReasoningEffort(controller, model) {
|
|
1701
|
+
const current = String(controller?.reasoningEffort ?? "").toLowerCase();
|
|
1702
|
+
const supported = (model?.supportedReasoningEfforts ?? []).map((entry) => (
|
|
1703
|
+
String(entry?.reasoningEffort ?? entry ?? "").toLowerCase()
|
|
1704
|
+
)).filter(Boolean);
|
|
1705
|
+
return supported.includes(current)
|
|
1706
|
+
? current
|
|
1707
|
+
: String(model?.defaultReasoningEffort ?? supported[0] ?? "none").toLowerCase();
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
function openControllerModelMenu(parentMenu, modelButton, selector) {
|
|
1711
|
+
if (modelSelectorControllerMenu) {
|
|
1712
|
+
closeControllerModelMenu();
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
const controller = nativeComposerModelController();
|
|
1716
|
+
if (!Array.isArray(controller?.models) || typeof controller.onSelectModel !== "function") return;
|
|
1717
|
+
const models = controller.models.filter((model) => !model.hidden && String(model.model ?? "").trim());
|
|
1718
|
+
if (!models.length) return;
|
|
1719
|
+
|
|
1720
|
+
const menu = document.createElement("div");
|
|
1721
|
+
menu.className = parentMenu.className;
|
|
1722
|
+
menu.setAttribute("role", "menu");
|
|
1723
|
+
menu.setAttribute("data-state", "open");
|
|
1724
|
+
menu.setAttribute("data-codex-model-slider-menu", "");
|
|
1725
|
+
menu.setAttribute("data-codex-model-slider-controller-menu", "");
|
|
1726
|
+
models.forEach((model, index) => {
|
|
1727
|
+
const item = document.createElement("div");
|
|
1728
|
+
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";
|
|
1729
|
+
item.setAttribute("role", "menuitem");
|
|
1730
|
+
item.setAttribute("tabindex", "-1");
|
|
1731
|
+
enhanceModelItem(item, menu, selector, index, {
|
|
1732
|
+
rawValue: String(model.model),
|
|
1733
|
+
selected: String(model.model) === String(controller.model),
|
|
1734
|
+
scheduleReturn: false,
|
|
1735
|
+
});
|
|
1736
|
+
menu.append(item);
|
|
1737
|
+
});
|
|
1738
|
+
menu.addEventListener("click", (event) => {
|
|
1739
|
+
const star = event.target.closest?.("[data-codex-model-slider-star]");
|
|
1740
|
+
const item = event.target.closest?.("[data-codex-model-slider-item]");
|
|
1741
|
+
if (!item) return;
|
|
1742
|
+
if (star) {
|
|
1743
|
+
const raw = item.dataset.codexModelSliderRaw;
|
|
1744
|
+
const label = item.querySelector("[data-codex-model-slider-name]")?.textContent ?? raw;
|
|
1745
|
+
toggleModelFavorite(event, menu, selector, raw, label);
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
const currentController = nativeComposerModelController();
|
|
1749
|
+
const raw = item.dataset.codexModelSliderRaw;
|
|
1750
|
+
const model = currentController?.models?.find((candidate) => String(candidate.model) === raw);
|
|
1751
|
+
if (!model || typeof currentController.onSelectModel !== "function") return;
|
|
1752
|
+
const reasoningEffort = controllerModelReasoningEffort(currentController, model);
|
|
1753
|
+
currentController.onSelectModel(raw, reasoningEffort);
|
|
1754
|
+
closeControllerModelMenu();
|
|
1755
|
+
queueEnsure();
|
|
1756
|
+
});
|
|
1757
|
+
menu.addEventListener("keydown", (event) => {
|
|
1758
|
+
if (event.key === "Escape") {
|
|
1759
|
+
event.preventDefault();
|
|
1760
|
+
closeControllerModelMenu();
|
|
1761
|
+
modelButton.focus();
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
1765
|
+
const item = event.target.closest?.("[data-codex-model-slider-item]");
|
|
1766
|
+
if (!item) return;
|
|
1767
|
+
event.preventDefault();
|
|
1768
|
+
item.click();
|
|
1769
|
+
});
|
|
1770
|
+
renderEnhancedModelMenu(menu, selector);
|
|
1771
|
+
|
|
1772
|
+
modelSelectorControllerMenu = {
|
|
1773
|
+
menu,
|
|
1774
|
+
parentMenu,
|
|
1775
|
+
button: modelButton,
|
|
1776
|
+
position: parentMenu.style.position,
|
|
1777
|
+
overflow: parentMenu.style.overflow,
|
|
1778
|
+
};
|
|
1779
|
+
parentMenu.style.position = "relative";
|
|
1780
|
+
parentMenu.style.overflow = "visible";
|
|
1781
|
+
parentMenu.append(menu);
|
|
1782
|
+
sizeModelMenuViewport(menu, selector.maxVisibleItems);
|
|
1783
|
+
modelButton.setAttribute("aria-expanded", "true");
|
|
1784
|
+
(menu.querySelector('[data-codex-model-slider-selected]')
|
|
1785
|
+
?? menu.querySelector('[role="menuitem"]'))?.focus();
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1668
1788
|
function openNativeModelSubmenu(parentMenu) {
|
|
1669
1789
|
let attempts = 0;
|
|
1670
1790
|
const open = () => {
|
|
@@ -1942,6 +2062,8 @@ export function createInjectionSource(features, {
|
|
|
1942
2062
|
modelButton.setAttribute("tabindex", "0");
|
|
1943
2063
|
modelButton.setAttribute("data-codex-model-slider-model-button", "");
|
|
1944
2064
|
modelButton.setAttribute("aria-label", `Model ${modelDisplayLabel(identity, selector)}`);
|
|
2065
|
+
modelButton.setAttribute("aria-haspopup", "menu");
|
|
2066
|
+
modelButton.setAttribute("aria-expanded", "false");
|
|
1945
2067
|
const buttonContent = document.createElement("span");
|
|
1946
2068
|
buttonContent.className = classes.ViewToggleContent;
|
|
1947
2069
|
const buttonLabel = document.createElement("span");
|
|
@@ -1955,7 +2077,7 @@ export function createInjectionSource(features, {
|
|
|
1955
2077
|
buttonContent.append(buttonLabel);
|
|
1956
2078
|
}
|
|
1957
2079
|
modelButton.append(buttonContent);
|
|
1958
|
-
const openModels = () =>
|
|
2080
|
+
const openModels = () => openControllerModelMenu(parentMenu, modelButton, selector);
|
|
1959
2081
|
modelButton.addEventListener("click", openModels);
|
|
1960
2082
|
modelButton.addEventListener("keydown", (event) => {
|
|
1961
2083
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
@@ -2006,6 +2128,11 @@ export function createInjectionSource(features, {
|
|
|
2006
2128
|
viewControls.append(modelButton);
|
|
2007
2129
|
}
|
|
2008
2130
|
|
|
2131
|
+
if (efforts.length < 2) {
|
|
2132
|
+
shell.append(viewControls);
|
|
2133
|
+
return shell;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2009
2136
|
const nativeSlider = cloneNativePowerSlider(parentMenu, efforts.length, fastEnabled);
|
|
2010
2137
|
const rail = nativeSlider?.querySelector('[role="slider"]') ?? document.createElement("div");
|
|
2011
2138
|
rail.setAttribute("data-codex-model-slider-rail", "");
|
|
@@ -2126,7 +2253,7 @@ export function createInjectionSource(features, {
|
|
|
2126
2253
|
supportedReasoningLevels: model?.supportedReasoningLevels ?? catalogModel?.supportedReasoningLevels ?? [],
|
|
2127
2254
|
};
|
|
2128
2255
|
const efforts = visibleReasoningLevelsFor(modelWithCapability);
|
|
2129
|
-
if (!model
|
|
2256
|
+
if (!model) return false;
|
|
2130
2257
|
// 控制器推导的任务内实际 effort 优先,避免子任务/主任务切换后触发器属性滞后;
|
|
2131
2258
|
// 触发器属性仅在控制器无法解析时作为实时回退,静态目录默认值最后兜底。
|
|
2132
2259
|
const nativeModel = nativeCatalogModelFor(identity);
|
|
@@ -2134,7 +2261,8 @@ export function createInjectionSource(features, {
|
|
|
2134
2261
|
?? document.querySelector("[data-codex-intelligence-trigger]")
|
|
2135
2262
|
?.getAttribute("data-selected-reasoning-effort")
|
|
2136
2263
|
?? model.defaultReasoningLevel
|
|
2137
|
-
?? efforts[0]
|
|
2264
|
+
?? efforts[0]?.effort
|
|
2265
|
+
?? "none";
|
|
2138
2266
|
const effortRank = (value) => {
|
|
2139
2267
|
const order = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
2140
2268
|
const index = order.indexOf(String(value ?? "").toLowerCase());
|
|
@@ -2143,7 +2271,8 @@ export function createInjectionSource(features, {
|
|
|
2143
2271
|
const visibleEffort = efforts.find((level) => level.effort === selectedEffort)?.effort
|
|
2144
2272
|
?? efforts.filter((level) => effortRank(level.effort) <= effortRank(selectedEffort))
|
|
2145
2273
|
.sort((left, right) => effortRank(right.effort) - effortRank(left.effort))[0]?.effort
|
|
2146
|
-
?? efforts[0]
|
|
2274
|
+
?? efforts[0]?.effort
|
|
2275
|
+
?? "none";
|
|
2147
2276
|
const selectedEffortValue = visibleEffort;
|
|
2148
2277
|
const selectedServiceTier = nativeComposerModelController()?.selectedServiceTier;
|
|
2149
2278
|
const serviceTierKey = selectedServiceTier?.id ?? selectedServiceTier ?? "standard";
|
|
@@ -2252,12 +2381,18 @@ export function createInjectionSource(features, {
|
|
|
2252
2381
|
if (!modelSelectorStyle.isConnected) document.head?.append(modelSelectorStyle);
|
|
2253
2382
|
enhanceComposerModelTrigger(selector);
|
|
2254
2383
|
const parentMenu = nativeModelPickerMenu();
|
|
2384
|
+
if (modelSelectorControllerMenu && modelSelectorControllerMenu.parentMenu !== parentMenu) {
|
|
2385
|
+
closeControllerModelMenu();
|
|
2386
|
+
}
|
|
2255
2387
|
if (!parentMenu) {
|
|
2256
|
-
if (
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2388
|
+
if (
|
|
2389
|
+
modelSelectorPendingReturn
|
|
2390
|
+
&& !visibleBlockingDialog()
|
|
2391
|
+
&& nativeModelPickerSubmenus().length === 0
|
|
2392
|
+
) {
|
|
2393
|
+
clearTimeout(modelSelectorReturnTimer);
|
|
2394
|
+
modelSelectorReturnTimer = null;
|
|
2395
|
+
returnToNativeModelSlider();
|
|
2261
2396
|
}
|
|
2262
2397
|
} else {
|
|
2263
2398
|
enhanceNativeModelPicker(parentMenu, selector);
|
|
@@ -2273,6 +2408,7 @@ export function createInjectionSource(features, {
|
|
|
2273
2408
|
}
|
|
2274
2409
|
|
|
2275
2410
|
function restoreModelSelectorEnhancements() {
|
|
2411
|
+
closeControllerModelMenu();
|
|
2276
2412
|
clearTimeout(modelSelectorReturnTimer);
|
|
2277
2413
|
clearTimeout(modelSelectorEffortTransactionTimer);
|
|
2278
2414
|
modelSelectorTriggerObserver?.disconnect();
|
|
@@ -3863,6 +3999,11 @@ export function createInjectionSource(features, {
|
|
|
3863
3999
|
document.addEventListener(
|
|
3864
4000
|
"click",
|
|
3865
4001
|
(event) => {
|
|
4002
|
+
if (
|
|
4003
|
+
modelSelectorControllerMenu
|
|
4004
|
+
&& !modelSelectorControllerMenu.menu.contains(event.target)
|
|
4005
|
+
&& !modelSelectorControllerMenu.button.contains(event.target)
|
|
4006
|
+
) closeControllerModelMenu();
|
|
3866
4007
|
if (isNativeSummaryButton(event.target) && activePinnedFeatureId) hidePinnedSurfaces();
|
|
3867
4008
|
if (activePinnedFeatureId && summaryDisplayMode() === "overlay") {
|
|
3868
4009
|
const state = summaryState(activePinnedFeatureId);
|
|
@@ -359,6 +359,12 @@ export async function loadConfiguredModelCatalog(
|
|
|
359
359
|
id: String(tier.id ?? "").trim(),
|
|
360
360
|
name: String(tier.name ?? "").trim(),
|
|
361
361
|
})),
|
|
362
|
+
capabilityKey: [
|
|
363
|
+
model.opencodex_capability_provenance?.provider,
|
|
364
|
+
model.opencodex_capability_provenance?.model_id,
|
|
365
|
+
].every((value) => typeof value === "string" && value.trim())
|
|
366
|
+
? `${model.opencodex_capability_provenance.provider.trim()}/${model.opencodex_capability_provenance.model_id.trim()}`
|
|
367
|
+
: null,
|
|
362
368
|
})).filter((model) => model.slug);
|
|
363
369
|
} catch (error) {
|
|
364
370
|
if (error.code === "ENOENT") return [];
|
|
@@ -29,7 +29,7 @@ for (const feature of features) {
|
|
|
29
29
|
feature.modelSelector.models = modelCatalog.map((model) => (
|
|
30
30
|
applyVisibleReasoningLevels(
|
|
31
31
|
model,
|
|
32
|
-
modelCapabilities.get(String(model.slug ?? "").toLowerCase()),
|
|
32
|
+
modelCapabilities.get(String(model.capabilityKey ?? model.slug ?? "").toLowerCase()),
|
|
33
33
|
model.supportedReasoningLevels,
|
|
34
34
|
)
|
|
35
35
|
));
|