minecodex 2.0.5 → 2.0.7

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 (85) hide show
  1. package/DESIGN.md +19 -0
  2. package/README.md +5 -1
  3. package/apps/dashboard/DESIGN.md +53 -31
  4. package/apps/dashboard/app.js +55 -39
  5. package/apps/dashboard/assets/OFL.txt +93 -0
  6. package/apps/dashboard/assets/info.svg +15 -0
  7. package/apps/dashboard/assets/moon.svg +13 -0
  8. package/apps/dashboard/assets/refresh-cw.svg +16 -0
  9. package/apps/dashboard/assets/silkscreen.ttf +0 -0
  10. package/apps/dashboard/assets/sun.svg +21 -0
  11. package/apps/dashboard/assets/terrain.js +89 -0
  12. package/apps/dashboard/assets/triangle-alert.svg +15 -0
  13. package/apps/dashboard/favicon.svg +12 -17
  14. package/apps/dashboard/index.html +24 -18
  15. package/apps/dashboard/styles.css +147 -181
  16. package/assets/brand/README.md +19 -0
  17. package/assets/brand/minecodex-icon-dark.svg +9 -0
  18. package/assets/brand/minecodex-icon-light.svg +9 -0
  19. package/assets/brand/minecodex-icon.svg +8 -0
  20. package/assets/brand/references/lucide-LICENSE.txt +43 -0
  21. package/assets/readme/hero.svg +93 -0
  22. package/features/hide-upsell-banner/README.md +7 -5
  23. package/features/hide-upsell-banner/codex-feature.json +2 -2
  24. package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +30 -12
  25. package/features/images/DESIGN.md +18 -3
  26. package/features/images/README.md +4 -3
  27. package/features/images/codex-feature.json +2 -2
  28. package/features/images/src/adapters/codex/native-image-modules.mjs +220 -0
  29. package/features/images/src/adapters/codex/native-image-source.mjs +113 -198
  30. package/features/images/src/assets/prompts-menu.svg +15 -0
  31. package/features/images/src/frontend/cloud-image-actions.mjs +8 -6
  32. package/features/images/src/frontend/local-image-source.mjs +2 -1
  33. package/features/images/src/library-prompt-page.mjs +10 -4
  34. package/features/images/src/library-prompt-source.mjs +4 -5
  35. package/features/model-slider/codex-feature.json +2 -2
  36. package/features/notes/DESIGN.md +1 -1
  37. package/features/notes/codex-feature.json +2 -2
  38. package/features/notes/src/frontend/entry.mjs +1 -1
  39. package/features/session-status/codex-feature.json +2 -2
  40. package/features/sidebar-focus/DESIGN.md +4 -4
  41. package/features/sidebar-focus/codex-feature.json +3 -2
  42. package/features/sidebar-focus/src/adapters/codex/renderer.mjs +1 -1
  43. package/features/sidebar-focus/src/sidebar-focus.mjs +9 -7
  44. package/features/sites/DESIGN.md +25 -3
  45. package/features/sites/README.md +6 -2
  46. package/features/sites/codex-feature.json +3 -2
  47. package/features/sites/src/adapters/codex/renderer.mjs +25 -4
  48. package/features/sites/src/backend/project-sites.mjs +18 -6
  49. package/features/sites/src/backend/sites-service.mjs +22 -10
  50. package/features/sites/src/domain/discovery-candidates.mjs +53 -0
  51. package/features/sites/src/frontend/entry.mjs +76 -38
  52. package/features/sites/src/frontend/styles.css +3 -3
  53. package/features/unread-inbox/DESIGN.md +24 -25
  54. package/features/unread-inbox/codex-feature.json +3 -2
  55. package/features/unread-inbox/src/adapters/codex/modules.mjs +52 -0
  56. package/features/unread-inbox/src/adapters/codex/renderer.mjs +90 -0
  57. package/features/unread-inbox/src/domain/notifications.mjs +21 -0
  58. package/features/unread-inbox/src/injection-source.mjs +5 -2
  59. package/features/unread-inbox/src/unread-inbox.mjs +212 -297
  60. package/features/usage-meter/codex-feature.json +2 -2
  61. package/package.json +11 -3
  62. package/packages/cli/native/MineCodex.app/Contents/Info.plist +4 -2
  63. package/packages/cli/native/MineCodex.app/Contents/MacOS/MineCodexServiceManager +0 -0
  64. package/packages/cli/native/MineCodex.app/Contents/Resources/MineCodex.icns +0 -0
  65. package/packages/cli/native/MineCodex.app/Contents/_CodeSignature/CodeResources +11 -0
  66. package/packages/cli/native/README.md +31 -0
  67. package/packages/cli/src/browser-control-panel.mjs +48 -0
  68. package/packages/cli/src/commands.mjs +3 -1
  69. package/packages/cli/src/control-server.mjs +6 -3
  70. package/packages/cli/src/platform.mjs +5 -0
  71. package/packages/runtime-host/README.md +10 -5
  72. package/packages/runtime-host/src/codex-injection.mjs +12 -3
  73. package/packages/runtime-host/src/codex-runtime.mjs +10 -0
  74. package/packages/ui/DESIGN.md +5 -1
  75. package/packages/ui/assets/arrow-up-right.svg +13 -0
  76. package/packages/ui/assets/lucide-LICENSE.txt +43 -0
  77. package/packages/ui/assets/minecodex-menu-16.svg +7 -0
  78. package/packages/ui/assets/minecodex-menu-20.svg +7 -0
  79. package/packages/ui/src/components/account-menu.mjs +88 -0
  80. package/packages/ui/src/components/page.mjs +2 -2
  81. package/packages/ui/src/foundation/layers.mjs +1 -0
  82. package/packages/ui/src/install.mjs +3 -0
  83. package/packages/ui/src/native/account-menu.mjs +21 -0
  84. package/packages/ui/src/native/page.mjs +17 -2
  85. package/packages/ui/src/native/tokens.mjs +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "2.0.5",
