minecodex 2.0.42 → 2.0.44

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.
@@ -1,8 +1,21 @@
1
1
  export async function loadComposerExpandControls(signal) {
2
- const urls = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(node => node.href || node.src);
3
- const initialUrl = urls.find(url => /\/app-initial-[^/]+\.js/.test(url));
4
- const mainUrl = urls.find(url => /\/app-main-[^/]+\.js/.test(url));
5
- if (!initialUrl || !mainUrl) throw new Error('composer-expand-modules-unavailable');
2
+ signal.throwIfAborted();
3
+ function modules() {
4
+ const urls = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(node => node.href || node.src);
5
+ const initialUrl = urls.find(url => /\/app-initial-[^/]+\.js/.test(url));
6
+ const mainUrl = urls.find(url => /\/app-main-[^/]+\.js/.test(url));
7
+ return initialUrl && mainUrl ? {initialUrl,mainUrl} : null;
8
+ }
9
+ // 冷启动时入口标签可能晚于 DOMContentLoaded 挂载。
10
+ const {initialUrl,mainUrl} = modules() ?? await new Promise((resolve,reject) => {
11
+ const observer = new MutationObserver(check);
12
+ function cleanup() { observer.disconnect();signal.removeEventListener('abort',abort); }
13
+ function abort() { cleanup();reject(signal.reason); }
14
+ function check() { const found=modules();if(found){cleanup();resolve(found);} }
15
+ observer.observe(document.documentElement,{subtree:true,childList:true,attributes:true,attributeFilter:['src','href','rel']});
16
+ signal.addEventListener('abort',abort,{once:true});
17
+ check();
18
+ });
6
19
  const read = async url => { const response=await fetch(url,{signal}); if(!response.ok) throw new Error('composer-expand-source-unavailable'); return response.text(); };
7
20
  const [initialSource,mainSource] = await Promise.all([read(initialUrl),read(mainUrl)]);
8
21
  const file = initialSource.match(/["'`]\.\/(app-primary-[\w-]+\.js)["'`]/)?.[1];
@@ -13,17 +26,40 @@ export async function loadComposerExpandControls(signal) {
13
26
  if (!local) throw new Error('composer-expand-binding-unavailable');
14
27
  const escaped = local.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
15
28
  const alias = source.slice(source.lastIndexOf('export{')).match(new RegExp(`(?:\\{|,)${escaped} as ([\\w$]+)(?:,|\\})`))?.[1];
16
- if (alias) return (await import(url))[alias];
29
+ if (alias) {
30
+ const module=await import(url);
31
+ if (module[alias]===undefined) {
32
+ // 图标导出可能仍在原生 once 初始化器中,先初始化再读取实时导出。
33
+ const assignment=source.search(new RegExp(`(?:[,;{])${escaped}=`));
34
+ const initializer=assignment<0?null:source.slice(source.lastIndexOf('function ',assignment),assignment+1)
35
+ .match(/^function ([\w$]+)\(\)\{return\(\1=[\w$]+\(\(\(\)=>\{/);
36
+ if (!initializer) throw new Error('composer-expand-binding-unavailable');
37
+ const initialize=await binding(source,url,initializer[1]);
38
+ if (typeof initialize!=='function') throw new Error('composer-expand-binding-unavailable');
39
+ initialize();
40
+ }
41
+ if (module[alias]===undefined) throw new Error('composer-expand-binding-unavailable');
42
+ return module[alias];
43
+ }
17
44
  for (const match of source.matchAll(/import\{([^}]+)\}from["'`]([^"'`]+)["'`]/g)) {
18
45
  for (const entry of match[1].split(',')) {
19
46
  const parts=entry.trim().split(/\s+as\s+/);
20
- if (parts.at(-1)===local) return (await import(new URL(match[2],url).href))[parts[0]];
47
+ if (parts.at(-1)===local) {
48
+ const dependencyUrl=new URL(match[2],url).href,module=await import(dependencyUrl);
49
+ if (module[parts[0]]!==undefined) return module[parts[0]];
50
+ const dependency=await read(dependencyUrl);
51
+ const exported=parts[0].replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
52
+ const name=dependency.slice(dependency.lastIndexOf('export{')).match(new RegExp(`(?:\\{|,)([\\w$]+) as ${exported}(?:,|\\})`))?.[1];
53
+ return binding(dependency,dependencyUrl,name);
54
+ }
21
55
  }
22
56
  }
23
57
  throw new Error('composer-expand-binding-unavailable');
24
58
  }
25
59
  // 语义定位原生展开按钮,导入它实际使用的控件和成对图标。
26
- const start=source.indexOf('function ',source.lastIndexOf('function ',source.indexOf('data-composer-expand-toggle')));
60
+ // 只匹配控件属性,避免先命中布局 className 中的同名 CSS 选择器。
61
+ const marker=source.search(/["'`]data-composer-expand-toggle["'`]:/);
62
+ const start=marker<0?-1:source.lastIndexOf('function ',marker);
27
63
  const control=source.slice(start,source.indexOf('var ',start));
28
64
  const button=control.match(/jsx\)\(([\w$]+),\{"aria-label"/ )?.[1];
29
65
  const icons=control.match(/children:[\w$]+\?\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\):\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\)/);
@@ -8,11 +8,24 @@ export async function loadNativeMarkdownRichText(native) {
8
8
  const sharedSource = await (await fetch(sharedUrl)).text(),
9
9
  shared = await import(sharedUrl);
10
10
  const codeAt = sharedSource.indexOf('id:`codeSnippet.wrap.disable`');
11
- const codeSection = sharedSource.slice(
11
+ let codeSection = sharedSource.slice(
12
12
  sharedSource.lastIndexOf('function ', codeAt),
13
13
  sharedSource.indexOf('function ', codeAt),
14
14
  );
15
- const codeName = codeSection.match(/^function ([\w$]+)/)?.[1];
15
+ let codeName = codeSection.match(/^function ([\w$]+)/)?.[1];
16
+ // 新版将高亮内容放在上层组件,底层只负责外壳,不能直接用作代码渲染器。
17
+ const wrapper = [...sharedSource.matchAll(/function ([\w$]+)\(/g)]
18
+ .map((match) => sharedSource.slice(match.index, sharedSource.indexOf('function ', match.index + 9)))
19
+ .find((section) =>
20
+ section.includes('renderSvgAsImage:') &&
21
+ section.includes('codeContainerClassName:') &&
22
+ section.includes(`jsx)(${codeName},{`) &&
23
+ section.includes('source:'),
24
+ );
25
+ if (wrapper) {
26
+ codeSection = wrapper;
27
+ codeName = wrapper.match(/^function ([\w$]+)/)?.[1];
28
+ }
16
29
  const sharedExports = sharedSource.slice(sharedSource.lastIndexOf('export{'));
17
30
  const sharedBinding = (name) =>
18
31
  shared[sharedExports.match(new RegExp(`${name.replace(/\$/g, '\\$')} as ([\\w$]+)`))?.[1]];
@@ -21,19 +21,29 @@ export function quickChatLifecycleExports(source) {
21
21
  const exportOf = name => name && exports.match(new RegExp(`(?:\\{|,)${escaped(name)} as ([\\w$]+)(?:,|\\})`))?.[1];
22
22
  const createExport = exportOf(create), openState = exportOf(state);
23
23
  if (!createExport || !openState) throw new Error('quick-chat-lifecycle-contract-unavailable');
24
- return {open, create:createExport, openState};
24
+ // 冷启动时状态 atom 也可能尚未初始化,定位其所属的原生 once 初始化器。
25
+ const assignment = source.search(new RegExp(`(?:[,;{])${escaped(state)}=[\\w$]+\\([\\w$]+,!1\\)`));
26
+ const initializer = assignment < 0 ? null : source.slice(source.lastIndexOf('function ', assignment), assignment + 1)
27
+ .match(/^function ([\w$]+)\(\)\{return\(\1=[\w$]+\(\(\(\)=>\{/);
28
+ const initialize = exportOf(initializer?.[1]);
29
+ return {open, create:createExport, openState, ...(initialize ? {initialize} : {})};
30
+ }
31
+
32
+ // 命令目录是懒初始化导出;按命令定义识别,不能要求数组已经初始化。
33
+ export function quickChatNativeCommand(source) {
34
+ const ids = new Set([...source.matchAll(/\{id:[`"']([\w$.-]+)[`"'],titleIntlId:[`"']codex\.command\.[^`"']+[`"']/g)].map(match => match[1]));
35
+ if (!ids.has('temporaryChat') || !ids.has('newTask')) throw new Error('quick-chat-command-catalog-unavailable');
36
+ return ids.has('quickChat');
25
37
  }
26
38
 
27
39
  export function installQuickChatShortcut(signal, codex, discover = null) {
28
40
  const windows = navigator.platform.startsWith('Win');
29
- let bindings = null, disposed = false, pendingFocus = null;
30
- const floatingAt = element => codex.props(element, p => typeof p.onMinimizedChange === 'function' && typeof p.onPresentationChange === 'function'
31
- && (p.unifiedDocking === true || typeof p.dockThreadKey === 'string' && typeof p.renderMenu === 'function'));
32
- function activateExisting() {
41
+ let bindings = null, disposed = false, pendingFocus = null, stopWaiting = null;
42
+ function activateExisting(minimize = false) {
33
43
  // 原生恢复按钮同时恢复 presentation、取消最小化并聚焦输入框。
34
44
  for (const button of document.querySelectorAll('button')) {
35
45
  if (!button.checkVisibility({visibilityProperty:true})) continue;
36
- const floating = floatingAt(button);
46
+ const floating = codex.props(button, p => typeof p.isMinimized === 'boolean' && typeof p.onMinimizedChange === 'function' && typeof p.onPresentationChange === 'function');
37
47
  if (!floating?.isMinimized || floating.isHidden) continue;
38
48
  const restore = codex.props(button, p => p.uniform === true && p.size === 'composer' && typeof p.onClick === 'function'
39
49
  && typeof p.onPointerEnter === 'function' && typeof p.onPointerDown === 'function' && typeof p.onKeyDown === 'function');
@@ -43,11 +53,12 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
43
53
  }
44
54
  for (const surface of document.querySelectorAll('[data-floating-chat-surface]')) {
45
55
  if (surface.closest('[hidden], [inert], [aria-hidden="true"]')) continue;
46
- const floating = floatingAt(surface);
56
+ const floating = codex.props(surface, p => typeof p.isMinimized === 'boolean' && typeof p.onMinimizedChange === 'function' && typeof p.onPresentationChange === 'function');
47
57
  if (!floating || floating.isMinimized || floating.isHidden) continue;
48
58
  const editable = surface.querySelector('[data-chatgpt-composer] [contenteditable="true"]');
49
59
  if (!editable?.checkVisibility({visibilityProperty:true})) continue;
50
- editable.focus({preventScroll:true});
60
+ if (minimize) { pendingFocus=null; floating.onMinimizedChange(true); }
61
+ else editable.focus({preventScroll:true});
51
62
  return true;
52
63
  }
53
64
  return false;
@@ -69,8 +80,9 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
69
80
  function context() {
70
81
  for (const button of document.querySelectorAll('button[aria-haspopup="menu"]')) {
71
82
  if (!button.checkVisibility({visibilityProperty:true})) continue;
72
- const mode = codex.props(button, p => typeof p.onModeSelect === 'function' && typeof p.mode === 'string', 80)?.mode;
73
- if (mode !== 'codex') continue;
83
+ const picker = codex.props(button, p => typeof p.onModeSelect === 'function' && typeof p.mode === 'string', 80);
84
+ // 原生 ChatGPT 也使用 work 路由,具体产品由 workModeAccess 区分。
85
+ if (picker?.mode !== 'codex' && !(picker?.mode === 'work' && picker.workModeAccess === 'chatgpt')) continue;
74
86
  for (let fiber = codex.currentFiber(button), depth = 0; fiber && depth++ < 200; fiber = fiber.return) {
75
87
  const values = [];
76
88
  for (let hook = fiber.memoizedState, n = 0; hook && n++ < 150; hook = hook.next) values.push(hook.memoizedState?.current);
@@ -82,14 +94,35 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
82
94
  return null;
83
95
  }
84
96
  async function load() {
85
- const url = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(e => e.href || e.src).find(url => /\/app-initial-[^/]+\.js(?:$|\?)/.test(url));
86
- if (!url) throw new Error('quick-chat-module-unavailable');
97
+ const findUrl = () => [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(e => e.href || e.src).find(url => /\/app-initial-[^/]+\.js(?:$|\?)/.test(url));
98
+ // 宿主可早于原生模块入口注入;等待入口挂载,不能把正常启动顺序当作协议失效。
99
+ const url = findUrl() ?? await new Promise((resolve, reject) => {
100
+ codex.report('quick-chat-shortcut', false, 'quick-chat-module-pending');
101
+ const cleanup = () => { observer.disconnect(); signal.removeEventListener('abort', cancel); stopWaiting=null; };
102
+ const cancel = () => { cleanup(); reject(new Error('quick-chat-load-aborted')); };
103
+ const check = () => { const found=findUrl(); if (found) { cleanup(); resolve(found); } };
104
+ const observer = new MutationObserver(check);
105
+ stopWaiting=cancel;
106
+ observer.observe(document.documentElement, {childList:true,subtree:true,attributes:true,attributeFilter:['href','src']});
107
+ signal.addEventListener('abort', cancel, {once:true});
108
+ if (disposed || signal.aborted) cancel(); else check();
109
+ });
87
110
  const source = await (await fetch(url, {signal})).text();
88
111
  const module = await import(url);
89
- const commands = Object.values(module).find(v => Array.isArray(v) && v.some(c => c?.id === 'temporaryChat') && v.some(c => c?.id === 'newTask'));
90
- if (!commands) throw new Error('quick-chat-command-catalog-unavailable');
91
- if (commands.some(c => c.id === 'quickChat')) return {native:true};
112
+ let native;
113
+ try { native=quickChatNativeCommand(source); }
114
+ catch (error) {
115
+ if (error.message !== 'quick-chat-command-catalog-unavailable') throw error;
116
+ // 新版命令目录移入共享模块;打开动作仍由入口模块导出。
117
+ const file=source.match(/["'`]\.\/(app-shared-[\w-]+\.js)["'`]/)?.[1];
118
+ if (!file) throw error;
119
+ const response=await fetch(new URL(file,url).href,{signal});
120
+ if (!response.ok) throw error;
121
+ native=quickChatNativeCommand(await response.text());
122
+ }
123
+ if (native) return {native:true};
92
124
  const roles = quickChatLifecycleExports(source);
125
+ if (!module[roles.openState] && typeof module[roles.initialize] === 'function') module[roles.initialize]();
93
126
  const open = module[roles.open], create = module[roles.create], openState = module[roles.openState];
94
127
  if (typeof open !== 'function' || typeof create !== 'function' || !openState) throw new Error('quick-chat-open-action-unavailable');
95
128
  return {native:false, open, create, isOpen:scope => scope.get(openState)};
@@ -98,15 +131,16 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
98
131
  if (disposed || signal.aborted) return;
99
132
  bindings = value;
100
133
  codex.report('quick-chat-shortcut', true);
101
- }).catch(() => { if (!disposed && !signal.aborted) codex.report('quick-chat-shortcut', false, 'native-shortcut-contract-unavailable'); });
102
- function activate() {
134
+ }).catch(error => { if (!disposed && !signal.aborted) codex.report('quick-chat-shortcut', false,
135
+ /^quick-chat-[\w-]+$/.test(error?.message) ? error.message : 'native-shortcut-contract-unavailable'); });
136
+ function activate({minimize=false} = {}) {
103
137
  if (disposed || signal.aborted || !bindings || bindings.native) return false;
104
- const scope = bindings && context();
138
+ const scope = context();
105
139
  if (scope && bindings.isOpen(scope) === false) {
106
140
  pendingFocus={scope,previous:new Set([...document.querySelectorAll('[data-floating-chat-surface]')].map(sessionId))};
107
141
  bindings.create(scope);
108
142
  focusNew();
109
- } else if (!activateExisting()) {
143
+ } else if (!activateExisting(minimize)) {
110
144
  if (!scope) return false;
111
145
  bindings.open(scope);
112
146
  }
@@ -115,12 +149,12 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
115
149
  function keydown(event) {
116
150
  const primaryModifier = windows ? event.ctrlKey && !event.metaKey : event.metaKey && !event.ctrlKey;
117
151
  if (event.defaultPrevented || event.repeat || event.isComposing
118
- || !primaryModifier || !event.altKey || event.shiftKey || event.code !== 'KeyN' || !activate()) return;
152
+ || !primaryModifier || !event.altKey || event.shiftKey || event.code !== 'KeyN' || !activate({minimize:true})) return;
119
153
  event.preventDefault();
120
154
  event.stopPropagation();
121
155
  }
122
156
  document.addEventListener('keydown', keydown, {signal});
123
- function dispose() { disposed = true; document.removeEventListener('keydown', keydown); bindings = null; pendingFocus=null; }
157
+ function dispose() { disposed = true; stopWaiting?.(); document.removeEventListener('keydown', keydown); bindings = null; pendingFocus=null; }
124
158
  signal.addEventListener('abort', dispose, {once:true});
125
159
  return {ready, dispose, sync:focusNew, activate, available:() => Boolean(bindings && !bindings.native && !disposed && !signal.aborted)};
126
160
  }
@@ -3,7 +3,7 @@ export async function loadQuickChatSidebarIcon(signal) {
3
3
  const initialUrl = urls.find(url => /\/app-initial-[^/]+\.js/.test(url));
4
4
  if (!initialUrl) throw new Error('quick-chat-icon-module-unavailable');
5
5
  const initial = await (await fetch(initialUrl,{signal})).text();
6
- const file = initial.match(/["']\.\/(quick-chat-floating-surface-[\w-]+\.js)["']/)?.[1];
6
+ const file = initial.match(/["']\.\/((?:quick-chat-floating-surface|floating-chat-surface)-[\w-]+\.js)["']/)?.[1];
7
7
  if (!file) throw new Error('quick-chat-icon-module-unavailable');
8
8
  const url = new URL(file,initialUrl).href;
9
9
  const source = await (await fetch(url,{signal})).text();
@@ -1,7 +1,7 @@
1
1
  import {quickChatHistoryVisible} from "./adapters/codex/history.mjs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { createQuickChatAdapter } from "./adapters/codex/renderer.mjs";
4
- import { quickChatOpenExport, quickChatLifecycleExports, installQuickChatShortcut } from "./adapters/codex/shortcut.mjs";
4
+ import { quickChatOpenExport, quickChatLifecycleExports, quickChatNativeCommand, installQuickChatShortcut } from "./adapters/codex/shortcut.mjs";
5
5
  import { createSubmissionTracker } from "./domain/submission.mjs";
6
6
  import { createQuickChatSidebarAdapter, loadQuickChatSidebarIcon } from "./adapters/codex/sidebar.mjs";
7
7
  import { installQuickChatSidebar } from "./frontend/sidebar.mjs";
@@ -9,7 +9,7 @@ import { createHistoryTracker } from "./domain/history.mjs";
9
9
  export async function createPageScriptSource() {
10
10
  const source = await readFile(new URL("./frontend/entry.mjs", import.meta.url), "utf8");
11
11
  const functions = [quickChatHistoryVisible, loadQuickChatSidebarIcon, createQuickChatSidebarAdapter, installQuickChatSidebar,
12
- quickChatOpenExport, quickChatLifecycleExports, installQuickChatShortcut, createQuickChatAdapter,
12
+ quickChatOpenExport, quickChatLifecycleExports, quickChatNativeCommand, installQuickChatShortcut, createQuickChatAdapter,
13
13
  createSubmissionTracker, createHistoryTracker];
14
14
  return functions.map(fn => `const ${fn.name}=${fn.toString()};`).join('\n') + '\n' + source;
15
15
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "2.0.42",
3
+ "version": "2.0.44",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -851,7 +851,14 @@ export class WindowsPlatformAdapter {
851
851
  async codex() {
852
852
  if (!this.codexInstallation) return this.assertCodexInstalled();
853
853
  if (this.codexInstallationInjected) return this.codexInstallation;
854
- const currentFingerprint = await this.readCodexInstallationFingerprint(this.codexInstallation);
854
+ let currentFingerprint;
855
+ try {
856
+ currentFingerprint = await this.readCodexInstallationFingerprint(this.codexInstallation);
857
+ } catch (error) {
858
+ // MSIX 更新会删除旧版本目录,重新发现并校验当前官方包。
859
+ if (error.code !== "ENOENT") throw error;
860
+ return this.assertCodexInstalled();
861
+ }
855
862
  if (this.codexInstallationFingerprint == null) {
856
863
  this.codexInstallationFingerprint = currentFingerprint;
857
864
  } else if (currentFingerprint !== this.codexInstallationFingerprint) {