minecodex 2.0.8 → 2.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.
Files changed (59) hide show
  1. package/README.md +18 -4
  2. package/apps/dashboard/app.js +59 -10
  3. package/apps/dashboard/assets/diagnostics.js +18 -0
  4. package/apps/dashboard/index.html +5 -0
  5. package/apps/dashboard/styles.css +5 -1
  6. package/assets/brand/README.md +2 -2
  7. package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +2 -0
  8. package/features/images/src/adapters/codex/image-sources.mjs +8 -1
  9. package/features/images/src/adapters/codex/native-image-source.mjs +2 -2
  10. package/features/images/src/frontend/local-image-source.mjs +7 -0
  11. package/features/images/src/library-prompt-page.mjs +9 -0
  12. package/features/model-slider/src/adapters/codex/controller.mjs +14 -1
  13. package/features/model-slider/src/frontend/install.mjs +4 -1
  14. package/features/notes/src/frontend/entry.mjs +10 -0
  15. package/features/session-status/src/session-status.mjs +11 -0
  16. package/features/sidebar-focus/src/sidebar-focus.mjs +8 -0
  17. package/features/sites/src/frontend/entry.mjs +8 -0
  18. package/features/usage-meter/src/adapters/codex/renderer.mjs +2 -1
  19. package/features/usage-meter/src/usage-meter.mjs +8 -0
  20. package/package.json +1 -14
  21. package/packages/cli/src/commands.mjs +6 -1
  22. package/packages/cli/src/control-server.mjs +4 -1
  23. package/packages/cli/src/platform.mjs +10 -0
  24. package/packages/cli/src/runtime-manager.mjs +11 -3
  25. package/packages/codex-adapter/src/renderer/environment.mjs +6 -1
  26. package/packages/runtime-host/src/codex-injection.mjs +49 -16
  27. package/packages/runtime-host/src/codex-runtime.mjs +51 -0
  28. package/packages/runtime-host/src/feature-registry.mjs +2 -3
  29. package/packages/runtime-host/src/main.mjs +8 -4
  30. package/packages/runtime-host/src/renderer-self-check.mjs +56 -0
  31. package/packages/ui/src/components/composer-menu.mjs +7 -2
  32. package/packages/ui/src/native/theme.mjs +16 -1
  33. package/DESIGN.md +0 -192
  34. package/apps/dashboard/DESIGN.md +0 -67
  35. package/features/hide-upsell-banner/README.md +0 -30
  36. package/features/images/DESIGN.md +0 -151
  37. package/features/images/README.md +0 -37
  38. package/features/model-slider/DESIGN.md +0 -36
  39. package/features/model-slider/README.md +0 -45
  40. package/features/notes/DESIGN.md +0 -74
  41. package/features/notes/README.md +0 -33
  42. package/features/session-status/DESIGN.md +0 -45
  43. package/features/session-status/README.md +0 -43
  44. package/features/sidebar-focus/DESIGN.md +0 -17
  45. package/features/sites/DESIGN.md +0 -82
  46. package/features/sites/README.md +0 -31
  47. package/features/unread-inbox/DESIGN.md +0 -41
  48. package/features/unread-inbox/codex-feature.json +0 -23
  49. package/features/unread-inbox/src/adapters/codex/modules.mjs +0 -58
  50. package/features/unread-inbox/src/adapters/codex/renderer.mjs +0 -198
  51. package/features/unread-inbox/src/domain/notifications.mjs +0 -21
  52. package/features/unread-inbox/src/injection-source.mjs +0 -9
  53. package/features/unread-inbox/src/unread-inbox.mjs +0 -307
  54. package/features/usage-meter/DESIGN.md +0 -49
  55. package/features/usage-meter/README.md +0 -19
  56. package/packages/cli/native/README.md +0 -49
  57. package/packages/codex-adapter/README.md +0 -16
  58. package/packages/runtime-host/README.md +0 -213
  59. package/packages/ui/DESIGN.md +0 -38
@@ -8,6 +8,11 @@ export const RUNTIME_PLUGIN_STATES = Object.freeze([
8
8
  "launching",
9
9
  "ready",
10
10
  "injected",
11
+ "checking",
12
+ "partial",
13
+ "degraded",
14
+ "unavailable",
15
+ "not-applicable",
11
16
  "failed",
12
17
  "stopped",
13
18
  ]);