3
+ "version": "2.0.7",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,6 +41,7 @@
41
41
  "features/session-status/vendor/lucide-LICENSE",
42
42
  "features/session-status/vendor/phosphor-LICENSE",
43
43
  "apps/dashboard/app.js",
44
+ "apps/dashboard/assets",
44
45
  "apps/dashboard/favicon.svg",
45
46
  "apps/dashboard/index.html",
46
47
  "apps/dashboard/styles.css",
@@ -65,7 +66,13 @@
65
66
  "packages/codex-adapter/src",
66
67
  "packages/codex-adapter/package.json",
67
68
  "packages/ui/package.json",
68
- "DESIGN.md"
69
+ "DESIGN.md",
70
+ "assets/brand/minecodex-icon.svg",
71
+ "assets/brand/minecodex-icon-light.svg",
72
+ "assets/brand/minecodex-icon-dark.svg",
73
+ "assets/brand/references/lucide-LICENSE.txt",
74
+ "assets/readme/hero.svg",
75
+ "packages/ui/assets"
69
76
  ],
70
77
  "engines": {
71
78
  "node": ">=22.5.0"
@@ -80,7 +87,8 @@
80
87
  "pack:check": "npm pack --dry-run",
81
88
  "pack:audit": "node scripts/audit-package.mjs",
82
89
  "prepublishOnly": "npm test && npm run check && npm run pack:audit",
83
- "test:ui": "node --test packages/ui/test/*.test.mjs"
90
+ "test:ui": "node --test packages/ui/test/*.test.mjs",
91
+ "build:icons": "node scripts/build-brand-icons.mjs"
84
92
  },
85
93
  "repository": {
86
94
  "type": "git",
@@ -14,12 +14,14 @@
14
14
  <string>6.0</string>
15
15
  <key>CFBundleName</key>
16
16
  <string>MineCodex</string>
17
+ <key>CFBundleIconFile</key>
18
+ <string>MineCodex.icns</string>
17
19
  <key>CFBundlePackageType</key>
18
20
  <string>APPL</string>
19
21
  <key>CFBundleShortVersionString</key>
20
- <string>1.0.0</string>
22
+ <string>1.0.1</string>
21
23
  <key>CFBundleVersion</key>
22
- <string>1</string>
24
+ <string>2</string>
23
25
  <key>LSBackgroundOnly</key>
24
26
  <true/>
25
27
  <key>LSMinimumSystemVersion</key>
@@ -4,6 +4,10 @@
4
4
  <dict>
5
5
  <key>files</key>
6
6
  <dict>
7
+ <key>Resources/MineCodex.icns</key>
8
+ <data>
9
+ mSztCiWeEvLkAINXLTTBTVSA0f8=
10
+ </data>
7
11
  <key>Resources/MineCodexServiceHost</key>
8
12
  <data>
9
13
  kB0iYBcULzqcS1yOgYpY3LZjeq4=
@@ -18,6 +22,13 @@
18
22
  mGWkf+X17Pau3uy8xUUB1s+JXofeKYKNRzFPk3FXeaA=
19
23
  </data>
20
24
  </dict>
25
+ <key>Resources/MineCodex.icns</key>
26
+ <dict>
27
+ <key>hash2</key>
28
+ <data>
29
+ Wg3Q9bbvnrEwou/t4h9Gk/nWmlwm0UGGt+qR4+LySAI=
30
+ </data>
31
+ </dict>
21
32
  <key>Resources/MineCodexServiceHost</key>
22
33
  <dict>
23
34
  <key>hash2</key>
@@ -16,3 +16,34 @@ Rebuild deliberately with:
16
16
  ```bash
17
17
  npm run build:service
18
18
  ```
19
+
20
+ ## Icon bundle revision 1.0.1
21
+
22
+ This is an explicit native resource revision: `CFBundleIconFile` now points to
23
+ `Contents/Resources/MineCodex.icns`, bundle version is `2`, and the outer app is
24
+ re-signed. The service host executable remains byte-for-byte unchanged. The
25
+ manager's ad-hoc designated requirement changes when its Info.plist changes.
26
+ The service identifier and launch-agent plist remain unchanged.
27
+
28
+ The existing `MacPlatformAdapter.installService()` is the migration path: run the
29
+ old installed manager's `unregister` before replacing its bundle; verify the new
30
+ staged bundle; replace it; then register and kickstart the new service. An old
31
+ registration failure aborts replacement. Use `mcx install` / the normal update
32
+ flow; do not copy a new icon or re-sign the installed app in place. System approval
33
+ may be required after re-registration; preserve macOS's decision and existing CLI
34
+ reporting. Real Background Items approval and UI upgrade acceptance must be checked
35
+ on a target installation before release.
36
+
37
+ The icon uses the filled project mark on a neutral rounded backing so one ICNS
38
+ remains legible in light and dark system surfaces. SVG artwork is generated at
39
+ `assets/brand/minecodex-service-icon.svg` by `npm run build:icons`. Rasterize that
40
+ SVG into an iconset at 16/32/128/256/512px and each size's @2x resolution, then run
41
+ `iconutil -c icns <iconset> -o app-template/Contents/Resources/MineCodex.icns`.
42
+ Commit the resulting template ICNS with any deliberate native artwork revision.
43
+ `npm run build:service` copies it into the built bundle before signing. Ordinary
44
+ JavaScript releases continue shipping the existing signed bundle unchanged.
45
+
46
+ Validation for this revision: strict deep signature verification, universal
47
+ architectures, ICNS structure and plist checks, system icon loading through
48
+ `NSWorkspace`, installer regressions, and npm package audit. These checks do not
49
+ register a background item or change an installed service.
@@ -0,0 +1,48 @@
1
+ // 标签页枚举留在平台适配层;只匹配控制台 origin 的根页或一次性登录页。
2
+ export const CONTROL_PANEL_BROWSER_SCRIPT = `
3
+ ObjC.import('CoreServices');
4
+ function run(argv) {
5
+ const url = argv[0], origin = argv[1];
6
+ const bundle = ObjC.unwrap(ObjC.castRefToObject($.LSCopyDefaultHandlerForURLScheme($('http'))));
7
+ const chromium = ['com.google.Chrome', 'com.google.Chrome.canary', 'org.chromium.Chromium', 'com.microsoft.edgemac', 'com.brave.Browser', 'com.vivaldi.Vivaldi'];
8
+ if (bundle !== 'com.apple.Safari' && chromium.indexOf(bundle) < 0) return JSON.stringify({mode:'open', reuseSupported:false});
9
+ const browser = Application(bundle);
10
+ if (!browser.running()) return JSON.stringify({mode:'open', reuseSupported:true});
11
+ try {
12
+ const windows = browser.windows();
13
+ for (let w = 0; w < windows.length; w++) {
14
+ const tabs = windows[w].tabs();
15
+ for (let t = 0; t < tabs.length; t++) {
16
+ const candidate = tabs[t].url();
17
+ if (candidate !== origin + '/' && candidate.indexOf(origin + '/auth?') !== 0) continue;
18
+ if (bundle === 'com.apple.Safari') windows[w].currentTab = tabs[t];
19
+ else windows[w].activeTabIndex = t + 1;
20
+ windows[w].minimized = false;
21
+ windows[w].index = 1;
22
+ browser.activate();
23
+ return JSON.stringify({mode:'reused', reuseSupported:true});
24
+ }
25
+ }
26
+ return JSON.stringify({mode:'open', reuseSupported:true});
27
+ } catch (error) {
28
+ return JSON.stringify({mode:'failed', code:error.errorNumber || 0});
29
+ }
30
+ }`;
31
+
32
+ export async function openMacControlPanel(execFile, url, origin) {
33
+ const target = new URL(url), base = new URL(origin);
34
+ if (target.origin !== base.origin || base.protocol !== "http:" || base.hostname !== "127.0.0.1"
35
+ || target.pathname !== "/auth" || !target.searchParams.get("exchange")) throw new Error("Invalid control panel URL");
36
+ let stdout;
37
+ try { ({ stdout } = await execFile("/usr/bin/osascript", ["-l", "JavaScript", "-e", CONTROL_PANEL_BROWSER_SCRIPT, url, base.origin], { timeout:10_000 })); }
38
+ catch { throw new Error("无法连接默认浏览器,请重试打开 MineCodex。"); }
39
+ const result = JSON.parse(stdout);
40
+ if (result.mode === "failed") {
41
+ throw new Error(result.code === -1743
42
+ ? "无法定位 MineCodex 标签页。请在系统设置的自动化权限中允许控制默认浏览器后重试。"
43
+ : "无法定位默认浏览器中的 MineCodex 标签页,请重试。");
44
+ }
45
+ if (result.mode === "reused") return result;
46
+ await execFile("/usr/bin/open", [url]);
47
+ return { mode:"opened", reuseSupported:result.reuseSupported };
48
+ }
@@ -187,7 +187,9 @@ async function openCommand({ paths, platform, output, serviceProbe, fetchImpl })
187
187
  if (!exchangeResponse.ok) throw new Error(`MineCodex browser exchange failed: HTTP ${exchangeResponse.status}`);
188
188
  const { exchange } = await exchangeResponse.json();
189
189
  if (!exchange) throw new Error("MineCodex browser exchange did not return a short-lived value.");
190
- await platform.openBrowser(`${controlOrigin()}/auth?exchange=${encodeURIComponent(exchange)}`);
190
+ const url = `${controlOrigin()}/auth?exchange=${encodeURIComponent(exchange)}`;
191
+ if (platform.openControlPanel) await platform.openControlPanel(url, controlOrigin());
192
+ else await platform.openBrowser(url);
191
193
  };
