minecodex 2.0.48 → 2.0.50

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 (29) hide show
  1. package/features/composer-expand/src/injection-source.mjs +2 -1
  2. package/features/composer-expand/src/native.mjs +6 -18
  3. package/features/file-navigation/src/adapters/codex/inline-modules.mjs +54 -0
  4. package/features/file-navigation/src/adapters/codex/modules.mjs +16 -4
  5. package/features/file-navigation/src/injection-source.mjs +3 -1
  6. package/features/images/src/adapters/codex/library-api.mjs +8 -7
  7. package/features/images/src/adapters/codex/native-image-modules.mjs +27 -15
  8. package/features/images/src/backend/cloud-library.mjs +3 -1
  9. package/features/images/src/library-prompt-source.mjs +3 -2
  10. package/features/markdown-reading/src/adapters/codex/links.mjs +2 -3
  11. package/features/markdown-reading/src/adapters/codex/rich-text.mjs +5 -4
  12. package/features/markdown-reading/src/injection-source.mjs +2 -1
  13. package/features/mermaid-diagrams/src/adapters/codex/native.mjs +3 -2
  14. package/features/mermaid-diagrams/src/injection-source.mjs +2 -1
  15. package/features/model-slider/src/adapters/codex/controller.mjs +4 -4
  16. package/features/model-slider/src/frontend/install.mjs +96 -28
  17. package/features/model-slider/src/injection-source.mjs +2 -0
  18. package/features/quick-chat/src/adapters/codex/shortcut.mjs +12 -19
  19. package/features/quick-chat/src/adapters/codex/sidebar.mjs +5 -5
  20. package/features/quick-chat/src/injection-source.mjs +2 -1
  21. package/features/session-status/src/adapters/codex/context-usage.mjs +4 -15
  22. package/features/session-status/src/injection-source.mjs +2 -1
  23. package/package.json +2 -1
  24. package/packages/codex-adapter/src/markdown/block-actions.mjs +3 -3
  25. package/packages/codex-adapter/src/markdown/editor-api.mjs +4 -4
  26. package/packages/codex-adapter/src/markdown/toc-native.mjs +17 -13
  27. package/packages/codex-adapter/src/renderer/environment.mjs +4 -1
  28. package/packages/codex-adapter/src/renderer/modules.mjs +66 -0
  29. package/packages/runtime-host/src/codex-injection.mjs +7 -17
@@ -1,7 +1,8 @@
1
+ import { findCodexModule } from '../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  import {readFile} from 'node:fs/promises';
2
3
  import {loadComposerExpandControls,createComposerExpandAdapter} from './native.mjs';
3
4
  import {composerCanExpand,installComposerExpand} from './install.mjs';
4
5
  export async function createPageScriptSource(){
5
- return [loadComposerExpandControls,createComposerExpandAdapter,composerCanExpand,installComposerExpand]
6
+ return [findCodexModule, loadComposerExpandControls,createComposerExpandAdapter,composerCanExpand,installComposerExpand]
6
7
  .map(fn=>`const ${fn.name}=${fn.toString()};`).join('\n')+'\n'+await readFile(new URL('./entry.mjs',import.meta.url),'utf8');
7
8
  }
@@ -1,26 +1,14 @@
1
+ import { findCodexModule } from '../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  export async function loadComposerExpandControls(signal) {
2
3
  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
- });
4
+ const initialUrl=await findCodexModule('app-initial',{signal,waitForEntry:true});
5
+ const mainUrl=await findCodexModule('app-main',{signal,waitForEntry:true});
6
+ if (!initialUrl || !mainUrl) throw new Error('composer-expand-modules-unavailable');
19
7
  const read = async url => { const response=await fetch(url,{signal}); if(!response.ok) throw new Error('composer-expand-source-unavailable'); return response.text(); };
20
8
  const [initialSource,mainSource] = await Promise.all([read(initialUrl),read(mainUrl)]);
