minecodex 0.1.16 → 0.1.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "0.1.16",
3
+ "version": "0.1.19",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -613,6 +613,18 @@ export class MacPlatformAdapter {
613
613
  return true;
614
614
  }
615
615
 
616
+ async codexProcessStartedMsAgo(processInfo) {
617
+ if (!processInfo || !Number.isInteger(processInfo.pid) || processInfo.pid <= 1) return null;
618
+ const { stdout } = await this.execFile("/bin/ps", ["-axo", "pid=,lstart="]);
619
+ const line = String(stdout).split("\n").find((row) => {
620
+ const match = row.match(/\s*(\d+)\s+(.*)/);
621
+ return match?.[1] === String(processInfo.pid);
622
+ });
623
+ const startedAt = line ? Date.parse(line.replace(/^\s*\d+\s+/, "")) : Number.NaN;
624
+ if (!line || !Number.isFinite(startedAt)) return null;
625
+ return Math.max(0, Date.now() - startedAt);
626
+ }
627
+
616
628
  async terminateNativeCodex(pid, expectedCommand = null) {
617
629
  if (!Number.isInteger(pid) || pid <= 1) {
618
630
  throw new Error("MineCodex requires an exact ChatGPT PID to terminate.");
@@ -120,7 +120,8 @@ export class RuntimeManager {
120
120
  waitForReady: readyWaiter = waitForReady,
121
121
  sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
122
122
  stopTimeoutMs = 5_000,
123
- manualLaunchPollMs = 1_000,
123
+ manualLaunchPollMs = 150,
124
+ manualTakeoverGraceMs = 60_000,
124
125
  cdpPort = Number(process.env.CODEX_RUNTIME_CDP_PORT ?? 9231),
125
126
  launchCoordinator = null,
126
127
  }) {
@@ -132,6 +133,7 @@ export class RuntimeManager {
132
133
  this.sleep = sleep;
133
134
  this.stopTimeoutMs = stopTimeoutMs;
134
135
  this.manualLaunchPollMs = manualLaunchPollMs;
136
+ this.manualTakeoverGraceMs = manualTakeoverGraceMs;
135
137
  this.cdpPort = cdpPort;
136
138
  this.child = null;
137
139
  this.appliedFeatures = [];
@@ -327,6 +329,19 @@ export class RuntimeManager {
327
329
  this.manualLaunchArmed = false;
328
330
  }
329
331
 
332
+ async isManualTakeoverAllowed(processInfo) {
333
+ if (typeof this.platform?.codexProcessStartedMsAgo !== "function") return true;
334
+ let startedMsAgo;
335
+ try {
336
+ startedMsAgo = await this.platform.codexProcessStartedMsAgo(processInfo);
337
+ } catch (error) {
338
+ this.logger.warn?.("MineCodex could not read the ChatGPT start time", error.message);
339
+ return true;
340
+ }
341
+ if (!Number.isInteger(startedMsAgo) || startedMsAgo < 0) return true;
342
+ return startedMsAgo < this.manualTakeoverGraceMs;
343
+ }
344
+
330
345
  async reconcileManualCodexLaunch() {
331
346
  if (this.manualLaunchMonitorStopped || this.manualLaunchSuppressionDepth > 0) return false;
332
347
  const snapshot = await this.nativeCodexSnapshot();
@@ -372,6 +387,14 @@ export class RuntimeManager {
372
387
  return false;
373
388
  }
374
389
  try {
390
+ if (!(await this.isManualTakeoverAllowed(currentProcess))) {
391
+ this.enhancementFailure = {
392
+ code: "OFFICIAL_CHATGPT_IN_USE",
393
+ message: "MineCodex left the official ChatGPT instance running because it was already in use.",
394
+ phase: "launch",
395
+ };
396
+ return null;
397
+ }
375
398
  const result = await this.launchCoordinator.ensureObservedLaunch(currentProcess, {
376
399
  stopRuntime: () => this.stopInternal(),
377
400
  startRuntime: () => this.startInternal(),
@@ -298,6 +298,8 @@ export function createInjectionSource(features, {
298
298
  let modelSelectorTriggerObserver = null;
299
299
  let modelSelectorObservedTrigger = null;
300
300
  let modelSelectorNativeFastIcons = null;
301
+ // Fast 图标来源标记:bundle 静态提取 vs DOM 克隆,DOM 克隆优先以保持像素一致。
302
+ let modelSelectorNativeFastIconSources = null;
301
303
  const modelSelectorStyle = document.createElement("style");
302
304
  modelSelectorStyle.setAttribute("data-codex-model-slider-style", "");
303
305
  modelSelectorStyle.textContent = `
@@ -376,6 +378,17 @@ export function createInjectionSource(features, {
376
378
  if (modelSelectorFeature) {
377
379
  document.documentElement.setAttribute("data-codex-model-slider-catalog-ready", "");
378
380
  document.documentElement.append(modelSelectorStyle);
381
+ // 启动阶段就解析 Fast 图标:先读持久化的原生克隆,再从 app bundle 静态提取兜底,
382
+ // 避免首次会话必须等用户点开弹窗后 DOM 里才出现图标。
383
+ const persistedIcons = loadPersistedNativeFastIcons();
384
+ if (persistedIcons) {
385
+ modelSelectorNativeFastIcons = persistedIcons;
386
+ modelSelectorNativeFastIconSources = {};
387
+ for (const state of Object.keys(persistedIcons)) {
388
+ modelSelectorNativeFastIconSources[state] = "persisted";
389
+ }
390
+ }
391
+ void loadNativeFastIconFromBundle();
379
392
  }
380
393
 
381
394
  for (const feature of pageScriptFeatures) {
@@ -649,9 +662,99 @@ export function createInjectionSource(features, {
649
662
  return null;
650
663
  }
651
664
 
665
+ const modelSelectorFastIconStorageKey = "codex-model-slider:fast-icons:v1";
666
+
667
+ function persistNativeFastIcons() {
668
+ if (!modelSelectorNativeFastIcons) return;
669
+ try {
670
+ const payload = {};
671
+ for (const [state, template] of Object.entries(modelSelectorNativeFastIcons)) {
672
+ payload[state] = template.outerHTML;
673
+ }
674
+ localStorage.setItem(modelSelectorFastIconStorageKey, JSON.stringify(payload));
675
+ } catch {
676
+ // localStorage 不可用时静默降级,仅本次会话内生效。
677
+ }
678
+ }
679
+
680
+ function loadPersistedNativeFastIcons() {
681
+ try {
682
+ const payload = JSON.parse(localStorage.getItem(modelSelectorFastIconStorageKey) ?? "null");
683
+ if (!payload || typeof payload !== "object") return null;
684
+ const icons = {};
685
+ for (const [state, markup] of Object.entries(payload)) {
686
+ if (typeof markup !== "string") continue;
687
+ const container = document.createElement("template");
688
+ container.innerHTML = markup;
689
+ const svg = container.content.querySelector("svg");
690
+ if (svg) icons[state] = svg;
691
+ }
692
+ return Object.keys(icons).length ? icons : null;
693
+ } catch {
694
+ return null;
695
+ }
696
+ }
697
+
698
+ function extractFastIconPath(source, prefix) {
699
+ const match = String(source).match(new RegExp(`d:\`(${prefix}[^\`]*)\``));
700
+ return match?.[1] ?? null;
701
+ }
702
+
703
+ async function loadNativeFastIconFromBundle() {
704
+ const sourceLink = document.querySelector('link[href*="/assets/app-initial-"][href$=".js"]');
705
+ if (!sourceLink?.href) return;
706
+ try {
707
+ const source = await fetch(sourceLink.href).then((response) => response.text());
708
+ if (!modelSelectorNativeFastIcons) {
709
+ modelSelectorNativeFastIcons = {};
710
+ modelSelectorNativeFastIconSources = {};
711
+ }
712
+ let gained = false;
713
+ const define = (state, prefix, definition) => {
714
+ if (modelSelectorNativeFastIcons[state]) return;
715
+ const path = extractFastIconPath(source, prefix);
716
+ if (!path) return;
717
+ const svg = createModelSelectorIcon({
718
+ viewBox: definition.viewBox,
719
+ markup: definition.transform
720
+ ? `<g transform="${definition.transform}"><path d="${path}" fill="currentColor" /></g>`
721
+ : `<path d="${path}" fill="currentColor" />`,
722
+ }, { brand: true });
723
+ if (!svg) return;
724
+ modelSelectorNativeFastIcons[state] = svg;
725
+ modelSelectorNativeFastIconSources[state] = "bundle";
726
+ gained = true;
727
+ };
728
+ define("active", "M11\\.9125 21\\.4125", { viewBox: "0 0 24 24" });
729
+ define("inactive", "M7\\.38 16\\.2207", { viewBox: "0 0 20 20", transform: "translate(2.43 1.609)" });
730
+ if (gained) {
731
+ persistNativeFastIcons();
732
+ applyNativeFastIconsToShells();
733
+ queueEnsure();
734
+ }
735
+ } catch {
736
+ // 私有资源移动时保持原生控件不变,等待 DOM 克隆兜底。
737
+ }
738
+ }
739
+
740
+ function hasVisibleFastControl() {
741
+ return Boolean(
742
+ document.querySelector("[data-codex-model-slider-fast]")
743
+ || document.querySelector('[class*="_FastModeToggle_"]')
744
+ || document.querySelector('[class*="_ModelPickerTriggerFastIndicator_"] svg'),
745
+ );
746
+ }
747
+
652
748
  function captureNativeFastIcon() {
749
+ // Fast 图标只在 Fast 控件渲染后才出现在 DOM(打开菜单或 GPT 模型);
750
+ // 启动阶段已有 bundle/持久化兜底,因此无可见 Fast 控件时不做全页扫描。
751
+ if (!hasVisibleFastControl()) {
752
+ return modelSelectorNativeFastIcons;
753
+ }
653
754
  const capture = (state, pathPrefix) => {
654
- if (modelSelectorNativeFastIcons?.[state]) return;
755
+ const current = modelSelectorNativeFastIcons?.[state];
756
+ const currentSource = modelSelectorNativeFastIconSources?.[state];
757
+ if (current && currentSource === "dom") return;
655
758
  const path = Array.from(document.querySelectorAll("svg path")).find(
656
759
  (candidate) => candidate.getAttribute("d")?.startsWith(pathPrefix),
657
760
  );
@@ -661,14 +764,33 @@ export function createInjectionSource(features, {
661
764
  template.removeAttribute("id");
662
765
  template.removeAttribute("width");
663
766
  template.removeAttribute("height");
664
- if (!modelSelectorNativeFastIcons) modelSelectorNativeFastIcons = {};
767
+ if (!modelSelectorNativeFastIcons) {
768
+ modelSelectorNativeFastIcons = {};
769
+ modelSelectorNativeFastIconSources = {};
770
+ }
665
771
  modelSelectorNativeFastIcons[state] = template;
772
+ modelSelectorNativeFastIconSources[state] = "dom";
666
773
  };
667
774
  capture("active", "M11.9125 21.4125");
668
775
  capture("inactive", "M7.38 16.2207");
776
+ const ready = Boolean(
777
+ modelSelectorNativeFastIcons?.active && modelSelectorNativeFastIcons?.inactive,
778
+ );
779
+ if (ready) {
780
+ persistNativeFastIcons();
781
+ applyNativeFastIconsToShells();
782
+ }
669
783
  return modelSelectorNativeFastIcons;
670
784
  }
671
785
 
786
+ function applyNativeFastIconsToShells() {
787
+ document.querySelectorAll("[data-codex-model-slider-fast]").forEach((button) => {
788
+ const icon = createNativeFastIcon(button.getAttribute("aria-pressed") === "true");
789
+ const content = button.querySelector("[data-codex-model-slider-fast-content]");
790
+ if (icon && content) content.replaceChildren(icon);
791
+ });
792
+ }
793
+
672
794
  function createNativeFastIcon(active) {
673
795
  const template = captureNativeFastIcon()?.[active ? "active" : "inactive"];
674
796
  if (!template) return null;
@@ -2992,42 +3114,6 @@ export function createInjectionSource(features, {
2992
3114
  return null;
2993
3115
  }
2994
3116
 
2995
- function nativeJsxRuntime(appModule) {
2996
- const candidates = Object.values(appModule).filter((value) => {
2997
- if (typeof value !== "function" || value.length !== 0) return false;
2998
- let source;
2999
- try {
3000
- source = Function.prototype.toString.call(value);
3001
- } catch {
3002
- return false;
3003
- }
3004
- if (source.length > 1200) return false;
3005
- if (
3006
- source.includes("useState")
3007
- || source.includes("memo_cache_sentinel")
3008
- || source.includes("(0,")
3009
- || source.startsWith("class ")
3010
- ) return false;
3011
- return source.includes("jsxs") && source.includes("Fragment");
3012
- });
3013
- for (const candidate of candidates) {
3014
- let runtime;
3015
- try {
3016
- runtime = candidate();
3017
- } catch {
3018
- continue;
3019
- }
3020
- if (
3021
- runtime
3022
- && typeof runtime === "object"
3023
- && typeof runtime.jsx === "function"
3024
- && typeof runtime.jsxs === "function"
3025
- && "Fragment" in runtime
3026
- ) return runtime;
3027
- }
3028
- return null;
3029
- }
3030
-
3031
3117
  async function loadNativeTabCapability() {
3032
3118
  if (nativeTabCapability) return nativeTabCapability;
3033
3119
  if (nativeTabCapabilityPromise) return nativeTabCapabilityPromise;
@@ -3045,15 +3131,9 @@ export function createInjectionSource(features, {
3045
3131
  && value.panelId === "right"
3046
3132
  ));
3047
3133
  if (!controller) throw new Error("Codex right-panel controller is unavailable");
3048
- const jsx = nativeJsxRuntime(appModule);
3049
- if (
3050
- !controller?.tabById$
3051
- || typeof jsx?.jsx !== "function"
3052
- || typeof jsx?.jsxs !== "function"
3053
- ) throw new Error("Codex native tab capability is incomplete");
3134
+ if (!controller?.tabById$) throw new Error("Codex native tab capability is incomplete");
3054
3135
  nativeTabCapability = {
3055
3136
  controller,
3056
- jsx,
3057
3137
  moduleAsset: moduleUrl.split("/").pop(),
3058
3138
  };
3059
3139
  nativeTabCapabilityError = null;
@@ -3068,10 +3148,20 @@ export function createInjectionSource(features, {
3068
3148
  return capability;
3069
3149
  }
3070
3150
 
3071
- function nativeDetailIcon(feature, detail, jsx) {
3151
+ function nativeReactElement(type, props) {
3152
+ return {
3153
+ $$typeof: Symbol.for("react.transitional.element"),
3154
+ type,
3155
+ key: null,
3156
+ ref: null,
3157
+ props: props ?? {},
3158
+ };
3159
+ }
3160
+
3161
+ function nativeDetailIcon(feature, detail) {
3072
3162
  const icon = detail.icon ?? feature.icon;
3073
3163
  if (!icon?.markup) return undefined;
3074
- return jsx.jsx("svg", {
3164
+ return nativeReactElement("svg", {
3075
3165
  width: 16,
3076
3166
  height: 16,
3077
3167
  viewBox: icon.viewBox ?? "0 0 24 24",
@@ -3086,14 +3176,13 @@ export function createInjectionSource(features, {
3086
3176
  });
3087
3177
  }
3088
3178
 
3089
- function nativeDetailComponent(feature, detail, capability) {
3179
+ function nativeDetailComponent(feature, detail) {
3090
3180
  const detailKey = `${feature.id}:${detail.id}`;
3091
3181
  let Component = nativeDetailComponents.get(detailKey);
3092
3182
  if (Component) return Component;
3093
- const { jsx } = capability;
3094
3183
  const frameName = surfaceFrameName(feature.id);
3095
3184
  Component = function CodexPersonalDetailTab() {
3096
- return jsx.jsx("div", {
3185
+ return nativeReactElement("div", {
3097
3186
  "data-codex-personal-native-detail": detailKey,
3098
3187
  style: {
3099
3188
  height: "100%",
@@ -3101,7 +3190,7 @@ export function createInjectionSource(features, {
3101
3190
  overflow: "hidden",
3102
3191
  background: "var(--color-token-main-surface-primary)",
3103
3192
  },
3104
- children: jsx.jsx("iframe", {
3193
+ children: nativeReactElement("iframe", {
3105
3194
  name: frameName,
3106
3195
  src: "about:blank",
3107
3196
  title: detail.label,
@@ -3152,7 +3241,7 @@ export function createInjectionSource(features, {
3152
3241
  return null;
3153
3242
  }
3154
3243
 
3155
- const { controller, jsx } = capability;
3244
+ const { controller } = capability;
3156
3245
  const tabId = `${feature.id}-${detail.id}`;
3157
3246
  const detailKey = `${feature.id}:${detail.id}`;
3158
3247
  let session = Array.from(nativeOpenTabSessions).find((candidate) => (
@@ -3171,12 +3260,12 @@ export function createInjectionSource(features, {
3171
3260
  }
3172
3261
 
3173
3262
  try {
3174
- controller.openTab(scope, nativeDetailComponent(feature, detail, capability), {
3263
+ controller.openTab(scope, nativeDetailComponent(feature, detail), {
3175
3264
  id: tabId,
3176
3265
  kind: tabId,
3177
3266
  title: detail.label,
3178
3267
  tooltip: detail.label,
3179
- icon: nativeDetailIcon(feature, detail, jsx),
3268
+ icon: nativeDetailIcon(feature, detail),
3180
3269
  isClosable: true,
3181
3270
  props: {},
3182
3271
  onClose: () => {