minecodex 2.0.39 → 2.0.41

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 (50) hide show
  1. package/README.md +4 -1
  2. package/apps/dashboard/assets/preview-content.js +3 -0
  3. package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +2 -0
  4. package/features/markdown-reading/codex-feature.json +1 -6
  5. package/features/markdown-reading/src/adapters/codex/lists.mjs +0 -26
  6. package/features/markdown-reading/src/adapters/codex/reading-runtime.mjs +5 -13
  7. package/features/markdown-reading/src/adapters/codex/rich-text.mjs +1 -1
  8. package/features/markdown-reading/src/adapters/codex/table-actions.mjs +1 -122
  9. package/features/markdown-reading/src/adapters/codex/toc.mjs +1 -1
  10. package/features/markdown-reading/src/injection-source.mjs +9 -26
  11. package/features/markdown-reading/src/markdown-reading.mjs +3 -2
  12. package/features/mermaid-diagrams/assets/workflow.svg +15 -0
  13. package/features/mermaid-diagrams/codex-feature.json +31 -0
  14. package/features/{markdown-reading → mermaid-diagrams}/src/adapters/codex/mermaid.mjs +1 -1
  15. package/features/mermaid-diagrams/src/adapters/codex/native.mjs +34 -0
  16. package/features/mermaid-diagrams/src/adapters/codex/runtime.mjs +42 -0
  17. package/features/mermaid-diagrams/src/injection-source.mjs +26 -0
  18. package/features/{markdown-reading → mermaid-diagrams}/src/mermaid/frame.mjs +1 -1
  19. package/features/mermaid-diagrams/src/mermaid-diagrams.mjs +85 -0
  20. package/features/mermaid-diagrams/vendor/lucide-LICENSE +43 -0
  21. package/features/quick-chat/src/adapters/codex/shortcut.mjs +3 -1
  22. package/features/session-status/src/adapters/codex/context-usage.mjs +11 -6
  23. package/features/sidebar-focus/src/adapters/codex/renderer.mjs +11 -5
  24. package/features/sidebar-focus/src/sidebar-focus.mjs +5 -3
  25. package/features/sites/src/adapters/codex/renderer.mjs +1 -1
  26. package/features/sites/src/frontend/entry.mjs +1 -0
  27. package/package.json +9 -5
  28. package/packages/cli/src/config.mjs +2 -1
  29. package/packages/cli/src/control-server.mjs +1 -1
  30. package/packages/cli/src/paths.mjs +2 -1
  31. package/packages/cli/src/platform.mjs +43 -12
  32. package/packages/codex-adapter/src/markdown/block-actions.mjs +107 -0
  33. package/packages/codex-adapter/src/markdown/editor-api.mjs +26 -0
  34. package/{features/markdown-reading/src/adapters/codex → packages/codex-adapter/src/markdown}/markdown-native.mjs +7 -4
  35. package/{features/markdown-reading/src/adapters/codex → packages/codex-adapter/src/markdown}/preview.mjs +2 -8
  36. package/{features/markdown-reading/src/adapters/codex → packages/codex-adapter/src/markdown}/toc-native.mjs +5 -4
  37. package/packages/plugin/assets/app.js +2 -2
  38. /package/features/{markdown-reading → mermaid-diagrams}/assets/qv-dependencies.js +0 -0
  39. /package/features/{markdown-reading → mermaid-diagrams}/assets/qv-export.js +0 -0
  40. /package/features/{markdown-reading → mermaid-diagrams}/assets/qv-loading.css +0 -0
  41. /package/features/{markdown-reading → mermaid-diagrams}/assets/qv-template.json +0 -0
  42. /package/features/{markdown-reading → mermaid-diagrams}/src/adapters/codex/mermaid-viewer.mjs +0 -0
  43. /package/features/{markdown-reading → mermaid-diagrams}/src/mermaid/export.mjs +0 -0
  44. /package/features/{markdown-reading → mermaid-diagrams}/src/mermaid/loading.mjs +0 -0
  45. /package/features/{markdown-reading → mermaid-diagrams}/src/mermaid/theme.css +0 -0
  46. /package/features/{markdown-reading → mermaid-diagrams}/src/mermaid/viewport.mjs +0 -0
  47. /package/features/{markdown-reading → mermaid-diagrams}/vendor/qv/LICENSE +0 -0
  48. /package/features/{markdown-reading → mermaid-diagrams}/vendor/qv/THIRD_PARTY_NOTICES.txt +0 -0
  49. /package/features/{markdown-reading → mermaid-diagrams}/vendor/qv/provenance.json +0 -0
  50. /package/{features/markdown-reading/src/adapters/codex → packages/codex-adapter/src/markdown}/providers.mjs +0 -0
@@ -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.
@@ -25,6 +25,7 @@ export function quickChatLifecycleExports(source) {
25
25
  }
26
26
 
