minecodex 0.2.1 → 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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -4,6 +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 { createProgressReporter } from "./progress.mjs";
7
8
  import { createNpmAdapter } from "./npm-adapter.mjs";
8
9
  import { resolvePaths } from "./paths.mjs";
9
10
  import { RuntimeManager, writeServicePid } from "./runtime-manager.mjs";
@@ -118,20 +119,37 @@ export async function confirmRestartNow({ input = process.stdin, terminalOutput
118
119
  }
119
120
 
120
121
  async function installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, confirmRestart, nodeVersion = process.versions.node }) {
121
- assertNodeSupported(nodeVersion);
122
- platform.assertInstallSupported();
123
- await platform.assertCodexInstalled();
124
- const previousService = await serviceSnapshot(platform, paths);
125
- await ensureConfig(paths.configPath);
126
- await ensureControlToken(paths.tokenPath);
122
+ const reporter = createProgressReporter({ output });
123
+ reporter.heading("Installing MineCodex");
124
+ let previousService = null;
127
125
  try {
128
- await platform.installService({ cliPath, paths, start: true });
129
- const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
130
- const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
131
- if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))
132
- || !(await waitForManagedReadyImpl(paths, { fetchImpl }))) {
133
- throw new Error(`MineCodex service did not become ready. Check ${paths.serviceErrorLogPath}`);
134
- }
126
+ await reporter.step(1, 6, "Checking your system", async () => {
127
+ assertNodeSupported(nodeVersion);
128
+ platform.assertInstallSupported();
129
+ await platform.assertCodexInstalled();
130
+ });
131
+ await reporter.step(2, 6, "Preparing your settings", async () => {
132
+ await ensureConfig(paths.configPath);
133
+ await ensureControlToken(paths.tokenPath);
134
+ });
135
+ previousService = await reporter.step(3, 6, "Checking your current setup", async () => {
136
+ return serviceSnapshot(platform, paths);
137
+ });
138
+ await reporter.step(4, 6, "Installing the background helper", async () => {
139
+ await platform.installService({ cliPath, paths, start: true });
140
+ });
141
+ await reporter.step(5, 6, "Waiting for the background helper (up to 20 seconds)", async () => {
142
+ const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
143
+ if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))) {
144
+ throw new Error("MineCodex service did not become ready. Check " + paths.serviceErrorLogPath);
145
+ }
146
+ });
147
+ await reporter.step(6, 6, "Waiting for MineCodex to be ready (up to 30 seconds)", async () => {
148
+ const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
149
+ if (!(await waitForManagedReadyImpl(paths, { fetchImpl }))) {
150
+ throw new Error("MineCodex service did not become ready. Check " + paths.serviceErrorLogPath);
151
+ }
152
+ });
135
153
  } catch (error) {
136
154
  let rollbackError = null;
137
155
  try {
@@ -141,29 +159,38 @@ async function installCommand({ paths, platform, cliPath, output, serviceProbe,
141
159
  }
142
160
  throw appendRollbackError(error, rollbackError);
143
161
  }
144
- output("MineCodex installed. It will be enabled the next time Codex restarts.");
162
+ reporter.heading("MineCodex installed. It will be enabled the next time Codex restarts.");
145
163
  if (await confirmRestart()) {
146
164
  await restartCommand({ paths, output, fetchImpl });
147
165
  } else {
148
- output("Run `mcx restart` when you are ready.");
149
- }
166
+ reporter.heading("Run `mcx restart` when you are ready.");
167
+ }
150
168
  }
151
169
 
152
- async function openCommand({ paths, platform, serviceProbe, fetchImpl }) {
170
+ async function openCommand({ paths, platform, output, serviceProbe, fetchImpl }) {
153
171
  const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
172
+ let reporter = null;
154
173
  if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl, timeoutMs: 750 }))) {
155
174
  if (!(await serviceInstalled(platform, paths))) throw new Error("MineCodex is not installed. Run `mcx install` first.");
156
- await platform.startService(paths);
157
- if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))) throw new Error("MineCodex service could not be started.");
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
+ });
158
181
  }
