minecodex 2.0.43 → 2.0.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/apps/dashboard/app.js +1 -1
- package/apps/dashboard/assets/preview-content.js +1 -3
- package/features/composer-expand/src/native.mjs +43 -7
- package/features/file-navigation/src/adapters/codex/modules.mjs +10 -11
- package/features/images/src/adapters/codex/native-image-modules.mjs +11 -4
- package/features/images/src/frontend/local-image-source.mjs +11 -1
- package/features/markdown-reading/src/adapters/codex/table-actions.mjs +16 -17
- package/features/markdown-reading/src/adapters/codex/tables.mjs +18 -4
- package/features/markdown-reading/src/injection-source.mjs +2 -1
- package/features/mermaid-diagrams/src/adapters/codex/native.mjs +14 -6
- package/features/model-slider/src/frontend/install.mjs +11 -2
- package/features/model-slider/src/injection-source.mjs +2 -1
- package/features/quick-chat/src/adapters/codex/shortcut.mjs +55 -21
- package/features/quick-chat/src/adapters/codex/sidebar.mjs +1 -1
- package/features/quick-chat/src/injection-source.mjs +2 -2
- package/package.json +2 -4
- package/packages/cli/src/config.mjs +3 -2
- package/packages/cli/src/control-server.mjs +1 -1
- package/packages/cli/src/platform.mjs +8 -1
- package/features/hide-upsell-banner/codex-feature.json +0 -24
- package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +0 -94
package/README.md
CHANGED
package/apps/dashboard/app.js
CHANGED
|
@@ -240,7 +240,7 @@ function findPluginCard(featureId) {
|
|
|
240
240
|
|
|
241
241
|
function renderPluginCards(features) {
|
|
242
242
|
// Task 暂停接入,旧版服务返回的卡片也不展示。
|
|
243
|
-
features = features.filter(feature =>
|
|
243
|
+
features = features.filter(feature => !["unread-inbox", "hide-upsell-banner"].includes(feature.id));
|
|
244
244
|
for (const child of [...elements.pluginList.children]) {
|
|
245
245
|
if (!child.dataset.featureId) child.remove();
|
|
246
246
|
}
|
|
@@ -36,9 +36,7 @@ const content = {
|
|
|
36
36
|
'sidebar-focus': [
|
|
37
37
|
['Codex 左侧栏的分组标题。', '多个侧栏分组展开时,当前分组不容易聚焦。', '点击当前分组,一键收起其他分组;再次点击恢复先前的展开状态。'],
|
|
38
38
|
['Group headings in the Codex sidebar.', 'Several expanded groups make the current group harder to focus on.', 'Click a group to collapse other groups. Click again to restore their previous expanded state.']],
|
|
39
|
-
|
|
40
|
-
['Codex 的额度推广提示区域。', '推广提示占用日常操作空间。', '隐藏额度推广提示,保留真实额度与运行错误反馈。'],
|
|
41
|
-
['Usage promotion banners in Codex.', 'Promotions take space during everyday tasks.', 'Hide usage promotions while retaining real usage and error feedback.']],
|
|
39
|
+
|
|
42
40
|
};
|
|
43
41
|
export function previewContent(id, locale) {
|
|
44
42
|
return content[id]?.[locale === 'zh-CN' ? 0 : 1] ?? (locale === 'zh-CN'
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
export async function loadComposerExpandControls(signal) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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$]+),\{\}\)/);
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
// 依照原生 breadcrumb 的依赖角色查找公开模块,不绑定产物 hash 或短变量名。
|
|
2
|
-
export async function loadFileNavigationModules() {
|
|
2
|
+
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
3
|
const urls = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(n => n.href || n.src);
|
|
4
4
|
const breadcrumbUrl = urls.find(url => /\/review-file-source-breadcrumb-[^/]+\.js$/.test(url));
|
|
5
5
|
const mainUrl = urls.find(url => /\/app-main-[^/]+\.js$/.test(url));
|
|
6
6
|
if (!breadcrumbUrl || !mainUrl) throw new Error('file-navigation-modules-unavailable');
|
|
7
|
-
const read = async url => {
|
|
8
|
-
const response = await fetch(url);
|
|
9
|
-
if (!response.ok) throw new Error('file-navigation-source-unavailable');
|
|
10
|
-
return response.text();
|
|
11
|
-
};
|
|
12
7
|
const imports = (source, url) => {
|
|
13
8
|
const result = new Map();
|
|
14
9
|
for (const match of source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g))
|
|
@@ -21,14 +16,14 @@ export async function loadFileNavigationModules() {
|
|
|
21
16
|
const binding = async (map, name) => {
|
|
22
17
|
const value = map.get(name);
|
|
23
18
|
if (!value) throw new Error('file-navigation-dependency-unavailable');
|
|
24
|
-
return (await
|
|
19
|
+
return (await load(value.url))[value.name];
|
|
25
20
|
};
|
|
26
21
|
const source = await read(breadcrumbUrl), deps = imports(source, breadcrumbUrl);
|
|
27
22
|
const treeUrl = [...deps.values()].find(v => /\/base-file-tree-/.test(v.url))?.url;
|
|
28
23
|
const queryUrl = [...deps.values()].find(v => /\/workspace-directory-entries-query-/.test(v.url))?.url;
|
|
29
24
|
if (!treeUrl || !queryUrl) throw new Error('file-navigation-contract-changed');
|
|
30
25
|
const [treeSource, querySource, main] = await Promise.all([read(treeUrl), read(queryUrl), read(mainUrl)]);
|
|
31
|
-
const [treeModule, queryModule] = await Promise.all([
|
|
26
|
+
const [treeModule, queryModule] = await Promise.all([load(treeUrl), load(queryUrl)]);
|
|
32
27
|
for (const module of [treeModule, queryModule])
|
|
33
28
|
for (const value of Object.values(module))
|
|
34
29
|
if (typeof value === 'function' && /^function ([\w$]+)\(\)\{return\(\1=/.test(String(value))) value();
|
|
@@ -38,16 +33,20 @@ export async function loadFileNavigationModules() {
|
|
|
38
33
|
const rootOwner = main.match(/\(0,([\w$]+)\.createRoot\)/)?.[1];
|
|
39
34
|
const rootFactory = rootOwner && main.match(new RegExp(`\\b${rootOwner}=([\\w$]+)\\(\\)`))?.[1];
|
|
40
35
|
const requestName = querySource.match(/queryFn:\(\{signal:[\w$]+\}\)=>([\w$]+)\(`workspace-directory-entries`/)?.[1];
|
|
41
|
-
const
|
|
36
|
+
const directoryName = querySource.match(/queryFn:\(\{signal:[\w$]+\}\)=>([\w$]+)\([\w$]+,[\w$]+\)/)?.[1];
|
|
37
|
+
const [react, dom, directoryRequest] = await Promise.all([
|
|
42
38
|
binding(imports(treeSource, treeUrl), reactFactory),
|
|
43
39
|
binding(imports(main, mainUrl), rootFactory),
|
|
44
|
-
binding(imports(querySource, queryUrl), requestName),
|
|
40
|
+
binding(imports(querySource, queryUrl), requestName ?? directoryName),
|
|
45
41
|
]);
|
|
42
|
+
// 新版 query 将目录读取委托给 workspaceFiles.listDirectory,参数和取消信号保持原生归属。
|
|
43
|
+
const request = requestName ? directoryRequest : (_method, {params, signal}) => directoryRequest(params, signal);
|
|
44
|
+
if (typeof directoryRequest !== 'function') throw new Error('file-navigation-directory-unavailable');
|
|
46
45
|
const React = react(), ReactDOM = dom();
|
|
47
46
|
if (!Tree || !React.createElement || !ReactDOM.createRoot || typeof request !== 'function')
|
|
48
47
|
throw new Error('file-navigation-components-unavailable');
|
|
49
48
|
const sharedUrl = [...imports(treeSource, treeUrl).values()].find(value => /\/app-shared-/.test(value.url))?.url;
|
|
50
|
-
const shared = sharedUrl && await
|
|
49
|
+
const shared = sharedUrl && await load(sharedUrl);
|
|
51
50
|
const folder = shared && Object.values(shared).find(value => value?.name === 'folder-light-16');
|
|
52
51
|
if (!folder?.canvas?.viewBox || typeof folder.body !== 'string')
|
|
53
52
|
throw new Error('file-navigation-folder-icon-unavailable');
|
|
@@ -75,13 +75,18 @@ export function createNativeImageModules({read = async url => (await fetch(url))
|
|
|
75
75
|
const [initialSource, mainSource] = await Promise.all([read(initialUrl), read(mainUrl)]);
|
|
76
76
|
const initial = await load(initialUrl);
|
|
77
77
|
const asset = (name, references = initialSource) => {
|
|
78
|
+
const loaded = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(node => node.href || node.src)
|
|
79
|
+
.find(url => new RegExp(`/${name}-[\\w-]+\\.js$`).test(url));
|
|
80
|
+
if (loaded) return loaded;
|
|
78
81
|
const file = references.match(new RegExp(`(?:["'./])(${name}-[\\w-]+\\.js)`))?.[1];
|
|
79
82
|
if (!file) throw new Error(`Native Images capability is unavailable: ${name}`);
|
|
80
83
|
return new URL(file, initialUrl).href;
|
|
81
84
|
};
|
|
82
85
|
const module = async (name,references) => {
|
|
83
86
|
// 26.1013 将图库排布与卡片合入入口,其他能力仍按独立模块发现。
|
|
84
|
-
|
|
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)
|
|
85
90
|
&& !new RegExp(`(?:["'./])(${name}-[\\w-]+\\.js)`).test(references ?? initialSource)) {
|
|
86
91
|
return {url:initialUrl, source:initialSource, exports:initial};
|
|
87
92
|
}
|
|
@@ -129,7 +134,9 @@ export function createNativeImageModules({read = async url => (await fetch(url))
|
|
|
129
134
|
module("generated-image-context-menu"),module("library-selection-toolbar"),
|
|
130
135
|
]);
|
|
131
136
|
// 26.1007 将选择状态与图库动作移到 selectable-library-item;工具栏保留按钮与材质。
|
|
132
|
-
const
|
|
137
|
+
const selectionRows=await module("selectable-library-item",toolbar.source);
|
|
138
|
+
const selection = /library-move-dialog-[\w-]+\.js/.test(selectionRows.source)
|
|
139
|
+
? selectionRows : await module("library-item-menu",selectionRows.source);
|
|
133
140
|
const [moveDialog,dialogs]=await Promise.all([module("library-move-dialog",selection.source),module("library-item-dialogs",toolbar.source)]);
|
|
134
141
|
const routerLocal = selection.source.match(/get\(([\w$]+)\)\.prepareNavigation\(/)?.[1];
|
|
135
142
|
const routerAtom = await binding(selection.source, selection.url, routerLocal);
|
|
@@ -143,9 +150,9 @@ export function createNativeImageModules({read = async url => (await fetch(url))
|
|
|
143
150
|
const iconLocal = toolbar.source.match(/id:`libraryNext.selection.more`[^;]+?children:\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\)/)?.[1];
|
|
144
151
|
const [Dropdown, Menu] = await Promise.all([dropdownLocal, itemsLocal].map(local => binding(selection.source, selection.url, local)));
|
|
145
152
|
const [Button, MoreIcon] = await Promise.all([buttonLocal, iconLocal].map(local => binding(toolbar.source, toolbar.url, local)));
|
|
146
|
-
const checkboxFunction = component(
|
|
153
|
+
const checkboxFunction = component(selectionRows.exports,source=>/onCheckedChange:/.test(source)&&/viewMode:/.test(source),"selection checkbox");
|
|
147
154
|
const checkboxLocal = String(checkboxFunction).match(/jsx\)\(([\w$]+),\{className:`cursor-interaction`,/)?.[1];
|
|
148
|
-
const Checkbox = await binding(
|
|
155
|
+
const Checkbox = await binding(selectionRows.source,selectionRows.url,checkboxLocal);
|
|
149
156
|
const rangeSelection = component(selection.exports,source=>/^function [\w$]+\(\{orderedIds:/.test(source)&&/extendRange:/.test(source),"range selection");
|
|
150
157
|
const selectionLocal = toolbar.source.match(/\.set\(([\w$]+),new Set[,)]/)?.[1];
|
|
151
158
|
const nativeSelectedIds = await binding(toolbar.source,toolbar.url,selectionLocal);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// 只编排来源与加载状态;图库、标签、菜单及详情由原生 Adapter 承载。
|
|
2
2
|
export function installLocalImageSource(config, signal, sources, native) {
|
|
3
3
|
let mounted;
|
|
4
|
+
let failedEntry;
|
|
4
5
|
let revision = 0;
|
|
5
6
|
let pendingPrompt;
|
|
6
7
|
const text = (en, zh) => (document.documentElement.lang || navigator.language).startsWith("zh") ? zh : en;
|
|
@@ -71,6 +72,7 @@ export function installLocalImageSource(config, signal, sources, native) {
|
|
|
71
72
|
record.entry.content.hidden = record.active || record.contentHidden;
|
|
72
73
|
record.area.hidden = !record.active;
|
|
73
74
|
if (record.entry.search) record.entry.search.value = record.active ? record.query : record.searchValue;
|
|
75
|
+
record.selectedKey = key;
|
|
74
76
|
record.nav.render(key);
|
|
75
77
|
if (record.active) void fetchPage(record, true);
|
|
76
78
|
}
|
|
@@ -79,9 +81,14 @@ export function installLocalImageSource(config, signal, sources, native) {
|
|
|
79
81
|
if (signal.aborted) return;
|
|
80
82
|
const entry = sources.read();
|
|
81
83
|
if (mounted && mounted.entry.tabs === entry?.tabs && mounted.entry.accountKey === entry?.accountKey && mounted.entry.mode === entry?.mode) {
|
|
82
|
-
if (!mounted.active
|
|
84
|
+
if (!mounted.active && mounted.nav && mounted.selectedKey !== entry.selectedKey) {
|
|
85
|
+
mounted.selectedKey = entry.selectedKey;
|
|
86
|
+
mounted.nav.render(entry.selectedKey);
|
|
87
|
+
}
|
|
83
88
|
return;
|
|
84
89
|
}
|
|
90
|
+
if (failedEntry?.tabs === entry?.tabs && failedEntry?.accountKey === entry?.accountKey && failedEntry?.mode === entry?.mode) return;
|
|
91
|
+
failedEntry = null;
|
|
85
92
|
unmount();
|
|
86
93
|
if (!entry) return;
|
|
87
94
|
const navigation = document.createElement("div");
|
|
@@ -126,6 +133,7 @@ export function installLocalImageSource(config, signal, sources, native) {
|
|
|
126
133
|
onRetry:error => {
|
|
127
134
|
console.error("MineCodex native Images rendering",error);
|
|
128
135
|
if (mounted !== record) return;
|
|
136
|
+
failedEntry = entry;
|
|
129
137
|
unmount();
|
|
130
138
|
config.codex.report("images.local-source", false, "Native image rendering failed");
|
|
131
139
|
},
|
|
@@ -145,6 +153,7 @@ export function installLocalImageSource(config, signal, sources, native) {
|
|
|
145
153
|
record.nav = nav;
|
|
146
154
|
record.view = view;
|
|
147
155
|
entry.tabs.hidden = true;
|
|
156
|
+
record.selectedKey = entry.selectedKey;
|
|
148
157
|
nav.render(entry.selectedKey);
|
|
149
158
|
retry.onclick = () => fetchPage(record, true);
|
|
150
159
|
record.observer = new IntersectionObserver(entries => {
|
|
@@ -169,6 +178,7 @@ export function installLocalImageSource(config, signal, sources, native) {
|
|
|
169
178
|
entry.search?.addEventListener("keydown", record.onSearchKey, true);
|
|
170
179
|
} catch {
|
|
171
180
|
if (mounted === record) {
|
|
181
|
+
failedEntry = entry;
|
|
172
182
|
unmount();
|
|
173
183
|
config.codex.report("images.local-source", false, "Native image capability unavailable");
|
|
174
184
|
}
|
|
@@ -68,23 +68,22 @@ export function installMarkdownTableActions(view, native, codex, controller) {
|
|
|
68
68
|
h(
|
|
69
69
|
Actions,
|
|
70
70
|
{ 'data-visible': item.visible, 'data-markdown-copy': 'exclude' },
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
: null,
|
|
71
|
+
h(
|
|
72
|
+
Tooltip,
|
|
73
|
+
{ tooltipContent: wrap, delayOpen: true },
|
|
74
|
+
h(
|
|
75
|
+
Button,
|
|
76
|
+
{
|
|
77
|
+
'aria-label': wrap,
|
|
78
|
+
'aria-pressed': item.fit,
|
|
79
|
+
color: 'blockAction',
|
|
80
|
+
radius: 'full',
|
|
81
|
+
size: 'inputIcon',
|
|
82
|
+
onClick: () => controller.toggle(),
|
|
83
|
+
},
|
|
84
|
+
h(item.fit ? wrapIcons.disable : wrapIcons.enable),
|
|
85
|
+
),
|
|
86
|
+
),
|
|
88
87
|
h(CopyButton, {
|
|
89
88
|
ariaLabel: copy,
|
|
90
89
|
buttonText: copy,
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
// 操作栏锚定阅读区域;表格横向展开与滚动不改变恢复按钮的位置。
|
|
2
|
+
export function markdownTableActionGeometry(first, last, frame, scale, scrollTop, padding, headerInset) {
|
|
3
|
+
const left = padding.left;
|
|
4
|
+
const width = Math.max(0, frame.width / scale - padding.left - padding.right);
|
|
5
|
+
const viewportTop = Math.max(0, (first.top - frame.top) / scale + headerInset - 40);
|
|
6
|
+
const visibleBottom = (Math.min(last.bottom, frame.bottom) - frame.top) / scale;
|
|
7
|
+
return {left, width, top: scrollTop + Math.min(viewportTop, Math.max(0, visibleBottom - 48)),
|
|
8
|
+
inView: last.bottom > frame.top && first.top < frame.bottom};
|
|
9
|
+
}
|
|
10
|
+
|
|
1
11
|
// 保留原生逐行表格及选区;每张表的行共享横向位置,不移动正文节点。
|
|
2
12
|
export function installMarkdownTables(view, api, wrapState) {
|
|
3
13
|
const { Decoration, ViewPlugin, StateEffect, Compartment, WidgetType } = api;
|
|
@@ -323,14 +333,18 @@ export function installMarkdownTables(view, api, wrapState) {
|
|
|
323
333
|
if (!rows.length) continue;
|
|
324
334
|
const first = rows[0].getBoundingClientRect();
|
|
325
335
|
const headerInset = parseFloat(getComputedStyle(rows[0]).paddingTop);
|
|
336
|
+
const padding = getComputedStyle(view.scrollDOM);
|
|
337
|
+
const geometry = markdownTableActionGeometry(first, rows.at(-1).getBoundingClientRect(), frameRect, scale,
|
|
338
|
+
view.scrollDOM.scrollTop, {left:parseFloat(padding.paddingLeft),right:parseFloat(padding.paddingRight)}, headerInset);
|
|
326
339
|
items.push({
|
|
327
340
|
id,
|
|
328
341
|
fit: wrapState.get(),
|
|
329
342
|
overflow: (scrollLimits.get(id) ?? 0) > 0,
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
343
|
+
// 关闭换行后常显恢复入口;键盘用户聚焦任一表格控件也保持显示。
|
|
344
|
+
visible: geometry.inView && (hovered === id || tableId(document.activeElement) === id || !wrapState.get()),
|
|
345
|
+
left: geometry.left,
|
|
346
|
+
top: geometry.top,
|
|
347
|
+
width: geometry.width,
|
|
334
348
|
source: view.state.sliceDoc(Number(id), Number(table.dataset.to)),
|
|
335
349
|
});
|
|
336
350
|
}
|
|
@@ -14,7 +14,7 @@ import { loadNativeMarkdownLinks, installMarkdownLinks } from './adapters/codex/
|
|
|
14
14
|
import { loadNativeMarkdownToc } from '../../../packages/codex-adapter/src/markdown/toc-native.mjs';
|
|
15
15
|
import { installMarkdownToc } from './adapters/codex/toc.mjs';
|
|
16
16
|
import { readFile } from 'node:fs/promises';
|
|
17
|
-
import { installMarkdownTables } from './adapters/codex/tables.mjs';
|
|
17
|
+
import { installMarkdownTables, markdownTableActionGeometry } from './adapters/codex/tables.mjs';
|
|
18
18
|
import { installMarkdownLists } from './adapters/codex/lists.mjs';
|
|
19
19
|
import { createMarkdownPreviewAdapter } from '../../../packages/codex-adapter/src/markdown/preview.mjs';
|
|
20
20
|
export async function createPageScriptSource() {
|
|
@@ -29,6 +29,7 @@ export async function createPageScriptSource() {
|
|
|
29
29
|
installMarkdownLists,
|
|
30
30
|
loadNativeMarkdownLinks,
|
|
31
31
|
installMarkdownLinks,
|
|
32
|
+
markdownTableActionGeometry,
|
|
32
33
|
installMarkdownTables,
|
|
33
34
|
loadNativeMarkdownToc,
|
|
34
35
|
installMarkdownToc,
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { saveQvDiagram } from '../../mermaid/export.mjs';
|
|
2
2
|
|
|
3
3
|
// 图表下载协议只由实验性功能加载,阅读增强不依赖此能力。
|
|
4
|
-
export async function loadNativeMermaidActions(native) {
|
|
4
|
+
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
5
|
const url = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')]
|
|
6
6
|
.map(node => node.href || node.src).find(url => /\/app-initial-[^/]+\.js/.test(url));
|
|
7
|
-
const
|
|
8
|
-
if (!response.ok) throw new Error('native-diagram-source-unavailable');
|
|
9
|
-
const source = await response.text();
|
|
7
|
+
const source = await read(url);
|
|
10
8
|
const section = marker => {
|
|
11
9
|
const at = source.indexOf(marker);
|
|
12
10
|
if (at < 0) throw new Error('native-diagram-contract-changed');
|
|
@@ -15,7 +13,9 @@ export async function loadNativeMermaidActions(native) {
|
|
|
15
13
|
const preview = section('id:`imagePreviewDialog.label`');
|
|
16
14
|
const downloadSection = preview.slice(preview.indexOf('suggestedFilename:', preview.indexOf('downloadSrc:')));
|
|
17
15
|
const downloadIcon = downloadSection.match(/children:\(0,[\w$]+\.jsx\)\(([\w$]+),\{className:`icon-xs`\}/)?.[1];
|
|
18
|
-
const
|
|
16
|
+
const download = section('Failed to download image');
|
|
17
|
+
const downloadName = download.match(/^function ([\w$]+)\(/)?.[1];
|
|
18
|
+
const requestName = download.match(/await ([\w$]+)\(`save-file`,/)?.[1];
|
|
19
19
|
const imports = new Map();
|
|
20
20
|
for (const match of source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g))
|
|
21
21
|
for (const entry of match[1].split(',')) {
|
|
@@ -23,10 +23,18 @@ export async function loadNativeMermaidActions(native) {
|
|
|
23
23
|
imports.set(parts.at(-1), { name:parts[0], url:new URL(match[2],url).href });
|
|
24
24
|
}
|
|
25
25
|
const binding = async name => {
|
|
26
|
+
const alias = markdownExportAlias(source, name);
|
|
27
|
+
if (alias) return (await load(url))[alias];
|
|
26
28
|
const item = imports.get(name);
|
|
27
29
|
if (!item) throw new Error('native-diagram-save-request-unavailable');
|
|
28
|
-
return (await
|
|
30
|
+
return (await load(item.url))[item.name];
|
|
29
31
|
};
|
|
32
|
+
// 新版原生下载使用 workspaceFiles.saveStream,沿用原生字节流与保存动作。
|
|
33
|
+
if (download.includes('workspaceFiles.saveStream')) {
|
|
34
|
+
const [DownloadIcon, downloadImage] = await Promise.all([binding(downloadIcon), binding(downloadName)]);
|
|
35
|
+
if (typeof DownloadIcon !== 'function' || typeof downloadImage !== 'function') throw new Error('native-diagram-download-unavailable');
|
|
36
|
+
return {...native, DownloadIcon, downloadImage};
|
|
37
|
+
}
|
|
30
38
|
const [DownloadIcon, saveFile] = await Promise.all([binding(downloadIcon),binding(requestName)]);
|
|
31
39
|
return { ...native, DownloadIcon, downloadImage({downloadSrc,suggestedFilename}) {
|
|
32
40
|
return saveQvDiagram(downloadSrc,saveFile,markdownDownloadsDirectory,suggestedFilename);
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
// 强度提交会重建面板;只延续滑条自身的焦点,不抢走其他控件或已关闭菜单的焦点。
|
|
2
|
+
export function replaceModelSliderShell(parentMenu, existing, shell) {
|
|
3
|
+
const previousRail = existing?.querySelector('[data-codex-model-slider-rail]');
|
|
4
|
+
const restore = Boolean(previousRail && document.activeElement === previousRail);
|
|
5
|
+
existing?.remove();
|
|
6
|
+
parentMenu.append(shell);
|
|
7
|
+
if (restore && parentMenu.isConnected && shell.isConnected)
|
|
8
|
+
shell.querySelector('[data-codex-model-slider-rail]')?.focus({preventScroll:true});
|
|
9
|
+
}
|
|
10
|
+
|
|
1
11
|
// 专有原生控制器适配与呈现由插件维护;宿主只提供刷新和共用 UI。
|
|
2
12
|
export function installModelSlider(signal, { feature, ui, createIcon, requestRefresh, locale, codex, requestAction }, rules, createModelControllerAdapter) {
|
|
3
13
|
const modelSelectorFeature = feature;
|
|
@@ -2079,7 +2089,6 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
2079
2089
|
return false;
|
|
2080
2090
|
}
|
|
2081
2091
|
modelSelectorGenericContainers.get(nativeContainer)?.effectMirror?.disconnect();
|
|
2082
|
-
existing?.remove();
|
|
2083
2092
|
if (!modelSelectorGenericContainers.has(nativeContainer)) {
|
|
2084
2093
|
modelSelectorGenericContainers.set(nativeContainer, {
|
|
2085
2094
|
position: nativeContainer.style.position,
|
|
@@ -2099,7 +2108,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
2099
2108
|
nativeContainer.inert = true;
|
|
2100
2109
|
shell.setAttribute("data-codex-model-slider-generic", key);
|
|
2101
2110
|
shell.setAttribute("data-codex-model-slider-structure-key", structureKey);
|
|
2102
|
-
parentMenu
|
|
2111
|
+
replaceModelSliderShell(parentMenu, existing, shell);
|
|
2103
2112
|
modelSelectorGenericContainers.get(nativeContainer).effectMirror = syncClonedPowerEffects(nativeContainer, shell);
|
|
2104
2113
|
centerGenericModelPicker(parentMenu);
|
|
2105
2114
|
return true;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createModelControllerAdapter } from "./adapters/codex/controller.mjs";
|
|
2
2
|
import { createModelRules } from "./domain/model-rules.mjs";
|
|
3
|
-
import { installModelSlider } from "./frontend/install.mjs";
|
|
3
|
+
import { installModelSlider, replaceModelSliderShell } from "./frontend/install.mjs";
|
|
4
4
|
|
|
5
5
|
// 两部分均显式序列化,Renderer 不捕获 Node imports。
|
|
6
6
|
export function createModelSliderInstallerSource() {
|
|
7
7
|
return `function installModelSliderFeature(signal, context) {
|
|
8
|
+
const replaceModelSliderShell = ${replaceModelSliderShell.toString()};
|
|
8
9
|
return (${installModelSlider.toString()})(signal, context, (${createModelRules.toString()})(), ${createModelControllerAdapter.toString()});
|
|
9
10
|
}`;
|
|
10
11
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
73
|
-
|
|
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
|
|
86
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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(
|
|
102
|
-
|
|
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 =
|
|
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.
|
|
3
|
+
"version": "2.0.45",
|
|
4
4
|
"description": "Lightweight, local-first plugins for the Codex desktop app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,8 +30,6 @@
|
|
|
30
30
|
"features/images/src",
|
|
31
31
|
"features/images/web",
|
|
32
32
|
"features/images/codex-feature.json",
|
|
33
|
-
"features/hide-upsell-banner/src",
|
|
34
|
-
"features/hide-upsell-banner/codex-feature.json",
|
|
35
33
|
"features/session-status/src",
|
|
36
34
|
"features/session-status/codex-feature.json",
|
|
37
35
|
"features/session-status/vendor/lucide-LICENSE",
|
|
@@ -94,7 +92,7 @@
|
|
|
94
92
|
},
|
|
95
93
|
"scripts": {
|
|
96
94
|
"start": "node packages/cli/bin/mcx.mjs serve",
|
|
97
|
-
"test": "node --test packages/cli/test/*.test.mjs packages/runtime-host/test/*.test.mjs features/model-slider/test/*.test.mjs features/notes/test/*.test.mjs features/images/test/*.test.mjs features/
|
|
95
|
+
"test": "node --test packages/cli/test/*.test.mjs packages/runtime-host/test/*.test.mjs features/model-slider/test/*.test.mjs features/notes/test/*.test.mjs features/images/test/*.test.mjs features/session-status/test/*.test.mjs features/usage-meter/test/*.test.mjs apps/dashboard/test/*.test.mjs features/unread-inbox/test/*.test.mjs features/quick-chat/test/*.test.mjs features/composer-expand/test/*.test.mjs features/sidebar-focus/test/*.test.mjs features/sites/test/*.test.mjs packages/ui/test/*.test.mjs packages/codex-adapter/test/*.test.mjs features/markdown-reading/test/*.test.mjs features/file-navigation/test/*.test.mjs packages/plugin/test/*.test.mjs features/mermaid-diagrams/test/*.test.mjs",
|
|
98
96
|
"test:cli": "node --test packages/cli/test/*.test.mjs",
|
|
99
97
|
"test:runtime": "node --test packages/runtime-host/test/*.test.mjs",
|
|
100
98
|
"check": "node scripts/check-syntax.mjs",
|
|
@@ -2,14 +2,13 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
export const FEATURE_IDS = Object.freeze(["model-slider", "notes", "images", "
|
|
5
|
+
export const FEATURE_IDS = Object.freeze(["model-slider", "notes", "images", "session-status", "usage-meter", "sidebar-focus", "sites", "markdown-reading", "mermaid-diagrams", "file-navigation", "quick-chat", "composer-expand"]);
|
|
6
6
|
export const DEFAULT_CONFIG = Object.freeze({
|
|
7
7
|
schemaVersion: 1,
|
|
8
8
|
features: Object.freeze({
|
|
9
9
|
"model-slider": true,
|
|
10
10
|
notes: true,
|
|
11
11
|
images: true,
|
|
12
|
-
"hide-upsell-banner": true,
|
|
13
12
|
"session-status": true,
|
|
14
13
|
"usage-meter": true,
|
|
15
14
|
"sidebar-focus": true,
|
|
@@ -44,6 +43,8 @@ export function normalizeConfig(value = {}) {
|
|
|
44
43
|
};
|
|
45
44
|
// 暂停的 Task 通知不继承旧配置,也不再提供开启入口。
|
|
46
45
|
delete features["unread-inbox"];
|
|
46
|
+
// 已移除的额度推广隐藏功能不继承旧启用项。
|
|
47
|
+
delete features["hide-upsell-banner"];
|
|
47
48
|
// 清理已退役的独立 Quick Chat 修复开关。
|
|
48
49
|
delete features["quick-chat-repair"];
|
|
49
50
|
return {
|
|
@@ -14,7 +14,7 @@ import { createDataTransfer, MAX_TRANSFER_BYTES } from "./data-transfer/service.
|
|
|
14
14
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
15
15
|
const packageMetadata = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
|
|
16
16
|
const MAX_BODY_BYTES = 64 * 1024;
|
|
17
|
-
const DASHBOARD_FEATURE_ORDER = ["model-slider", "notes", "usage-meter", "session-status", "markdown-reading", "mermaid-diagrams", "file-navigation", "images", "sites", "quick-chat", "composer-expand", "sidebar-focus"
|
|
17
|
+
const DASHBOARD_FEATURE_ORDER = ["model-slider", "notes", "usage-meter", "session-status", "markdown-reading", "mermaid-diagrams", "file-navigation", "images", "sites", "quick-chat", "composer-expand", "sidebar-focus"];
|
|
18
18
|
const CONTENT_TYPES = new Map([
|
|
19
19
|
[".css", "text/css; charset=utf-8"],
|
|
20
20
|
[".html", "text/html; charset=utf-8"],
|
|
@@ -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
|
-
|
|
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) {
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schemaVersion": 1,
|
|
3
|
-
"id": "hide-upsell-banner",
|
|
4
|
-
"version": "0.1.11",
|
|
5
|
-
"label": {
|
|
6
|
-
"en": "Hide Usage Promotions",
|
|
7
|
-
"zh-CN": "隐藏额度推广"
|
|
8
|
-
},
|
|
9
|
-
"description": {
|
|
10
|
-
"en": "Hide plan upgrade banners above the message box for a cleaner chat.",
|
|
11
|
-
"zh-CN": "隐藏输入框上方的套餐升级推广,让对话更清爽。"
|
|
12
|
-
},
|
|
13
|
-
"entry": {
|
|
14
|
-
"kind": "page-script",
|
|
15
|
-
"script": "src/hide-upsell-banner.mjs"
|
|
16
|
-
},
|
|
17
|
-
"icon": {
|
|
18
|
-
"viewBox": "0 0 24 24",
|
|
19
|
-
"markup": "<rect width=\"18\" height=\"12\" x=\"3\" y=\"6\" rx=\"2.5\"></rect><path d=\"m5 5 14 14\"></path>"
|
|
20
|
-
},
|
|
21
|
-
"placement": {
|
|
22
|
-
"order": 40
|
|
23
|
-
}
|
|
24
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
// page-script feature body.
|
|
2
|
-
// Injected by CodexRuntimeHost into the Codex renderer through
|
|
3
|
-
// `new Function("signal", "window", "document", "MutationObserver", source)`,
|
|
4
|
-
// so this file is a function body: it runs once with those bindings in scope
|
|
5
|
-
// and may return a cleanup function. It hides the "You're out of Codex and
|
|
6
|
-
// Work usage" upsell banner that the desktop app renders above the composer
|
|
7
|
-
// when the account rate-limit snapshot marks the plan as exhausted. It never
|
|
8
|
-
// touches the rate-limit query path: `account/rateLimits/read` and the
|
|
9
|
-
// app-server JSON-RPC traffic stay intact.
|
|
10
|
-
const HIDDEN_MARKER = "data-codex-upsell-banner-hidden";
|
|
11
|
-
const hiddenBanners = new Map();
|
|
12
|
-
let disposed = false;
|
|
13
|
-
|
|
14
|
-
function isUpsellBanner(element) {
|
|
15
|
-
if (!(element instanceof window.HTMLElement)) return false;
|
|
16
|
-
// 文件侧栏也是 aside,正文中的用量说明不能使整个预览被隐藏。
|
|
17
|
-
if (element.querySelector("[role='tabpanel']")) return false;
|
|
18
|
-
const text = (element.textContent ?? "").trim();
|
|
19
|
-
if (!text) return false;
|
|
20
|
-
const title = (
|
|
21
|
-
element.querySelector("h1, h2, h3, [role='heading']")?.textContent
|
|
22
|
-
?? text.split("\n", 1)[0]
|
|
23
|
-
).trim();
|
|
24
|
-
return /out of Codex(?: and Work)? usage/i.test(title)
|
|
25
|
-
|| /out of Codex messages/i.test(title)
|
|
26
|
-
|| /Codex (?:message )?usage limit reached/i.test(title)
|
|
27
|
-
|| /Codex.*(?:用量|使用量|使用额度|额度|消息).*(?:用完|用尽|耗尽|已达|达到.*上限|上限)/i.test(title)
|
|
28
|
-
|| /(?:已达到|达到|已用完|已用尽|耗尽).{0,20}Codex.{0,20}(?:用量|使用量|额度|消息|上限)/i.test(title)
|
|
29
|
-
// 通用标题须同时具有原生用量横幅的关闭语义,不能仅按耗尽文案隐藏通知。
|
|
30
|
-
|| (/^you['’]re out of usage[.!]?$/i.test(title)
|
|
31
|
-
&& [...element.querySelectorAll("button[aria-label]")].some(button =>
|
|
32
|
-
/^(?:Dismiss usage banner|不再显示用量横幅|關閉用量橫幅|關閉用量橫額)$/i.test(button.getAttribute("aria-label") ?? "")));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function hideBanner(element) {
|
|
36
|
-
if (!hiddenBanners.has(element)) hiddenBanners.set(element, {
|
|
37
|
-
display:element.style.getPropertyValue("display"), priority:element.style.getPropertyPriority("display"),
|
|
38
|
-
});
|
|
39
|
-
if (!element.hasAttribute(HIDDEN_MARKER)) element.setAttribute(HIDDEN_MARKER, "");
|
|
40
|
-
// 标记不代表样式仍有效;只在需要时写入,避免 observer 自触发循环。
|
|
41
|
-
if (element.style.display !== "none") element.style.display = "none";
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function restoreBanner(element) {
|
|
45
|
-
const original = hiddenBanners.get(element);
|
|
46
|
-
element.removeAttribute(HIDDEN_MARKER);
|
|
47
|
-
if (element.style.display === "none") {
|
|
48
|
-
if (original.display) element.style.setProperty("display", original.display, original.priority);
|
|
49
|
-
else element.style.removeProperty("display");
|
|
50
|
-
}
|
|
51
|
-
hiddenBanners.delete(element);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function hideVisibleBanners(root = document) {
|
|
55
|
-
for (const element of hiddenBanners.keys()) if (!element.isConnected || !isUpsellBanner(element)) restoreBanner(element);
|
|
56
|
-
if (root.matches?.("aside") && isUpsellBanner(root)) hideBanner(root);
|
|
57
|
-
for (const element of root.querySelectorAll("aside")) {
|
|
58
|
-
if (isUpsellBanner(element)) hideBanner(element);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
let rescanQueued = false;
|
|
63
|
-
function rescanBanners() {
|
|
64
|
-
// React can render the banner in stages: an empty <aside> shell first, then
|
|
65
|
-
// text and style updates on the same element. A childList-only observer
|
|
66
|
-
// misses those later mutations, so every mutation triggers a full rescan
|
|
67
|
-
// instead of only inspecting freshly added nodes.
|
|
68
|
-
if (rescanQueued) return;
|
|
69
|
-
rescanQueued = true;
|
|
70
|
-
queueMicrotask(() => {
|
|
71
|
-
rescanQueued = false;
|
|
72
|
-
if (!disposed) hideVisibleBanners(document);
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
hideVisibleBanners();
|
|
77
|
-
const observer = new MutationObserver(() => {
|
|
78
|
-
rescanBanners();
|
|
79
|
-
});
|
|
80
|
-
observer.observe(document.documentElement, {
|
|
81
|
-
childList: true,
|
|
82
|
-
subtree: true,
|
|
83
|
-
attributes: true,
|
|
84
|
-
attributeFilter: ["style", "class", "hidden", "aria-label"],
|
|
85
|
-
characterData: true,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
config.registerSelfCheck?.(() => ({state:hiddenBanners.size ? "available" : "not-applicable",reason:hiddenBanners.size ? null : "no-usage-banner",
|
|
89
|
-
checks:[...hiddenBanners.keys()].filter(node=>node.isConnected).map(()=>({name:"usage-banner-hidden",available:true}))}));
|
|
90
|
-
return () => {
|
|
91
|
-
disposed = true;
|
|
92
|
-
observer.disconnect();
|
|
93
|
-
for (const element of hiddenBanners.keys()) restoreBanner(element);
|
|
94
|
-
};
|