27
27
  export function installQuickChatShortcut(signal, codex, discover = null) {
28
+ const windows = navigator.platform.startsWith('Win');
28
29
  let bindings = null, disposed = false, pendingFocus = null;
29
30
  function activateExisting() {
30
31
  // 原生恢复按钮同时恢复 presentation、取消最小化并聚焦输入框。
@@ -97,8 +98,9 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
97
98
  codex.report('quick-chat-shortcut', true);
98
99
  }).catch(() => { if (!disposed && !signal.aborted) codex.report('quick-chat-shortcut', false, 'native-shortcut-contract-unavailable'); });
99
100
  function keydown(event) {
101
+ const primaryModifier = windows ? event.ctrlKey && !event.metaKey : event.metaKey && !event.ctrlKey;
100
102
  if (disposed || signal.aborted || bindings?.native || event.defaultPrevented || event.repeat || event.isComposing
101
- || !event.metaKey || !event.altKey || event.ctrlKey || event.shiftKey || event.code !== 'KeyN') return;
103
+ || !primaryModifier || !event.altKey || event.shiftKey || event.code !== 'KeyN') return;
102
104
  const scope = bindings && context();
103
105
  if (scope && bindings.isOpen(scope) === false) {
104
106
  pendingFocus={scope,previous:new Set([...document.querySelectorAll('[data-floating-chat-surface]')].map(sessionId))};
@@ -38,12 +38,17 @@ export function createContextUsageAdapter(signal, codex, onChange, loadFamily =
38
38
  }
39
39
  async function discover() {
40
40
  if (loadFamily) return loadFamily();
41
- const url=[...document.querySelectorAll('link[rel=modulepreload]')].map(link=>link.href).find(url=>/\/app-shared-[^/]+\.js(?:$|\?)/.test(url));
42
- if (!url) throw new Error("context-bundle-unavailable");
43
- const source=await (await fetch(url,{signal})).text();
44
- const exported=contextUsageExport(source);
45
- if (!exported) throw new Error("context-export-unavailable");
46
- return (await import(url))[exported];
41
+ const urls=[...document.querySelectorAll('link[rel=modulepreload],script[src]')].map(node=>node.href || node.src);
42
+ const initial=urls.find(url=>/\/app-initial-[^/]+\.js(?:$|\?)/.test(url));
43
+ const shared=urls.find(url=>/\/app-shared-[^/]+\.js(?:$|\?)/.test(url));
44
+ if (!initial && !shared) throw new Error("context-bundle-unavailable");
45
+ // 当前 Codex 将上下文接口放在 app-initial,按实际导出识别。
46
+ for (const url of [initial,shared].filter(Boolean)) {
47
+ const source=await (await fetch(url,{signal})).text();
48
+ const exported=contextUsageExport(source);
49
+ if (exported) return (await import(url))[exported];
50
+ }
51
+ throw new Error("context-export-unavailable");
47
52
  }
48
53
  void discover().then(value=>{ if (!signal.aborted) { family=value; connect(thread,composer); } })
49
54
  .catch(()=>codex.report("context-usage",false,"context-capability-unavailable"));
@@ -1,13 +1,18 @@
1
1
  export function createFocusAdapter(codex) {
2
2
  const sectionSelector="[data-app-action-sidebar-section]", projectSelector="[data-app-action-sidebar-project-row]";
3
3
  const props = (element,predicate) => codex.props(element,predicate,30) ?? {};
4
+ const visible = element => element.checkVisibility({visibilityProperty:true});
5
+ function activity() {
6
+ return [...document.querySelectorAll('#app-shell-sidebar button[aria-pressed]')]
7
+ .find(element => visible(element) && /activity|活动|动态/i.test(element.getAttribute("aria-label") || ""));
8
+ }
4
9
  function branches() {
5
- return [...document.querySelectorAll("#app-shell-sidebar " + sectionSelector)].map(element => {
10
+ return [...document.querySelectorAll("#app-shell-sidebar " + sectionSelector)].filter(visible).map(element => {
6
11
  const native = props(element, p => !!p.sectionKey);
7
12
  const data = props(element, p => Array.isArray(p.section?.threadKeys)).section;
8
13
  return { element, key:"section:" + native.sectionKey, data, expanded:element.dataset.appActionSidebarSectionCollapsed === "false",
9
14
  toggle:() => native.onCollapsedChange?.(element.dataset.appActionSidebarSectionCollapsed === "false") };
10
- }).concat([...document.querySelectorAll("#app-shell-sidebar " + projectSelector)].map(element => ({
15
+ }).concat([...document.querySelectorAll("#app-shell-sidebar " + projectSelector)].filter(visible).map(element => ({
11
16
  element, key:"project:" + element.dataset.appActionSidebarProjectId,
12
17
  data:props(element, p => !!p.group?.projectId).group,
13
18
  expanded:element.getAttribute("aria-expanded") === "true",
@@ -15,14 +20,15 @@ function branches() {
15
20
  })));
16
21
  }
17
22
  function target(items) {
18
- const selected = document.querySelector('#app-shell-sidebar [data-app-action-sidebar-thread-selected="true"]');
23
+ // 两种模式的侧栏同时挂载,只在当前分支中读取原生选中身份。
24
+ const selected = items.map(b => b.element.querySelector?.('[data-app-action-sidebar-thread-selected="true"], [data-sidebar-chatgpt-conversation-key]:has([aria-current="page"])')).find(Boolean);
19
25
  const id = codex.currentThreadId();
20
- const key = selected?.dataset.appActionSidebarThreadId || (id ? "local:" + id : null);
26
+ const key = selected?.dataset.appActionSidebarThreadId || selected?.dataset.sidebarChatgptConversationKey || (id ? "local:" + id : null);
21
27
  const activeProject = items.map(b => props(b.element, p => p.activeProjectId != null).activeProjectId).find(Boolean);
22
28
  const project = items.find(b => b.key.startsWith("project:") && (b.data?.threadKeys?.includes(key) || b.data?.projectId === activeProject));
23
29
  const section = items.find(b => b.key.startsWith("section:") && (b.data?.threadKeys?.includes(key) || (activeProject && b.data?.itemKeys?.includes("codex:project:" + activeProject))))
24
30
  || items.find(b => b.key.startsWith("section:") && (selected && b.element.contains(selected) || project && b.element.contains(project.element)));
25
31
  return section ? { section, project } : null;
26
32
  }
27
- return {branches,target,isBranchToggle:element=>!!element.closest("[data-app-action-sidebar-section-toggle], " + projectSelector)};
33
+ return {activity,branches,target,isBranchToggle:element=>!!element.closest("[data-app-action-sidebar-section-toggle], " + projectSelector)};
28
34
  }
@@ -4,7 +4,7 @@ const EXPAND_GLYPH = "<svg aria-hidden=\"true\"\n xmlns=\"http://www.w3.org/200
4
4
 
5
5
 
6
6
  const states = new Map();
7
- let active = false, restoring = false, button, timer = null, disposed = false;
7
+ let active = false, restoring = false, button, currentBell, timer = null, disposed = false;
8
8
  const text = (en, cn) => (document.documentElement.lang || navigator.language).startsWith("zh") ? cn : en;
9
9
 
10
10
 
@@ -15,8 +15,10 @@ function remember(items) {
15
15
  function reconcile() {
16
16
  if (disposed) return;
17
17
  const sidebar = document.querySelector("#app-shell-sidebar");
18
- const bell = [...(sidebar?.querySelectorAll("button[aria-pressed]") || [])].find(e => /activity|活动|动态/.test(e.getAttribute("aria-label") || ""));
18
+ const bell = nativeFocus.activity();
19
19
  if (!bell) { button?.remove(); button = null; return; }
20
+ if (currentBell !== bell) { button?.remove(); button = null; }
21
+ currentBell = bell;
20
22
  if (!button?.isConnected) {
21
23
  button = bell.cloneNode(false);
22
24
  button.removeAttribute("id");
@@ -93,7 +95,7 @@ const observer = new MutationObserver(records => {
93
95
  )) schedule();
94
96
  });
95
97
  observer.observe(document.documentElement, { subtree:true, childList:true, attributes:true, attributeFilter:[
96
- "aria-expanded", "aria-pressed", "data-app-action-sidebar-thread-selected",
98
+ "aria-expanded", "aria-pressed", "aria-current", "style", "class", "data-app-action-sidebar-thread-selected",
97
99
  "data-app-action-sidebar-section-collapsed", "data-above-composer-conversation-id", "lang"
98
100
  ] });
99
101
  reconcile();
@@ -34,7 +34,7 @@ function pageInfo() {
34
34
  const ownershipTabs=[...page.querySelectorAll('[role="tablist"]')].find(node=>!node.closest(`[${owned}]`)&&nativeProps(node,p=>p.tabs?.some(tab=>tab.key==="owned")&&p.tabs?.some(tab=>tab.key==="shared")));
35
35
  const ownership=ownershipTabs&&nativeProps(ownershipTabs,p=>p.tabs?.some(tab=>tab.key==="owned")&&p.tabs?.some(tab=>tab.key==="shared"));
36
36
  const shared = ownership ? ownership.selectedKey === "shared" : native ? !!native.shared : [...page.querySelectorAll("button[aria-pressed=true]")].some(b => /Shared with you|与你共享|與你共用/.test(b.textContent));
37
- const create = [...(plus ? page : page.firstElementChild).querySelectorAll("button")].find(b => visible(b) && (plus ? /^(Create Site|创建站点|建立網站)$/ : /^(New|新建|新增)$/).test(b.textContent.trim()) && !b.hasAttribute(owned));
37
+ const create = [...(plus ? page : page.firstElementChild).querySelectorAll("button")].find(b => visible(b) && (plus ? /^(Create Site|创建站点|创建新站点|建立網站)$/ : /^(New|新建|新增)$/).test(b.textContent.trim()) && !b.hasAttribute(owned));
38
38
  codex.report("sites-page",true);
39
39
  return { page, input, content, list, grid, shared, create, plus, ownershipTabs:plus?(ownershipTabs??null):null, accountKey:account.read().key, empty: list ? null : [...content.children].findLast(e => !e.hasAttribute(owned)) };
40
40
  }
@@ -36,6 +36,7 @@ function setButtonDisabled(button, disabled) {
36
36
  button.disabled = disabled; button.setAttribute("aria-disabled", String(disabled)); button.toggleAttribute("data-disabled", disabled);
37
37
  }
38
38
  function notice(message) {
39
+ if (disposed || signal.aborted) return;
39
40
  document.querySelector(`[${owned}=notice]`)?.remove();
40
41
  const n = element("div", "", message); n.setAttribute(owned, "notice"); n.setAttribute("role", "status"); document.body.append(n);
41
42
  setTimeout(() => n.remove(), 7000);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "2.0.39",
3
+ "version": "2.0.41",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -77,16 +77,20 @@
77
77
  "packages/plugin/src",
78
78
  "packages/plugin/assets",
79
79
  "packages/plugin/plugin.json",
80
- "features/markdown-reading/vendor/qv/LICENSE",
81
- "features/markdown-reading/vendor/qv/provenance.json",
82
- "features/markdown-reading/vendor/qv/THIRD_PARTY_NOTICES.txt"
80
+ "features/mermaid-diagrams/vendor/qv/LICENSE",
81
+ "features/mermaid-diagrams/vendor/qv/provenance.json",
82
+ "features/mermaid-diagrams/vendor/qv/THIRD_PARTY_NOTICES.txt",
83
+ "features/mermaid-diagrams/src",
84
+ "features/mermaid-diagrams/assets",
85
+ "features/mermaid-diagrams/codex-feature.json",
86
+ "features/mermaid-diagrams/vendor/lucide-LICENSE"
83
87
  ],
84
88
  "engines": {
85
89
  "node": ">=22.5.0"
86
90
  },
87
91
  "scripts": {
88
92
  "start": "node packages/cli/bin/mcx.mjs serve",
89
- "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/hide-upsell-banner/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/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",
93
+ "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/hide-upsell-banner/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/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",
90
94
  "test:cli": "node --test packages/cli/test/*.test.mjs",
91
95
  "test:runtime": "node --test packages/runtime-host/test/*.test.mjs",
92
96
  "check": "node scripts/check-syntax.mjs",
@@ -2,7 +2,7 @@ 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", "hide-upsell-banner", "session-status", "usage-meter", "sidebar-focus", "sites", "markdown-reading", "file-navigation", "quick-chat"]);
5
+ export const FEATURE_IDS = Object.freeze(["model-slider", "notes", "images", "hide-upsell-banner", "session-status", "usage-meter", "sidebar-focus", "sites", "markdown-reading", "mermaid-diagrams", "file-navigation", "quick-chat"]);
6
6
  export const DEFAULT_CONFIG = Object.freeze({
7
7
  schemaVersion: 1,
8
8
  features: Object.freeze({
@@ -15,6 +15,7 @@ export const DEFAULT_CONFIG = Object.freeze({
15
15
  "sidebar-focus": true,
16
16
  sites: true,
17
17
  "markdown-reading": true,
18
+ "mermaid-diagrams": false,
18
19
  "file-navigation": true,
19
20
  "quick-chat": true,
20
21
  }),
@@ -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", "file-navigation", "images", "sites", "quick-chat", "sidebar-focus", "hide-upsell-banner"];
17
+ const DASHBOARD_FEATURE_ORDER = ["model-slider", "notes", "usage-meter", "session-status", "markdown-reading", "mermaid-diagrams", "file-navigation", "images", "sites", "quick-chat", "sidebar-focus", "hide-upsell-banner"];
18
18
  const CONTENT_TYPES = new Map([
19
19
  [".css", "text/css; charset=utf-8"],
20
20
  [".html", "text/html; charset=utf-8"],
@@ -25,9 +25,10 @@ export function resolvePaths({ homeDir = os.homedir(), platform = process.platfo
25
25
  supportDir,
26
26
  logsDir,
27
27
  serviceDir,
28
- // Windows 无 .app 服务包;服务入口是计划任务拉起的 cmd 脚本。
28
+ // Windows 通过隐藏启动器等待 cmd 服务,保留计划任务的退出状态。
29
29
  serviceBundleSource: null,
30
30
  serviceAppPath: winPath.join(serviceDir, "MineCodexService.cmd"),
31
+ serviceLauncherPath: winPath.join(serviceDir, "MineCodexService-hidden.vbs"),
31
32
  serviceManagerPath: null,
32
33
  serviceRuntimeConfigPath: winPath.join(supportDir, "service-runtime.json"),
33
34
  configPath: winPath.join(supportDir, "config.json"),
@@ -217,6 +217,20 @@ export function buildWindowsServiceCommand({ nodePath, cliPath, paths }) {
217
217
  ].join("\r\n");
218
218
  }
219
219
 
220
+ export function buildWindowsServiceLauncher(paths) {
221
+ const serviceCmd = String(paths.serviceAppPath).replaceAll('"', '""');
222
+ return [
223
+ "Option Explicit",
224
+ "Dim shell, command, exitCode",
225
+ 'Set shell = CreateObject("WScript.Shell")',
226
+ "' 隐藏窗口并等待服务退出,向计划任务传递真实退出码。",
227
+ 'command = Chr(34) & shell.ExpandEnvironmentStrings("%ComSpec%") & Chr(34) & " /d /s /c " & Chr(34) & Chr(34) & "' + serviceCmd + '" & Chr(34) & Chr(34)',
228
+ "exitCode = shell.Run(command, 0, True)",
229
+ "WScript.Quit exitCode",
230
+ "",
231
+ ].join("\r\n");
232
+ }
233
+
220
234
  // cmd 的引号转义是内部双引号翻倍,与 JSON 转义规则不同,不能混用。
221
235
  function cmdQuote(value) {
222
236
  return '"' + String(value).replaceAll('"', '""') + '"';
@@ -907,6 +921,8 @@ export class WindowsPlatformAdapter {
907
921
 
908
922
  async writeServiceCommand(paths, content) {
909
923
  await this.fileSystem.writeFile(paths.serviceAppPath, content, { mode: 0o700 });
924
+ // WSH 用 UTF-16 BOM 识别中文路径,避免依赖系统 ANSI 代码页。
925
+ await this.fileSystem.writeFile(paths.serviceLauncherPath, Buffer.from("\uFEFF" + buildWindowsServiceLauncher(paths), "utf16le"), { mode: 0o700 });
910
926
  }
911
927
 
912
928
  async snapshotServiceState(paths) {
@@ -956,8 +972,9 @@ export class WindowsPlatformAdapter {
956
972
  async registerServiceTask(paths) {
957
973
  // ExecutionTimeLimit 0 表示不限时:服务是常驻进程,不能被默认 72h 限制回收。
958
974
  const script = [
959
- "$serviceCmd = '" + String(paths.serviceAppPath).replaceAll("'", "''") + "';",
960
- '$action = New-ScheduledTaskAction -Execute \'cmd.exe\' -Argument (\'/c ""\' + $serviceCmd + \'""\');',
975
+ "$launcher = '" + String(paths.serviceLauncherPath).replaceAll("'", "''") + "';",
976
+ "$serviceDir = '" + String(paths.serviceDir).replaceAll("'", "''") + "';",
977
+ '$action = New-ScheduledTaskAction -Execute (Join-Path $env:SystemRoot \'System32\\wscript.exe\') -Argument (\'//B //Nologo "\' + $launcher + \'"\') -WorkingDirectory $serviceDir;',
961
978
  "$trigger = New-ScheduledTaskTrigger -AtLogOn;",
962
979
  "$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1);",
963
980
  "Register-ScheduledTask -TaskName '" + WINDOWS_SERVICE_TASK_NAME + "' -Action $action -Trigger $trigger -Settings $settings -Force | Out-Null;",
@@ -1179,18 +1196,32 @@ export class WindowsPlatformAdapter {
1179
1196
 
1180
1197
  async openNativeCodex({ cdpPort } = {}) {
1181
1198
  const installation = await this.codex();
1182
- if (!Number.isInteger(cdpPort) || cdpPort <= 0 || cdpPort > 65_535) {
1183
- await this.powershell("Start-Process -FilePath '" + String(installation.executable).replaceAll("'", "''") + "'");
1184
- return null;
1185
- }
1186
- const existingPids = new Set((await this.listCodexProcesses()).map(({ pid }) => pid));
1187
- const safeExecutable = String(installation.executable).replaceAll("'", "''");
1199
+ const withCdp = Number.isInteger(cdpPort) && cdpPort > 0 && cdpPort <= 65_535;
1200
+ const existingPids = new Set(withCdp ? (await this.listCodexProcesses()).map(({ pid }) => pid) : []);
1201
+ const appId = `${installation.packageFamilyName}!App`.replaceAll("'", "''");
1202
+ const args = withCdp
1203
+ ? `--remote-debugging-address=127.0.0.1 --remote-debugging-port=${cdpPort} --remote-allow-origins=http://127.0.0.1:${cdpPort}`
1204
+ : "";
1205
+ // 通过已注册应用入口激活,保留任务栏身份及扩展所需参数。
1188
1206
  await this.powershell([
1189
- "Start-Process -FilePath '" + safeExecutable + "' -ArgumentList @(",
1190
- " '--remote-debugging-address=127.0.0.1',",
1191
- " '--remote-debugging-port=" + cdpPort + "',",
1192
- " '--remote-allow-origins=http://127.0.0.1:" + cdpPort + "');",
1207
+ "Add-Type -TypeDefinition @'",
1208
+ "using System;",
1209
+ "using System.Runtime.InteropServices;",
1210
+ "public static class MineCodexRegisteredLaunch {",
1211
+ " [ComImport, Guid(\"2E941141-7F97-4756-BA1D-9DECDE894A3D\"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]",
1212
+ " interface Activation {",
1213
+ " [PreserveSig] int ActivateApplication([MarshalAs(UnmanagedType.LPWStr)] string id, [MarshalAs(UnmanagedType.LPWStr)] string args, uint options, out uint pid);",
1214
+ " }",
1215
+ " public static uint Launch(string id, string args) {",
1216
+ " object manager = Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid(\"45BA127D-10A8-46EA-8AB7-56EA9078943C\")));",
1217
+ " try { uint pid; Marshal.ThrowExceptionForHR(((Activation)manager).ActivateApplication(id, args, 0, out pid)); return pid; }",
1218
+ " finally { Marshal.ReleaseComObject(manager); }",
1219
+ " }",
1220
+ "}",
1221
+ "'@",
1222
+ `[MineCodexRegisteredLaunch]::Launch('${appId}', '${args}') | Out-Null`,
1193
1223
  ].join("\n"));
1224
+ if (!withCdp) return null;
1194
1225
  return this.waitForNativeCodex({ cdpPort, excludedPids: existingPids });
1195
1226
  }
1196
1227
 
@@ -0,0 +1,107 @@
1
+ import { loadNativeMarkdownRenderer } from './markdown-native.mjs';
2
+
3
+ export function markdownExportAlias(source, name) {
4
+ const start = source.lastIndexOf('export{');
5
+ if (start < 0) return undefined;
6
+ const entries = source.slice(start + 7).split('}')[0].split(',');
7
+ for (const entry of entries) {
8
+ const [local, alias = local] = entry.trim().split(/\s+as\s+/);
9
+ if (local === name) return alias;
10
+ }
11
+ return undefined;
12
+ }
13
+
14
+ // 从 Chat Markdown 的公开组件发现悬浮操作栏和控件,不绑定构建 hash。
15
+ 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));
19
+ const response = await fetch(url);
20
+ if (!response.ok) throw new Error('native-table-actions-source-unavailable');
21
+ const source = await response.text();
22
+ const section = (marker) => {
23
+ const at = source.indexOf(marker),
24
+ start = source.lastIndexOf('function ', at),
25
+ end = source.indexOf('function ', at);
26
+ if (at < 0 || start < 0 || end < 0) throw new Error('native-table-actions-contract-changed');
27
+ return source.slice(start, end);
28
+ };
29
+ const actions = section('"data-block-actions":!0'),
30
+ table = section('id:`markdown.expandTable`');
31
+ const exported = source.slice(source.lastIndexOf('export{'));
32
+ const module = await import(url);
33
+ const publicBinding = (name) => {
34
+ const alias = markdownExportAlias(exported, name);
35
+ if (!alias || !module[alias]) throw new Error('native-table-actions-export-unavailable');
36
+ return module[alias];
37
+ };
38
+ // 初始化原生 lazy module,再使用其公开 React 组件。
39
+ const initializer = source
40
+ .slice(source.indexOf(table) + table.length)
41
+ .match(/^function ([\w$]+)\(\)\{return\(\1=/)?.[1];
42
+ if (!initializer) throw new Error('native-table-actions-initializer-unavailable');
43
+ publicBinding(initializer)();
44
+ const imports = new Map();
45
+ for (const match of source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g))
46
+ for (const entry of match[1].split(',')) {
47
+ const parts = entry.trim().split(/\s+as\s+/);
48
+ imports.set(parts.at(-1), { name: parts[0], url: new URL(match[2], url).href });
49
+ }
50
+ const binding = async (name) => {
51
+ const item = imports.get(name);
52
+ if (!item) throw new Error('native-table-actions-dependency-unavailable');
53
+ return (await import(item.url))[item.name];
54
+ };
55
+ const sharedUrl = [...imports.values()].find((item) => /\/app-shared-[^/]+\.js/.test(item.url))?.url;
56
+ if (!sharedUrl) throw new Error('native-code-wrap-module-unavailable');
57
+ const sharedSource = await (await fetch(sharedUrl)).text(),
58
+ shared = await import(sharedUrl);
59
+ const wrapAt = sharedSource.indexOf('id:`codeSnippet.wrap.disable`');
60
+ const wrapSection = sharedSource.slice(
61
+ sharedSource.lastIndexOf('function ', wrapAt),
62
+ sharedSource.indexOf('function ', wrapAt),
63
+ );
64
+ const wrapPair = wrapSection.match(
65
+ /children:[\w$]+\?\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\):\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\)/,
66
+ );
67
+ if (wrapAt < 0 || !wrapPair) throw new Error('native-code-wrap-icons-unavailable');
68
+ const sharedExports = sharedSource.slice(sharedSource.lastIndexOf('export{'));
69
+ const sharedBinding = (name) => shared[markdownExportAlias(sharedExports, name)];
70
+ const assignment = sharedSource.indexOf(`${wrapPair[1]}=`);
71
+ const initialize = sharedSource
72
+ .slice(sharedSource.lastIndexOf('function ', assignment))
73
+ .match(/^function ([\w$]+)\(\)/)?.[1];
74
+ if (initialize && typeof sharedBinding(initialize) === 'function') sharedBinding(initialize)();
75
+ const wrapIcons = { disable: sharedBinding(wrapPair[1]), enable: sharedBinding(wrapPair[2]) };
76
+ if (!wrapIcons.disable || !wrapIcons.enable) throw new Error('native-code-wrap-icon-exports-unavailable');
77
+ const button = table.match(/jsx\)\(([\w$]+),\{"aria-label":[\w$]+,"aria-expanded"/)?.[1];
78
+ const copy = table.match(/jsx\)\(([\w$]+),\{ariaLabel:[\w$]+,buttonText:/)?.[1];
79
+ const tooltip = table.match(/jsx\)\(([\w$]+),\{tooltipContent:[\w$]+,delayOpen:!0/)?.[1];
80
+ if (!button || !copy || !tooltip) throw new Error('native-table-actions-controls-unavailable');
81
+ const [Button, CopyButton, Tooltip] = await Promise.all([binding(button), binding(copy), binding(tooltip)]);
82
+ const previewName = table.match(/jsx\)\(([\w$]+),\{src:``,alt:/)?.[1];
83
+ const tableName = table.match(/jsx\)\(([\w$]+),\{className:[\w$]+,children:[\w$]+\}/)?.[1];
84
+ const classesName = table.match(/([\w$]+)\.MarkdownTablePreview/)?.[1];
85
+ const icon = table.match(/children:\(0,[\w$]+\.jsx\)\(([\w$]+),\{icon:\{16:([\w$]+),20:([\w$]+)\}\}\)/);
86
+ if (!previewName || !tableName || !classesName || !icon)
87
+ throw new Error('native-table-preview-contract-unavailable');
88
+ const [Icon, small, medium, renderer] = await Promise.all([
89
+ binding(icon[1]),
90
+ binding(icon[2]),
91
+ binding(icon[3]),
92
+ loadNativeMarkdownRenderer(source, url, module),
93
+ ]);
94
+ return {
95
+ ...native,
96
+ ...renderer,
97
+ TablePreview: publicBinding(previewName),
98
+ Table: publicBinding(tableName),
99
+ tableClasses: publicBinding(classesName),
100
+ ExpandIcon: () => native.React.createElement(Icon, { icon: { 16: small, 20: medium } }),
101
+ Actions: publicBinding(actions.match(/^function ([\w$]+)/)[1]),
102
+ Button,
103
+ wrapIcons,
104
+ CopyButton,
105
+ Tooltip,
106
+ };
107
+ }
@@ -0,0 +1,26 @@
1
+ // 使用当前编辑器所属的原生 CodeMirror 实例,避免引入第二套编辑器依赖。
2
+ 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));
7
+ if (!exports.includes(view.constructor)) continue;
8
+ const Decoration = exports.find((value) => value?.line && value?.replace && value?.mark);
9
+ const ViewPlugin = exports.find((value) => value?.fromClass && value?.define);
10
+ const StateEffect = exports.find((value) => value?.appendConfig && value?.reconfigure);
11
+ const Compartment = exports.find(
12
+ (value) => value?.prototype?.reconfigure && value?.prototype?.of && value?.prototype?.get,
13
+ );
14
+ const WidgetType = exports.find(
15
+ (value) => value?.prototype?.eq && value?.prototype?.ignoreEvent && value?.prototype?.updateDOM,
16
+ );
17
+ const StateField = exports.find(
18
+ (value) =>
19
+ value?.define && value?.prototype?.slot && value?.prototype?.init && value?.prototype?.create,
20
+ );
21
+ const Prec = exports.find((value) => value?.highest && value?.lowest && value?.high);
22
+ if (Decoration && ViewPlugin && StateEffect && Compartment && WidgetType && Prec && StateField)
23
+ return { Decoration, ViewPlugin, StateEffect, Compartment, WidgetType, Prec, StateField };
24
+ }
25
+ throw new Error('native-markdown-list-api-unavailable');
26
+ }
@@ -13,16 +13,19 @@ export async function loadNativeMarkdownRenderer(source, url, module) {
13
13
  ].find((m) => m[2].includes(`${init}()`) && binding(m[1]));
14
14
  if (!initializer || !binding(markdownName)) throw new Error('native-rich-markdown-contract-unavailable');
15
15
  binding(initializer[1])();
16
- let lexer, sharedUrl;
17
16
  const lexerName = source.match(/tokens:([\w$]+)\(e,void 0\)/)?.[1];
18
- for (const m of source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g))
17
+ let lexer = lexerName ? binding(lexerName) : undefined, sharedUrl;
18
+ // lexer 可在入口模块本地导出;代码组件仍从独立的共享模块加载。
19
+ for (const m of source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g)) {
20
+ const importUrl = new URL(m[2], url).href;
21
+ if (/\/app-shared-[^/]+\.(?:js|mjs)$/.test(importUrl)) sharedUrl = importUrl;
19
22
  for (const entry of m[1].split(',')) {
20
23
  const parts = entry.trim().split(/\s+as\s+/);
21
24
  if (parts.at(-1) === lexerName) {
22
- sharedUrl = new URL(m[2], url).href;
23
- lexer = (await import(sharedUrl))[parts[0]];
25
+ lexer = (await import(importUrl))[parts[0]];
24
26
  }
25
27
  }
28
+ }
26
29
  if (!sharedUrl || typeof lexer !== 'function') throw new Error('native-markdown-lexer-unavailable');
27
30
  return { Markdown: binding(markdownName), lexer, sharedUrl };
28
31
  }
@@ -1,8 +1,5 @@
1
- import { createMarkdownReadingRuntime } from './reading-runtime.mjs';
2
-
3
1
  // 当前原生 Markdown Preview 是 CodeMirror 视图;Source 与 Pages 不属于此契约。
4
- export function createMarkdownPreviewAdapter(codex, resources) {
5
- let runtime;
2
+ export function createMarkdownPreviewAdapter(codex, attach) {
6
3
  function preview(editor) {
7
4
  if (!editor.isConnected || !editor.closest('[data-editor-search-surface]')) return null;
8
5
  const content = editor.querySelector('.cm-content[data-language="markdown"]');
@@ -30,9 +27,6 @@ export function createMarkdownPreviewAdapter(codex, resources) {
30
27
  return {
31
28
  preview,
32
29
  previews,
33
- attach(item) {
34
- runtime ??= createMarkdownReadingRuntime(codex, undefined, resources);
35
- return runtime.attach(item);
36
- },
30
+ attach(item) { return attach(item); },
37
31
  };
38
32
  }
@@ -69,7 +69,7 @@ export async function loadNativeMarkdownToc() {
69
69
  )?.[1];
70
70
  const Tooltip = await binding(tooltipLocal);
71
71
  const layoutUrl = [...imports.values()].find((item) =>
72
- item.url.includes('/floating-navigation-rail-layout-'),
72
+ /\/floating-navigation-rail-(?:layout-)?[\w-]+\.js(?:$|\?)/.test(item.url),
73
73
  )?.url;
74
74
  if (!layoutUrl) throw new Error('native-page-toc-layout-unavailable');
75
75
  const layout = await import(layoutUrl);
@@ -84,7 +84,7 @@ export async function loadNativeMarkdownToc() {
84
84
  mainImports.set(parts.at(-1), { name: parts[0], url: new URL(match[2], mainUrl).href });
85
85
  }
86
86
  const ReactDOM = (await binding(rootFactory, mainImports))();
87
- const css = initial.match(/["']\.\/((?:floating-navigation-rail-layout)-[\w-]+\.css)["']/)?.[1];
87
+ const css = initial.match(/["']\.\/((?:floating-navigation-rail(?:-layout)?)-[\w-]+\.css)["']/)?.[1];
88
88
  if (!css) throw new Error('native-page-toc-styles-unavailable');
89
89
  const cssUrl = new URL(css, initialUrl).href;
90
90
  if (![...document.querySelectorAll('link[rel="stylesheet"]')].some((n) => n.href === cssUrl)) {
@@ -114,9 +114,10 @@ export async function loadNativeMarkdownToc() {
114
114
  const classes = {
115
115
  rail: requireClass(railBody, /ref:[\w$]+,className:`([^`]+)`,"data-floating-navigation-rail-list"/),
116
116
  railButton: requireClass(railBody, /className:`([^`]+)`,"aria-current"/),
117
- menu: requireClass(listBody, /ref:[\w$]+,className:`([^`]+)`,children:/),
117
+ menu: requireClass(listBody, /ref:[\w$]+,className:`([^`]+)`,(?:variant:`viewer`,)?children:/),
118
118
  menuButton: requireClass(listBody, /className:[\w$]+\(`([^`]+)`/),
119
- nav: requireClass(source, /className:`(absolute top-1\/2 left-1\.5[^`]+)`/),
119
+ // 按 TOC 调用角色找导航样式,避免绑定随布局变化的 top utility。
120
+ nav: requireClass(bodyMatching(/skipDelayKey:`page-table-of-contents`/), /jsx\)\(`nav`,\{"aria-label":[\w$]+,className:`([^`]+)`/),
120
121
  };
121
122
  const indentName = listBody.match(/`block`,([\w$]+)\[e\.level\]/)?.[1];
122
123
  const indentValue = indentName && source.match(new RegExp(`\\b${indentName}=(\\[[^\\]]+\\])`))?.[1];