@@ -48,7 +53,7 @@ export function normalizeRuntimeReady(ready = {}) {
48
53
  : [];
49
54
  const features = [...new Set([
50
55
  ...legacyFeatures,
51
- ...plugins.filter(({ state }) => ["configured", "launching", "ready", "injected"].includes(state)).map(({ featureId }) => featureId),
56
+ ...plugins.filter(({ state }) => ["configured", "launching", "ready", "injected", "checking", "partial", "degraded", "unavailable", "not-applicable"].includes(state)).map(({ featureId }) => featureId),
52
57
  ])];
53
58
  const renderer = ready.renderer && typeof ready.renderer === "object"
54
59
  ? {
@@ -78,12 +83,13 @@ function rendererUnavailableReady(ready) {
78
83
  const normalized = normalizeRuntimeReady(ready);
79
84
  return normalizeRuntimeReady({
80
85
  ...normalized,
81
- plugins: normalized.plugins.map((plugin) => plugin.state === "injected"
82
- ? { ...plugin, state: "ready", health: null }
86
+ plugins: normalized.plugins.map((plugin) => ["injected","partial","degraded","not-applicable","unavailable","checking"].includes(plugin.state)
87
+ ? { ...plugin, state: "checking", health: null, targets:[] }
83
88
  : plugin),
84
89
  renderer: {
85
90
  ...(normalized.renderer ?? {}),
86
91
  discovered: false,
92
+ targets:[],
87
93
  injectedTargets: 0,
88
94
  active: false,
89
95
  runtimeSessionId: normalized.runtimeSessionId,
@@ -170,6 +176,7 @@ export class RuntimeManager {
170
176
  const config = await readConfig(this.paths.configPath);
171
177
  const features = enabledFeatureIds(config);
172
178
  if (typeof this.platform?.codex === "function") await this.platform.codex();
179
+ const codexBuild=await this.platform?.codexBuild?.();
173
180
  await mkdir(this.paths.supportDir, { recursive: true, mode: 0o700 });
174
181
  await unlink(this.paths.runtimeReadyPath).catch((error) => {
175
182
  if (error.code !== "ENOENT") throw error;
@@ -179,6 +186,7 @@ export class RuntimeManager {
179
186
  CODEX_FEATURES_ROOT: this.paths.featuresRoot,
180
187
  CODEX_RUNTIME_PROFILE_DIR: this.paths.profileDir,
181
188
  CODEX_RUNTIME_CDP_PORT: String(this.cdpPort),
189
+ MINECODEX_CODEX_BUILD: codexBuild ?? "",
182
190
  CODEX_IMAGE_HOST_DATA_DIR: this.paths.imagesDataDir,
183
191
  CODEX_NOTES_DATA_DIR: this.paths.notesDataDir,
184
192
  MINECODEX_ENABLED_FEATURES: features.join(","),
@@ -65,7 +65,12 @@ export function createCodexEnvironment() {
65
65
  const group = [...(scroll?.children ?? [])].find(child => child !== ownGroup && child.classList.contains('flex-col') && child.classList.contains('shrink-0'));
66
66
  return node && template && scroll ? { rail:node, template, scroll, group } : null;
67
67
  }
68
- return { composer, composerSurface, rail, railSlot, currentThreadId:element => composer(element)?.threadId ?? null,
68
+ function route() {
69
+ const node = rail();
70
+ const scope = node && props(node, value => Boolean((value.router ?? value.value?.router)?.state?.location));
71
+ return (scope?.router ?? scope?.value?.router)?.state?.location?.pathname ?? null;
72
+ }
73
+ return { composer, composerSurface, rail, railSlot, route, currentThreadId:element => composer(element)?.threadId ?? null,
69
74
  // 仅供 UI/插件 native Adapter 使用,交互层不读取 React props。
70
75
  currentFiber, props, report, diagnostics:() => [...capabilities.values()].map(item => ({...item})) };
71
76
  }
@@ -1,3 +1,4 @@
1
+ import { createRendererSelfCheck, tokenRequirements } from "./renderer-self-check.mjs";
1
2
  import { readFileSync } from "node:fs";
2
3
  import { createCodexEnvironment } from "../../codex-adapter/src/renderer/environment.mjs";
3
4
  // 注入源生成:createInjectionSource 产出注入到 Codex Renderer 的自包含脚本字符串。
@@ -9,7 +10,7 @@ import { createUIInstallerSource } from "../../ui/src/install.mjs";
9
10
  import { randomBytes } from "node:crypto";
10
11
  import { CODEX_THEME_TOKENS, CODEX_THEME_TOKEN_ALIASES, MINECODEX_THEME_BINDINGS } from "../../ui/src/native/tokens.mjs";
11
12
 
12
- export const RUNTIME_VERSION = 108;
13
+ export const RUNTIME_VERSION = 109;
13
14
  const PROJECT_ICON = Object.fromEntries([16, 20].map(size => [size,
14
15
  readFileSync(new URL(`../../ui/assets/minecodex-menu-${size}.svg`, import.meta.url), "utf8"),
15
16
  ]));
@@ -127,14 +128,14 @@ export function createInjectionSource(features, {
127
128
  bindingToken = "test-binding-token",
128
129
  runtimeSessionId = randomBytes(16).toString("hex"),
129
130
  } = {}) {
130
- function install(config, installUI, installModelSliderFeature, createCodexEnvironment) {
131
+ function install(config, installUI, installModelSliderFeature, createCodexEnvironment, createRendererSelfCheck) {
131
132
  if (window.top !== window) return;
132
133
  if (!document.documentElement) {
133
134
  // The document bootstrap runs before the root element exists. Defer the
134
135
  // whole install until the DOM is ready instead of crashing on a null
135
136
  // documentElement (which previously left the runtime permanently absent
136
137
  // after a renderer reload).
137
- document.addEventListener("DOMContentLoaded", () => install(config, installUI, installModelSliderFeature, createCodexEnvironment), { once: true });
138
+ document.addEventListener("DOMContentLoaded", () => install(config, installUI, installModelSliderFeature, createCodexEnvironment, createRendererSelfCheck), { once: true });
138
139
  return;
139
140
  }
140
141
  const currentRuntime = window.__codexPersonalRuntime;
@@ -211,6 +212,7 @@ export function createInjectionSource(features, {
211
212
  }));
212
213
  featureUI.accountMenuEntry?.({ label:"MineCodex", icon:config.projectIcon, externalIcon:config.externalIcon,
213
214
  onOpen:() => requestPageHostAction("minecodex", "open-control-panel", {}) });
215
+ const selfCheck = createRendererSelfCheck(features, config.sessionId, featureUI.theme);
214
216
  const featureById = new Map(features.map((feature) => [feature.id, feature]));
215
217
  const sidebarFeatures = features.filter((feature) => feature.entry?.kind === "sidebar");
216
218
  const toolbarFeatures = features.filter((feature) => feature.entry?.kind === "conversation-toolbar");
@@ -248,9 +250,19 @@ export function createInjectionSource(features, {
248
250
  let nativeTabCapabilityPromise = null;
249
251
  let nativeTabCapability = null;
250
252
  let nativeTabCapabilityError = null;
251
- const modelSlider = installModelSliderFeature(lifetime.signal, {
252
- feature: modelSelectorFeature, codex, ui: featureUI, createIcon: createMarkupIcon, requestRefresh: queueEnsure, locale,
253
- });
253
+ let modelSlider = {ensure() {}, dispose() {}};
254
+ try {
255
+ modelSlider = installModelSliderFeature(lifetime.signal, {
256
+ feature: modelSelectorFeature, codex, ui: featureUI, createIcon: createMarkupIcon, requestRefresh: queueEnsure, locale,
257
+ });
258
+ if (modelSelectorFeature) {
259
+ selfCheck.register(modelSelectorFeature.id, () => modelSlider.selfCheck());
260
+ selfCheck.installed(modelSelectorFeature.id);
261
+ }
262
+ } catch (error) {
263
+ if (modelSelectorFeature) selfCheck.failed(modelSelectorFeature.id);
264
+ console.warn("[codex-personal] model selector installation failed", error);
265
+ }
254
266
 
255
267
  // ========================================================================
256
268
  // page-script 特征安装 — 执行各 feature 声明的页面脚本并登记清理
@@ -271,6 +283,7 @@ export function createInjectionSource(features, {
271
283
  const pageConfig = feature.pageScript.config ??= {};
272
284
  pageConfig.ui = featureUI;
273
285
  pageConfig.codex = codex;
286
+ pageConfig.registerSelfCheck = probe => selfCheck.register(feature.id, probe);
274
287
  pageConfig.requestHostAction = (action, payload) => requestPageHostAction(feature.id, action, payload);
275
288
  const cleanup = installer(
276
289
  lifetime.signal,
@@ -280,7 +293,9 @@ export function createInjectionSource(features, {
280
293
  pageConfig,
281
294
  );
282
295
  if (typeof cleanup === "function") pageScriptCleanups.push(cleanup);
296
+ selfCheck.installed(feature.id);
283
297
  } catch (error) {
298
+ selfCheck.failed(feature.id);
284
299
  console.warn(`[codex-personal] page-script feature failed: ${feature.id}`, error);
285
300
  }
286
301
  }
@@ -2369,6 +2384,7 @@ export function createInjectionSource(features, {
2369
2384
  sessionId: config.sessionId,
2370
2385
  updateHostBinding,
2371
2386
  nativeCapabilities: () => codex.diagnostics(),
2387
+ selfCheck: () => ({...selfCheck.snapshot(),capabilities:codex.diagnostics()}),
2372
2388
  ensure: queueEnsure,
2373
2389
  show: showPageSurface,
2374
2390
  hide: () => {
@@ -2422,6 +2438,7 @@ export function createInjectionSource(features, {
2422
2438
  }
2423
2439
 
2424
2440
  const runtimeFeatures = features.map((feature) => ({
2441
+ requiredTokens:tokenRequirements(`${feature.pageScript?.source ?? ""} ${Object.values(feature.pageScript?.resources ?? {}).filter(value => typeof value === "string").join(" ")} ${feature.modelSelector ? createModelSliderInstallerSource() : ""}`,MINECODEX_THEME_BINDINGS),
2425
2442
  id: feature.id,
2426
2443
  label: feature.label,
2427
2444
  labels: feature.labels ?? null,
@@ -2457,7 +2474,7 @@ export function createInjectionSource(features, {
2457
2474
  themeTokenAliases: CODEX_THEME_TOKEN_ALIASES,
2458
2475
  themeBindings: MINECODEX_THEME_BINDINGS,
2459
2476
  features: runtimeFeatures,
2460
- })}, ${createUIInstallerSource()}, ${createModelSliderInstallerSource()}, ${createCodexEnvironment.toString()})`;
2477
+ })}, ${createUIInstallerSource()}, ${createModelSliderInstallerSource()}, ${createCodexEnvironment.toString()}, ${createRendererSelfCheck.toString()})`;
2461
2478
  }
2462
2479
 
2463
2480
  export function createRuntimeReadyContract({
@@ -2478,15 +2495,28 @@ export function createRuntimeReadyContract({
2478
2495
  features: features.map((feature) => typeof feature === "string" ? feature : feature.id),
2479
2496
  failures,
2480
2497
  runtimeSessionId,
2481
- plugins: plugins.map((plugin) => ({
2482
- featureId: plugin.featureId,
2483
- state: active && (plugin.state === "configured" || plugin.state === "ready")
2484
- ? "injected"
2485
- : plugin.state,
2486
- owned: Boolean(plugin.owned),
2487
- health: plugin.health ?? null,
2488
- failure: plugin.failure ?? null,
2489
- })),
2498
+ plugins: plugins.map(plugin => {
2499
+ const targets=(renderer.targets ?? []).map(target => {
2500
+ const result=target.sessionId === runtimeSessionId && active
2501
+ ? target.plugins?.find(result => result.featureId === plugin.featureId) : null;
2502
+ return {targetId:target.targetId,checkedAt:target.checkedAt ?? null,
2503
+ ...(result ?? {state:"unknown",reason:target.reason ?? "self-check-pending",installation:"pending",checks:[],tokens:[]})};
2504
+ });
2505
+ const states=targets.map(target=>target.state);
2506
+ // 未观测的窗口也参与汇总,不能凭一项成功宣称全部窗口可用。
2507
+ const missing=Math.max(0,discoveredTargets-targets.length);
2508
+ let state=plugin.state;
2509
+ const definition=features.find(feature=>typeof feature === "object" && feature.id === plugin.featureId);
2510
+ if (definition?.healthUrl && state === "configured") state="launching";
2511
+ else if (active && ["configured","ready","injected"].includes(state)) {
2512
+ if (states.includes("available")) state=missing || states.some(value=>!["available","not-applicable"].includes(value)) ? "partial" : "injected";
2513
+ else if (states.includes("degraded")) state="degraded";
2514
+ else if (states.some(value=>["failed","unavailable"].includes(value))) state="unavailable";
2515
+ else if (!missing && states.length && states.every(value=>value === "not-applicable")) state="not-applicable";
2516
+ else state="checking";
2517
+ } else if (state === "injected") state="checking";
2518
+ return {featureId:plugin.featureId,state,serviceState:plugin.state,owned:Boolean(plugin.owned),health:plugin.health ?? null,failure:plugin.failure ?? null,targets};
2519
+ }),
2490
2520
  renderer: {
2491
2521
  discovered,
2492
2522
  discoveredTargets,
@@ -2495,6 +2525,9 @@ export function createRuntimeReadyContract({
2495
2525
  healthy: active && targetFailures.length === 0 && injectedTargets === discoveredTargets,
2496
2526
  targetFailures,
2497
2527
  runtimeSessionId: renderer.runtimeSessionId ?? runtimeSessionId,
2528
+ targets:renderer.targets ?? [],
2529
+ codexBuild:renderer.codexBuild ?? null,
2530
+ buildSource:renderer.buildSource ?? null,
2498
2531
  },
2499
2532
  };
2500
2533
  }
@@ -75,6 +75,7 @@ export class CodexRuntime {
75
75
  surfaceLoadTimeoutMs = 5_000,
76
76
  surfaceFramePollMs = 25,
77
77
  featureInstanceId = null,
78
+ codexBuild = null,
78
79
  runtimeSessionId = randomBytes(16).toString("hex"),
79
80
  onStatusChange = null,
80
81
  sitesService = null,
@@ -99,12 +100,14 @@ export class CodexRuntime {
99
100
  this.surfaceLoadTimeoutMs = surfaceLoadTimeoutMs;
100
101
  this.surfaceFramePollMs = surfaceFramePollMs;
101
102
  this.featureInstanceId = featureInstanceId;
103
+ this.codexBuild = codexBuild;
102
104
  this.runtimeSessionId = runtimeSessionId;
103
105
  this.onStatusChange = onStatusChange;
104
106
  this.onManagedCodexPidChange = onManagedCodexPidChange;
105
107
  this.injectedTargets = new Set();
106
108
  this.rendererTargetIds = new Set();
107
109
  this.targetFailures = new Map();
110
+ this.targetChecks = new Map();
108
111
  this.clients = new Map();
109
112
  this.scriptIds = new Map();
110
113
  this.bindingTokens = new Map();
@@ -171,6 +174,7 @@ export class CodexRuntime {
171
174
  })).sort((left, right) => left.targetId.localeCompare(right.targetId));
172
175
  const active = !this.stopping && this.rendererDiscovered && injectedTargets > 0;
173
176
  return {
177
+ ...(this.codexBuild ? {codexBuild:this.codexBuild,buildSource:"installation-at-start"} : {}),
174
178
  discovered: this.rendererDiscovered,
175
179
  discoveredTargets,
176
180
  injectedTargets,
@@ -178,6 +182,7 @@ export class CodexRuntime {
178
182
  healthy: active && targetFailures.length === 0 && injectedTargets === discoveredTargets,
179
183
  targetFailures,
180
184
  runtimeSessionId: this.runtimeSessionId,
185
+ targets:[...this.rendererTargetIds].map(targetId => ({targetId,...(this.targetChecks.get(targetId) ?? {sessionId:this.runtimeSessionId,checkedAt:null,plugins:[],tokens:[],reason:"self-check-pending"})})),
181
186
  };
182
187
  }
183
188
 
@@ -192,6 +197,7 @@ export class CodexRuntime {
192
197
  && previous.active === status.active
193
198
  && previous.healthy === status.healthy
194
199
  && JSON.stringify(previous.targetFailures) === JSON.stringify(status.targetFailures)
200
+ && JSON.stringify(previous.targets) === JSON.stringify(status.targets)
195
201
  && previous.runtimeSessionId === status.runtimeSessionId
196
202
  ) return;
197
203
  this.lastRendererStatus = status;
@@ -237,6 +243,7 @@ export class CodexRuntime {
237
243
  this.injectedTargets.clear();
238
244
  this.rendererTargetIds.clear();
239
245
  this.targetFailures.clear();
246
+ this.targetChecks.clear();
240
247
  this.rendererDiscovered = false;
241
248
  this.browserClient?.socket.close();
242
249
  this.browserClient = null;
@@ -695,6 +702,7 @@ export class CodexRuntime {
695
702
  this.clients.delete(id);
696
703
  this.scriptIds.delete(id);
697
704
  this.bindingTokens.delete(id);
705
+ this.targetChecks.delete(id);
698
706
  this.hostActionChains.delete(id);
699
707
  this.injectedTargets.delete(id);
700
708
  this.targetFailures.delete(id);
@@ -705,6 +713,18 @@ export class CodexRuntime {
705
713
  if (removedTarget || rendererChanged) this.notifyStatusChange();
706
714
 
707
715
  for (const target of targets) {
716
+ if (this.injectedTargets.has(target.id)) {
717
+ const present=await this.checkTarget(target.id);
718
+ if (present) continue;
719
+ const previousClient=this.clients.get(target.id),identifier=this.scriptIds.get(target.id);
720
+ if (identifier) await previousClient?.send("Page.removeScriptToEvaluateOnNewDocument",{identifier}).catch(()=>{});
721
+ previousClient?.socket.close();
722
+ this.clients.delete(target.id);
723
+ this.scriptIds.delete(target.id);
724
+ this.bindingTokens.delete(target.id);
725
+ this.injectedTargets.delete(target.id);
726
+ this.targetChecks.delete(target.id);
727
+ }
708
728
  if (target.type !== "page" || target.url !== "app://-/index.html" || this.injectedTargets.has(target.id)) {
709
729
  continue;
710
730
  }
@@ -724,6 +744,12 @@ export class CodexRuntime {
724
744
  await client.send("Runtime.addBinding", { name: HOST_BINDING_NAME });
725
745
  const bindingToken = randomBytes(24).toString("hex");
726
746
  this.bindingTokens.set(target.id, bindingToken);
747
+ client.on?.("Runtime.executionContextsCleared", () => {
748
+ if (this.clients.get(target.id) !== client) return;
749
+ // 新文档撤销启动检查,下一次接入周期重新检查一次。
750
+ this.targetChecks.delete(target.id);
751
+ this.notifyStatusChange();
752
+ });
727
753
  client.on?.("Runtime.bindingCalled", (params) => {
728
754
  let action;
729
755
  try { action = JSON.parse(params.payload)?.action; } catch { return; }
@@ -756,6 +782,7 @@ export class CodexRuntime {
756
782
  this.clients.set(target.id, client);
757
783
  this.scriptIds.set(target.id, scriptIdentifier);
758
784
  this.injectedTargets.add(target.id);
785
+ await this.checkTarget(target.id);
759
786
  this.targetFailures.delete(target.id);
760
787
  this.notifyStatusChange();
761
788
  } catch (error) {
@@ -784,6 +811,28 @@ export class CodexRuntime {
784
811
  this.notifyStatusChange();
785
812
  }
786
813
 
814
+ async checkTarget(targetId) {
815
+ const client=this.clients.get(targetId);
816
+ if (!client || this.stopping) return false;
817
+ if (this.targetChecks.has(targetId)) return true;
818
+ try {
819
+ const response=await client.send("Runtime.evaluate", {
820
+ expression:`(() => { const runtime=window.__codexPersonalRuntime; return runtime?.version === ${RUNTIME_VERSION} && runtime.sessionId === ${JSON.stringify(this.runtimeSessionId)} ? runtime.selfCheck() : null; })()`,
821
+ returnByValue:true,
822
+ }, {timeoutMs:3_000});
823
+ if (this.clients.get(targetId) !== client || this.stopping) return false;
824
+ const snapshot=response.result?.value;
825
+ if (!snapshot) return false;
826
+ if (snapshot.sessionId !== this.runtimeSessionId || !Array.isArray(snapshot.plugins)) throw new Error("Invalid self-check snapshot");
827
+ this.targetChecks.set(targetId,snapshot);
828
+ return true;
829
+ } catch {
830
+ // 无法读到当前证据时立即撤销旧成功,不把临时 CDP 故障解释为插件不兼容。
831
+ this.targetChecks.set(targetId,{sessionId:this.runtimeSessionId,checkedAt:null,plugins:[],tokens:[],reason:"self-check-unavailable"});
832
+ return true;
833
+ }
834
+ }
835
+
787
836
  async publishSessionStatus(onlyTargetId = null) {
788
837
  for (const [targetId, client] of this.clients) {
789
838
  if (onlyTargetId && targetId !== onlyTargetId) continue;
@@ -815,6 +864,7 @@ export class CodexRuntime {
815
864
  if (this.clients.get(targetId) === client) {
816
865
  client.socket.close();
817
866
  this.injectedTargets.delete(targetId);
867
+ this.targetChecks.delete(targetId);
818
868
  this.notifyStatusChange();
819
869
  }
820
870
  }
@@ -858,6 +908,7 @@ export class CodexRuntime {
858
908
  this.injectedTargets.clear();
859
909
  this.rendererTargetIds.clear();
860
910
  this.targetFailures.clear();
911
+ this.targetChecks.clear();
861
912
  this.rendererDiscovered = false;
862
913
  this.notifyStatusChange();
863
914
  this.monitorPromise = null;
@@ -834,10 +834,9 @@ export async function startFeatureProcesses(features, loggerOrOptions = console,
834
834
  getFailures() {
835
835
  return records.map(featureFailure).filter(Boolean);
836
836
  },
837
- getStatuses({ rendererActive = false } = {}) {
837
+ getStatuses() {
838
838
  return records.map((record) => {
839
- let state = record.state;
840
- if (rendererActive && (state === "configured" || state === "ready")) state = "injected";
839
+ const state = record.state;
841
840
  return {
842
841
  featureId: record.featureId,
843
842
  state,
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import os from "node:os";
3
- import { writeFile } from "node:fs/promises";
3
+ import { writeFile, rename } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { CodexRuntime, createRuntimeReadyContract } from "./codex-runtime.mjs";
@@ -43,16 +43,19 @@ const updateCodexPidFile = createManagedPidFileWriter(process.env.MINECODEX_CODE
43
43
  function writeReadyFile() {
44
44
  if (!process.env.MINECODEX_RUNTIME_READY_FILE) return Promise.resolve();
45
45
  const filePath = process.env.MINECODEX_RUNTIME_READY_FILE;
46
- readyWritePromise = readyWritePromise.catch(() => {}).then(() => writeFile(filePath, `${JSON.stringify(
46
+ readyWritePromise = readyWritePromise.catch(() => {}).then(async () => {
47
+ await writeFile(`${filePath}.pending`, `${JSON.stringify(
47
48
  createRuntimeReadyContract({
48
49
  pid: process.pid,
49
50
  features,
50
- plugins: featureProcesses.getStatuses({ rendererActive: runtime?.getRendererStatus().active }),
51
+ plugins: featureProcesses.getStatuses(),
51
52
  failures: featureProcesses.getFailures(),
52
53
  renderer: runtime?.getRendererStatus(),
53
54
  runtimeSessionId: runtime?.runtimeSessionId,
54
55
  }),
55
- )}\n`, { mode: 0o600 }));
56
+ )}\n`, { mode: 0o600 });
57
+ await rename(`${filePath}.pending`,filePath);
58
+ });
56
59
  return readyWritePromise;
57
60
  }
58
61
 
@@ -71,6 +74,7 @@ featureProcesses = await startFeatureProcesses(features, {
71
74
  runtime = new CodexRuntime({
72
75
  features,
73
76
  featureInstanceId,
77
+ codexBuild:process.env.MINECODEX_CODEX_BUILD || null,
74
78
  profileDir,
75
79
  cdpPort,
76
80
  onManagedCodexPidChange: updateCodexPidFile,
@@ -0,0 +1,56 @@
1
+ // 只管理安装证据;必要原生能力与适用场景由功能所属 Adapter 判定。
2
+ export function tokenRequirements(source, bindings) {
3
+ const nativeNames=new Map(Object.entries(bindings).map(([native,project])=>[project,native]));
4
+ const requirements=new Map();
5
+ for (const match of source.matchAll(/var\((--mcx-[a-z0-9-]+)\s*([,)])/g)) {
6
+ const name=nativeNames.get(match[1]) ?? match[1];
7
+ requirements.set(name,{name,required:match[2] === ")" || requirements.get(name)?.required === true,
8
+ ...(nativeNames.has(match[1]) ? {} : {unmapped:true})});
9
+ }
10
+ return [...requirements.values()];
11
+ }
12
+
13
+ export function createRendererSelfCheck(features, sessionId, theme) {
14
+ const records = new Map(features.map(feature => [feature.id, { featureId:feature.id, tokens:feature.requiredTokens ?? [], installation:"pending", probe:null }]));
15
+ function register(featureId, probe) {
16
+ if (typeof probe !== "function") throw new TypeError("Self-check must be a function");
17
+ records.get(featureId).probe = probe;
18
+ }
19
+ function installed(featureId) { records.get(featureId).installation = "installed"; }
20
+ function failed(featureId) { records.get(featureId).installation = "failed"; }
21
+ function snapshot() {
22
+ return {
23
+ sessionId,
24
+ checkedAt:Date.now(),
25
+ tokens:theme.diagnostics(),
26
+ plugins:[...records.values()].map(record => {
27
+ const base = { featureId:record.featureId, installation:record.installation };
28
+ if (record.installation === "failed") return {...base,state:"failed",reason:"installation-failed",checks:[]};
29
+ if (record.installation !== "installed" || !record.probe) return {...base,state:"unknown",reason:"self-check-pending",checks:[]};
30
+ try {
31
+ const result = record.probe();
32
+ const states = ["available","not-applicable","unavailable","unknown"];
33
+ if (!states.includes(result?.state)) throw new TypeError("Invalid self-check result");
34
+ const scopes = result.scopes ?? [];
35
+ const requirements=record.tokens.map(token=>typeof token === "string" ? {name:token,required:true} : token);
36
+ const tokens = scopes.filter(scope => scope.element?.isConnected).flatMap(scope =>
37
+ theme.checkTokens(scope.element,requirements.map(token=>token.name)).map(token => {
38
+ const requirement=requirements.find(item=>item.name === token.name);
39
+ return {...token,scope:scope.name,required:requirement.required,
40
+ ...(requirement.unmapped ? {available:false,reason:"token-mapping-unavailable"} : {})};
41
+ }));
42
+ const missingTokens = tokens.some(token => token.required && (!token.available || (token.projectName && (!token.projectValue || token.scopeMatches === false))));
43
+ // 不导出异常文本、原生对象、凭据或用户内容。
44
+ return {...base,state:result.state === "available" && missingTokens ? "degraded" : result.state,
45
+ reason:result.state === "available" && missingTokens ? "design-token-unavailable" : result.reason ?? null,tokens,
46
+ checks:(result.checks ?? []).map(check => ({
47
+ name:check.name,available:check.available === true,reason:check.reason ?? null,
48
+ }))};
49
+ } catch {
50
+ return {...base,state:"unavailable",reason:"self-check-failed",checks:[]};
51
+ }
52
+ }),
53
+ };
54
+ }
55
+ return {register,installed,failed,snapshot};
56
+ }
@@ -7,11 +7,16 @@ export function installComposerMenu(signal, ui) {
7
7
  ui.surface(node, "composer-menu");
8
8
  ui.layer(node, "floating");
9
9
  const rect = body.getBoundingClientRect();
10
+ const root = anchor.closest("[data-codex-composer-root], [data-chatgpt-composer]");
11
+ // 首页 Add 向下展开;已有会话保留向上的输入框菜单。
12
+ const below = root?.getAttribute("data-composer-placement") === "home";
10
13
  const zoom = Number.parseFloat(getComputedStyle(node).zoom) || 1;
11
14
  node.style.left = `${rect.left / zoom}px`;
12
- node.style.top = `${(rect.top - gap) / zoom}px`;
15
+ node.style.top = `${(below ? rect.bottom + gap : rect.top - gap) / zoom}px`;
16
+ node.style.transform = below ? "translateY(0)" : "translateY(-100%)";
13
17
  node.style.width = `${rect.width / zoom}px`;
14
- node.style.maxHeight = `${Math.max(0, Math.min(maxHeight, (rect.top - 16) / zoom))}px`;
18
+ const availableHeight = below ? innerHeight - rect.bottom - gap - 16 : rect.top - 16;
19
+ node.style.maxHeight = `${Math.max(0, Math.min(maxHeight, availableHeight / zoom))}px`;
15
20
  return node;
16
21
  },
17
22
  };
@@ -31,5 +31,20 @@ export function createNativeThemeAdapter(themeTokenNames,themeTokenAliases,theme
31
31
  }
32
32
 
33
33
  function dispose() {for (const [name,value] of previousThemeBindings) { if(value) document.documentElement.style.setProperty(name,value); else document.documentElement.style.removeProperty(name); } }
34
- return {read,dispose};
34
+ function checkTokens(element, names) {
35
+ const style = getComputedStyle(element);
36
+ return names.map(name => {
37
+ const candidates = [name,...(themeTokenAliases[name] ?? [])].flatMap(candidate => candidate.startsWith("--color-")
38
+ ? [candidate,candidate.replace("--color-","--app-color-")] : [candidate]);
39
+ const source = candidates.find(candidate => style.getPropertyValue(candidate).trim());
40
+ const value = source ? style.getPropertyValue(source).trim() : null;
41
+ const projectName = themeBindings[name];
42
+ const projectValue = projectName ? style.getPropertyValue(projectName).trim() || null : null;
43
+ return {name,source:source ?? null,value,available:Boolean(value),
44
+ projectName:projectName ?? null,projectValue,scopeMatches:!projectName || projectValue === value,
45
+ reason:!value ? "native-token-unavailable" : projectName && !projectValue ? "project-token-unavailable"
46
+ : projectName && projectValue !== value ? "scope-mapping-mismatch" : null};
47
+ });
48
+ }
49
+ return {read,dispose,checkTokens,diagnostics:() => checkTokens(document.documentElement,themeTokenNames)};
35
50
  }