minecodex 2.0.44 → 2.0.46

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 (34) hide show
  1. package/README.md +0 -1
  2. package/apps/dashboard/app.js +1 -1
  3. package/apps/dashboard/assets/preview-content.js +4 -3
  4. package/features/file-navigation/src/adapters/codex/modules.mjs +10 -11
  5. package/features/files-auto-refresh/assets/folder-sync.svg +7 -0
  6. package/features/files-auto-refresh/codex-feature.json +17 -0
  7. package/features/files-auto-refresh/src/adapters/codex/directories.mjs +75 -0
  8. package/features/files-auto-refresh/src/entry.mjs +58 -0
  9. package/features/files-auto-refresh/src/injection-source.mjs +8 -0
  10. package/features/files-auto-refresh/src/refresh.mjs +32 -0
  11. package/features/files-auto-refresh/vendor/lucide-LICENSE +43 -0
  12. package/features/images/src/adapters/codex/native-image-modules.mjs +11 -4
  13. package/features/images/src/frontend/local-image-source.mjs +11 -1
  14. package/features/markdown-reading/src/adapters/codex/table-actions.mjs +16 -17
  15. package/features/markdown-reading/src/adapters/codex/tables.mjs +18 -4
  16. package/features/markdown-reading/src/injection-source.mjs +2 -1
  17. package/features/mermaid-diagrams/src/adapters/codex/native.mjs +14 -6
  18. package/features/model-slider/src/frontend/install.mjs +11 -2
  19. package/features/model-slider/src/injection-source.mjs +2 -1
  20. package/features/quick-chat/assets/close.svg +15 -0
  21. package/features/quick-chat/codex-feature.json +2 -1
  22. package/features/quick-chat/src/adapters/codex/lifecycle.mjs +27 -0
  23. package/features/quick-chat/src/adapters/codex/renderer.mjs +8 -5
  24. package/features/quick-chat/src/adapters/codex/shortcut.mjs +38 -14
  25. package/features/quick-chat/src/frontend/entry.mjs +33 -18
  26. package/features/quick-chat/src/injection-source.mjs +2 -1
  27. package/features/usage-meter/src/adapters/codex/renderer.mjs +14 -4
  28. package/features/usage-meter/src/usage-meter.mjs +24 -19
  29. package/package.json +6 -4
  30. package/packages/cli/src/config.mjs +4 -2
  31. package/packages/cli/src/control-server.mjs +1 -1
  32. package/packages/ui/src/native/tokens.mjs +2 -0
  33. package/features/hide-upsell-banner/codex-feature.json +0 -24
  34. package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +0 -94
package/README.md CHANGED
@@ -21,7 +21,6 @@
21
21
  | 站点收藏 | 发现本地项目网页,收藏网址并快速打开。 |
22
22
  | 快捷聊天增强 | 恢复快捷键,发送后展开,支持历史与窗口固定。 |
23
23
  | 侧栏聚焦 | 收起其他任务分组,再次点击恢复布局。 |
24
- | 隐藏额度推广 | 隐藏输入框上方的套餐升级推广。 |
25
24
 
26
25
  ## 安装
27
26
 