159
- const exchangeResponse = await authorizedFetch(paths, "/api/auth/exchange", {
160
- method: "POST",
161
- body: "{}",
162
- }, { fetchImpl });
163
- if (!exchangeResponse.ok) throw new Error(`MineCodex browser exchange failed: HTTP ${exchangeResponse.status}`);
164
- const { exchange } = await exchangeResponse.json();
165
- if (!exchange) throw new Error("MineCodex browser exchange did not return a short-lived value.");
166
- await platform.openBrowser(`${controlOrigin()}/auth?exchange=${encodeURIComponent(exchange)}`);
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();
167
194
  }
168
195
 
169
196
  function featureStatusText(feature) {
@@ -221,95 +248,130 @@ async function statusCommand({ paths, platform, jsonOutput, output, fetchImpl })
221
248
  }
222
249
 
223
250
  async function restartCommand({ paths, output, fetchImpl }) {
224
- const response = await authorizedFetch(paths, "/api/restart", { method: "POST", body: "{}" }, { fetchImpl });
225
- if (!response.ok) throw new Error(`MineCodex restart failed: HTTP ${response.status}`);
226
- output("MineCodex restarted and saved plugin switches are now applied.");
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.");
227
258
  }
228
259
 
229
260
  async function uninstallCommand({ paths, platform, purge, output, cliPath }) {
230
- platform.assertInstallSupported();
231
- const previousService = await serviceSnapshot(platform, paths);
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
+ });
232
267
  let wasManaged = false;
233
268
  try {
234
- await platform.removeService(paths);
235
- wasManaged = await platform.terminateOwnedCodex(paths);
236
- await platform.assertServiceStopped?.();
237
- await platform.assertOwnedCodexStopped?.(paths);
238
- if (purge) {
239
- const targets = [paths.supportDir, paths.notesDataDir, paths.imagesDataDir];
240
- for (const target of targets) {
241
- if (!path.isAbsolute(target) || target === paths.homeDir || target.length <= paths.homeDir.length + 2) {
242
- throw new Error(`Refusing unsafe purge target: ${target}`);
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
+ }
243
280
  }
281
+ for (const target of targets) await rm(target, { recursive: true, force: true });
244
282
  }
245
- for (const target of targets) await rm(target, { recursive: true, force: true });
246
- output("MineCodex integration and MineCodex-owned data were removed.");
247
- } else {
248
- output("MineCodex integration was removed. Plugin data was preserved.");
249
- }
283
+ });
250
284
  } catch (error) {
251
285
  let rollbackError = null;
286
+ reporter.heading("Uninstall failed. Restoring the background helper.");
252
287
  try {
253
- await restoreService(platform, previousService, { cliPath, paths });
288
+ await reporter.step(1, 1, "Restoring the background helper", async () => {
289
+ await restoreService(platform, previousService, { cliPath, paths });
290
+ });
254
291
  } catch (failure) {
255
292
  rollbackError = failure;
256
293
  }
257
294
  throw appendRollbackError(error, rollbackError);
258
295
  }
259
- if (wasManaged) await platform.openNativeCodex();
260
- output("Codex generated images and files referenced by Notes were not modified.");
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.");
261
303
  }
262
304
 
263
305
  async function updateCommand({ paths, platform, cliPath, output, npm = createNpmAdapter(), serviceProbe, fetchImpl }) {
264
- platform.assertInstallSupported();
265
- const latest = await npm.viewLatestVersion();
266
- if (!latest) throw new Error("Could not resolve the latest MineCodex version.");
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
+ });
267
314
  if (latest === packageMetadata.version) {
268
- output(`MineCodex ${packageMetadata.version} is already up to date.`);
315
+ reporter.heading(`MineCodex ${packageMetadata.version} is already up to date.`);
269
316
  return;
270
317
  }