192
194
  if (reporter) await reporter.step(2, 2, "Opening the control panel", openControlPanel);
193
195
  else await openControlPanel();
@@ -17,6 +17,7 @@ const CONTENT_TYPES = new Map([
17
17
  [".html", "text/html; charset=utf-8"],
18
18
  [".js", "text/javascript; charset=utf-8"],
19
19
  [".svg", "image/svg+xml"],
20
+ [".ttf", "font/ttf"],
20
21
  ]);
21
22
 
22
23
  function tokenMatches(candidate, expected) {
@@ -197,10 +198,10 @@ async function iconResponse(id, paths, response) {
197
198
  iconPath = path.join(paths.featuresRoot, id, "assets", "ui", "sparkles.svg");
198
199
  } else if (id === "notes") {
199
200
  iconPath = path.join(paths.featuresRoot, id, "assets", "icons", "book-open.svg");
200
- } else if (["images", "hide-upsell-banner", "session-status", "usage-meter"].includes(id)) {
201
+ } else if (FEATURE_IDS.includes(id)) {
201
202
  const manifest = JSON.parse(await readFile(path.join(paths.featuresRoot, id, "codex-feature.json"), "utf8"));
202
203
  const viewBox = manifest.icon?.viewBox ?? "0 0 24 24";
203
- const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">${manifest.icon.markup}</svg>`;
204
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round">${manifest.icon.markup}</svg>`;
204
205
  response.writeHead(200, { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=3600" });
205
206
  response.end(svg);
206
207
  return;
@@ -209,7 +210,9 @@ async function iconResponse(id, paths, response) {
209
210
  return;
210
211
  }
211
212
  response.writeHead(200, { "Content-Type": "image/svg+xml", "Cache-Control": "public, max-age=3600" });
212
- await pipeline(createReadStream(iconPath), response);
213
+ // 仅统一控制台图标,不改变插件在 Codex 内使用的原始资源。
214
+ const svg = (await readFile(iconPath, "utf8")).replace(/stroke-width="[^"]+"/g, 'stroke-width="2.1"');
215
+ response.end(svg);
213
216
  }
214
217
 
215
218
  async function staticResponse(url, paths, response) {
@@ -1,3 +1,4 @@
1
+ import { openMacControlPanel } from "./browser-control-panel.mjs";
1
2
  import { execFile as execFileCallback } from "node:child_process";
2
3
  import { mkdir, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
3
4
  import os from "node:os";
@@ -704,6 +705,10 @@ export class MacPlatformAdapter {
704
705
  await this.execFile("/usr/bin/open", [url]);
705
706
  }
706
707
 
708
+ async openControlPanel(url, origin) {
709
+ return openMacControlPanel(this.execFile, url, origin);
710
+ }
711
+
707
712
  async openNativeCodex({ cdpPort } = {}) {
708
713
  const installation = await this.codex();
709
714
  if (!Number.isInteger(cdpPort) || cdpPort <= 0 || cdpPort > 65_535) {
@@ -1,13 +1,13 @@
1
1
  # Codex Runtime Host
2
2
 
3
- 本机个人 Codex 功能的统一运行宿主。它是唯一的 Injector / Adapter,负责:
3
+ 本机个人 Codex 功能的统一运行宿主。它是统一 Injector 与生命周期宿主,负责:
4
4
 
5
5
  - 连接或启动支持 loopback CDP 的隔离 Codex 桌面实例;
6
6
  - 从同级项目的 `codex-feature.json` 注册 Sidebar 或 Conversation toolbar 入口;
7
7
  - 挂载 full-page、Pinned Summary,并把 Detail iframe Surface 优先注册为 Codex 原生右侧 Task Tab;
8
8
  - 把 Codex 当前 resolved Theme Tokens 同步给每个 Surface;
9
9
  - 执行经过 manifest allowlist、Surface 身份和 renderer token 校验的 Host actions;
10
- - 把所有易变的 Codex DOM / CDP 细节留在一个 Adapter 中。
10
+ - 维护共享 CDP、注入与可信通信;插件专有的 Codex 协议由各功能 Adapter 维护。
11
11
 
12
12
  各功能项目继续拥有自己的数据、HTTP API 与页面。宿主不读取业务数据库;单个
13
13
  功能服务失效不会阻止 Codex 或其他功能运行。
@@ -173,8 +173,7 @@ Host 会再次发送当前状态。功能页面可据此暂停隐藏状态下的
173
173
  | `Runtime.addBinding`、`DOM.setFileInputFiles`、注入脚本 | high-risk CDP | 必须使用隔离 profile 验收 |
174
174
  | `togglePinnedSummary`、`composer.addFiles` 字符串 | 探索证据 | 当前实现不把它们当契约 |
175
175
 
176
- 这是非官方 CDP / DOM Adapter,不是公开的 Codex Contribution API。Codex 更新后应只
177
- 在这个宿主中修复;各功能的数据和 Web UI不需要跟随重写。
176
+ 这是非官方 CDP / DOM Adapter,不是公开的 Codex Contribution API。Codex 更新后按协议所有者修复:共享注入和生命周期归宿主,专有原生组件与 API 归对应插件 Adapter;领域规则与 Web UI不应直接跟随外部协议重写。
178
177
 
179
178
 
180
179
  ## 共享弹窗与浮层
@@ -197,8 +196,14 @@ config.ui.surface(popover); // 只统一材质,保留功能的定位与内容
197
196
 
198
197
  page-script 可以用 manifest 的 `entry.resources` 声明本功能内的 CSS/图标资源,宿主读取后通过 `config.resources` 一次传入。JSON 作为数据、其他文件作为文本;越出功能目录的路径会被拒绝。它不改变 host action、服务启动或消息协议。
199
198
 
200
- Images 提示词面板沿用原生 Version history 的特殊结构;iframe 页面继续通过宿主接收主题。会话统计与云 Library 业务归所属插件,Runtime Host 只组合业务能力和外部连接。领域规则与架构层参见根 [ARCHITECTURE.md](../../ARCHITECTURE.md)。
199
+ Images 提示词面板沿用原生 File info 的附属信息面板结构;iframe 页面继续通过宿主接收主题。会话统计与云 Library 业务归所属插件,Runtime Host 只组合业务能力和外部连接。领域规则与架构层参见根 [ARCHITECTURE.md](../../ARCHITECTURE.md)。
201
200
 
202
201
  `entry.sourceFactory` 可声明本插件内的源码工厂,导出 `createPageScriptSource()` 并返回 Renderer 函数体字符串。工厂路径不能越出插件;纯领域函数由插件工厂序列化,宿主不维护规则副本。Images 使用这一接口共享图片提示词溯源规则。
203
202
 
204
203
  Model Slider 的模型规则、原生控制器适配和专有呈现位于插件 `src/domain` / `src/frontend`,安装源码由插件工厂组装。宿主仅提供配置、locale、共享 UI、图标与刷新调度,维护 ensure/dispose 接线;原生菜单开合和节点恢复由插件负责。
204
+
205
+ ## 项目控制台入口
206
+
207
+ Runtime Host 在账号菜单 Settings 下挂载 MineCodex 项目入口,不属于可开关的插件。`minecodex/open-control-panel` 是独立 builtin action,必须通过对应 renderer 的 binding token 验证;它不接受任意 URL 或 shell 参数,固定调用包内 CLI 的 `gui` 命令。组件的项目图标、外链箭头与菜单模板由共享 UI 提供。
208
+
209
+ CLI 继续通过一次性 browser exchange 打开控制台。macOS 默认 Safari 与 Chrome/Chromium、Edge、Brave、Vivaldi 支持查找现有控制台标签页并激活;没有匹配页才调用系统默认浏览器打开登录地址。自动化权限被拒绝时返回错误,不用重复开页掩盖失败。其他浏览器和 Windows 保留系统 URL 打开行为,目前不保证标签页复用。
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import { createCodexEnvironment } from "../../codex-adapter/src/renderer/environment.mjs";
2
3
  // 注入源生成:createInjectionSource 产出注入到 Codex Renderer 的自包含脚本字符串。
3
4
  // 该字符串经 CDP 注入后独立执行,因此 install 函数体必须自包含;
@@ -8,7 +9,11 @@ import { createUIInstallerSource } from "../../ui/src/install.mjs";
8
9
  import { randomBytes } from "node:crypto";
9
10
  import { CODEX_THEME_TOKENS, CODEX_THEME_TOKEN_ALIASES, MINECODEX_THEME_BINDINGS } from "../../ui/src/native/tokens.mjs";
10
11
 
11
- export const RUNTIME_VERSION = 107;
12
+ export const RUNTIME_VERSION = 108;
13
+ const PROJECT_ICON = Object.fromEntries([16, 20].map(size => [size,
14
+ readFileSync(new URL(`../../ui/assets/minecodex-menu-${size}.svg`, import.meta.url), "utf8"),
15
+ ]));
16
+ const EXTERNAL_ICON = readFileSync(new URL("../../ui/assets/arrow-up-right.svg", import.meta.url), "utf8");
12
17
  export const HOST_BINDING_NAME = "__codexPersonalHostAction";
13
18
  export const CSP_BOOTSTRAP_VERSION = 1;
14
19
  export const CSP_BOOTSTRAP_KEY = "__mineCodexCspBootstrapVersion";
@@ -204,6 +209,8 @@ export function createInjectionSource(features, {
204
209
  } : null,
205
210
  hostActions: new Set(feature.hostActions ?? []),
206
211
  }));
212
+ featureUI.accountMenuEntry?.({ label:"MineCodex", icon:config.projectIcon, externalIcon:config.externalIcon,
213
+ onOpen:() => requestPageHostAction("minecodex", "open-control-panel", {}) });
207
214
  const featureById = new Map(features.map((feature) => [feature.id, feature]));
208
215
  const sidebarFeatures = features.filter((feature) => feature.entry?.kind === "sidebar");
209
216
  const toolbarFeatures = features.filter((feature) => feature.entry?.kind === "conversation-toolbar");
@@ -2170,14 +2177,14 @@ export function createInjectionSource(features, {
2170
2177
  }
2171
2178
 
2172
2179
  function requestPageHostAction(featureId, action, payload) {
2173
- if (!featureById.get(featureId)?.hostActions.has(action)) return Promise.reject(new Error("Host action is not registered"));
2180
+ if (!(featureId === "minecodex" && action === "open-control-panel") && !featureById.get(featureId)?.hostActions.has(action)) return Promise.reject(new Error("Host action is not registered"));
2174
2181
  if (typeof globalThis[config.bindingName] !== "function") return Promise.reject(new Error("Host bridge is unavailable"));
2175
2182
  return new Promise((resolve, reject) => {
2176
2183
  const requestId = crypto.randomUUID();
2177
2184
  const timer = setTimeout(() => {
2178
2185
  pendingHostActions.delete(requestId);
2179
2186
  reject(new Error("Host request timed out"));
2180
- }, 10_000);
2187
+ }, featureId === "minecodex" ? 30_000 : 10_000);
2181
2188
  pendingHostActions.set(requestId, { kind: "page-script", resolve, reject, timer });
2182
2189
  try {
2183
2190
  globalThis[config.bindingName](JSON.stringify({ token: config.bindingToken, featureId, requestId, action, payload }));
@@ -2438,6 +2445,8 @@ export function createInjectionSource(features, {
2438
2445
  } : null,
2439
2446
  }));
2440
2447
  return `(${install.toString()})(${JSON.stringify({
2448
+ projectIcon: PROJECT_ICON,
2449
+ externalIcon: EXTERNAL_ICON,
2441
2450
  version: RUNTIME_VERSION,
2442
2451
  sessionId: runtimeSessionId,
2443
2452
  bindingName,
@@ -1,3 +1,6 @@
1
+ import { execFile as execFileCallback } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { fileURLToPath } from "node:url";
1
4
  import { LocalImageSource } from "../../../features/images/src/backend/local-image-source.mjs";
2
5
  import { cloudImagesAction } from "../../../features/images/src/backend/cloud-library.mjs";
3
6
  // CodexRuntime:Codex Renderer 生命周期与 host action 执行。
@@ -76,7 +79,9 @@ export class CodexRuntime {
76
79
  onStatusChange = null,
77
80
  sitesService = null,
78
81
  onManagedCodexPidChange = null,
82
+ openControlPanel = () => promisify(execFileCallback)(process.execPath, [fileURLToPath(new URL("../../cli/bin/mcx.mjs", import.meta.url)), "gui"], { timeout:20_000 }),
79
83
  }) {
84
+ this.openControlPanel = openControlPanel;
80
85
  this.features = features;
81
86
  this.sitesService = sitesService ?? (features.some(feature => feature.id === "sites") ? new SitesService() : null);
82
87
  this.localImageSource = features.some(feature => feature.id === "images") ? new LocalImageSource({dataDir:path.join(profileDir,"minecodex-image-gen")}) : null;
@@ -524,6 +529,11 @@ export class CodexRuntime {
524
529
  return;
525
530
  }
526
531
  if (request.token !== this.bindingTokens.get(targetId) || typeof request.requestId !== "string") return;
532
+ if (request.featureId === "minecodex" && request.action === "open-control-panel") {
533
+ try { await this.openControlPanel(); await this.resolveHostAction(client, request.requestId, {ok:true,result:{opened:true}}); }
534
+ catch (error) { await this.resolveHostAction(client, request.requestId, {ok:false,error:{code:"CONTROL_PANEL_FAILED",message:error.stderr?.trim() || error.message}}); }
535
+ return;
536
+ }
527
537
  const feature = this.features.find((candidate) => candidate.id === request.featureId);
528
538
  if (feature?.id === "session-status" && request.action === "refresh-session-status") {
529
539
  if (this.sessionStatusReader && this.clients.get(targetId) === client && !this.stopping) {
@@ -23,7 +23,7 @@
23
23
 
24
24
  当前page-script通过 `config.ui` 使用公共能力;现有API包括 `heading`、`footer`、`action`、`overlay`、`surface`。`surface(node, kind, options)` 显式区分原生材质;`page`、`mount`、`trapFocus`、`composerMenu`、`tooltipSurface`、`navigationRail` 维护实际复用的层级、焦点与定位。`entry.resources` 通过 `config.resources` 提供插件自身 CSS 与图标,通用视觉仍由组件库维护。
25
25
 
26
- 独立全页面且无专属二级导航的功能使用 `config.ui.page(node, { sidebar:"none" })`。移除页面前调用 `config.ui.releasePage(node)`;若用户正在主动选择原生导航,则传 `{ restoreNavigation:false }`,避免恢复动作覆盖用户选择。默认 `sidebar:"inherit"` 保留现有接入行为,不自动迁移其他功能。页面壳的产品规则与验收标准见根 DESIGN.md 的「全页面功能的页面壳选择」。
26
+ 独立全页面且无专属二级导航的功能使用 `config.ui.page(node, { sidebar:"none" })`。移除页面前调用 `config.ui.releasePage(node)`;若用户正在主动选择原生导航,则传 `{ restoreNavigation:false }`,避免恢复动作覆盖用户选择。`sidebar:"none"` 页面通过 `onNavigate` 接收原生路由提交后的退出通知;一级、二级和程序化导航共用该契约,悬浮展开与加载中的通知不关闭页面。Adapter 在通知前取消路由订阅并放弃恢复旧位置,功能回调负责移除内容与恢复自身状态。默认 `sidebar:"inherit"` 保留现有接入行为,不自动迁移其他功能。页面壳的产品规则与验收标准见根 DESIGN.md 的「全页面功能的页面壳选择」。
27
27
 
28
28
  Renderer安装函数经序列化注入,必须自包含。组件代码与Node/CDP隔离,宿主负责组装注入源码、生命周期和iframe主题桥;不为整理目录引入不必要的打包系统。
29
29
 
@@ -32,3 +32,7 @@ Renderer安装函数经序列化注入,必须自包含。组件代码与Node/C
32
32
  ## 验证边界
33
33
 
34
34
  组件验证关注可注入性、结构与回调、状态清理、挂载/焦点以及所选原生类型的实际行为,不镜像每条CSS写实现测试。布局、材质和原生状态变化按风险做隔离验收;只移动文件时验证导入、序列化、发布名单与已有功能回归。
35
+
36
+ ## 账号菜单接入
37
+
38
+ `accountMenuEntry({label, icon, externalIcon, onOpen})` 通过 native/account-menu Adapter 获取原生 Settings 模板与关闭句柄。组件只修改脱离 React 的副本,负责重建去重、相邻键盘导航与卸载;回调拥有打开动作,组件不读取控制台 token 或浏览器状态。通用位置与视觉规则见根 DESIGN.md。
@@ -0,0 +1,13 @@
1
+ <svg
2
+ xmlns="http://www.w3.org/2000/svg"
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="1.575"
9
+ stroke-linecap="round"
10
+ stroke-linejoin="round"
11
+ >
12
+ <path d="M7 7h10v10M7 17 17 7" />
13
+ </svg>
@@ -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.
@@ -0,0 +1,7 @@
1
+ <!-- Generated by scripts/build-brand-icons.mjs from assets/brand/minecodex-icon.svg. -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M6.23992 1.43098C7.46406 1.1031 8.54982 1.45396 9.38542 2.21516C10.5383 1.98366 11.9155 2.29793 12.8093 3.19172C13.6945 4.07741 14.0164 5.30994 13.7829 6.44953C14.5557 7.31992 14.8933 8.54971 14.569 9.76008C14.2447 10.9704 13.3375 11.8664 12.2331 12.2337C11.8657 13.3376 10.9704 14.2443 9.76042 14.5687C8.54999 14.893 7.32025 14.5555 6.44988 13.7825C5.3103 14.0161 4.07777 13.6943 3.19206 12.8089C2.30603 11.9228 1.98129 10.6886 2.2155 9.54817C1.44398 8.67777 1.10733 7.44883 1.43132 6.23957C1.7556 5.02965 2.66205 4.13253 3.76628 3.76496C4.13401 2.66129 5.0304 1.75519 6.23992 1.43098ZM8.84636 3.15461C8.21242 2.48417 7.42544 2.20118 6.51238 2.44563C5.58126 2.69512 4.90916 3.43143 4.69988 4.30989L4.62566 4.62434L4.3112 4.69953C3.43254 4.90862 2.69555 5.58058 2.44597 6.51203C2.19659 7.44329 2.49878 8.39365 3.15495 9.01399L3.38933 9.23567L3.29753 9.54524C3.03946 10.4106 3.25264 11.384 3.93425 12.0657C4.61596 12.7473 5.59015 12.9608 6.45574 12.7025L6.76531 12.6097L6.98699 12.845C7.6075 13.5014 8.55777 13.8045 9.48894 13.555C10.4198 13.3054 11.0903 12.5676 11.2995 11.6888L11.3747 11.3753L11.6891 11.3001C12.568 11.091 13.3058 10.4198 13.5553 9.4886C13.8048 8.55744 13.5017 7.60715 12.8454 6.98664L12.611 6.76496L12.7028 6.45539C12.961 5.59001 12.7483 4.61562 12.0671 3.93391C11.3946 3.26145 10.2567 3.03482 9.37761 3.29719L9.06804 3.38996L8.84636 3.15461Z" fill="currentColor"/>
4
+ <g transform="scale(0.6666666666666666)"><g id="pickaxe" transform="translate(6.24 6.24) scale(.48)" fill="currentColor">
5
+ <path d="M15.686 4.314 16.3 3.7a1 1 0 0 1 1.4 0l2.6 2.6a1 1 0 0 1 0 1.4l-.614.614a12.501 12.501 0 0 1 1.356 10.225 1 1 0 0 1-1.751-.119 22 22 0 0 0-3.393-6.319l-.198.199a1 1 0 0 1-1.4 0l-.219.169L6.619 20.38a1 1 0 1 1-3-3l7.912-7.912.169.232a1 1 0 0 1 0-1.4l.198-.197A22 22 0 0 0 5.58 4.71a1 1 0 0 1-.119-1.752 12.5 12.5 0 0 1 10.225 1.356Z"/>
6
+ </g></g>
7
+ </svg>
@@ -0,0 +1,7 @@
1
+ <!-- Generated by scripts/build-brand-icons.mjs from assets/brand/minecodex-icon.svg. -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M7.79787 1.78095C9.33018 1.37054 10.6892 1.80862 11.7354 2.76044C13.1781 2.47254 14.8994 2.86492 16.0176 3.9831C17.1255 5.09146 17.5294 6.63391 17.2383 8.06025C18.2036 9.15 18.6252 10.6879 18.2197 12.2018C17.8139 13.7157 16.6796 14.8375 15.2988 15.2985C14.838 16.6794 13.7171 17.8134 12.2031 18.2194C10.689 18.6252 9.15041 18.2035 8.06057 17.238C6.63426 17.5291 5.09181 17.1254 3.98342 16.0173C2.87514 14.909 2.4696 13.3659 2.76076 11.9392C1.7958 10.8492 1.37577 9.31111 1.78127 7.79755C2.1869 6.28392 3.31944 5.16128 4.70022 4.69989C5.1615 3.31914 6.28421 2.18664 7.79787 1.78095ZM11.0518 3.94892C10.2613 3.11295 9.28034 2.76056 8.1426 3.06513C6.98179 3.37617 6.14459 4.29507 5.88381 5.39032L5.78908 5.78778L5.39065 5.88251C4.29532 6.14315 3.37656 6.98118 3.06545 8.14228C2.75466 9.30301 3.1314 10.4872 3.94924 11.2604L4.24709 11.5417L4.1299 11.9343C3.80831 13.013 4.07409 14.2271 4.92385 15.0768C5.77357 15.9262 6.98753 16.1917 8.06643 15.8698L8.459 15.7526L8.74025 16.0505C9.51379 16.8686 10.6978 17.2453 11.8584 16.9343C13.0186 16.6232 13.8554 15.7053 14.1162 14.6101L14.211 14.2116L14.6094 14.1169C15.705 13.8562 16.6245 13.0189 16.9356 11.8581C17.2464 10.6976 16.8687 9.51342 16.0508 8.73993L15.753 8.45868L15.8701 8.0661C16.192 6.98728 15.9265 5.77328 15.0772 4.92353C14.2391 4.08548 12.8208 3.80245 11.7246 4.12958L11.333 4.24677L11.0518 3.94892Z" fill="currentColor"/>
4
+ <g transform="scale(0.8333333333333334)"><g id="pickaxe" transform="translate(6.24 6.24) scale(.48)" fill="currentColor">
5
+ <path d="M15.686 4.314 16.3 3.7a1 1 0 0 1 1.4 0l2.6 2.6a1 1 0 0 1 0 1.4l-.614.614a12.501 12.501 0 0 1 1.356 10.225 1 1 0 0 1-1.751-.119 22 22 0 0 0-3.393-6.319l-.198.199a1 1 0 0 1-1.4 0l-.219.169L6.619 20.38a1 1 0 1 1-3-3l7.912-7.912.169.232a1 1 0 0 1 0-1.4l.198-.197A22 22 0 0 0 5.58 4.71a1 1 0 0 1-.119-1.752 12.5 12.5 0 0 1 10.225 1.356Z"/>
6
+ </g></g>
7
+ </svg>
@@ -0,0 +1,88 @@
1
+ export function installAccountMenuEntry(signal, ui, native) {
2
+ function accountMenuEntry({ label, icon, externalIcon, onOpen }) {
3
+ const marker = "data-mcx-account-menu-entry";
4
+ let node, anchor, entryBindings, opening = false;
5
+ const bindings = new AbortController();
6
+ function svg(source, template = null) {
7
+ if (typeof source !== "string") source = source[Number(template.getAttribute("width"))];
8
+ const result = new DOMParser().parseFromString(source, "image/svg+xml").documentElement;
9
+ result.setAttribute("aria-hidden", "true");
10
+ result.removeAttribute("role");
11
+ result.removeAttribute("aria-labelledby");
12
+ result.querySelectorAll("title, desc").forEach(n => n.remove());
13
+ result.setAttribute("class", template?.getAttribute("class") ?? "icon-xs shrink-0");
14
+ if (template) {
15
+ // 原生 16/20px 图标由外层切换,品牌资源的 24px 不能覆盖它。
16
+ for (const name of ["width", "height", "style"]) {
17
+ const value = template.getAttribute(name);
18
+ if (value == null) result.removeAttribute(name);
19
+ else result.setAttribute(name, value);
20
+ }
21
+ }
22
+ return result;
23
+ }
24
+ async function activate(event) {
25
+ event?.preventDefault();
26
+ event?.stopPropagation();
27
+ if (opening) return;
28
+ opening = true;
29
+ const close = anchor.close;
30
+ try { await onOpen(); close(); }
31
+ catch (error) { console.warn("[MineCodex] Control panel could not open", error); window.alert(error.message); }
32
+ finally { opening = false; }
33
+ }
34
+ function sync() {
35
+ if (signal.aborted) return;
36
+ const found = native.settings();
37
+ if (!found) { entryBindings?.abort(); node?.remove(); node = null; anchor = null; return; }
38
+ if (node?.isConnected && anchor.item === found.item) return;
39
+ entryBindings?.abort();
40
+ entryBindings = new AbortController();
41
+ node?.remove();
42
+ anchor = found;
43
+ // 只修改脱离 React 的副本,保留原生 Settings 节点及事件。
44
+ node = found.item.cloneNode(true);
45
+ for (const element of [node, ...node.querySelectorAll("*")]) {
46
+ for (const attr of [...element.attributes]) {
47
+ if (attr.name === "id" || attr.name === "title" || attr.name.startsWith("aria-")
48
+ || attr.name.startsWith("data-")) element.removeAttribute(attr.name);
49
+ }
50
+ }
51
+ node.setAttribute(marker, "");
52
+ node.setAttribute("role", "menuitem");
53
+ node.tabIndex = -1;
54
+ const [leading, title, trailing] = node.querySelector("div").children;
55
+ // 保留紧凑/普通图标的原生容器及颜色、透明度和状态规则。
56
+ for (const template of leading.querySelectorAll("svg")) template.replaceWith(svg(icon, template));
57
+ title.textContent = label;
58
+ // 右侧提示沿用原生 Help 箭头的 tertiary 色,不套左侧图标的透明度。
59
+ trailing.style.color = "inherit";
60
+ const external = svg(externalIcon);
61
+ external.setAttribute("class", "icon-xs shrink-0 text-tertiary");
62
+ trailing.replaceChildren(external);
63
+ node.addEventListener("click", activate, { signal:entryBindings.signal });
64
+ node.addEventListener("pointermove", () => node.focus({ preventScroll:true }), { signal:entryBindings.signal });
65
+ found.item.after(node);
66
+ }
67
+ function keyboard(event) {
68
+ if (!anchor || !node?.isConnected || !anchor.menu.contains(event.target)) return;
69
+ const current = event.target.closest('[role="menuitem"]');
70
+ let next;
71
+ if (current === anchor.item && event.key === "ArrowDown") next = node;
72
+ else if (current === node && event.key === "ArrowUp") next = anchor.item;
73
+ else if (current === node && event.key === "ArrowDown") {
74
+ const items = [...anchor.menu.querySelectorAll('[role="menuitem"]')];
75
+ next = items[items.indexOf(node) + 1] ?? items[0];
76
+ } else if (current === node && ["Enter", " "].includes(event.key)) { void activate(event); return; }
77
+ if (next) { event.preventDefault(); event.stopImmediatePropagation(); next.focus({ preventScroll:true }); }
78
+ }
79
+ document.addEventListener("keydown", keyboard, { capture:true, signal:bindings.signal });
80
+ const observer = new MutationObserver(sync);
81
+ observer.observe(document.documentElement, { childList:true, subtree:true });
82
+ const dispose = () => { observer.disconnect(); bindings.abort(); entryBindings?.abort(); node?.remove(); node = null; anchor = null; };
83
+ signal.addEventListener("abort", dispose, { once:true });
84
+ sync();
85
+ return { sync, dispose };
86
+ }
87
+ return { accountMenuEntry };
88
+ }
@@ -2,8 +2,8 @@ export function installPage(signal, ui, native) {
2
2
  const pages = new Map();
3
3
  signal.addEventListener("abort", () => { for (const lease of pages.values()) lease.release(); pages.clear(); }, { once:true });
4
4
  return {
5
- page(node, { transparent = false, sidebar = "inherit" } = {}) {
6
- if (sidebar === "none" && !pages.has(node)) pages.set(node, native.standalone());
5
+ page(node, { transparent = false, sidebar = "inherit", onNavigate } = {}) {
6
+ if (sidebar === "none" && !pages.has(node)) pages.set(node, native.standalone({ onNavigate }));
7
7
  ui.surface(node, "page", { variant:transparent ? "transparent" : "opaque" });
8
8
  return ui.layer(node, "page");
9
9
  },
@@ -4,6 +4,7 @@ export function installLayers(signal) {
4
4
  style.setAttribute("data-minecodex-ui-styles", "layers");
5
5
  style.textContent = `
6
6
  [data-mcx-ui-layer=page] { position:fixed; z-index:20; }
7
+ [data-mcx-ui-layer=notification] { position:fixed; z-index:30; }
7
8
  [data-mcx-ui-layer=floating], [data-mcx-ui-layer=modal] { position:fixed; z-index:50; }
8
9
  `;
9
10
  document.head.append(style);
@@ -1,3 +1,5 @@
1
+ import { createNativeAccountMenuAdapter } from "./native/account-menu.mjs";
2
+ import { installAccountMenuEntry } from "./components/account-menu.mjs";
1
3
  import { createNativeThemeAdapter } from "./native/theme.mjs";
2
4
  import { createNativePageAdapter } from "./native/page.mjs";
3
5
  import { createNativeRailAdapter } from "./native/navigation-rail.mjs";
@@ -19,6 +21,7 @@ export function createUIInstallerSource() {
19
21
  Object.assign(ui, (${installPage.toString()})(signal, ui, (${createNativePageAdapter.toString()})(context.codex)));
20
22
  Object.assign(ui, (${installNavigationRail.toString()})(signal, ui, (${createNativeRailAdapter.toString()})(context.codex)));
21
23
  Object.assign(ui, (${installFeatureTooltips.toString()})(signal));
24
+ Object.assign(ui, (${installAccountMenuEntry.toString()})(signal, ui, (${createNativeAccountMenuAdapter.toString()})(context.codex)));
22
25
  return ui;
23
26
  }`;
24
27
  }
@@ -0,0 +1,21 @@
1
+ export function createNativeAccountMenuAdapter(codex) {
2
+ function settings() {
3
+ for (const menu of document.querySelectorAll('[role="menu"]')) {
4
+ if (!menu.checkVisibility({ visibilityProperty:true })) continue;
5
+ for (const item of menu.querySelectorAll('[role="menuitem"]')) {
6
+ const profile = codex.props(item, p => typeof p.onOpenSettings === "function" && "settingsShortcut" in p);
7
+ if (!profile || !codex.props(item, p => p.onClick === profile.onOpenSettings)) continue;
8
+ const content = item.querySelector('[data-menu-row-content]');
9
+ const [leading, label, shortcut] = content?.children ?? [];
10
+ if (!leading?.querySelector('svg') || !label || !shortcut) continue;
11
+ const owner = codex.props(item, p => typeof p.onClose === "function" && "open" in p);
12
+ if (!owner) continue;
13
+ codex.report("account-menu", true);
14
+ return { menu, item, close:owner.onClose };
15
+ }
16
+ }
17
+ codex.report("account-menu", false, "profile-menu-unavailable");
18
+ return null;
19
+ }
20
+ return { settings };
21
+ }