@@ -240,7 +240,7 @@ function findPluginCard(featureId) {
240
240
 
241
241
  function renderPluginCards(features) {
242
242
  // Task 暂停接入,旧版服务返回的卡片也不展示。
243
- features = features.filter(feature => feature.id !== "unread-inbox");
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
  }
@@ -21,6 +21,9 @@ const content = {
21
21
  'file-navigation': [
22
22
  ['Codex 文件预览上方的路径区域。', '浏览相邻目录和文件需要额外跳转。', '点击路径展开分列目录,逐级浏览并打开另一个文件。'],
23
23
  ['The path above a file preview in Codex.', 'Browsing nearby files and folders takes extra navigation.', 'Open the path to browse cascading folder columns and choose another file.']],
24
+ 'files-auto-refresh': [
25
+ ['Codex Side Panel 的 Files 目录列表。', 'Agent 改变项目文件后,需要反复收起、展开才能看到新列表。', '保持 Files 打开,自动同步根目录和已展开目录;保留当前文件与浏览位置。'],
26
+ ['The Files tree in the Codex side panel.', 'File changes can require collapsing and reopening the tree.', 'Keep Files open to refresh the root and expanded folders while preserving the current file and browsing position.']],
24
27
  images: [
25
28
  ['Codex Library 的图片集合与图片查看器。', '查找图片及其提示词需要来回切换。', '浏览图片集合,打开图片查看大图及可追溯的提示词信息。'],
26
29
  ['Image collections in Codex Library and the image viewer.', 'Finding images and their prompts requires switching views.', 'Browse collections, open an image, and inspect available prompt provenance.']],
@@ -36,9 +39,7 @@ const content = {
36
39
  'sidebar-focus': [
37
40
  ['Codex 左侧栏的分组标题。', '多个侧栏分组展开时,当前分组不容易聚焦。', '点击当前分组,一键收起其他分组;再次点击恢复先前的展开状态。'],
38
41
  ['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
- 'hide-upsell-banner': [
40
- ['Codex 的额度推广提示区域。', '推广提示占用日常操作空间。', '隐藏额度推广提示,保留真实额度与运行错误反馈。'],
41
- ['Usage promotion banners in Codex.', 'Promotions take space during everyday tasks.', 'Hide usage promotions while retaining real usage and error feedback.']],
42
+
42
43
  };
43
44
  export function previewContent(id, locale) {
44
45
  return content[id]?.[locale === 'zh-CN' ? 0 : 1] ?? (locale === 'zh-CN'
@@ -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 import(value.url))[value.name];
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([import(treeUrl), import(queryUrl)]);
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 [react, dom, request] = await Promise.all([
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 import(sharedUrl);
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');
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
2
+ <path d="M9 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v.5" />
3
+ <path d="M12 10v4h4" />
4
+ <path d="m12 14 1.535-1.605a5 5 0 0 1 8 1.5" />
5
+ <path d="M22 22v-4h-4" />
6
+ <path d="m22 18-1.535 1.605a5 5 0 0 1-8-1.5" />
7
+ </svg>
@@ -0,0 +1,17 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "id": "files-auto-refresh",
4
+ "version": "0.1.0",
5
+ "label": { "en": "Files Auto Refresh", "zh-CN": "Files 自动刷新" },
6
+ "description": {
7
+ "en": "Keep the open Files tree up to date as files and folders are created, removed or renamed.",
8
+ "zh-CN": "文件或文件夹新增、删除、重命名后,自动更新打开的 Files 目录列表。"
9
+ },
10
+ "entry": {
11
+ "kind": "page-script",
12
+ "script": "src/entry.mjs",
13
+ "sourceFactory": "src/injection-source.mjs"
14
+ },
15
+ "icon": { "resource": "assets/folder-sync.svg" },
16
+ "placement": { "order": 92 }
17
+ }
@@ -0,0 +1,75 @@
1
+ export function createDirectoryRefreshAdapter(codex, signal = new AbortController().signal) {
2
+ const visible = element => element.isConnected && !element.closest('[hidden],[inert],[aria-hidden="true"]')
3
+ && element.checkVisibility({ visibilityProperty: true }) && element.getClientRects().length > 0;
4
+
5
+ function context(anchor) {
6
+ let directory = null, client = null, expanded = [];
7
+ codex.props(anchor, props => {
8
+ if (Array.isArray(props.initialExpandedPaths) && 'files' in props && 'renderTree' in props)
9
+ expanded = props.initialExpandedPaths;
10
+ if (!directory && typeof props.root === 'string' && typeof props.hostId === 'string'
11
+ && 'includeHidden' in props && 'renderTree' in props) directory = props;
12
+ for (const value of [props.value, props.client, props.queryClient, props.value?.queryClient])
13
+ if (!client && typeof value?.getQueryCache === 'function' && typeof value.setQueryData === 'function') client = value;
14
+ return Boolean(directory && client);
15
+ }, 360);
16
+ return { directory, client, expanded };
17
+ }
18
+
19
+ function directoryQueries(client, directory, expanded) {
20
+ const directories = new Set([null, ...expanded]);
21
+ return client.getQueryCache().getAll().filter(query => {
22
+ const key = query.queryKey;
23
+ if (key?.[0] !== 'vscode' || key[1] !== 'workspace-directory-entries' || !query.isActive()
24
+ || typeof query.options?.queryFn !== 'function') return false;
25
+ let params;
26
+ try { params = JSON.parse(key.at(-1)); } catch { return false; }
27
+ return params?.hostId === directory.hostId && params.workspaceRoot === directory.root
28
+ && params.includeHidden === directory.includeHidden && directories.has(params.directoryPath);
29
+ });
30
+ }
31
+
32
+ return {
33
+ scan() {
34
+ const views = [], roots = new Set();
35
+ // 搜索框在空目录时也存在,不能只依赖文件树节点。
36
+ const anchors = [...document.querySelectorAll('file-tree-container'), ...document.querySelectorAll('[id="workspace-directory-tree-search"]')];
37
+ for (const anchor of anchors) {
38
+ if (!visible(anchor)) continue;
39
+ const { directory, client, expanded } = context(anchor);
40
+ if (!directory) continue;
41
+ const key = JSON.stringify([directory.hostId, directory.environmentId, directory.root]);
42
+ if (roots.has(key)) continue;
43
+ roots.add(key);
44
+ const available = Boolean(client && directory.environmentId == null);
45
+ const current = () => {
46
+ if (signal.aborted || document.visibilityState === 'hidden' || !visible(anchor)) return false;
47
+ const next = context(anchor);
48
+ return next.client === client && next.directory?.hostId === directory.hostId
49
+ && next.directory?.root === directory.root && next.directory?.environmentId === directory.environmentId
50
+ && next.directory?.includeHidden === directory.includeHidden;
51
+ };
52
+ views.push({
53
+ key, anchor, available, current, boundary: anchor.closest('[data-app-shell-pane-content-viewport]'),
54
+ // 每轮重新取得 active queries,已收起的分支不会继续请求。
55
+ queries: available ? directoryQueries(client, directory, expanded).map(query => ({
56
+ async refresh() {
57
+ if (!current() || !query.isActive() || query.state.fetchStatus === 'fetching') return;
58
+ const path = JSON.parse(query.queryKey.at(-1)).directoryPath;
59
+ if (path !== null && !context(anchor).expanded.includes(path)) return;
60
+ const before = query.state.data, updatedAt = query.state.dataUpdatedAt;
61
+ // 失败不进入原生 query 的 error 状态,避免原生树用错误页替换已有列表。
62
+ const data = await query.options.queryFn({ client, queryKey: query.queryKey, meta: query.options.meta, signal });
63
+ if (!Array.isArray(data?.entries)) throw new Error('directory-refresh-response-unavailable');
64
+ if (!current() || !query.isActive() || query.state.fetchStatus === 'fetching'
65
+ || query.state.data !== before || query.state.dataUpdatedAt !== updatedAt) return;
66
+ if (path !== null && !context(anchor).expanded.includes(path)) return;
67
+ client.setQueryData(query.queryKey, data);
68
+ },
69
+ })) : [],
70
+ });
71
+ }
72
+ return views;
73
+ },
74
+ };
75
+ }
@@ -0,0 +1,58 @@
1
+ const reads = new AbortController();
2
+ const adapter = createDirectoryRefreshAdapter(config.codex, reads.signal);
3
+ const failures = new Map();
4
+ let views = [], frame = null;
5
+ const isVisible = () => !signal.aborted && document.visibilityState === 'visible';
6
+ const loop = createDirectoryRefreshLoop({
7
+ scan() {
8
+ views = adapter.scan();
9
+ for (const [key, item] of failures) if (!views.some(view => view.key === key && view.anchor === item.anchor)) {
10
+ item.button.remove(); failures.delete(key);
11
+ }
12
+ return views;
13
+ },
14
+ isVisible,
15
+ onResult(view, failed) {
16
+ if (!failed) { failures.get(view.key)?.button.remove(); failures.delete(view.key); return; }
17
+ if (!view.boundary) return;
18
+ let button = failures.get(view.key)?.button;
19
+ if (!button) {
20
+ button = document.createElement('button');
21
+ button.type = 'button'; button.setAttribute('data-mcx-directory-refresh-retry', '');
22
+ button.className = 'cursor-interaction rounded-md border border-default bg-surface px-2 py-1 text-sm text-secondary';
23
+ button.textContent = config.locale === 'zh-CN' ? '目录尚未同步 · 重试' : 'Directory not synced · Retry';
24
+ button.addEventListener('click', () => { loop.pause(); loop.request(); });
25
+ button.style.position = 'fixed'; button.style.zIndex = '1';
26
+ view.boundary.append(button); failures.set(view.key, { anchor: view.anchor, button });
27
+ }
28
+ // 提示放在列表底部,不把全高目录向下挤,也不改变原生容器的定位规则。
29
+ const bounds = view.boundary.getBoundingClientRect();
30
+ button.style.left = `${bounds.left + 8}px`; button.style.maxWidth = `${Math.max(0, bounds.width - 16)}px`;
31
+ button.style.top = `${Math.max(bounds.top + 8, bounds.bottom - button.offsetHeight - 8)}px`;
32
+ },
33
+ });
34
+ function reconcile() {
35
+ frame = null;
36
+ if (isVisible()) loop.request(); else loop.pause();
37
+ }
38
+ const observer = new MutationObserver(records => {
39
+ if (records.every(record => record.target.closest?.('[data-mcx-directory-refresh-retry],.cm-content'))) return;
40
+ if (frame === null) frame = requestAnimationFrame(reconcile);
41
+ });
42
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['hidden', 'inert', 'aria-hidden', 'class', 'style'] });
43
+ document.addEventListener('visibilitychange', reconcile);
44
+ config.registerSelfCheck?.(() => {
45
+ const current = adapter.scan();
46
+ return {
47
+ state: !current.length ? 'not-applicable' : current.every(view => view.available && view.queries.length && !failures.has(view.key)) ? 'available' : 'unavailable',
48
+ checks: current.map(view => ({ name: 'native-directory-query', available: view.available && view.queries.length > 0 && !failures.has(view.key) })),
49
+ };
50
+ });
51
+ function dispose() {
52
+ reads.abort(); loop.dispose(); observer.disconnect(); document.removeEventListener('visibilitychange', reconcile);
53
+ if (frame !== null) cancelAnimationFrame(frame);
54
+ for (const item of failures.values()) item.button.remove(); failures.clear(); views = [];
55
+ }
56
+ signal.addEventListener('abort', dispose, { once: true });
57
+ reconcile();
58
+ return dispose;
@@ -0,0 +1,8 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { createDirectoryRefreshAdapter } from './adapters/codex/directories.mjs';
3
+ import { createDirectoryRefreshLoop } from './refresh.mjs';
4
+
5
+ export async function createPageScriptSource() {
6
+ return [createDirectoryRefreshAdapter, createDirectoryRefreshLoop].map(fn => `const ${fn.name} = ${fn.toString()};`).join('\n')
7
+ + '\n' + await readFile(new URL('./entry.mjs', import.meta.url), 'utf8');
8
+ }
@@ -0,0 +1,32 @@
1
+ export function createDirectoryRefreshLoop({ scan, onResult, isVisible, schedule = setTimeout, cancel = clearTimeout, interval = 1000 }) {
2
+ let timer = null, running = false, disposed = false;
3
+ async function tick() {
4
+ timer = null;
5
+ if (disposed || !isVisible()) return;
6
+ running = true;
7
+ let views = [];
8
+ try {
9
+ views = scan();
10
+ for (const view of views) {
11
+ let failed = false;
12
+ // 限制并发;刷新只触达原生已订阅目录,不扫描未知子目录。
13
+ for (let index = 0; index < view.queries.length; index += 4) {
14
+ if (disposed || !isVisible() || !view.current()) break;
15
+ const results = await Promise.allSettled(view.queries.slice(index, index + 4).map(query => query.refresh()));
16
+ failed ||= results.some(result => result.status === 'rejected');
17
+ }
18
+ if (!disposed && isVisible() && view.current()) onResult(view, failed);
19
+ }
20
+ } finally {
21
+ running = false;
22
+ if (!disposed && isVisible() && views.length) timer = schedule(tick, interval);
23
+ }
24
+ }
25
+ return {
26
+ request() {
27
+ if (!disposed && !running && timer === null && isVisible()) timer = schedule(tick, 0);
28
+ },
29
+ pause() { if (timer !== null) cancel(timer); timer = null; },
30
+ dispose() { disposed = true; if (timer !== null) cancel(timer); timer = null; },
31
+ };
32
+ }
@@ -0,0 +1,43 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Lucide Icons and Contributors
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
+
17
+ ---
18
+
19
+ The following Lucide icons are derived from the Feather project:
20
+
21
+ airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
22
+
23
+ The MIT License (MIT) (for the icons listed above)
24
+
25
+ Copyright (c) 2013-present Cole Bemis
26
+
27
+ Permission is hereby granted, free of charge, to any person obtaining a copy
28
+ of this software and associated documentation files (the "Software"), to deal
29
+ in the Software without restriction, including without limitation the rights
30
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
31
+ copies of the Software, and to permit persons to whom the Software is
32
+ furnished to do so, subject to the following conditions:
33
+
34
+ The above copyright notice and this permission notice shall be included in all
35
+ copies or substantial portions of the Software.
36
+
37
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
40
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
41
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
42
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
43
+ SOFTWARE.
@@ -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
- if (["library-masonry", "library-item-layout"].includes(name)
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 selection=await module("selectable-library-item",toolbar.source);
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(selection.exports,source=>/onCheckedChange:/.test(source)&&/viewMode:/.test(source),"selection checkbox");
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(selection.source,selection.url,checkboxLocal);
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) mounted.nav?.render(entry.selectedKey);
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
- item.fit || item.overflow
72
- ? h(
73
- Tooltip,
74
- { tooltipContent: wrap, delayOpen: true },
75
- h(
76
- Button,
77
- {
78
- 'aria-label': wrap,
79
- color: 'blockAction',
80
- radius: 'full',
81
- size: 'inputIcon',
82
- onClick: () => controller.toggle(),
83
- },
84
- h(item.fit ? wrapIcons.disable : wrapIcons.enable),
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
- visible: hovered === id,
331
- left: (first.left - frameRect.left) / scale,
332
- top: (first.top - frameRect.top) / scale + view.scrollDOM.scrollTop + headerInset - 40,
333
- width: first.width / scale,
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 response = await fetch(url);
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 requestName = section('Failed to download image').match(/await ([\w$]+)\(`save-file`,/)?.[1];
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 import(item.url))[item.name];
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.append(shell);
2111
+ replaceModelSliderShell(parentMenu, existing, shell);
2103
2112
  modelSelectorGenericContainers.get(nativeContainer).effectMirror = syncClonedPowerEffects(nativeContainer, shell);
2104
2113
  centerGenericModelPicker(parentMenu);
2105
2114
  return true;