271
- const wasInstalled = await serviceInstalled(platform, paths);
272
- const previousService = wasInstalled
273
- ? (await serviceSnapshot(platform, paths) ?? { running: true, plist: { exists: true } })
274
- : null;
275
- const rollback = await npm.createRollbackArchive(paths.packageRoot);
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
+ });
276
327
  const rollbackPath = typeof rollback === "string" ? rollback : rollback.archivePath;
277
328
  if (!rollbackPath) throw new Error("Could not create a rollback archive for the installed MineCodex version.");
278
329
  try {
279
- if (wasInstalled && previousService?.running) {
280
- await platform.stopService(paths, { ignoreErrors: false });
281
- await platform.assertServiceStopped?.();
282
- }
283
- await npm.installGlobal("minecodex@latest");
284
- if (wasInstalled) {
285
- await platform.installService({ cliPath, paths, start: previousService.running });
286
- if (previousService.running) {
287
- const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
288
- const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
289
- if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))
290
- || !(await waitForManagedReadyImpl(paths, { fetchImpl }))) {
291
- throw new Error("MineCodex service did not become ready after update.");
292
- }
293
- } else {
330
+ await reporter.step(3, 5, "Pausing the background helper", async () => {
331
+ if (wasInstalled && previousService?.running) {
332
+ await platform.stopService(paths, { ignoreErrors: false });
294
333
  await platform.assertServiceStopped?.();
295
334
  }
296
- }
297
- output(`MineCodex updated from ${packageMetadata.version} to ${latest}.`);
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}.`);
298
355
  } catch (error) {
299
356
  let rollbackError = null;
357
+ reporter.heading("Update failed. Restoring the previous version.");
300
358
  try {
301
- await npm.installGlobal(rollbackPath);
302
- await restoreService(platform, previousService, { cliPath, paths });
303
- if (wasInstalled && previousService?.running) {
304
- const waitForServiceImpl = serviceProbe?.waitForService ?? waitForService;
305
- const waitForManagedReadyImpl = serviceProbe?.waitForManagedReady ?? waitForManagedReady;
306
- if (!(await waitForServiceImpl(DEFAULT_CONTROL_PORT, { fetchImpl }))
307
- || !(await waitForManagedReadyImpl(paths, { fetchImpl }))) {
308
- throw new Error("Rolled-back MineCodex service did not become ready.");
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?.();
309
373
  }
310
- } else if (wasInstalled && !previousService?.running) {
311
- await platform.assertServiceStopped?.();
312
- }
374
+ });
313
375
  } catch (failure) {
314
376
  rollbackError = failure;
315
377
  }
@@ -379,7 +441,7 @@ export async function runCli(argv, {
379
441
  if (["--help", "-h", "help"].includes(command)) return printHelp(output);
380
442
  if (["--version", "-v"].includes(command)) return output(packageMetadata.version);
381
443
  if (command === "install") return installCommand({ paths, platform, cliPath, output, serviceProbe, fetchImpl, confirmRestart, nodeVersion });
382
- if (command === "open" || command === "gui") return openCommand({ paths, platform, serviceProbe, fetchImpl });
444
+ if (command === "open" || command === "gui") return openCommand({ paths, platform, output, serviceProbe, fetchImpl });
383
445
  if (command === "status") return statusCommand({ paths, platform, jsonOutput: args.includes("--json"), output, fetchImpl });
384
446
  if (command === "restart") return restartCommand({ paths, output, fetchImpl });
385
447
  if (command === "uninstall" || command === "remove") {
@@ -0,0 +1,56 @@
1
+ const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
2
+
3
+ function createProgressReporter({ output = console.log, terminalOutput = process.stdout } = {}) {
4
+ // 只有真实终端才显示 spinner;测试或脚本环境保持普通逐行文本。
5
+ const interactive = output === console.log && Boolean(terminalOutput?.isTTY);
6
+ let timer = null;
7
+
8
+ function stopSpinner() {
9
+ if (timer) clearInterval(timer);
10
+ timer = null;
11
+ }
12
+
13
+ function writeLine(line) {
14
+ if (!interactive) {
15
+ output(line);
16
+ return;
17
+ }
18
+ terminalOutput.write(`\r\x1b[2K${line}\n`);
19
+ }
20
+
21
+ function startSpinner(line) {
22
+ if (!interactive) {
23
+ output(line);
24
+ return;
25
+ }
26
+ let frame = 0;
27
+ terminalOutput.write(`\r\x1b[2K${SPINNER_FRAMES[frame]} ${line}`);
28
+ timer = setInterval(() => {
29
+ frame = (frame + 1) % SPINNER_FRAMES.length;
30
+ terminalOutput.write(`\r\x1b[2K${SPINNER_FRAMES[frame]} ${line}`);
31
+ }, 80);
32
+ }
33
+
34
+ return {
35
+ heading(line) {
36
+ writeLine(line);
37
+ },
38
+
39
+ async step(index, total, label, action) {
40
+ const prefix = `[${index}/${total}] ${label}`;
41
+ startSpinner(prefix);
42
+ try {
43
+ const result = await action();
44
+ stopSpinner();
45
+ writeLine(`✓ ${prefix}`);
46
+ return result;
47
+ } catch (error) {
48
+ stopSpinner();
49
+ writeLine(`✗ ${prefix}`);
50
+ throw error;
51
+ }
52
+ },
53
+ };
54
+ }
55
+
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
- const fiberKey = Object.getOwnPropertyNames(trigger ?? {}).find((key) => key.startsWith("__reactFiber$"));
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;
@@ -1093,6 +1106,19 @@ export function createInjectionSource(features, {
1093
1106
  return formatIdentifier(identity.backend).toLowerCase() === nativeLabel ? native : thirdParty;
1094
1107
  }
1095
1108
 
1109
+ function visibleReasoningLevelsFor(model) {
1110
+ if (!model) return [];
1111
+ const capability = model.modelSelectorCapability;
1112
+ if (capability && Array.isArray(capability.levels)) {
1113
+ const allowed = new Set(capability.levels);
1114
+ return (model.supportedReasoningLevels ?? []).filter((level) => {
1115
+ const effort = String(level?.effort ?? level ?? "").toLowerCase();
1116
+ return allowed.has(effort);
1117
+ });
1118
+ }
1119
+ return model.supportedReasoningLevels ?? [];
1120
+ }
1121
+
1096
1122
  function loadModelFavorites(selector) {
1097
1123
  try {
1098
1124
  const value = JSON.parse(localStorage.getItem(selector.favoriteStorageKey) ?? "[]");
@@ -1467,8 +1493,12 @@ export function createInjectionSource(features, {
1467
1493
  renderEnhancedModelMenu(menu, selector);
1468
1494
  }
1469
1495
 
1470
- function enhanceModelItem(item, menu, selector, index) {
1471
- const observedRaw = modelItemRawValue(item);
1496
+ function enhanceModelItem(item, menu, selector, index, {
1497
+ rawValue = null,
1498
+ selected = null,
1499
+ scheduleReturn = true,
1500
+ } = {}) {
1501
+ const observedRaw = rawValue ?? modelItemRawValue(item);
1472
1502
  if (!observedRaw) return;
1473
1503
  const observedIdentity = modelIdentity(observedRaw);
1474
1504
  const nativeDescriptor = nativeModelDescriptorForItem(item, observedRaw);
@@ -1486,7 +1516,10 @@ export function createInjectionSource(features, {
1486
1516
  const display = modelDisplayLabel(identity, selector);
1487
1517
  const provider = providerLabel(identity.provider, selector);
1488
1518
  const brand = brandForModel(identity, selector);
1489
- item.toggleAttribute("data-codex-model-slider-selected", Boolean(item.querySelector("svg")));
1519
+ item.toggleAttribute(
1520
+ "data-codex-model-slider-selected",
1521
+ selected ?? Boolean(item.querySelector("svg")),
1522
+ );
1490
1523
  const favorites = loadModelFavorites(selector);
1491
1524
  const starred = favorites.has(raw);
1492
1525
  const row = document.createElement("span");
@@ -1515,7 +1548,7 @@ export function createInjectionSource(features, {
1515
1548
  star.append(createModelSelectorIcon(starred ? selector.icons.unstar : selector.icons.star));
1516
1549
  row.append(star);
1517
1550
  item.replaceChildren(row);
1518
- if (!item.hasAttribute("data-codex-model-slider-return-listener")) {
1551
+ if (scheduleReturn && !item.hasAttribute("data-codex-model-slider-return-listener")) {
1519
1552
  item.setAttribute("data-codex-model-slider-return-listener", "");
1520
1553
  item.addEventListener("click", (event) => {
1521
1554
  if (!event.target.closest?.("[data-codex-model-slider-star]")) scheduleModelSliderReturn();
@@ -1652,6 +1685,106 @@ export function createInjectionSource(features, {
1652
1685
  }
1653
1686
  }
1654
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
+
1655
1788
  function openNativeModelSubmenu(parentMenu) {
1656
1789
  let attempts = 0;
1657
1790
  const open = () => {
@@ -1929,6 +2062,8 @@ export function createInjectionSource(features, {
1929
2062
  modelButton.setAttribute("tabindex", "0");
1930
2063
  modelButton.setAttribute("data-codex-model-slider-model-button", "");
1931
2064
  modelButton.setAttribute("aria-label", `Model ${modelDisplayLabel(identity, selector)}`);
2065
+ modelButton.setAttribute("aria-haspopup", "menu");
2066
+ modelButton.setAttribute("aria-expanded", "false");
1932
2067
  const buttonContent = document.createElement("span");
1933
2068
  buttonContent.className = classes.ViewToggleContent;
1934
2069
  const buttonLabel = document.createElement("span");
@@ -1942,7 +2077,7 @@ export function createInjectionSource(features, {
1942
2077
  buttonContent.append(buttonLabel);
1943
2078
  }
1944
2079
  modelButton.append(buttonContent);
1945
- const openModels = () => openNativeModelSubmenu(parentMenu);
2080
+ const openModels = () => openControllerModelMenu(parentMenu, modelButton, selector);
1946
2081
  modelButton.addEventListener("click", openModels);
1947
2082
  modelButton.addEventListener("keydown", (event) => {
1948
2083
  if (event.key !== "Enter" && event.key !== " ") return;
@@ -1993,6 +2128,11 @@ export function createInjectionSource(features, {
1993
2128
  viewControls.append(modelButton);
1994
2129
  }
1995
2130
 
2131
+ if (efforts.length < 2) {
2132
+ shell.append(viewControls);
2133
+ return shell;
2134
+ }
2135
+
1996
2136
  const nativeSlider = cloneNativePowerSlider(parentMenu, efforts.length, fastEnabled);
1997
2137
  const rail = nativeSlider?.querySelector('[role="slider"]') ?? document.createElement("div");
1998
2138
  rail.setAttribute("data-codex-model-slider-rail", "");
@@ -2106,8 +2246,14 @@ export function createInjectionSource(features, {
2106
2246
  function enhanceGenericModelPicker(parentMenu, selector) {
2107
2247
  const identity = modelIdentity(nativeModelValue(parentMenu));
2108
2248
  const model = modelDefinitionFor(identity, selector);
2109
- const efforts = model?.supportedReasoningLevels ?? [];
2110
- if (!model || efforts.length < 2) return false;
2249
+ const catalogModel = catalogModelFor(identity, selector);
2250
+ const modelWithCapability = model?.modelSelectorCapability ? model : {
2251
+ ...(model ?? {}),
2252
+ ...(catalogModel ?? {}),
2253
+ supportedReasoningLevels: model?.supportedReasoningLevels ?? catalogModel?.supportedReasoningLevels ?? [],
2254
+ };
2255
+ const efforts = visibleReasoningLevelsFor(modelWithCapability);
2256
+ if (!model) return false;
2111
2257
  // 控制器推导的任务内实际 effort 优先,避免子任务/主任务切换后触发器属性滞后;
2112
2258
  // 触发器属性仅在控制器无法解析时作为实时回退,静态目录默认值最后兜底。
2113
2259
  const nativeModel = nativeCatalogModelFor(identity);
@@ -2115,10 +2261,22 @@ export function createInjectionSource(features, {
2115
2261
  ?? document.querySelector("[data-codex-intelligence-trigger]")
2116
2262
  ?.getAttribute("data-selected-reasoning-effort")
2117
2263
  ?? model.defaultReasoningLevel
2118
- ?? efforts[0].effort;
2264
+ ?? efforts[0]?.effort
2265
+ ?? "none";
2266
+ const effortRank = (value) => {
2267
+ const order = ["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
2268
+ const index = order.indexOf(String(value ?? "").toLowerCase());
2269
+ return index < 0 ? -1 : index;
2270
+ };
2271
+ const visibleEffort = efforts.find((level) => level.effort === selectedEffort)?.effort
2272
+ ?? efforts.filter((level) => effortRank(level.effort) <= effortRank(selectedEffort))
2273
+ .sort((left, right) => effortRank(right.effort) - effortRank(left.effort))[0]?.effort
2274
+ ?? efforts[0]?.effort
2275
+ ?? "none";
2276
+ const selectedEffortValue = visibleEffort;
2119
2277
  const selectedServiceTier = nativeComposerModelController()?.selectedServiceTier;
2120
2278
  const serviceTierKey = selectedServiceTier?.id ?? selectedServiceTier ?? "standard";
2121
- const key = `${identity.raw}:${selectedEffort}:${serviceTierKey}:${efforts.map((level) => level.effort).join(",")}`;
2279
+ const key = `${identity.raw}:${selectedEffortValue}:${serviceTierKey}:${efforts.map((level) => level.effort).join(",")}`;
2122
2280
  const existing = parentMenu.querySelector(":scope > [data-codex-model-slider-generic]");
2123
2281
  const nativeTemplateReady = Boolean(
2124
2282
  parentMenu.firstElementChild
@@ -2140,7 +2298,7 @@ export function createInjectionSource(features, {
2140
2298
  existing?.remove();
2141
2299
  const nativeContainer = parentMenu.firstElementChild;
2142
2300
  if (!nativeContainer) return false;
2143
- const shell = createGenericModelSlider(parentMenu, selector, identity, model, selectedEffort);
2301
+ const shell = createGenericModelSlider(parentMenu, selector, identity, model, selectedEffortValue);
2144
2302
  if (!shell) return false;
2145
2303
  if (!modelSelectorGenericContainers.has(nativeContainer)) {
2146
2304
  modelSelectorGenericContainers.set(nativeContainer, nativeContainer.style.display);
@@ -2223,12 +2381,18 @@ export function createInjectionSource(features, {
2223
2381
  if (!modelSelectorStyle.isConnected) document.head?.append(modelSelectorStyle);
2224
2382
  enhanceComposerModelTrigger(selector);
2225
2383
  const parentMenu = nativeModelPickerMenu();
2384
+ if (modelSelectorControllerMenu && modelSelectorControllerMenu.parentMenu !== parentMenu) {
2385
+ closeControllerModelMenu();
2386
+ }
2226
2387
  if (!parentMenu) {
2227
- if (modelSelectorPendingReturn && !visibleBlockingDialog() && modelSelectorReturnTimer == null) {
2228
- modelSelectorReturnTimer = setTimeout(() => {
2229
- modelSelectorReturnTimer = null;
2230
- returnToNativeModelSlider();
2231
- }, 80);
2388
+ if (
2389
+ modelSelectorPendingReturn
2390
+ && !visibleBlockingDialog()
2391
+ && nativeModelPickerSubmenus().length === 0
2392
+ ) {
2393
+ clearTimeout(modelSelectorReturnTimer);
2394
+ modelSelectorReturnTimer = null;
2395
+ returnToNativeModelSlider();
2232
2396
  }
2233
2397
  } else {
2234
2398
  enhanceNativeModelPicker(parentMenu, selector);
@@ -2244,6 +2408,7 @@ export function createInjectionSource(features, {
2244
2408
  }
2245
2409
 
2246
2410
  function restoreModelSelectorEnhancements() {
2411
+ closeControllerModelMenu();
2247
2412
  clearTimeout(modelSelectorReturnTimer);
2248
2413
  clearTimeout(modelSelectorEffortTransactionTimer);
2249
2414
  modelSelectorTriggerObserver?.disconnect();
@@ -3834,6 +3999,11 @@ export function createInjectionSource(features, {
3834
3999
  document.addEventListener(
3835
4000
  "click",
3836
4001
  (event) => {
4002
+ if (
4003
+ modelSelectorControllerMenu
4004
+ && !modelSelectorControllerMenu.menu.contains(event.target)
4005
+ && !modelSelectorControllerMenu.button.contains(event.target)
4006
+ ) closeControllerModelMenu();
3837
4007
  if (isNativeSummaryButton(event.target) && activePinnedFeatureId) hidePinnedSurfaces();
3838
4008
  if (activePinnedFeatureId && summaryDisplayMode() === "overlay") {
3839
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 [];
@@ -11,6 +11,7 @@ import {
11
11
  selectEnabledFeatures,
12
12
  startFeatureProcesses,
13
13
  } from "./feature-registry.mjs";
14
+ import { applyVisibleReasoningLevels, loadModelCapabilities } from "./model-capabilities.mjs";
14
15
 
15
16
  const runtimeRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
16
17
  const featuresRoot = process.env.CODEX_FEATURES_ROOT ?? path.dirname(runtimeRoot);
@@ -22,8 +23,17 @@ const discoveredFeatures = await discoverFeatures(featuresRoot);
22
23
  if (!discoveredFeatures.length) throw new Error(`No codex-feature.json files found under ${featuresRoot}`);
23
24
  const features = selectEnabledFeatures(discoveredFeatures, process.env.MINECODEX_ENABLED_FEATURES);
24
25
  const modelCatalog = await loadConfiguredModelCatalog();
26
+ const modelCapabilities = await loadModelCapabilities();
25
27
  for (const feature of features) {
26
- if (feature.modelSelector) feature.modelSelector.models = modelCatalog;
28
+ if (feature.modelSelector) {
29
+ feature.modelSelector.models = modelCatalog.map((model) => (
30
+ applyVisibleReasoningLevels(
31
+ model,
32
+ modelCapabilities.get(String(model.capabilityKey ?? model.slug ?? "").toLowerCase()),
33
+ model.supportedReasoningLevels,
34
+ )
35
+ ));
36
+ }
27
37
  }
28
38
 
29
39
  let readyWritePromise = Promise.resolve();
@@ -0,0 +1,86 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+
5
+ const CODEX_REASONING_ORDER = ["low", "medium", "high", "xhigh", "max", "ultra"];
6
+
7
+ function uniqueLevels(values) {
8
+ return [...new Set(values.filter(Boolean))];
9
+ }
10
+
11
+
12
+ function normalizedEfforts(entry) {
13
+ if (!Array.isArray(entry)) return null;
14
+ const levels = entry.map(String).filter((value) => (
15
+ CODEX_REASONING_ORDER.includes(value) || value === "none" || value === "minimal"
16
+ ));
17
+ return levels.length ? uniqueLevels(levels) : null;
18
+ }
19
+
20
+ export function resolveModelRecord(records, modelId) {
21
+ if (!records || typeof records !== "object") return undefined;
22
+ if (Object.prototype.hasOwnProperty.call(records, modelId)) return records[modelId];
23
+ const folded = modelId.toLowerCase();
24
+ for (const [key, value] of Object.entries(records)) {
25
+ if (key.toLowerCase() === folded) return value;
26
+ }
27
+ return undefined;
28
+ }
29
+
30
+ export function mergeCapability(providerConfig, modelId) {
31
+ if (!providerConfig) return null;
32
+ const configuredEfforts = normalizedEfforts(
33
+ resolveModelRecord(providerConfig.modelReasoningEfforts, modelId),
34
+ );
35
+ if (configuredEfforts) {
36
+ return {
37
+ source: "opencodex",
38
+ kind: "effort",
39
+ levels: uniqueLevels(configuredEfforts),
40
+ map: resolveModelRecord(providerConfig.modelReasoningEffortMap, modelId) ?? null,
41
+ rawEfforts: configuredEfforts,
42
+ };
43
+ }
44
+ if (Array.isArray(providerConfig.noReasoningModels) && providerConfig.noReasoningModels.includes(modelId)) {
45
+ return { source: "opencodex", kind: "no-effort", levels: null, map: null, rawEfforts: [] };
46
+ }
47
+ return null;
48
+ }
49
+
50
+ export async function loadModelCapabilities({
51
+ opencodexConfigPath = process.env.OPENCODEX_CONFIG_PATH ?? path.join(os.homedir(), ".opencodex", "config.json"),
52
+ read = readFile,
53
+ } = {}) {
54
+ const configPayload = await read(opencodexConfigPath, "utf8").then((value) => JSON.parse(value)).catch(() => null);
55
+ const providers = configPayload?.providers ?? {};
56
+ const result = new Map();
57
+ for (const [providerId, providerConfig] of Object.entries(providers)) {
58
+ const modelIds = new Set([
59
+ ...Object.keys(providerConfig?.modelReasoningEfforts ?? {}),
60
+ ...Object.keys(providerConfig?.modelReasoningEffortMap ?? {}),
61
+ ...Object.keys(providerConfig?.thinkingToggleModels ?? {}),
62
+ ...Object.keys(providerConfig?.thinkingBudgetModels ?? {}),
63
+ ...(Array.isArray(providerConfig?.noReasoningModels) ? providerConfig.noReasoningModels : []),
64
+ ]);
65
+ for (const modelId of modelIds) {
66
+ const capability = mergeCapability(providerConfig, modelId);
67
+ if (capability) result.set(`${providerId}/${modelId}`.toLowerCase(), capability);
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+
73
+ export function applyVisibleReasoningLevels(catalogModel, capability, fallbackLevels) {
74
+ if (!catalogModel) return catalogModel;
75
+ const rawLevels = Array.isArray(fallbackLevels) ? fallbackLevels : (catalogModel.supportedReasoningLevels ?? []);
76
+ if (!Array.isArray(rawLevels)) return catalogModel;
77
+ if (capability && Array.isArray(capability.levels)) {
78
+ const allowed = new Set(capability.levels);
79
+ const visible = rawLevels.filter((level) => {
80
+ const effort = String(level?.effort ?? level ?? "").toLowerCase();
81
+ return allowed.has(effort);
82
+ });
83
+ return { ...catalogModel, supportedReasoningLevels: visible, modelSelectorCapability: capability };
84
+ }
85
+ return { ...catalogModel, modelSelectorCapability: capability ?? null };
86
+ }