21
9
  const file = initialSource.match(/["'`]\.\/(app-primary-[\w-]+\.js)["'`]/)?.[1];
22
- if (!file) throw new Error('composer-expand-primary-unavailable');
23
- const primaryUrl = new URL(file,initialUrl).href;
10
+ const primaryUrl = file ? new URL(file,initialUrl).href : await findCodexModule('app-primary',{read,signal});
11
+ if (!primaryUrl) throw new Error('composer-expand-primary-unavailable');
24
12
  const source = await read(primaryUrl);
25
13
  async function binding(source,url,local) {
26
14
  if (!local) throw new Error('composer-expand-binding-unavailable');
@@ -0,0 +1,54 @@
1
+ // 新版将文件树和目录读取合入入口;仍按调用角色绑定,不执行无关初始化器。
2
+ export async function loadInlineFileNavigationModules(initialUrl, mainUrl, read, load) {
3
+ const [source,main]=await Promise.all([read(initialUrl),read(mainUrl)]);
4
+ const aliases=text=>new Map([...text.slice(text.lastIndexOf('export{')+7).split('}')[0].split(',')]
5
+ .map(entry=>{const [local,alias=local]=entry.trim().split(/\s+as\s+/);return [local,alias];}));
6
+ const imports=(text,url)=>new Map([...text.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g)]
7
+ .flatMap(match=>match[1].split(',').map(entry=>{const parts=entry.trim().split(/\s+as\s+/);return [parts.at(-1),{name:parts[0],url:new URL(match[2],url).href}];})));
8
+ const exported=aliases(source),dependencies=imports(source,initialUrl),module=await load(initialUrl);
9
+ const binding=async local=>{
10
+ if (exported.has(local)) return module[exported.get(local)];
11
+ const value=dependencies.get(local);
12
+ if(!value)throw new Error('file-navigation-inline-dependency-unavailable');
13
+ return (await load(value.url))[value.name];
14
+ };
15
+ const functions=[...source.matchAll(/function ([\w$]+)\(/g)];
16
+ const bodies=functions.map((match,index)=>({name:match[1],body:source.slice(match.index,functions[index+1]?.index??source.length)}));
17
+ const trees=bodies.filter(({name,body})=>exported.has(name)&&/contextMenuEnabled:/.test(body)&&/initialExpandedPaths:/.test(body)&&/onSelectionChange:/.test(body));
18
+ if(trees.length!==1)throw new Error('file-navigation-inline-tree-unavailable');
19
+ const tree=trees[0],cache=tree.body.match(/\(0,([\w$]+)\.c\)/)?.[1];
20
+ if(cache) {
21
+ const assignment=source.indexOf(`${cache}=`),before=source.slice(source.lastIndexOf('function ',assignment),assignment);
22
+ const initializer=before.match(/^function ([\w$]+)\(\)\{return\(\1=/)?.[1];
23
+ if(!initializer || typeof await binding(initializer)!=='function')throw new Error('file-navigation-inline-initializer-unavailable');
24
+ (await binding(initializer))();
25
+ }
26
+ const Tree=await binding(tree.name);
27
+ const reactOwner=tree.body.match(/\(0,([\w$]+)\.useState\)/)?.[1];
28
+ const reactFactory=reactOwner && source.match(new RegExp(`${reactOwner.replace(/\$/g,'\\$')}=([\\w$]+)\\(\\)`))?.[1];
29
+ const rootOwner=main.match(/\(0,([\w$]+)\.createRoot\)/)?.[1];
30
+ const rootFactory=rootOwner && main.match(new RegExp(`${rootOwner.replace(/\$/g,'\\$')}=([\\w$]+)\\(\\)`))?.[1];
31
+ const rootImport=imports(main,mainUrl).get(rootFactory);
32
+ if(!rootImport || !reactFactory)throw new Error('file-navigation-inline-react-unavailable');
33
+ const React=(await binding(reactFactory))(),ReactDOM=(await load(rootImport.url))[rootImport.name]();
34
+ const requests=bodies.filter(({body})=>body.includes('.workspaceFiles')&&body.includes('.listDirectory(')&&body.includes('.throwIfAborted('));
35
+ if(requests.length!==1)throw new Error('file-navigation-inline-directory-unavailable');
36
+ const requestBody=requests[0].body;
37
+ const host=requestBody.match(/let [\w$]+=([\w$]+)\.workspaceFiles/)?.[1];
38
+ const cancellable=requestBody.match(/return ([\w$]+)\([\w$]+\.listDirectory\(/)?.[1];
39
+ const bridge=await binding(host),withSignal=await binding(cancellable);
40
+ if(typeof bridge?.workspaceFiles?.listDirectory!=='function'||typeof withSignal!=='function')throw new Error('file-navigation-inline-directory-unavailable');
41
+ const request=(_method,{params,signal})=>{signal?.throwIfAborted();return withSignal(bridge.workspaceFiles.listDirectory(params),signal);};
42
+ const sharedUrl=[...dependencies.values()].find(value=>/\/app-shared-/.test(value.url))?.url;
43
+ if(!sharedUrl)throw new Error('file-navigation-inline-icons-unavailable');
44
+ const sharedSource=await read(sharedUrl),shared=await load(sharedUrl),sharedExports=aliases(sharedSource);
45
+ const folderAt=sharedSource.indexOf('name:`folder-light-16`'),prefix=sharedSource.slice(sharedSource.lastIndexOf('function ',folderAt),folderAt);
46
+ const folderInit=prefix.match(/^function ([\w$]+)\(\)\{return\(\1=/)?.[1];
47
+ if(folderAt<0||typeof shared[sharedExports.get(folderInit)]!=='function')throw new Error('file-navigation-folder-icon-unavailable');
48
+ shared[sharedExports.get(folderInit)]();
49
+ const folder=Object.values(shared).find(value=>value?.name==='folder-light-16');
50
+ if(typeof Tree!=='function')throw new Error('file-navigation-inline-tree-unavailable');
51
+ if(!React?.createElement||!ReactDOM?.createRoot)throw new Error('file-navigation-inline-react-unavailable');
52
+ if(!folder?.canvas?.viewBox||typeof folder.body!=='string')throw new Error('file-navigation-folder-icon-unavailable');
53
+ return {Tree,React,ReactDOM,request,folder};
54
+ }
@@ -1,9 +1,18 @@
1
+ import { loadInlineFileNavigationModules } from './inline-modules.mjs';
2
+ import { findCodexModule } from '../../../../../packages/codex-adapter/src/renderer/modules.mjs';
1
3
  // 依照原生 breadcrumb 的依赖角色查找公开模块,不绑定产物 hash 或短变量名。
2
4
  export async function loadFileNavigationModules({read = async url => { const response = await fetch(url); if (!response.ok) throw new Error('file-navigation-source-unavailable'); return response.text(); }, load = url => import(url)} = {}) {
3
- const urls = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(n => n.href || n.src);
4
- const breadcrumbUrl = urls.find(url => /\/review-file-source-breadcrumb-[^/]+\.js$/.test(url));
5
- const mainUrl = urls.find(url => /\/app-main-[^/]+\.js$/.test(url));
6
- if (!breadcrumbUrl || !mainUrl) throw new Error('file-navigation-modules-unavailable');
5
+ const [breadcrumbUrl,mainUrl]=await Promise.all([
6
+ findCodexModule('review-file-source-breadcrumb',{read}),findCodexModule('app-main',{read}),
7
+ ]);
8
+ if (!mainUrl) throw new Error('file-navigation-modules-unavailable');
9
+ let resolved;
10
+ if (!breadcrumbUrl) {
11
+ const initialUrl=await findCodexModule('app-initial',{read});
12
+ if (!initialUrl) throw new Error('file-navigation-modules-unavailable');
13
+ resolved=await loadInlineFileNavigationModules(initialUrl,mainUrl,read,load);
14
+ }
15
+ if (!resolved) {
7
16
  const imports = (source, url) => {
8
17
  const result = new Map();
9
18
  for (const match of source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g))
@@ -51,6 +60,9 @@ export async function loadFileNavigationModules({read = async url => { const res
51
60
  if (!folder?.canvas?.viewBox || typeof folder.body !== 'string')
52
61
  throw new Error('file-navigation-folder-icon-unavailable');
53
62
  // 分列导航没有纵向展开状态;通过原生样式入口使用固定的原生文件夹图标。
63
+ resolved={Tree,React,ReactDOM,request,folder};
64
+ }
65
+ const {Tree,React,ReactDOM,request,folder}=resolved;
54
66
  const folderImage = `url("data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${folder.canvas.viewBox}">${folder.body}</svg>`)}")`;
55
67
  const columnStyles = `
56
68
  /* 原生树的逻辑焦点也响应鼠标;描边仅保留给键盘可见焦点。 */
@@ -1,9 +1,11 @@
1
+ import { loadInlineFileNavigationModules } from './adapters/codex/inline-modules.mjs';
2
+ import { findCodexModule } from '../../../packages/codex-adapter/src/renderer/modules.mjs';
1
3
  import { readFile } from 'node:fs/promises';
2
4
  import { fileNavigationPath, selectFileNavigationDirectory } from './domain/navigation.mjs';
3
5
  import { loadFileNavigationModules } from './adapters/codex/modules.mjs';
4
6
  import { createFileNavigationAdapter } from './adapters/codex/navigation.mjs';
5
7
  import { installFileNavigation } from './frontend/navigation.mjs';
6
8
  export async function createPageScriptSource() {
7
- const bindings = [fileNavigationPath, selectFileNavigationDirectory, loadFileNavigationModules, createFileNavigationAdapter, installFileNavigation];
9
+ const bindings = [loadInlineFileNavigationModules,findCodexModule, fileNavigationPath, selectFileNavigationDirectory, loadFileNavigationModules, createFileNavigationAdapter, installFileNavigation];
8
10
  return bindings.map(fn => `const ${fn.name} = ${fn.toString()};`).join('\n') + '\n' + await readFile(new URL('./entry.mjs', import.meta.url), 'utf8');
9
11
  }
@@ -1,18 +1,19 @@
1
+ import { findCodexModule } from '../../../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  // 唯一的 Library 构建产物协议;调用方只消费命名操作。
2
3
  export function createLibraryApi({read:readSource=async url=>(await fetch(url)).text(),load:loadModule=url=>import(url)} = {}) {
3
4
  let capability=null, downloadCapability=null;
4
5
  async function connect() {
5
6
  if (!capability) capability=(async()=>{
6
- const initialUrl = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')]
7
- .map(element => element.href || element.src).find(url => /\/app-initial-[^/]+\.js/.test(url));
7
+ const initialUrl = await findCodexModule('app-initial',{read:readSource});
8
8
  if (!initialUrl) throw new Error("Cloud Library is unavailable in this Codex version");
9
9
  const initialSource = await readSource(initialUrl);
10
- const asset = name => {
10
+ const asset = async name => {
11
11
  const file = initialSource.match(new RegExp(`${name}-[\\w-]+\\.js`))?.[0];
12
- if (!file) throw new Error(`Cloud Library capability ${name} is unavailable`);
13
- return new URL(file, initialUrl).href;
12
+ const url=file ? new URL(file, initialUrl).href : await findCodexModule(name,{read:readSource,via:['library-page']});
13
+ if (!url) throw new Error(`Cloud Library capability ${name} is unavailable`);
14
+ return url;
14
15
  };
15
- const metadataUrl = asset("library-file-metadata");
16
+ const metadataUrl = await asset("library-file-metadata");
16
17
  const metadataSource = await readSource(metadataUrl);
17
18
  const apiLocal = metadataSource.match(/([\w$]+)\.safePost\(`\/files\/library`/)?.[1];
18
19
  const importBlock = [...metadataSource.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g)].find(match => match[1].split(",").some(binding=>binding.trim().split(/\s+as\s+/).at(-1)===apiLocal));
@@ -33,7 +34,7 @@ export function createLibraryApi({read:readSource=async url=>(await fetch(url)).
33
34
  async function download(file,signal) {
34
35
  if (!downloadCapability) downloadCapability=(async()=>{
35
36
  const {asset}=await connect();
36
- const downloadUrl = asset("library-file-download");
37
+ const downloadUrl = await asset("library-file-download");
37
38
  const downloadSource = await readSource(downloadUrl);
38
39
  const exports = downloadSource.match(/export\{([^}]+)\}/)?.[1].split(",").map(s => s.trim().split(/\s+as\s+/)) ?? [];
39
40
  const responseLocal = downloadSource.match(/async function ([\w$]+)\([^)]*\)\{return [\w$]+\(await [\w$]+\([^;]+showInline:!0/)?.[1];
@@ -1,3 +1,4 @@
1
+ import { findCodexModule } from '../../../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  // 以调用位置和参数结构识别角色,不绑定压缩后的局部变量名。
2
3
  export function discoverImageActionBinding(source, role) {
3
4
  const patterns = {
@@ -53,6 +54,18 @@ export function discoverFilePreviewComposerBindings(source) {
53
54
  return roles;
54
55
  }
55
56
 
57
+ export function discoverImageCanvasComponents(canvas,layout) {
58
+ const select=(exports,predicate,name)=>{
59
+ const matches=Object.values(exports).filter(value=>typeof value==='function'&&predicate(String(value)));
60
+ if(matches.length!==1)throw new Error(`Native Images canvas role unavailable: ${name}`);
61
+ return matches[0];
62
+ };
63
+ return {
64
+ Canvas:select(canvas,source=>/attachmentSrc:/.test(source)&&/onImageEditSubmit:/.test(source)&&/downloadSrc:/.test(source),'image preview'),
65
+ CanvasLayout:select(layout,source=>/reserveImageComposerSpace:/.test(source)&&/contentOwnsHeader:/.test(source),'preview layout'),
66
+ };
67
+ }
68
+
56
69
  // 构建产物发现只返回当前能力;原生组件呈现不参与引用解析。
57
70
  export function createNativeImageModules({read = async url => (await fetch(url)).text(), load = url => import(url)} = {}) {
58
71
  let starting, connecting, canvasConnecting;
@@ -68,29 +81,27 @@ export function createNativeImageModules({read = async url => (await fetch(url))
68
81
 
69
82
  async function bootstrap() {
70
83
  if (!starting) starting = (async () => {
71
- const urls = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(node => node.href || node.src);
72
- const initialUrl = urls.find(url => /\/app-initial-[^/]+\.js/.test(url));
73
- const mainUrl = urls.find(url => /\/app-main-[^/]+\.js/.test(url));
84
+ const [initialUrl,mainUrl]=await Promise.all([
85
+ findCodexModule('app-initial',{read}),findCodexModule('app-main',{read}),
86
+ ]);
74
87
  if (!initialUrl || !mainUrl) throw new Error("Native Images modules are unavailable");
75
88
  const [initialSource, mainSource] = await Promise.all([read(initialUrl), read(mainUrl)]);
76
89
  const initial = await load(initialUrl);
77
- const asset = (name, references = initialSource) => {
90
+ const asset = async (name, references = initialSource) => {
78
91
  const loaded = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(node => node.href || node.src)
79
92
  .find(url => new RegExp(`/${name}-[\\w-]+\\.js$`).test(url));
80
93
  if (loaded) return loaded;
81
94
  const file = references.match(new RegExp(`(?:["'./])(${name}-[\\w-]+\\.js)`))?.[1];
82
- if (!file) throw new Error(`Native Images capability is unavailable: ${name}`);
83
- return new URL(file, initialUrl).href;
95
+ const url=file ? new URL(file, initialUrl).href : await findCodexModule(name,{read,via:['library-page','library-trash-banner','library-item-menu','pinned-pages','recent-pages','space-trash-items','tab','cloud-file-preview.electron','library-marquee-selection','preview-tab','preview']});
96
+ return url;
84
97
  };
85
98
  const module = async (name,references) => {
86
- // 26.1013 将图库排布与卡片合入入口,其他能力仍按独立模块发现。
87
- const loaded = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].some(node =>
88
- new RegExp(`/${name}-[\\w-]+\\.js$`).test(node.href || node.src));
89
- if (!loaded && ["library-masonry", "library-item-layout"].includes(name)
90
- && !new RegExp(`(?:["'./])(${name}-[\\w-]+\\.js)`).test(references ?? initialSource)) {
91
- return {url:initialUrl, source:initialSource, exports:initial};
99
+ const url = await asset(name,references);
100
+ // 原生曾将这两个角色合入入口;先按实际资源引用查找,再使用已验证的内联分支。
101
+ if (!url && ['library-masonry','library-item-layout'].includes(name)) {
102
+ return {url:initialUrl,source:initialSource,exports:initial};
92
103
  }
93
- const url = asset(name,references);
104
+ if (!url) throw new Error(`Native Images capability is unavailable: ${name}`);
94
105
  const [source, exports] = await Promise.all([read(url), load(url)]);
95
106
  // 调用产物公开的惰性初始化器,不扫描或执行任意导出函数。
96
107
  for (const value of Object.values(exports)) {
@@ -233,14 +244,15 @@ export function createNativeImageModules({read = async url => (await fetch(url))
233
244
  // 文件画布使用原生 Home Composer 的控制器、附件 scope 和输入组件。
234
245
  const composerLocals=Object.values(discoverFilePreviewComposerBindings(filePreview.source));
235
246
  const [useComposerController,useHomeComposer,Composer,ComposerSurface,ScopeProvider,composerScope,createComposerValue,composerRouteScope,FileComposer,fileComposerAvailability,localAttachmentService,deleteLocalAttachment,useFileNavigate,localThreadRoute] = await Promise.all(composerLocals.map(local=>binding(filePreview.source,filePreview.url,local)));
236
- if (!canvas.exports.t || !canvasLayout.exports.r || !Composer || !ComposerSurface || !ScopeProvider
247
+ const {Canvas,CanvasLayout}=discoverImageCanvasComponents(canvas.exports,canvasLayout.exports);
248
+ if (!Canvas || !CanvasLayout || !Composer || !ComposerSurface || !ScopeProvider
237
249
  || !composerScope || !composerRouteScope?.parent
238
250
  || !FileComposer || !fileComposerAvailability
239
251
  || [createPortal,useComposerController,useHomeComposer,createComposerValue,localAttachmentService,deleteLocalAttachment,useFileNavigate,localThreadRoute].some(value=>typeof value!=="function")) {
240
252
  throw new Error("Native Images canvas contract changed");
241
253
  }
242
254
  return {
243
- createPortal,Canvas:canvas.exports.t,CanvasLayout:canvasLayout.exports.r,
255
+ createPortal,Canvas,CanvasLayout,
244
256
  useComposerController,useHomeComposer,Composer,ComposerSurface,ScopeProvider,composerScope,createComposerValue,composerRouteScope,
245
257
  FileComposer,fileComposerAvailability,localAttachmentService,deleteLocalAttachment,useFileNavigate,localThreadRoute,
246
258
  editImage:component(initial,source=>/async function/.test(source)&&/requireDispatchAcceptance:/.test(source)&&/sourceAttachment:/.test(source)&&/imageVariant/.test(source),"image edit submission"),
@@ -1,3 +1,4 @@
1
+ import { findCodexModule } from '../../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  import { cloudImagesInRenderer } from "../adapters/codex/cloud-import.mjs";
2
3
  import { createLibraryApi } from "../adapters/codex/library-api.mjs";
3
4
  import { promptResolverSource } from "../adapters/codex/prompt-resolver-source.mjs";
@@ -6,5 +7,6 @@ export async function cloudImagesAction(evaluate, operation, payload = {}) {
6
7
  if (!["list", "import"].includes(operation)) throw new Error("Unknown cloud image operation");
7
8
  if (payload.cursor != null && (typeof payload.cursor !== "string" || payload.cursor.length > 4096)) throw new Error("Invalid Library cursor");
8
9
  if (operation === "import" && (typeof payload.libraryId !== "string" || !/^libfile_[\w-]+$/.test(payload.libraryId))) throw new Error("Invalid Library file ID");
9
- return evaluate(`(${cloudImagesInRenderer.toString()})(${JSON.stringify(operation)},${JSON.stringify(payload)},${promptResolverSource()},${createLibraryApi.toString()})`, { timeoutMs: 110_000 });
10
+ return evaluate(`(async()=>{const findCodexModule=${findCodexModule.toString()};
11
+ return (${cloudImagesInRenderer.toString()})(${JSON.stringify(operation)},${JSON.stringify(payload)},${promptResolverSource()},${createLibraryApi.toString()});})()`, { timeoutMs: 110_000 });
10
12
  }
@@ -1,7 +1,8 @@
1
+ import { findCodexModule } from '../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  import { createCloudImageActions } from "./frontend/cloud-image-actions.mjs";
2
3
  import { createImageSourcesAdapter } from "./adapters/codex/image-sources.mjs";
3
4
  import { createNativeImageSource } from "./adapters/codex/native-image-source.mjs";
4
- import {createNativeImageModules, discoverImageActionBinding, discoverFilePreviewComposerBindings} from "./adapters/codex/native-image-modules.mjs";
5
+ import {createNativeImageModules, discoverImageCanvasComponents, discoverImageActionBinding, discoverFilePreviewComposerBindings} from "./adapters/codex/native-image-modules.mjs";
5
6
  import { installLocalImageSource } from "./frontend/local-image-source.mjs";
6
7
  import { createLibraryApi } from "./adapters/codex/library-api.mjs";
7
8
  import { createLibraryViewerAdapter } from "./adapters/codex/library-viewer.mjs";
@@ -11,7 +12,7 @@ import { promptResolverSource } from "./adapters/codex/prompt-resolver-source.mj
11
12
  export async function createPageScriptSource() {
12
13
  const source=await readFile(new URL("./library-prompt-page.mjs",import.meta.url),"utf8");
13
14
  const promptGlyphSource=await readFile(new URL("./assets/prompts-menu.svg",import.meta.url),"utf8");
14
- const functions={createCloudImageActions,createImageSourcesAdapter,discoverImageActionBinding,discoverFilePreviewComposerBindings,createNativeImageModules,createNativeImageSource,installLocalImageSource,createLibraryApi,createLibraryViewerAdapter};
15
+ const functions={findCodexModule,discoverImageCanvasComponents,createCloudImageActions,createImageSourcesAdapter,discoverImageActionBinding,discoverFilePreviewComposerBindings,createNativeImageModules,createNativeImageSource,installLocalImageSource,createLibraryApi,createLibraryViewerAdapter};
15
16
  const declarations=Object.entries(functions).map(([name,fn])=>`const ${name}=${fn.toString()};`).join("\n");
16
17
  return `${declarations}\nconst promptGlyphSource=${JSON.stringify(promptGlyphSource)};\nconst promptFromConversation=${promptResolverSource()};\n${source}`;
17
18
  }
@@ -1,8 +1,7 @@
1
+ import { findCodexModule } from '../../../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  // 修补原生 Preview 的链接装饰;源码、语法树与编辑器实例继续归 Codex。
2
3
  export async function loadNativeMarkdownLinks() {
3
- const url = [...document.querySelectorAll('link[rel="modulepreload"]')]
4
- .map((n) => n.href)
5
- .find((u) => /\/text-file-editor-extensions-[^/]+\.js$/.test(u));
4
+ const url = await findCodexModule('text-file-editor-extensions',{via:['text-file-editor-tab-content.electron']});
6
5
  if (!url) throw new Error('native-markdown-link-module-unavailable');
7
6
  const source = await (await fetch(url)).text();
8
7
  const opener = source.match(/([\w$]+)\(\{event:[\w$]+,href:[\w$]+,initiator:`markdown_link_click`/)?.[1];
@@ -1,9 +1,9 @@
1
+ import { findCodexModule } from '../../../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  import { collectMarkdownFootnotes } from '../../domain/footnotes.mjs';
2
3
  import { captureMarkdownProviders } from '../../../../../packages/codex-adapter/src/markdown/providers.mjs';
3
4
 
4
5
  // 复用消息流的代码组件与 Markdown lexer;文件内容及编辑器仍由 Codex 持有。
5
6
  export async function loadNativeMarkdownRichText(native) {
6
- const links = [...document.querySelectorAll('link[rel="modulepreload"]')].map((n) => n.href);
7
7
  const { sharedUrl } = native;
8
8
  const sharedSource = await (await fetch(sharedUrl)).text(),
9
9
  shared = await import(sharedUrl);
@@ -37,16 +37,17 @@ export async function loadNativeMarkdownRichText(native) {
37
37
  if (!codeInit || !sharedBinding(codeInit) || !sharedBinding(codeName))
38
38
  throw new Error('native-code-snippet-unavailable');
39
39
  sharedBinding(codeInit)();
40
- let mathUrl = links.find((u) => /\/katex-[^/]+\.js$/.test(u));
40
+ let mathUrl = await findCodexModule('katex');
41
41
  if (!mathUrl) {
42
- const editorUrl = links.find((u) => /\/text-file-editor-extensions-[^/]+\.js$/.test(u));
42
+ const editorUrl = await findCodexModule('text-file-editor-extensions',{via:['text-file-editor-tab-content.electron']});
43
43
  if (editorUrl) {
44
44
  const editorSource = await (await fetch(editorUrl)).text(),
45
45
  specifier = editorSource.match(/import\(`(\.\/katex-[^`]+\.js)`\)/)?.[1];
46
46
  if (specifier) mathUrl = new URL(specifier, editorUrl).href;
47
47
  }
48
48
  }
49
- const katex = mathUrl ? (await import(mathUrl)).default : null;
49
+ const mathModule=mathUrl ? await import(mathUrl) : null;
50
+ const katex=mathModule?.default ?? mathModule?.katex;
50
51
  if (!katex?.renderToString) throw new Error('native-katex-unavailable');
51
52
  return { ...native, CodeSnippet: sharedBinding(codeName), katex };
52
53
  }
@@ -1,3 +1,4 @@
1
+ import { findCodexModule, codexModuleStyles } from '../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  import { loadMarkdownListAPI } from '../../../packages/codex-adapter/src/markdown/editor-api.mjs';
2
3
  import { markdownExportAlias, loadNativeMarkdownTableActions } from '../../../packages/codex-adapter/src/markdown/block-actions.mjs';
3
4
  import { loadNativeMarkdownRenderer } from '../../../packages/codex-adapter/src/markdown/markdown-native.mjs';
@@ -18,7 +19,7 @@ import { installMarkdownTables, markdownTableActionGeometry } from './adapters/c
18
19
  import { installMarkdownLists } from './adapters/codex/lists.mjs';
19
20
  import { createMarkdownPreviewAdapter } from '../../../packages/codex-adapter/src/markdown/preview.mjs';
20
21
  export async function createPageScriptSource() {
21
- const bindings = [
22
+ const bindings = [findCodexModule, codexModuleStyles,
22
23
  collectMarkdownFootnotes,
23
24
  captureMarkdownProviders,
24
25
  createMarkdownReadingRuntime,
@@ -1,9 +1,10 @@
1
+ import { findCodexModule } from '../../../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  import { saveQvDiagram } from '../../mermaid/export.mjs';
2
3
 
3
4
  // 图表下载协议只由实验性功能加载,阅读增强不依赖此能力。
4
5
  export async function loadNativeMermaidActions(native, {read = async url => { const response = await fetch(url); if (!response.ok) throw new Error('native-diagram-source-unavailable'); return response.text(); }, load = url => import(url)} = {}) {
5
- const url = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')]
6
- .map(node => node.href || node.src).find(url => /\/app-initial-[^/]+\.js/.test(url));
6
+ const url = await findCodexModule('app-initial',{read});
7
+ if (!url) throw new Error('native-diagram-module-unavailable');
7
8
  const source = await read(url);
8
9
  const section = marker => {
9
10
  const at = source.indexOf(marker);
@@ -1,3 +1,4 @@
1
+ import { findCodexModule, codexModuleStyles } from '../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  import { readFile } from 'node:fs/promises';
2
3
  import { homedir } from 'node:os';
3
4
  import { join } from 'node:path';
@@ -16,7 +17,7 @@ import { loadNativeMermaidActions } from './adapters/codex/native.mjs';
16
17
  import { createMermaidRuntime } from './adapters/codex/runtime.mjs';
17
18
 
18
19
  export async function createPageScriptSource() {
19
- const bindings = [createMarkdownPreviewAdapter, loadMarkdownListAPI, captureMarkdownProviders,
20
+ const bindings = [findCodexModule, codexModuleStyles, createMarkdownPreviewAdapter, loadMarkdownListAPI, captureMarkdownProviders,
20
21
  loadNativeMarkdownToc, loadNativeMarkdownRenderer, markdownExportAlias, loadNativeMarkdownTableActions,
21
22
  createQvResources, createQvFrame, readQvTheme, exportQvDiagram, saveQvDiagram, createQvLoading,
22
23
  createMarkdownMermaidControls, installMarkdownMermaid, loadNativeMermaidActions, createMermaidRuntime];
@@ -1,3 +1,4 @@
1
+ import { findCodexModule } from '../../../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  export function createModelControllerAdapter(codex,comparableModelLabel) {
2
3
  function controllerView(props) {const view=Object.fromEntries(["models","model","reasoningEffort","powerSelections","selectedPowerSelection","fallbackPowerSelection","serviceTierOptions","selectedServiceTier","onSelectModel","onSelectReasoningEffort","onSelectPower","onCommitPower","onSelectServiceTier"].map(key=>[key,props[key]]));codex.report('model-controller',true);return view;}
3
4
  function nativeComposerTriggers() {
@@ -83,10 +84,9 @@ function invokeNativeMenuItemSelect(item) {
83
84
  }
84
85
 
85
86
  async function fastIcons(signal) {
86
- const sourceLink=document.querySelector('link[href*="/assets/app-primary-"][href$=".js"]')
87
- ?? document.querySelector('link[href*="/assets/app-initial-"][href$=".js"]');
88
- if (!sourceLink?.href) {codex.report("fast-icons",false,"bundle-unavailable");return null;}
89
- const source=await (await fetch(sourceLink.href,{signal})).text();
87
+ const sourceUrl=await findCodexModule('app-primary',{signal}) ?? await findCodexModule('app-initial',{signal});
88
+ if (!sourceUrl) {codex.report("fast-icons",false,"bundle-unavailable");return null;}
89
+ const source=await (await fetch(sourceUrl,{signal})).text();
90
90
  const active=extractFastIconPath(source,"M11\\.9125 21\\.4125"),inactive=extractFastIconPath(source,"M9\\.80999 17\\.8302");
91
91
  codex.report("fast-icons",Boolean(active && inactive),active && inactive ? null : "icon-resource-contract-changed");
92
92
  return {active:{path:active,viewBox:"0 0 24 24"},inactive:{path:inactive,viewBox:"0 0 20 20"}};
@@ -18,6 +18,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
18
18
  const modelSelectorNativeToggleHandlers = new Map();
19
19
  const modelSelectorPositionObservers = new Map();
20
20
  const modelSelectorMenuSizeFrames = new Map();
21
+ const modelSelectorOpeningFrames = new Map();
21
22
  let modelSelectorReturnTimer = null;
22
23
  let modelSelectorPendingReturn = false;
23
24
  let modelSelectorEffortTransactionTimer = null;
@@ -91,6 +92,16 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
91
92
  [data-codex-model-slider-trigger][data-codex-model-slider-trigger-placeholder] [class*="_ModelPickerTriggerContent_"] {
92
93
  width: 160px; min-width: 160px !important; max-width: 160px;
93
94
  }
95
+ /* 打开时先完成占位态,避免原生 crossfade 的宽度动画带动弹层。 */
96
+ [data-codex-model-slider-trigger][data-state="open"][data-codex-model-slider-trigger-placeholder] [class*="_ModelPickerTriggerContent_"] > span:has(> span > [class*="_ModelPickerTriggerPlaceholder_"]) {
97
+ width: 160px !important;
98
+ }
99
+ [data-codex-model-slider-trigger][data-state="open"][data-codex-model-slider-trigger-placeholder] span:has(> [class*="_ModelPickerTriggerPlaceholder_"]) {
100
+ opacity: 1 !important;
101
+ }
102
+ [data-codex-model-slider-trigger][data-state="open"][data-codex-model-slider-trigger-placeholder] [class*="_ModelPickerTriggerLabel_"] {
103
+ visibility: hidden;
104
+ }
94
105
  [data-codex-model-slider-trigger] [class*="_ModelPickerTriggerEffortLabel_"] {
95
106
  display: inline-flex; align-items: center; align-self: center;
96
107
  }
@@ -126,6 +137,11 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
126
137
  [data-codex-model-slider-generic-menu] {
127
138
  overflow-x: hidden;
128
139
  transform: translateX(var(--codex-model-slider-menu-center-shift, 0px)) !important;
140
+ /* 居中修正不能继承原生 transform 过渡,否则定位本身会播放位移。 */
141
+ transition-property: opacity, filter !important;
142
+ }
143
+ html[data-codex-model-slider-catalog-ready] [role="menu"][data-state="open"]:has([data-reasoning-slider]):not([data-codex-model-slider-opening-ready]) {
144
+ visibility: hidden !important;
129
145
  }
130
146
  [data-codex-model-slider-generic] { box-sizing: border-box; width: 100%; }
131
147
  [data-codex-model-slider-fast] { border: 0; background: transparent; cursor: pointer; }
@@ -991,11 +1007,13 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
991
1007
  const original = modelSelectorGenericContainers.get(container);
992
1008
  if (!original) return;
993
1009
  original.effectMirror?.disconnect();
1010
+ original.positionObserver?.disconnect();
994
1011
  if (container.isConnected) {
995
1012
  const menu = container.parentElement;
996
1013
  menu?.querySelector(":scope > [data-codex-model-slider-generic]")?.remove();
997
1014
  menu?.removeAttribute("data-codex-model-slider-generic-menu");
998
1015
  menu?.style.removeProperty("--codex-model-slider-menu-center-shift");
1016
+ menu?.removeAttribute("data-codex-model-slider-opening-ready");
999
1017
  container.style.position = original.position;
1000
1018
  container.style.inset = original.inset;
1001
1019
  container.style.opacity = original.opacity;
@@ -1095,11 +1113,10 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
1095
1113
  if (!record) return false;
1096
1114
  if (record.closing && !immediate) return true;
1097
1115
  record.reveal?.cancel();
1098
- const placeholder = document.querySelector(
1099
- '[data-codex-intelligence-trigger] [class*="_ModelPickerTriggerPlaceholder_"]',
1100
- );
1101
- if (placeholder?.firstChild && record.placeholderText != null) {
1102
- placeholder.firstChild.nodeValue = record.placeholderText;
1116
+ const placeholder = record.trigger?.querySelector('[class*="_ModelPickerTriggerPlaceholder_"]');
1117
+ const placeholderNode = placeholder && firstVisibleTextNode(placeholder);
1118
+ if (placeholderNode && record.placeholderText != null) {
1119
+ placeholderNode.nodeValue = record.placeholderText;
1103
1120
  }
1104
1121
  record.button.setAttribute("aria-expanded", "false");
1105
1122
  record.closing = true;
@@ -1279,15 +1296,15 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
1279
1296
  });
1280
1297
  renderEnhancedModelMenu(menu, selector);
1281
1298
 
1282
- const placeholder = document.querySelector(
1283
- '[data-codex-intelligence-trigger] [class*="_ModelPickerTriggerPlaceholder_"]',
1284
- );
1285
- const placeholderText = placeholder?.firstChild?.nodeValue ?? null;
1299
+ const trigger = nativeComposerTrigger();
1300
+ const placeholder = trigger?.querySelector('[class*="_ModelPickerTriggerPlaceholder_"]');
1301
+ const placeholderNode = placeholder && firstVisibleTextNode(placeholder);
1302
+ const placeholderText = placeholderNode?.nodeValue ?? null;
1286
1303
  // 保留产品行为:模型列表打开时 chip 显示 Select model。只能原地更新
1287
1304
  // 同一个文本节点(nodeValue),不能 textContent 替换整个节点,否则
1288
1305
  // React 卸载菜单时 removeChild 找不到旧文本节点会触发错误边界。
1289
- if (placeholder?.firstChild) {
1290
- placeholder.firstChild.nodeValue = locale === "zh-CN" ? "选择模型" : "Select model";
1306
+ if (placeholderNode) {
1307
+ placeholderNode.nodeValue = locale === "zh-CN" ? "选择模型" : "Select model";
1291
1308
  }
1292
1309
 
1293
1310
  modelSelectorControllerMenu = {
@@ -1295,6 +1312,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
1295
1312
  content,
1296
1313
  parentMenu,
1297
1314
  button: modelButton,
1315
+ trigger,
1298
1316
  placeholderText,
1299
1317
  position: parentMenu.style.position,
1300
1318
  overflow: parentMenu.style.overflow,
@@ -2000,11 +2018,42 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
2000
2018
  const previousShift = Number.parseFloat(
2001
2019
  parentMenu.style.getPropertyValue("--codex-model-slider-menu-center-shift"),
2002
2020
  ) || 0;
2003
- const unshiftedCenter = menuRect.left + (menuRect.width / 2) - previousShift;
2004
- const nextShift = triggerRect.left + (triggerRect.width / 2) - unshiftedCenter;
2021
+ const zoom = parentMenu.currentCSSZoom || 1;
2022
+ const unshiftedCenter = menuRect.left + (menuRect.width / 2) - previousShift * zoom;
2023
+ const nextShift = (triggerRect.left + (triggerRect.width / 2) - unshiftedCenter) / zoom;
2005
2024
  parentMenu.style.setProperty("--codex-model-slider-menu-center-shift", `${nextShift}px`);
2006
2025
  }
2007
2026
 
2027
+ function revealCenteredModelPicker(parentMenu) {
2028
+ if (parentMenu.hasAttribute("data-codex-model-slider-opening-ready")) {
2029
+ centerGenericModelPicker(parentMenu);
2030
+ return;
2031
+ }
2032
+ parentMenu.style.visibility = "hidden";
2033
+ if (modelSelectorOpeningFrames.has(parentMenu)) return;
2034
+ let previous = null;
2035
+ const position = () => {
2036
+ modelSelectorOpeningFrames.delete(parentMenu);
2037
+ const trigger = nativeComposerTrigger();
2038
+ if (!parentMenu.isConnected || !trigger || signal.aborted) return;
2039
+ centerGenericModelPicker(parentMenu);
2040
+ const menuRect = parentMenu.getBoundingClientRect();
2041
+ const triggerRect = trigger.getBoundingClientRect();
2042
+ const geometry = [menuRect.left, menuRect.width, triggerRect.left, triggerRect.width];
2043
+ // 等原生 Popper 跟上入口宽度;首次可见的位置必须已经稳定且居中。
2044
+ if (menuRect.width > 0 && triggerRect.width > 0
2045
+ && Math.abs(menuRect.left + menuRect.width / 2 - triggerRect.left - triggerRect.width / 2) < 1
2046
+ && previous && geometry.every((value, index) => Math.abs(value - previous[index]) < 0.5)) {
2047
+ parentMenu.setAttribute("data-codex-model-slider-opening-ready", "");
2048
+ parentMenu.style.removeProperty("visibility");
2049
+ return;
2050
+ }
2051
+ previous = geometry;
2052
+ modelSelectorOpeningFrames.set(parentMenu, requestAnimationFrame(position));
2053
+ };
2054
+ modelSelectorOpeningFrames.set(parentMenu, requestAnimationFrame(position));
2055
+ }
2056
+
2008
2057
  // ========================================================================
2009
2058
  // 通用 picker 增强与恢复 — 菜单/触发器增强、布局居中与恢复降级
2010
2059
  // ========================================================================
@@ -2053,13 +2102,12 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
2053
2102
  return true;
2054
2103
  }
2055
2104
  parentMenu.removeAttribute("data-codex-model-slider-template-wait");
2056
- parentMenu.style.removeProperty("visibility");
2057
2105
  const canUpgradeFallback = Boolean(
2058
2106
  existing?.querySelector("[data-codex-model-slider-fallback]") && nativeTemplateReady,
2059
2107
  );
2060
2108
  if (existing?.getAttribute("data-codex-model-slider-structure-key") === structureKey
2061
2109
  && existing.hasAttribute("data-codex-model-slider-fast-transitioning") && !canUpgradeFallback) {
2062
- centerGenericModelPicker(parentMenu);
2110
+ revealCenteredModelPicker(parentMenu);
2063
2111
  return true;
2064
2112
  }
2065
2113
  const currentFast = existing?.querySelector("[data-codex-model-slider-rail]")?.getAttribute("data-fast-mode");
@@ -2068,11 +2116,11 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
2068
2116
  if (existing?.getAttribute("data-codex-model-slider-structure-key") === structureKey
2069
2117
  && currentFast != null && currentFast === nativeFast && !canUpgradeFallback) {
2070
2118
  existing.setAttribute("data-codex-model-slider-generic", key);
2071
- centerGenericModelPicker(parentMenu);
2119
+ revealCenteredModelPicker(parentMenu);
2072
2120
  return true;
2073
2121
  }
2074
2122
  if (existing?.getAttribute("data-codex-model-slider-generic") === key && !canUpgradeFallback) {
2075
- centerGenericModelPicker(parentMenu);
2123
+ revealCenteredModelPicker(parentMenu);
2076
2124
  return true;
2077
2125
  }
2078
2126
  const nativeContainer = parentMenu.firstElementChild;
@@ -2090,7 +2138,13 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
2090
2138
  }
2091
2139
  modelSelectorGenericContainers.get(nativeContainer)?.effectMirror?.disconnect();
2092
2140
  if (!modelSelectorGenericContainers.has(nativeContainer)) {
2141
+ // Popper 会在后续帧改写 wrapper;同一轮微任务内重新居中,避免露出一帧旧位置。
2142
+ const positionObserver = new MutationObserver(() => centerGenericModelPicker(parentMenu));
2143
+ if (parentMenu.parentElement?.hasAttribute("data-radix-popper-content-wrapper")) {
2144
+ positionObserver.observe(parentMenu.parentElement, { attributes: true, attributeFilter: ["style"] });
2145
+ }
2093
2146
  modelSelectorGenericContainers.set(nativeContainer, {
2147
+ positionObserver,
2094
2148
  position: nativeContainer.style.position,
2095
2149
  inset: nativeContainer.style.inset,
2096
2150
  opacity: nativeContainer.style.opacity,
@@ -2110,12 +2164,14 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
2110
2164
  shell.setAttribute("data-codex-model-slider-structure-key", structureKey);
2111
2165
  replaceModelSliderShell(parentMenu, existing, shell);
2112
2166
  modelSelectorGenericContainers.get(nativeContainer).effectMirror = syncClonedPowerEffects(nativeContainer, shell);
2113
- centerGenericModelPicker(parentMenu);
2167
+ revealCenteredModelPicker(parentMenu);
2114
2168
  return true;
2115
2169
  }
2116
2170
 
2117
2171
  function enhanceNativeModelPicker(parentMenu, selector) {
2118
2172
  if (enhanceGenericModelPicker(parentMenu, selector)) return;
2173
+ parentMenu.setAttribute("data-codex-model-slider-opening-ready", "");
2174
+ parentMenu.style.removeProperty("visibility");
2119
2175
  const toggle = parentMenu.querySelector("[data-model-picker-view-toggle]");
2120
2176
  if (toggle && typeof nativeComposerModelController()?.onSelectModel === "function") {
2121
2177
  // 私有 DOM 无法满足 MCX shell 契约时,保留原生菜单作为降级路径。
@@ -2171,10 +2227,20 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
2171
2227
 
2172
2228
  function enhanceComposerModelTrigger(trigger, selector) {
2173
2229
  if (!modelSelectorTriggerObservers.has(trigger)) {
2174
- const observer = new MutationObserver(queueEnsure);
2175
- observer.observe(trigger, { childList: true, subtree: true, characterData: true });
2230
+ const observer = new MutationObserver(() => {
2231
+ enhanceComposerModelTrigger(trigger, selector);
2232
+ queueEnsure();
2233
+ });
2234
+ observer.observe(trigger, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ["data-state"] });
2176
2235
  modelSelectorTriggerObservers.set(trigger, observer);
2177
2236
  }
2237
+ const openPlaceholder = trigger.getAttribute("data-state") === "open"
2238
+ && trigger.querySelector('[class*="_ModelPickerTriggerPlaceholder_"]');
2239
+ if (openPlaceholder && modelSelectorControllerMenu?.trigger === trigger && !modelSelectorControllerMenu.closing) {
2240
+ const textNode = firstVisibleTextNode(openPlaceholder);
2241
+ const label = locale === "zh-CN" ? "选择模型" : "Select model";
2242
+ if (textNode && textNode.nodeValue !== label) textNode.nodeValue = label;
2243
+ }
2178
2244
  const nativeLabels = Array.from(trigger.querySelectorAll(
2179
2245
  '[class*="_ModelPickerTriggerModelText_"]:not([data-codex-model-slider-trigger-label])',
2180
2246
  ));
@@ -2182,7 +2248,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
2182
2248
  trigger.setAttribute("data-codex-model-slider-trigger", "");
2183
2249
  trigger.toggleAttribute(
2184
2250
  "data-codex-model-slider-trigger-placeholder",
2185
- isComposerModelPlaceholder(trigger.innerText),
2251
+ isComposerModelPlaceholder(openPlaceholder?.textContent || trigger.innerText),
2186
2252
  );
2187
2253
  restoreTriggerLabelWithoutNative(trigger, selector);
2188
2254
  return;
@@ -2199,7 +2265,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
2199
2265
  trigger.setAttribute("data-codex-model-slider-trigger-raw", raw);
2200
2266
  // 区分占位态(Select model / Select effort)与真实模型态:占位态沿用
2201
2267
  // 原生宽度契约,真实模型态让芯片 HUG 完整名称,避免第三方长名被截断。
2202
- const placeholder = isComposerModelPlaceholder(label);
2268
+ const placeholder = isComposerModelPlaceholder(openPlaceholder?.textContent || label);
2203
2269
  trigger.toggleAttribute("data-codex-model-slider-trigger-placeholder", placeholder);
2204
2270
  for (const nativeLabel of nativeLabels) {
2205
2271
  let displayLabel = nativeLabel.nextElementSibling;
@@ -2216,12 +2282,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
2216
2282
  }
2217
2283
  if (displayLabel.textContent !== label) displayLabel.textContent = label;
2218
2284
  }
2219
- // 原生开合的临时 min-width 钉定与弹层水平居中都基于对 Measurement
2220
- // 探针的测量;把探针文本同步为 MCX 显示标签,原生机制即可测得
2221
- // 真实显示宽度。保留 React 持有的子节点,避免切模型时卸载失败。
2222
- const measurement = trigger.querySelector('[class*="_ModelPickerTriggerMeasurement_"]');
2223
- const measurementText = measurement && firstVisibleTextNode(measurement);
2224
- if (measurementText && measurementText.nodeValue !== label) measurementText.nodeValue = label;
2285
+ // Measurement 属于原生 openLabel,占位宽度不能用真实模型名覆盖。
2225
2286
  }
2226
2287
 
2227
2288
  function ensureModelSelector() {
@@ -2284,6 +2345,11 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
2284
2345
  modelSelectorPositionObservers.clear();
2285
2346
  for (const frame of modelSelectorMenuSizeFrames.values()) cancelAnimationFrame(frame);
2286
2347
  modelSelectorMenuSizeFrames.clear();
2348
+ for (const [menu, frame] of modelSelectorOpeningFrames) {
2349
+ cancelAnimationFrame(frame);
2350
+ menu.style.removeProperty("visibility");
2351
+ }
2352
+ modelSelectorOpeningFrames.clear();
2287
2353
  modelSelectorReturnTimer = null;
2288
2354
  modelSelectorEffortTransactionTimer = null;
2289
2355
  modelSelectorPendingOutsideDismiss = false;
@@ -2318,6 +2384,8 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
2318
2384
  document.querySelectorAll("[data-codex-model-slider-generic]").forEach((element) => element.remove());
2319
2385
  document.querySelectorAll("[data-codex-model-slider-generic-menu]")
2320
2386
  .forEach((element) => element.removeAttribute("data-codex-model-slider-generic-menu"));
2387
+ document.querySelectorAll("[data-codex-model-slider-opening-ready]")
2388
+ .forEach((element) => element.removeAttribute("data-codex-model-slider-opening-ready"));
2321
2389
  document.querySelectorAll("[data-codex-model-slider-menu]")
2322
2390
  .forEach((element) => element.removeAttribute("data-codex-model-slider-menu"));
2323
2391
  document.querySelectorAll("[data-codex-model-slider-trigger]").forEach((element) => {
@@ -1,3 +1,4 @@
1
+ import { findCodexModule } from '../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  import { createModelControllerAdapter } from "./adapters/codex/controller.mjs";
2
3
  import { createModelRules } from "./domain/model-rules.mjs";
3
4
  import { installModelSlider, replaceModelSliderShell } from "./frontend/install.mjs";
@@ -5,6 +6,7 @@ import { installModelSlider, replaceModelSliderShell } from "./frontend/install.
5
6
  // 两部分均显式序列化,Renderer 不捕获 Node imports。
6
7
  export function createModelSliderInstallerSource() {
7
8
  return `function installModelSliderFeature(signal, context) {
9
+ const findCodexModule = ${findCodexModule.toString()};
8
10
  const replaceModelSliderShell = ${replaceModelSliderShell.toString()};
9
11
  return (${installModelSlider.toString()})(signal, context, (${createModelRules.toString()})(), ${createModelControllerAdapter.toString()});
10
12
  }`;
@@ -1,7 +1,8 @@
1
+ import { findCodexModule } from '../../../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  import {quickChatLifecycleActions, quickChatScope} from "./lifecycle.mjs";
2
3
  // 通过原生能力检查与已有会话分支定位打开动作,不依赖构建哈希或压缩名称。
3
4
  export function quickChatOpenExport(source) {
4
- const match = source.match(/function ([\w$]+)\(([\w$]+),[\w$]+=\{\}\)\{if\(!\2\.get\([\w$]+,\{name:[`"']chatgpt\.quick-chat[`"']\}\)\.isCapable\)return;let [\w$]+=\2\.get\([\w$]+\),[\w$]+=\2\.get\([\w$]+\);if\(/);
5
+ const match = source.match(/function ([\w$]+)\(([\w$]+),[\w$]+=\{\}\)\{if\(!\2\.get\([\w$]+,\{name:[`"']chatgpt\.quick-chat[`"']\}\)\.isCapable\)return;let [\w$]+=\2\.get\([\w$]+\),[\w$]+=\2\.get\([\w$]+\);(?:if\(|[\w$]+==null\?)/);
5
6
  if (!match) throw new Error('quick-chat-open-contract-unavailable');
6
7
  const escaped = match[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7
8
  const name = source.slice(source.lastIndexOf('export{')).match(new RegExp(`(?:\\{|,)${escaped} as ([\\w$]+)(?:,|\\})`))?.[1];
@@ -17,7 +18,8 @@ export function quickChatLifecycleExports(source) {
17
18
  const local = exports.match(new RegExp(`(?:\\{|,)([\\w$]+) as ${escaped(open)}(?:,|\\})`))?.[1];
18
19
  const start = source.indexOf(`function ${local}(`);
19
20
  const body = source.slice(start, source.indexOf('function ', start + 9));
20
- const create = body.match(/if\([\w$]+==null\)\{([\w$]+)\([\w$]+,[\w$]+\);return\}/)?.[1];
21
+ const create = body.match(/if\([\w$]+==null\)\{([\w$]+)\([\w$]+,[\w$]+\);return\}/)?.[1]
22
+ ?? body.match(/[\w$]+==null\?([\w$]+)\([\w$]+,[\w$]+\):/)?.[1];
21
23
  const state = body.match(/[\w$]+\.get\(([\w$]+)\)\|\|/)?.[1];
22
24
  const exportOf = name => name && exports.match(new RegExp(`(?:\\{|,)${escaped(name)} as ([\\w$]+)(?:,|\\})`))?.[1];
23
25
  const createExport = exportOf(create), openState = exportOf(state);
@@ -39,7 +41,8 @@ export function quickChatNativeCommand(source) {
39
41
 
40
42
  export function installQuickChatShortcut(signal, codex, discover = null) {
41
43
  const windows = navigator.platform.startsWith('Win');
42
- let bindings = null, disposed = false, pendingFocus = null, stopWaiting = null;
44
+ let bindings = null, disposed = false, pendingFocus = null;
45
+ const loading=new AbortController();
43
46
  function activateExisting(minimize = false) {
44
47
  // 原生恢复按钮同时恢复 presentation、取消最小化并聚焦输入框。
45
48
  for (const button of document.querySelectorAll('button')) {
@@ -95,20 +98,10 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
95
98
  return null;
96
99
  }
97
100
  async function load() {
98
- const findUrl = () => [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(e => e.href || e.src).find(url => /\/app-initial-[^/]+\.js(?:$|\?)/.test(url));
99
- // 宿主可早于原生模块入口注入;等待入口挂载,不能把正常启动顺序当作协议失效。
100
- const url = findUrl() ?? await new Promise((resolve, reject) => {
101
- codex.report('quick-chat-shortcut', false, 'quick-chat-module-pending');
102
- const cleanup = () => { observer.disconnect(); signal.removeEventListener('abort', cancel); stopWaiting=null; };
103
- const cancel = () => { cleanup(); reject(new Error('quick-chat-load-aborted')); };
104
- const check = () => { const found=findUrl(); if (found) { cleanup(); resolve(found); } };
105
- const observer = new MutationObserver(check);
106
- stopWaiting=cancel;
107
- observer.observe(document.documentElement, {childList:true,subtree:true,attributes:true,attributeFilter:['href','src']});
108
- signal.addEventListener('abort', cancel, {once:true});
109
- if (disposed || signal.aborted) cancel(); else check();
110
- });
111
- const source = await (await fetch(url, {signal})).text();
101
+ codex.report('quick-chat-shortcut', false, 'quick-chat-module-pending');
102
+ const url=await findCodexModule('app-initial',{signal:loading.signal,waitForEntry:true});
103
+ if (!url) throw new Error('quick-chat-module-unavailable');
104
+ const source = await (await fetch(url, {signal:loading.signal})).text();
112
105
  const module = await import(url);
113
106
  let native;
114
107
  try { native=quickChatNativeCommand(source); }
@@ -117,7 +110,7 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
117
110
  // 新版命令目录移入共享模块;打开动作仍由入口模块导出。
118
111
  const file=source.match(/["'`]\.\/(app-shared-[\w-]+\.js)["'`]/)?.[1];
119
112
  if (!file) throw error;
120
- const response=await fetch(new URL(file,url).href,{signal});
113
+ const response=await fetch(new URL(file,url).href,{signal:loading.signal});
121
114
  if (!response.ok) throw error;
122
115
  native=quickChatNativeCommand(await response.text());
123
116
  }
@@ -178,7 +171,7 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
178
171
  event.stopPropagation();
179
172
  }
180
173
  document.addEventListener('keydown', keydown, {signal});
181
- function dispose() { disposed = true; stopWaiting?.(); document.removeEventListener('keydown', keydown); bindings = null; pendingFocus=null; }
174
+ function dispose() { disposed = true; loading.abort(); document.removeEventListener('keydown', keydown); bindings = null; pendingFocus=null; }
182
175
  signal.addEventListener('abort', dispose, {once:true});
183
176
  return {ready, dispose, sync:focusNew, activate, close, canClose:() => Boolean(bindings?.close && !disposed && !signal.aborted), available:() => Boolean(bindings && !bindings.native && !disposed && !signal.aborted)};
184
177
  }
@@ -1,11 +1,11 @@
1
+ import { findCodexModule } from '../../../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  export async function loadQuickChatSidebarIcon(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));
3
+ const initialUrl = await findCodexModule('app-initial',{signal});
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|floating-chat-surface)-[\w-]+\.js)["']/)?.[1];
7
- if (!file) throw new Error('quick-chat-icon-module-unavailable');
8
- const url = new URL(file,initialUrl).href;
6
+ const file = initial.match(/["'`]\.\/((?:quick-chat-floating-surface|floating-chat-surface)-[\w-]+\.js)["'`]/)?.[1];
7
+ const url = file ? new URL(file,initialUrl).href : await findCodexModule('floating-chat-surface',{signal,via:['quick-chat-window','page-codex-quick-chat-window','content-chat-surface']}) ?? await findCodexModule('quick-chat-floating-surface',{signal});
8
+ if (!url) throw new Error('quick-chat-icon-module-unavailable');
9
9
  const source = await (await fetch(url,{signal})).text();
10
10
  // 原生 Restore 在未传 minimizedIcon 时使用的默认聊天图标。
11
11
  const local = source.match(/children:[\w$]+\?\?\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\)/)?.[1];
@@ -1,3 +1,4 @@
1
+ import { findCodexModule } from '../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  import {quickChatLifecycleActions, quickChatScope} from "./adapters/codex/lifecycle.mjs";
2
3
  import {quickChatHistoryVisible} from "./adapters/codex/history.mjs";
3
4
  import { readFile } from "node:fs/promises";
@@ -9,7 +10,7 @@ import { installQuickChatSidebar } from "./frontend/sidebar.mjs";
9
10
  import { createHistoryTracker } from "./domain/history.mjs";
10
11
  export async function createPageScriptSource() {
11
12
  const source = await readFile(new URL("./frontend/entry.mjs", import.meta.url), "utf8");
12
- const functions = [quickChatHistoryVisible, loadQuickChatSidebarIcon, createQuickChatSidebarAdapter, installQuickChatSidebar,
13
+ const functions = [findCodexModule, quickChatHistoryVisible, loadQuickChatSidebarIcon, createQuickChatSidebarAdapter, installQuickChatSidebar,
13
14
  quickChatLifecycleActions, quickChatScope, quickChatOpenExport, quickChatLifecycleExports, quickChatNativeCommand, installQuickChatShortcut, createQuickChatAdapter,
14
15
  createSubmissionTracker, createHistoryTracker];
15
16
  return functions.map(fn => `const ${fn.name}=${fn.toString()};`).join('\n') + '\n' + source;
@@ -1,3 +1,4 @@
1
+ import { findCodexModule } from '../../../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  export function contextUsageExport(source) {
2
3
  // 新版从宿主感知的 tokenUsageInfo selector 派生 thread atom family。
3
4
  const selector = source.match(/([\w$]+)=[\w$]+\(([`"'])tokenUsageInfo\2\),([\w$]+)=[\w$]+\(/);
@@ -38,24 +39,12 @@ export function createContextUsageAdapter(signal, codex, onChange, loadFamily =
38
39
  }
39
40
  async function discover() {
40
41
  if (loadFamily) return loadFamily();
41
- const urls=[...document.querySelectorAll('link[rel=modulepreload],script[src]')].map(node=>node.href || node.src);
42
- const matching = pattern => urls.filter(url=>pattern.test(url));
43
- const queue=[...matching(/\/app-initial-[^/]+\.js(?:$|\?)/),
44
- ...matching(/\/app-shared-[^/]+\.js(?:$|\?)/),
45
- ...matching(/\/(?:app-main|index)-[^/]+\.js(?:$|\?)/)];
46
- if (!queue.length) throw new Error("context-bundle-unavailable");
47
- const visited=new Set();
48
- while (queue.length) {
49
- const url=queue.shift();
50
- if (visited.has(url)) continue;
51
- visited.add(url);
42
+ for (const name of ['app-initial','app-shared']) {
43
+ const url=await findCodexModule(name,{signal});
44
+ if (!url) continue;
52
45
  const source=await (await fetch(url,{signal})).text();
53
46
  const exported=contextUsageExport(source);
54
47
  if (exported) return (await import(url))[exported];
55
- // modulepreload 只是优化提示;沿真实入口引用发现不再预加载的原生状态模块。
56
- const references=[...source.matchAll(/["'`](\.\/app-(?:initial|main|shared)-[^/"'`]+\.js)["'`]/g)]
57
- .map(match=>new URL(match[1],url).href);
58
- queue.push(...references);
59
48
  }
60
49
  throw new Error("context-export-unavailable");
61
50
  }
@@ -1,6 +1,7 @@
1
+ import { findCodexModule } from '../../../packages/codex-adapter/src/renderer/modules.mjs';
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { contextUsageExport, createContextUsageAdapter } from "./adapters/codex/context-usage.mjs";
3
4
  export async function createPageScriptSource() {
4
5
  const source=await readFile(new URL("./session-status.mjs",import.meta.url),"utf8");
5
- return `const contextUsageExport=${contextUsageExport.toString()};\nconst createContextUsageAdapter=${createContextUsageAdapter.toString()};\n${source}`;
6
+ return `const findCodexModule=${findCodexModule.toString()};\nconst contextUsageExport=${contextUsageExport.toString()};\nconst createContextUsageAdapter=${createContextUsageAdapter.toString()};\n${source}`;
6
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "2.0.48",
3
+ "version": "2.0.50",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -100,6 +100,7 @@
100
100
  "test:cli": "node --test packages/cli/test/*.test.mjs",
101
101
  "test:runtime": "node --test packages/runtime-host/test/*.test.mjs",
102
102
  "check": "node scripts/check-syntax.mjs",
103
+ "check:native": "node scripts/check-native-capabilities.mjs",
103
104
  "build:service": "scripts/build-service-app.zsh",
104
105
  "pack:check": "npm pack --dry-run",
105
106
  "pack:audit": "node scripts/audit-package.mjs",
@@ -1,3 +1,4 @@
1
+ import { findCodexModule } from '../renderer/modules.mjs';
1
2
  import { loadNativeMarkdownRenderer } from './markdown-native.mjs';
2
3
 
3
4
  export function markdownExportAlias(source, name) {
@@ -13,9 +14,8 @@ export function markdownExportAlias(source, name) {
13
14
 
14
15
  // 从 Chat Markdown 的公开组件发现悬浮操作栏和控件,不绑定构建 hash。
15
16
  export async function loadNativeMarkdownTableActions(native) {
16
- const url = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')]
17
- .map((n) => n.href || n.src)
18
- .find((url) => /\/app-initial-[^/]+\.js/.test(url));
17
+ const url = await findCodexModule('app-initial');
18
+ if (!url) throw new Error('native-table-actions-module-unavailable');
19
19
  const response = await fetch(url);
20
20
  if (!response.ok) throw new Error('native-table-actions-source-unavailable');
21
21
  const source = await response.text();
@@ -1,9 +1,9 @@
1
+ import { findCodexModule } from '../renderer/modules.mjs';
1
2
  // 使用当前编辑器所属的原生 CodeMirror 实例,避免引入第二套编辑器依赖。
2
3
  export async function loadMarkdownListAPI(view) {
3
- const links = [...document.querySelectorAll('link[rel="modulepreload"]')];
4
- for (const link of links) {
5
- if (!/\/dist-[\w-]+\.js$/.test(link.href)) continue;
6
- const exports = Object.values(await import(link.href));
4
+ const urls=await findCodexModule('dist',{all:true});
5
+ for (const url of urls) {
6
+ const exports = Object.values(await import(url));
7
7
  if (!exports.includes(view.constructor)) continue;
8
8
  const Decoration = exports.find((value) => value?.line && value?.replace && value?.mark);
9
9
  const ViewPlugin = exports.find((value) => value?.fromClass && value?.define);
@@ -1,10 +1,12 @@
1
+ import { findCodexModule, codexModuleStyles } from '../renderer/modules.mjs';
1
2
  // 直接导入原生 Marker、目录浮层和 Tooltip;仅按源码调用关系发现公开角色。
2
3
  export async function loadNativeMarkdownToc() {
3
4
  const urls = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(
4
5
  (n) => n.href || n.src,
5
6
  );
6
- const initialUrl = urls.find((url) => /\/app-initial-[^/]+\.js/.test(url));
7
- const mainUrl = urls.find((url) => /\/app-main-[^/]+\.js/.test(url));
7
+ const [initialUrl,mainUrl]=await Promise.all([
8
+ findCodexModule('app-initial'),findCodexModule('app-main'),
9
+ ]);
8
10
  if (!initialUrl || !mainUrl) throw new Error('native-page-toc-modules-unavailable');
9
11
  const read = async (url) => {
10
12
  const response = await fetch(url);
@@ -12,8 +14,10 @@ export async function loadNativeMarkdownToc() {
12
14
  return response.text();
13
15
  };
14
16
  const [initial, main] = await Promise.all([read(initialUrl), read(mainUrl)]);
17
+ const discovered=await findCodexModule('content',{all:true});
15
18
  const candidates = [
16
19
  ...new Set([
20
+ ...discovered,
17
21
  ...urls.filter((url) => /\/content-[^/]+\.js/.test(url)),
18
22
  ...[...initial.matchAll(/["']\.\/((?:content)-[\w-]+\.js)["']/g)].map(
19
23
  (m) => new URL(m[1], initialUrl).href,
@@ -85,18 +89,18 @@ export async function loadNativeMarkdownToc() {
85
89
  }
86
90
  const ReactDOM = (await binding(rootFactory, mainImports))();
87
91
  const css = initial.match(/["']\.\/((?:floating-navigation-rail(?:-layout)?)-[\w-]+\.css)["']/)?.[1];
88
- if (!css) throw new Error('native-page-toc-styles-unavailable');
89
- const cssUrl = new URL(css, initialUrl).href;
90
- if (![...document.querySelectorAll('link[rel="stylesheet"]')].some((n) => n.href === cssUrl)) {
91
- const link = document.createElement('link');
92
- link.rel = 'stylesheet';
93
- link.href = cssUrl;
94
- document.head.append(link);
95
- await new Promise((resolve, reject) => {
96
- link.onload = resolve;
97
- link.onerror = () => reject(new Error('native-page-toc-styles-unavailable'));
92
+ const cssUrls=css ? [new URL(css,initialUrl).href] : codexModuleStyles(initial,initialUrl,sourceUrl);
93
+ if (!cssUrls.length) throw new Error('native-page-toc-styles-unavailable');
94
+ await Promise.all(cssUrls.map(async cssUrl=>{
95
+ if ([...document.querySelectorAll('link[rel="stylesheet"]')].some(node=>node.href===cssUrl)) return;
96
+ const link=document.createElement('link');link.rel='stylesheet';link.href=cssUrl;
97
+ await new Promise((resolve,reject)=>{
98
+ link.onload=resolve;
99
+ link.onerror=()=>reject(new Error('native-page-toc-styles-unavailable'));
100
+ document.head.append(link);
98
101
  });
99
- }
102
+ }));
103
+
100
104
  const component = (predicate) => {
101
105
  const matches = Object.values(layout).filter(
102
106
  (value) => typeof value === 'function' && predicate(String(value)),
@@ -125,7 +125,10 @@ export function createCodexEnvironment() {
125
125
  const unknown = {status:"unknown", planType:null, source:null, key:null};
126
126
  if (signal?.aborted || !auth || auth.isLoading || !auth.userId || !auth.accountId
127
127
  || !["chatgpt", "chatgptAuthTokens"].includes(auth.authMethod)) return unknown;
128
- const data = client?.getQueryData(["accounts", "check"]);
128
+ const data = client?.getQueryCache().findAll({queryKey:["accounts","check"]})
129
+ .filter(query=>query.state.data)
130
+ .sort((a,b)=>(b.state.dataUpdatedAt??0)-(a.state.dataUpdatedAt??0))[0]?.state.data;
131
+ if (data && !Array.isArray(data.accounts)) return unknown;
129
132
  const current = data?.accounts?.find(value => value.id === auth.accountId);
130
133
  // 已有账号清单却找不到当前身份时,不借用上一个账号的登录套餐。
131
134
  if (data && !current) return unknown;
@@ -0,0 +1,66 @@
1
+ // 只沿原生应用入口链读取模块引用;预加载标签不是完整的模块清单。
2
+ export async function findCodexModule(name, {read = null, signal, all = false, waitForEntry = false, via = []} = {}) {
3
+ const links = () => [...document.querySelectorAll('link[rel="modulepreload"],script[src]')]
4
+ .map(node => node.href || node.src).filter(Boolean);
5
+ const entry = url => /\/(?:index|app-(?:main|initial))-[^/]+\.js(?:$|\?)/.test(url);
6
+ let urls=links();
7
+ signal?.throwIfAborted();
8
+ if (waitForEntry && !urls.some(entry)) {
9
+ await new Promise((resolve,reject) => {
10
+ const cleanup=()=>{observer.disconnect();signal?.removeEventListener('abort',abort);};
11
+ const abort=()=>{cleanup();reject(signal.reason);};
12
+ const check=()=>{if(links().some(entry)){cleanup();resolve();}};
13
+ const observer=new MutationObserver(check);
14
+ observer.observe(document.documentElement,{childList:true,subtree:true,attributes:true,attributeFilter:['src','href','rel']});
15
+ signal?.addEventListener('abort',abort,{once:true});
16
+ if(signal?.aborted) abort(); else check();
17
+ });
18
+ urls=links();
19
+ }
20
+ const matches=url=>new URL(url).pathname.split('/').at(-1)?.startsWith(`${name}-`)
21
+ && /\.js(?:$|\?)/.test(url);
22
+ const found=new Set(urls.filter(matches));
23
+ if (found.size && !all) return [...found][0];
24
+ const core=url=>/\/(?:index|app-(?:main|initial|primary|runtime|shared))-[^/]+\.js(?:$|\?)/.test(url)
25
+ || via.some(role=>new URL(url).pathname.split('/').at(-1)?.startsWith(`${role}-`));
26
+ const queue=urls.filter(core).sort((a,b)=>Number(a.includes('/app-shared-'))-Number(b.includes('/app-shared-')));
27
+ const visited=new Set();
28
+ const source=read ?? (async url=>{
29
+ const response=await fetch(url,{signal});
30
+ if(!response.ok) throw new Error('native-module-source-unavailable');
31
+ return response.text();
32
+ });
33
+ while(queue.length) {
34
+ signal?.throwIfAborted();
35
+ const url=queue.shift();
36
+ if(visited.has(url)) continue;
37
+ visited.add(url);
38
+ const text=await source(url);
39
+ signal?.throwIfAborted();
40
+ for(const match of text.matchAll(/["'`](\.\/[\w.-]+\.js)["'`]/g)) {
41
+ const dependency=new URL(match[1],url).href;
42
+ if(matches(dependency)) {found.add(dependency);if(!all)return dependency;}
43
+ if(core(dependency) && !visited.has(dependency))queue.push(dependency);
44
+ }
45
+ }
46
+ return all ? [...found] : null;
47
+ }
48
+
49
+ // 使用原生 lazy import 声明的 CSS 依赖,兼容 Vite 将语义样式名改成纯 hash。
50
+ export function codexModuleStyles(source, ownerUrl, moduleUrl) {
51
+ const table=source.match(/\.f=\[([^\]]+)\]/)?.[1];
52
+ if(!table)return [];
53
+ const dependencies=JSON.parse(`[${table}]`);
54
+ for(const match of source.matchAll(/import\(["'`]([^"'`]+)["'`]\)/g)) {
55
+ if(new URL(match[1],ownerUrl).href!==moduleUrl)continue;
56
+ const call=source.slice(match.index,source.indexOf('import.meta.url',match.index));
57
+ const indices=call.match(/__vite__mapDeps\(\[([\d,]+)\]\)/)?.[1];
58
+ if(!indices)return [];
59
+ return [...new Set(indices.split(',').map(Number).map(index=>{
60
+ const file=dependencies[index];
61
+ if(typeof file!=='string')throw new Error('native-module-style-dependency-unavailable');
62
+ return file;
63
+ }).filter(file=>file.endsWith('.css')).map(file=>new URL(file,ownerUrl).href))];
64
+ }
65
+ return [];
66
+ }
@@ -1,3 +1,4 @@
1
+ import { findCodexModule } from '../../codex-adapter/src/renderer/modules.mjs';
1
2
  import { createRendererSelfCheck, tokenRequirements } from "./renderer-self-check.mjs";
2
3
  import { createCodexEnvironment } from "../../codex-adapter/src/renderer/environment.mjs";
3
4
  // 注入源生成:createInjectionSource 产出注入到 Codex Renderer 的自包含脚本字符串。
@@ -10,7 +11,7 @@ import { MCP_NAVIGATION_ICONS } from "../../plugin/src/navigation-icons.mjs";
10
11
  import { randomBytes } from "node:crypto";
11
12
  import { CODEX_THEME_TOKENS, CODEX_THEME_TOKEN_ALIASES, MINECODEX_THEME_BINDINGS } from "../../ui/src/native/tokens.mjs";
12
13
 
13
- export const RUNTIME_VERSION = 113;
14
+ export const RUNTIME_VERSION = 114;
14
15
  export const HOST_BINDING_NAME = "__codexPersonalHostAction";
15
16
  export const CSP_BOOTSTRAP_VERSION = 1;
16
17
  export const CSP_BOOTSTRAP_KEY = "__mineCodexCspBootstrapVersion";
@@ -124,14 +125,14 @@ export function createInjectionSource(features, {
124
125
  bindingToken = "test-binding-token",
125
126
  runtimeSessionId = randomBytes(16).toString("hex"),
126
127
  } = {}) {
127
- function install(config, installUI, installModelSliderFeature, createCodexEnvironment, createRendererSelfCheck) {
128
+ function install(config, installUI, installModelSliderFeature, createCodexEnvironment, createRendererSelfCheck, findCodexModule) {
128
129
  if (window.top !== window) return;
129
130
  if (!document.documentElement) {
130
131
  // The document bootstrap runs before the root element exists. Defer the
131
132
  // whole install until the DOM is ready instead of crashing on a null
132
133
  // documentElement (which previously left the runtime permanently absent
133
134
  // after a renderer reload).
134
- document.addEventListener("DOMContentLoaded", () => install(config, installUI, installModelSliderFeature, createCodexEnvironment, createRendererSelfCheck), { once: true });
135
+ document.addEventListener("DOMContentLoaded", () => install(config, installUI, installModelSliderFeature, createCodexEnvironment, createRendererSelfCheck, findCodexModule), { once: true });
135
136
  return;
136
137
  }
137
138
  const currentRuntime = window.__codexPersonalRuntime;
@@ -1506,11 +1507,6 @@ export function createInjectionSource(features, {
1506
1507
  // 原生 Detail Tab capability — React fiber/右侧面板探测、原生详情页签打开与线程跳转
1507
1508
  // ========================================================================
1508
1509
 
1509
- function nativeTabModuleUrl() {
1510
- return Array.from(document.querySelectorAll('link[rel="modulepreload"]'))
1511
- .map((link) => link.href)
1512
- .find((url) => /\/assets\/app-initial-[^/]+\.js(?:\?|$)/.test(url)) ?? null;
1513
- }
1514
1510
 
1515
1511
  function reactFiberFor(element) {
1516
1512
  if (!(element instanceof Element)) return null;
@@ -1600,13 +1596,7 @@ export function createInjectionSource(features, {
1600
1596
  }
1601
1597
 
1602
1598
  async function waitForNativeTabModuleUrl() {
1603
- const deadline = performance.now() + 2_000;
1604
- do {
1605
- const moduleUrl = nativeTabModuleUrl();
1606
- if (moduleUrl) return moduleUrl;
1607
- await new Promise((resolve) => setTimeout(resolve, 50));
1608
- } while (performance.now() < deadline);
1609
- return null;
1599
+ return findCodexModule('app-initial',{signal:lifetime.signal,waitForEntry:true});
1610
1600
  }
1611
1601
 
1612
1602
  async function loadNativeTabCapability() {
@@ -1615,7 +1605,7 @@ export function createInjectionSource(features, {
1615
1605
  nativeTabCapabilityPromise = (async () => {
1616
1606
  try {
1617
1607
  const moduleUrl = await waitForNativeTabModuleUrl();
1618
- if (!moduleUrl) throw new Error("Codex app-initial modulepreload was not found");
1608
+ if (!moduleUrl) throw new Error("Codex app-initial module was not found");
1619
1609
  const appModule = await import(moduleUrl);
1620
1610
  const controller = Object.values(appModule).find((value) => (
1621
1611
  value
@@ -2468,7 +2458,7 @@ export function createInjectionSource(features, {
2468
2458
  themeBindings: MINECODEX_THEME_BINDINGS,
2469
2459
  mcpNavigationIcons: MCP_NAVIGATION_ICONS,
2470
2460
  features: runtimeFeatures,
2471
- })}, ${createUIInstallerSource()}, ${createModelSliderInstallerSource()}, ${createCodexEnvironment.toString()}, ${createRendererSelfCheck.toString()})`;
2461
+ })}, ${createUIInstallerSource()}, ${createModelSliderInstallerSource()}, ${createCodexEnvironment.toString()}, ${createRendererSelfCheck.toString()}, ${findCodexModule.toString()})`;
2472
2462
  }
2473
2463
 
2474
2464
  export function createRuntimeReadyContract({