minecodex 2.0.5 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +15 -0
- package/README.md +5 -1
- package/apps/dashboard/DESIGN.md +53 -31
- package/apps/dashboard/app.js +55 -39
- package/apps/dashboard/assets/OFL.txt +93 -0
- package/apps/dashboard/assets/info.svg +15 -0
- package/apps/dashboard/assets/moon.svg +13 -0
- package/apps/dashboard/assets/refresh-cw.svg +16 -0
- package/apps/dashboard/assets/silkscreen.ttf +0 -0
- package/apps/dashboard/assets/sun.svg +21 -0
- package/apps/dashboard/assets/terrain.js +89 -0
- package/apps/dashboard/assets/triangle-alert.svg +15 -0
- package/apps/dashboard/favicon.svg +12 -17
- package/apps/dashboard/index.html +24 -18
- package/apps/dashboard/styles.css +147 -181
- package/assets/brand/README.md +19 -0
- package/assets/brand/minecodex-icon-dark.svg +9 -0
- package/assets/brand/minecodex-icon-light.svg +9 -0
- package/assets/brand/minecodex-icon.svg +8 -0
- package/assets/brand/references/lucide-LICENSE.txt +43 -0
- package/assets/readme/hero.svg +93 -0
- package/features/hide-upsell-banner/README.md +7 -5
- package/features/hide-upsell-banner/codex-feature.json +2 -2
- package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +30 -12
- package/features/images/DESIGN.md +13 -2
- package/features/images/README.md +4 -3
- package/features/images/codex-feature.json +2 -2
- package/features/images/src/adapters/codex/native-image-modules.mjs +211 -0
- package/features/images/src/adapters/codex/native-image-source.mjs +70 -177
- package/features/images/src/frontend/cloud-image-actions.mjs +8 -6
- package/features/images/src/frontend/local-image-source.mjs +2 -1
- package/features/images/src/library-prompt-page.mjs +6 -1
- package/features/images/src/library-prompt-source.mjs +3 -5
- package/features/model-slider/codex-feature.json +2 -2
- package/features/notes/DESIGN.md +1 -1
- package/features/notes/codex-feature.json +2 -2
- package/features/notes/src/frontend/entry.mjs +1 -1
- package/features/session-status/codex-feature.json +2 -2
- package/features/sidebar-focus/DESIGN.md +4 -4
- package/features/sidebar-focus/codex-feature.json +3 -2
- package/features/sidebar-focus/src/adapters/codex/renderer.mjs +1 -1
- package/features/sidebar-focus/src/sidebar-focus.mjs +9 -7
- package/features/sites/DESIGN.md +25 -3
- package/features/sites/README.md +6 -2
- package/features/sites/codex-feature.json +3 -2
- package/features/sites/src/adapters/codex/renderer.mjs +25 -4
- package/features/sites/src/backend/project-sites.mjs +18 -6
- package/features/sites/src/backend/sites-service.mjs +22 -10
- package/features/sites/src/domain/discovery-candidates.mjs +53 -0
- package/features/sites/src/frontend/entry.mjs +76 -38
- package/features/sites/src/frontend/styles.css +3 -3
- package/features/unread-inbox/DESIGN.md +24 -25
- package/features/unread-inbox/codex-feature.json +3 -2
- package/features/unread-inbox/src/adapters/codex/modules.mjs +52 -0
- package/features/unread-inbox/src/adapters/codex/renderer.mjs +90 -0
- package/features/unread-inbox/src/domain/notifications.mjs +21 -0
- package/features/unread-inbox/src/injection-source.mjs +5 -2
- package/features/unread-inbox/src/unread-inbox.mjs +212 -297
- package/features/usage-meter/codex-feature.json +2 -2
- package/package.json +11 -3
- package/packages/cli/native/MineCodex.app/Contents/Info.plist +4 -2
- package/packages/cli/native/MineCodex.app/Contents/MacOS/MineCodexServiceManager +0 -0
- package/packages/cli/native/MineCodex.app/Contents/Resources/MineCodex.icns +0 -0
- package/packages/cli/native/MineCodex.app/Contents/_CodeSignature/CodeResources +11 -0
- package/packages/cli/native/README.md +31 -0
- package/packages/cli/src/browser-control-panel.mjs +48 -0
- package/packages/cli/src/commands.mjs +3 -1
- package/packages/cli/src/control-server.mjs +6 -3
- package/packages/cli/src/platform.mjs +5 -0
- package/packages/runtime-host/README.md +10 -5
- package/packages/runtime-host/src/codex-injection.mjs +10 -3
- package/packages/runtime-host/src/codex-runtime.mjs +10 -0
- package/packages/ui/DESIGN.md +5 -1
- package/packages/ui/assets/arrow-up-right.svg +14 -0
- package/packages/ui/assets/lucide-LICENSE.txt +43 -0
- package/packages/ui/src/components/account-menu.mjs +74 -0
- package/packages/ui/src/components/page.mjs +2 -2
- package/packages/ui/src/foundation/layers.mjs +1 -0
- package/packages/ui/src/install.mjs +3 -0
- package/packages/ui/src/native/account-menu.mjs +21 -0
- package/packages/ui/src/native/page.mjs +17 -2
- 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.
|
|
3
|
+
"version": "2.0.6",
|
|
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.
|
|
22
|
+
<string>1.0.1</string>
|
|
21
23
|
<key>CFBundleVersion</key>
|
|
22
|
-
<string>
|
|
24
|
+
<string>2</string>
|
|
23
25
|
<key>LSBackgroundOnly</key>
|
|
24
26
|
<true/>
|
|
25
27
|
<key>LSMinimumSystemVersion</key>
|
|
Binary file
|
|
@@ -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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
-
|
|
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 提示词面板沿用原生
|
|
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,9 @@ 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 =
|
|
12
|
+
export const RUNTIME_VERSION = 108;
|
|
13
|
+
const PROJECT_ICON = readFileSync(new URL("../../../assets/brand/minecodex-icon.svg", import.meta.url), "utf8");
|
|
14
|
+
const EXTERNAL_ICON = readFileSync(new URL("../../ui/assets/arrow-up-right.svg", import.meta.url), "utf8");
|
|
12
15
|
export const HOST_BINDING_NAME = "__codexPersonalHostAction";
|
|
13
16
|
export const CSP_BOOTSTRAP_VERSION = 1;
|
|
14
17
|
export const CSP_BOOTSTRAP_KEY = "__mineCodexCspBootstrapVersion";
|
|
@@ -204,6 +207,8 @@ export function createInjectionSource(features, {
|
|
|
204
207
|
} : null,
|
|
205
208
|
hostActions: new Set(feature.hostActions ?? []),
|
|
206
209
|
}));
|
|
210
|
+
featureUI.accountMenuEntry?.({ label:"MineCodex", icon:config.projectIcon, externalIcon:config.externalIcon,
|
|
211
|
+
onOpen:() => requestPageHostAction("minecodex", "open-control-panel", {}) });
|
|
207
212
|
const featureById = new Map(features.map((feature) => [feature.id, feature]));
|
|
208
213
|
const sidebarFeatures = features.filter((feature) => feature.entry?.kind === "sidebar");
|
|
209
214
|
const toolbarFeatures = features.filter((feature) => feature.entry?.kind === "conversation-toolbar");
|
|
@@ -2170,14 +2175,14 @@ export function createInjectionSource(features, {
|
|
|
2170
2175
|
}
|
|
2171
2176
|
|
|
2172
2177
|
function requestPageHostAction(featureId, action, payload) {
|
|
2173
|
-
if (!featureById.get(featureId)?.hostActions.has(action)) return Promise.reject(new Error("Host action is not registered"));
|
|
2178
|
+
if (!(featureId === "minecodex" && action === "open-control-panel") && !featureById.get(featureId)?.hostActions.has(action)) return Promise.reject(new Error("Host action is not registered"));
|
|
2174
2179
|
if (typeof globalThis[config.bindingName] !== "function") return Promise.reject(new Error("Host bridge is unavailable"));
|
|
2175
2180
|
return new Promise((resolve, reject) => {
|
|
2176
2181
|
const requestId = crypto.randomUUID();
|
|
2177
2182
|
const timer = setTimeout(() => {
|
|
2178
2183
|
pendingHostActions.delete(requestId);
|
|
2179
2184
|
reject(new Error("Host request timed out"));
|
|
2180
|
-
}, 10_000);
|
|
2185
|
+
}, featureId === "minecodex" ? 30_000 : 10_000);
|
|
2181
2186
|
pendingHostActions.set(requestId, { kind: "page-script", resolve, reject, timer });
|
|
2182
2187
|
try {
|
|
2183
2188
|
globalThis[config.bindingName](JSON.stringify({ token: config.bindingToken, featureId, requestId, action, payload }));
|
|
@@ -2438,6 +2443,8 @@ export function createInjectionSource(features, {
|
|
|
2438
2443
|
} : null,
|
|
2439
2444
|
}));
|
|
2440
2445
|
return `(${install.toString()})(${JSON.stringify({
|
|
2446
|
+
projectIcon: PROJECT_ICON,
|
|
2447
|
+
externalIcon: EXTERNAL_ICON,
|
|
2441
2448
|
version: RUNTIME_VERSION,
|
|
2442
2449
|
sessionId: runtimeSessionId,
|
|
2443
2450
|
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) {
|
package/packages/ui/DESIGN.md
CHANGED
|
@@ -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 }
|
|
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,14 @@
|
|
|
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="2"
|
|
9
|
+
stroke-linecap="round"
|
|
10
|
+
stroke-linejoin="round"
|
|
11
|
+
>
|
|
12
|
+
<path d="M7 7h10v10" />
|
|
13
|
+
<path d="M7 17 17 7" />
|
|
14
|
+
</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,74 @@
|
|
|
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
|
+
const result = new DOMParser().parseFromString(source, "image/svg+xml").documentElement;
|
|
8
|
+
result.setAttribute("aria-hidden", "true");
|
|
9
|
+
result.removeAttribute("role");
|
|
10
|
+
result.removeAttribute("aria-labelledby");
|
|
11
|
+
result.querySelectorAll("title, desc").forEach(n => n.remove());
|
|
12
|
+
result.setAttribute("class", template?.getAttribute("class") ?? "icon-xs shrink-0");
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
15
|
+
async function activate(event) {
|
|
16
|
+
event?.preventDefault();
|
|
17
|
+
event?.stopPropagation();
|
|
18
|
+
if (opening) return;
|
|
19
|
+
opening = true;
|
|
20
|
+
const close = anchor.close;
|
|
21
|
+
try { await onOpen(); close(); }
|
|
22
|
+
catch (error) { console.warn("[MineCodex] Control panel could not open", error); window.alert(error.message); }
|
|
23
|
+
finally { opening = false; }
|
|
24
|
+
}
|
|
25
|
+
function sync() {
|
|
26
|
+
if (signal.aborted) return;
|
|
27
|
+
const found = native.settings();
|
|
28
|
+
if (!found) { entryBindings?.abort(); node?.remove(); node = null; anchor = null; return; }
|
|
29
|
+
if (node?.isConnected && anchor.item === found.item) return;
|
|
30
|
+
entryBindings?.abort();
|
|
31
|
+
entryBindings = new AbortController();
|
|
32
|
+
node?.remove();
|
|
33
|
+
anchor = found;
|
|
34
|
+
// 只修改脱离 React 的副本,保留原生 Settings 节点及事件。
|
|
35
|
+
node = found.item.cloneNode(true);
|
|
36
|
+
for (const element of [node, ...node.querySelectorAll("*")]) {
|
|
37
|
+
for (const attr of [...element.attributes]) {
|
|
38
|
+
if (attr.name === "id" || attr.name === "title" || attr.name.startsWith("aria-")
|
|
39
|
+
|| attr.name.startsWith("data-")) element.removeAttribute(attr.name);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
node.setAttribute(marker, "");
|
|
43
|
+
node.setAttribute("role", "menuitem");
|
|
44
|
+
node.tabIndex = -1;
|
|
45
|
+
const [leading, title, trailing] = node.querySelector("div").children;
|
|
46
|
+
leading.replaceChildren(svg(icon, leading.querySelector("svg")));
|
|
47
|
+
title.textContent = label;
|
|
48
|
+
trailing.replaceChildren(svg(externalIcon));
|
|
49
|
+
node.addEventListener("click", activate, { signal:entryBindings.signal });
|
|
50
|
+
node.addEventListener("pointermove", () => node.focus({ preventScroll:true }), { signal:entryBindings.signal });
|
|
51
|
+
found.item.after(node);
|
|
52
|
+
}
|
|
53
|
+
function keyboard(event) {
|
|
54
|
+
if (!anchor || !node?.isConnected || !anchor.menu.contains(event.target)) return;
|
|
55
|
+
const current = event.target.closest('[role="menuitem"]');
|
|
56
|
+
let next;
|
|
57
|
+
if (current === anchor.item && event.key === "ArrowDown") next = node;
|
|
58
|
+
else if (current === node && event.key === "ArrowUp") next = anchor.item;
|
|
59
|
+
else if (current === node && event.key === "ArrowDown") {
|
|
60
|
+
const items = [...anchor.menu.querySelectorAll('[role="menuitem"]')];
|
|
61
|
+
next = items[items.indexOf(node) + 1] ?? items[0];
|
|
62
|
+
} else if (current === node && ["Enter", " "].includes(event.key)) { void activate(event); return; }
|
|
63
|
+
if (next) { event.preventDefault(); event.stopImmediatePropagation(); next.focus({ preventScroll:true }); }
|
|
64
|
+
}
|
|
65
|
+
document.addEventListener("keydown", keyboard, { capture:true, signal:bindings.signal });
|
|
66
|
+
const observer = new MutationObserver(sync);
|
|
67
|
+
observer.observe(document.documentElement, { childList:true, subtree:true });
|
|
68
|
+
const dispose = () => { observer.disconnect(); bindings.abort(); entryBindings?.abort(); node?.remove(); node = null; anchor = null; };
|
|
69
|
+
signal.addEventListener("abort", dispose, { once:true });
|
|
70
|
+
sync();
|
|
71
|
+
return { sync, dispose };
|
|
72
|
+
}
|
|
73
|
+
return { accountMenuEntry };
|
|
74
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,23 +1,38 @@
|
|
|
1
1
|
// 原生页面壳由路由决定;借用 Projects 的无上下文侧栏壳,不改侧栏展开偏好。
|
|
2
2
|
export function createNativePageAdapter(codex) {
|
|
3
|
-
function standalone() {
|
|
3
|
+
function standalone({ onNavigate } = {}) {
|
|
4
4
|
const rail = codex.rail();
|
|
5
5
|
const scope = codex.props(rail, props => Boolean((props.router ?? props.value?.router)?.navigate));
|
|
6
6
|
const router = scope?.router ?? scope?.value?.router;
|
|
7
7
|
const origin = router?.state?.location;
|
|
8
8
|
if (!origin) throw new Error("The native page navigation was not found");
|
|
9
|
+
if (onNavigate && typeof router.subscribe !== "function") throw new Error("The native page navigation subscription was not found");
|
|
9
10
|
const target = "/projects";
|
|
10
11
|
const changed = origin.pathname !== target;
|
|
11
12
|
if (changed) void router.navigate(target, { replace:true, state:origin.state });
|
|
12
13
|
let released = false;
|
|
13
|
-
|
|
14
|
+
let unsubscribe = null;
|
|
15
|
+
let shell = router.state.location.pathname === target ? router.state.location : null;
|
|
16
|
+
const lease = {
|
|
14
17
|
release({ restoreNavigation=true } = {}) {
|
|
15
18
|
if (released) return;
|
|
16
19
|
released = true;
|
|
20
|
+
unsubscribe?.();
|
|
17
21
|
if (changed && restoreNavigation && router.state.location.pathname === target)
|
|
18
22
|
void router.navigate(origin, { replace:true, state:origin.state });
|
|
19
23
|
},
|
|
20
24
|
};
|
|
25
|
+
if (onNavigate) unsubscribe = router.subscribe(state => {
|
|
26
|
+
if (released || (state.navigation?.state && state.navigation.state !== "idle")) return;
|
|
27
|
+
const location = state.location;
|
|
28
|
+
// 等待借用页面壳的首次提交;hover 和 loader 通知不代表用户离开。
|
|
29
|
+
if (!shell && location === origin) return;
|
|
30
|
+
if (!shell && location.pathname === target) { shell = location; return; }
|
|
31
|
+
if (location === shell) return;
|
|
32
|
+
lease.release({ restoreNavigation:false });
|
|
33
|
+
onNavigate();
|
|
34
|
+
});
|
|
35
|
+
return lease;
|
|
21
36
|
}
|
|
22
37
|
return { standalone };
|
|
23
38
|
}
|
|
@@ -39,6 +39,7 @@ export const CODEX_THEME_TOKENS = Object.freeze([
|
|
|
39
39
|
"--color-background-elevated-primary-opaque",
|
|
40
40
|
"--color-background-primary-ghost-hover",
|
|
41
41
|
"--color-chart-blue",
|
|
42
|
+
"--color-chart-green",
|
|
42
43
|
"--color-chart-purple",
|
|
43
44
|
"--color-background-panel",
|
|
44
45
|
"--color-background-control",
|