minecodex 2.0.14 → 2.0.16

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 (44) hide show
  1. package/README.md +9 -0
  2. package/apps/dashboard/app.js +8 -5
  3. package/apps/dashboard/assets/data-transfer.js +2 -1
  4. package/apps/dashboard/transport.js +18 -0
  5. package/assets/brand/minecodex-icon-selected.svg +4 -0
  6. package/features/markdown-reading/src/adapters/codex/rich-text.mjs +12 -5
  7. package/features/markdown-reading/src/adapters/codex/table-actions.mjs +13 -2
  8. package/features/markdown-reading/src/domain/footnotes.mjs +4 -2
  9. package/features/markdown-reading/src/injection-source.mjs +2 -0
  10. package/features/markdown-reading/src/reading.css +3 -3
  11. package/features/notes/assets/icons/prompt-entry-selected.svg +1 -0
  12. package/features/notes/assets/icons/prompt-entry.svg +16 -0
  13. package/features/notes/codex-feature.json +3 -2
  14. package/features/notes/src/adapters/codex/insertion.mjs +92 -0
  15. package/features/notes/src/adapters/codex/requests.mjs +7 -0
  16. package/features/notes/src/frontend/entry.mjs +39 -137
  17. package/features/notes/src/frontend/icons.json +0 -2
  18. package/features/notes/src/injection-source.mjs +5 -0
  19. package/package.json +18 -5
  20. package/packages/cli/src/commands.mjs +10 -0
  21. package/packages/codex-adapter/src/renderer/environment.mjs +30 -1
  22. package/packages/plugin/assets/app.js +116 -0
  23. package/packages/plugin/assets/prompts.js +437 -0
  24. package/packages/plugin/plugin.json +16 -0
  25. package/packages/plugin/src/app.mjs +17 -0
  26. package/packages/plugin/src/control-client.mjs +41 -0
  27. package/packages/plugin/src/install.mjs +26 -0
  28. package/packages/plugin/src/navigation-icons.mjs +8 -0
  29. package/packages/plugin/src/prompts-app.mjs +45 -0
  30. package/packages/plugin/src/prompts-client.mjs +16 -0
  31. package/packages/plugin/src/server.mjs +74 -0
  32. package/packages/plugin/src/ui-resource.mjs +22 -0
  33. package/packages/runtime-host/src/codex-injection.mjs +5 -12
  34. package/packages/runtime-host/src/codex-runtime.mjs +3 -16
  35. package/packages/ui/src/install.mjs +4 -3
  36. package/packages/ui/src/native/mcp-app.mjs +32 -0
  37. package/packages/ui/src/native/mcp-navigation.mjs +52 -0
  38. package/packages/ui/src/native/mcp-theme.css +4 -0
  39. package/packages/ui/src/native/mcp-theme.mjs +20 -0
  40. package/packages/ui/assets/arrow-up-right.svg +0 -13
  41. package/packages/ui/assets/minecodex-menu-16.svg +0 -7
  42. package/packages/ui/assets/minecodex-menu-20.svg +0 -7
  43. package/packages/ui/src/components/account-menu.mjs +0 -88
  44. package/packages/ui/src/native/account-menu.mjs +0 -21
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "minecodex",
3
+ "version": "0.1.0",
4
+ "description": "Manage local MineCodex enhancements without changing their runtime.",
5
+ "author": { "name": "Vontean" },
6
+ "mcpServers": "./.mcp.json",
7
+ "interface": {
8
+ "displayName": "MineCodex",
9
+ "shortDescription": "Manage your local Codex enhancements",
10
+ "developerName": "Vontean",
11
+ "category": "Developer Tools",
12
+ "capabilities": ["Interactive", "Read", "Write"],
13
+ "logo": "./assets/icon.svg",
14
+ "composerIcon": "./assets/icon.svg"
15
+ }
16
+ }
@@ -0,0 +1,17 @@
1
+ import { App } from "@modelcontextprotocol/ext-apps";
2
+
3
+ const app = new App({ name: "MineCodex", version: "0.1.0" });
4
+ app.ontoolresult = () => {};
5
+ await app.connect();
6
+ window.minecodexMcpApp = app;
7
+ function syncHost(context) {
8
+ // 宿主事件是增量更新;尺寸变化不能覆盖已同步的主题。
9
+ if (!Object.hasOwn(context ?? {}, "theme")) return;
10
+ window.minecodexHostTheme = context?.theme;
11
+ window.dispatchEvent(new Event("minecodex-host-theme"));
12
+ }
13
+ app.addEventListener("hostcontextchanged", syncHost);
14
+ syncHost(app.getHostContext());
15
+ await import("../../../apps/dashboard/app.js");
16
+ await import("../../../apps/dashboard/assets/data-transfer.js");
17
+ await import("../../../apps/dashboard/assets/terrain.js");
@@ -0,0 +1,41 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { FEATURE_IDS } from "../../cli/src/config.mjs";
3
+
4
+ const READ_ROUTES = new Set(["/api/status", "/api/features", "/api/update", "/api/data", "/api/data/export"]);
5
+ export function assertControlRequest({ route, method = "GET", body }) {
6
+ if (method === "GET" && READ_ROUTES.has(route) && body === undefined) return;
7
+ if (method === "PATCH" && route.startsWith("/api/features/") && FEATURE_IDS.includes(route.slice(14))
8
+ && body && Object.keys(body).length === 1 && typeof body.enabled === "boolean") return;
9
+ if (method === "POST" && ["/api/restart", "/api/data/preview", "/api/data/import"].includes(route)
10
+ && body && typeof body === "object" && !Array.isArray(body)) return;
11
+ throw new Error("Unsupported MineCodex operation");
12
+ }
13
+
14
+ export function createControlClient({ paths, port = 47830, fetchImpl = fetch }) {
15
+ const origin = `http://127.0.0.1:${port}`;
16
+ const icons = new Map();
17
+ async function request(route, method = "GET", body, locale = "en") {
18
+ assertControlRequest({ route, method, body });
19
+ const token = (await readFile(paths.tokenPath, "utf8")).trim();
20
+ const headers = { Authorization: `Bearer ${token}`, Origin: origin, "Accept-Language": locale };
21
+ if (body !== undefined) headers["Content-Type"] = "application/json";
22
+ const response = await fetchImpl(origin + route, {
23
+ method, headers, ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
24
+ signal: AbortSignal.timeout(route === "/api/restart" ? 60_000 : 20_000),
25
+ redirect: "error",
26
+ });
27
+ const data = await response.json();
28
+ if (Array.isArray(data.features)) {
29
+ for (const feature of data.features) {
30
+ if (!FEATURE_IDS.includes(feature.id) || !feature.iconUrl) continue;
31
+ if (!icons.has(feature.id)) {
32
+ const icon = await fetchImpl(`${origin}/api/icons/${feature.id}.svg`, { headers, signal: AbortSignal.timeout(5_000), redirect: "error" });
33
+ if (icon.ok) icons.set(feature.id, `data:image/svg+xml;base64,${Buffer.from(await icon.text()).toString("base64")}`);
34
+ }
35
+ feature.iconUrl = icons.get(feature.id) ?? "";
36
+ }
37
+ }
38
+ return { status: response.status, body: data };
39
+ }
40
+ return { request };
41
+ }
@@ -0,0 +1,26 @@
1
+ import { mkdir, readFile, writeFile, copyFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ const execute = promisify(execFile);
6
+
7
+ export async function installPlugin({ paths, execFileImpl = execute, codex = "codex", node = process.execPath }) {
8
+ const root = path.join(paths.supportDir, "plugin-marketplace");
9
+ const plugin = path.join(root, "plugins/minecodex");
10
+ await mkdir(path.join(plugin, ".codex-plugin"), { recursive: true });
11
+ await mkdir(path.join(plugin, "assets"), { recursive: true });
12
+ await mkdir(path.join(root, ".agents/plugins"), { recursive: true });
13
+ const manifest = JSON.parse(await readFile(path.join(paths.packageRoot, "packages/plugin/plugin.json"), "utf8"));
14
+ await writeFile(path.join(plugin, ".codex-plugin/plugin.json"), JSON.stringify(manifest, null, 2) + "\n");
15
+ await copyFile(path.join(paths.packageRoot, "assets/brand/minecodex-icon.svg"), path.join(plugin, "assets/icon.svg"));
16
+ await writeFile(path.join(plugin, ".mcp.json"), JSON.stringify({ mcpServers: { minecodex: {
17
+ command: node, args: [path.join(paths.packageRoot, "packages/cli/bin/mcx.mjs"), "plugin", "serve"],
18
+ } } }, null, 2) + "\n");
19
+ await writeFile(path.join(root, ".agents/plugins/marketplace.json"), JSON.stringify({
20
+ name: "minecodex-local", interface: { displayName: "MineCodex" },
21
+ plugins: [{ name: "minecodex", source: { source: "local", path: "./plugins/minecodex" }, policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" } }],
22
+ }, null, 2) + "\n");
23
+ await execFileImpl(codex, ["plugin", "marketplace", "add", root, "--json"]);
24
+ const result = await execFileImpl(codex, ["plugin", "add", "minecodex@minecodex-local", "--json"]);
25
+ return { root, result: JSON.parse(result.stdout) };
26
+ }
@@ -0,0 +1,8 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ // 入口与品牌资源归官方插件;UI Adapter 只消费默认/选中资源契约。
4
+ const icon = path => `data:image/svg+xml;base64,${readFileSync(new URL(path, import.meta.url)).toString("base64")}`;
5
+ export const MCP_NAVIGATION_ICONS = [
6
+ { tool: "minecodex_open", default: icon("../../../assets/brand/minecodex-icon.svg"), selected: icon("../../../assets/brand/minecodex-icon-selected.svg") },
7
+ { tool: "minecodex_prompts_open", default: icon("../../../features/notes/assets/icons/prompt-entry.svg"), selected: icon("../../../features/notes/assets/icons/prompt-entry-selected.svg") },
8
+ ];
@@ -0,0 +1,45 @@
1
+ import { App } from '@modelcontextprotocol/ext-apps';
2
+ import { installPrompts } from 'minecodex:prompts';
3
+ import icons from '../../../features/notes/src/frontend/icons.json';
4
+ import previews from '../../../features/notes/assets/prompt-previews.json';
5
+ import styles from '../../../features/notes/src/frontend/styles.css' with {type:'text'};
6
+ import { applyMcpTheme } from '../../ui/src/native/mcp-theme.mjs';
7
+ import { installLayers } from '../../ui/src/foundation/layers.mjs';
8
+ import { installSurfaces } from '../../ui/src/foundation/surfaces.mjs';
9
+ import { installDialog } from '../../ui/src/components/dialog.mjs';
10
+ import { installButton } from '../../ui/src/components/button.mjs';
11
+ import { installFeatureTooltips } from '../../ui/src/components/tooltip.mjs';
12
+
13
+ const app = new App({name:'Prompt',version:'0.1.0'});
14
+ app.ontoolresult = () => {};
15
+ await app.connect();
16
+ applyMcpTheme(app.getHostContext());
17
+ app.addEventListener('hostcontextchanged', applyMcpTheme);
18
+ const controller = new AbortController();
19
+ const signal = controller.signal;
20
+ const ui = {};
21
+ for (const installer of [installLayers,installSurfaces,installDialog,installButton,installFeatureTooltips]) Object.assign(ui,installer(signal,ui));
22
+ ui.page = node => {ui.surface(node,'page'); return ui.layer(node,'page');};
23
+ ui.releasePage = () => {};
24
+ const cleanup = installPrompts(signal, {
25
+ management:true, ui, resources:{icons,previews,styles},
26
+ insertPrompt: promptId => new Promise((resolve,reject) => {
27
+ const channel = new MessageChannel();
28
+ let timer;
29
+ const finish = error => {
30
+ clearTimeout(timer);channel.port1.close();signal.removeEventListener('abort',abort);
31
+ error ? reject(error) : resolve();
32
+ };
33
+ const abort = () => finish(new Error('Prompt page closed'));
34
+ signal.addEventListener('abort',abort,{once:true});
35
+ timer = setTimeout(() => finish(new Error('No available input in the focused window')),4500);
36
+ channel.port1.onmessage = ({data}) => finish(data.ok ? null : new Error(data.error));
37
+ window.top.postMessage({type:'minecodex:insert-prompt',promptId,expiresAt:Date.now()+4000},'*',[channel.port2]);
38
+ }),
39
+ requestHostAction: async (_,args) => {
40
+ const result = await app.callServerTool({name:args.method && args.method !== 'GET' ? 'minecodex_prompts_write' : 'minecodex_prompts_read',arguments:args});
41
+ if (result.isError) throw new Error(result.content?.find(item=>item.type === 'text')?.text ?? 'Prompts request failed');
42
+ return result.structuredContent.result;
43
+ },
44
+ });
45
+ app.onteardown = async () => {controller.abort();cleanup();return {};};
@@ -0,0 +1,16 @@
1
+ import { assertPromptsRequest } from "../../../features/notes/src/adapters/codex/requests.mjs";
2
+ import { readConfig } from '../../cli/src/config.mjs';
3
+
4
+ export function createPromptsClient({ paths, port = 47832, fetchImpl = fetch }) {
5
+ return { async request(route, method = 'GET', body) {
6
+ assertPromptsRequest(route, method);
7
+ if (!(await readConfig(paths.configPath)).features.notes) throw new Error('Enable Prompts in MineCodex first');
8
+ const response = await fetchImpl(`http://127.0.0.1:${port}${route}`, {
9
+ method, headers: {Origin:'app://-', 'Content-Type':'application/json'},
10
+ ...(body !== undefined ? {body:JSON.stringify(body)} : {}), redirect:'error', signal:AbortSignal.timeout(8000),
11
+ });
12
+ const result = response.status === 204 ? null : await response.json();
13
+ if (!response.ok) throw new Error(result?.error?.message ?? 'Prompts service unavailable');
14
+ return result;
15
+ } };
16
+ }
@@ -0,0 +1,74 @@
1
+ import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { registerAppResource, registerAppTool as registerSdkAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
5
+ import { z } from "zod";
6
+ import { readFileSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { resolvePaths } from "../../cli/src/paths.mjs";
9
+ import { createControlClient } from "./control-client.mjs";
10
+ import { createPromptsClient } from "./prompts-client.mjs";
11
+ import { createAppHtml, createPromptsHtml } from "./ui-resource.mjs";
12
+
13
+ export const APP_URI = "ui://minecodex/control-panel.html";
14
+ export const PROMPTS_URI = "ui://minecodex/prompts.html";
15
+ export function createPluginServer({ promptsClient, paths = resolvePaths(), client = createControlClient({ paths, port: Number(process.env.MINECODEX_CONTROL_PORT ?? 47830) }) } = {}) {
16
+ const icons = [{ src: `data:image/svg+xml;base64,${readFileSync(path.join(paths.packageRoot, "assets/brand/minecodex-icon.svg")).toString("base64")}`, mimeType: "image/svg+xml" }];
17
+ const server = new McpServer({ name: "minecodex", version: "0.1.0", icons });
18
+ const promptIcons = [{src:`data:image/svg+xml;base64,${readFileSync(path.join(paths.packageRoot,"features/notes/assets/icons/prompt-entry.svg")).toString("base64")}`,mimeType:"image/svg+xml",sizes:["20x20"]}];
19
+ const tools = [];
20
+ // 使用公开 tools/list 协议补齐 SDK 高层注册尚未转发的工具图标。
21
+ function registerAppTool(target, name, config, callback) {
22
+ registerSdkAppTool(target,name,config,callback);
23
+ tools.push({name,title:config.title,description:config.description,inputSchema:z.toJSONSchema(z.object(config.inputSchema)),
24
+ annotations:config.annotations,_meta:config._meta,icons:name.startsWith("minecodex_prompts_") ? promptIcons : icons});
25
+ }
26
+ const ui = { resourceUri: APP_URI, visibility: ["app"] };
27
+ registerAppTool(server, "minecodex_open", {
28
+ title: "MineCodex", description: "Open the local MineCodex control panel.", inputSchema: {},
29
+ annotations: { readOnlyHint: true, openWorldHint: false },
30
+ _meta: { ui, "openai/ui": { entrypoints: [{ type: "global" }] } },
31
+ }, async () => ({ content: [], structuredContent: {view:"control-panel"} }));
32
+ const inputSchema = { route: z.string(), method: z.enum(["GET", "PATCH", "POST"]).default("GET"), locale: z.enum(["en", "zh-CN"]).default("en"), body: z.record(z.string(), z.unknown()).optional() };
33
+ for (const write of [false, true]) {
34
+ registerAppTool(server, write ? "minecodex_write" : "minecodex_read", {
35
+ description: write ? "Save a user-selected setting, transfer data, or explicitly restart Codex from the control panel." : "Read local MineCodex control panel information.",
36
+ inputSchema, _meta: { ui },
37
+ annotations: { readOnlyHint: !write, destructiveHint: write, openWorldHint: false },
38
+ }, async (input) => {
39
+ if ((input.method !== "GET") !== write) throw new Error("Operation does not match the tool");
40
+ return { content: [], structuredContent: await client.request(input.route, input.method, input.body, input.locale) };
41
+ });
42
+ }
43
+ promptsClient ??= createPromptsClient({paths});
44
+ const promptsUi = {resourceUri:PROMPTS_URI,visibility:["app"]};
45
+ registerAppTool(server,"minecodex_prompts_open",{
46
+ title:"Prompt",description:"Manage MineCodex prompts, categories and usage.",inputSchema:{},
47
+ annotations:{readOnlyHint:true,openWorldHint:false},
48
+ _meta:{ui:promptsUi,"openai/ui":{entrypoints:[{type:"global"}]}},
49
+ },async()=>({content:[],structuredContent:{view:"prompts"}}));
50
+ for (const write of [false,true]) registerAppTool(server,write ? "minecodex_prompts_write" : "minecodex_prompts_read",{
51
+ description:write ? "Apply an explicit Prompts edit or record successful usage." : "Read MineCodex Prompts.",
52
+ inputSchema:{route:z.string(),method:z.enum(["GET","POST","PATCH","DELETE"]).default("GET"),body:z.record(z.string(),z.unknown()).optional()},
53
+ annotations:{readOnlyHint:!write,openWorldHint:false},_meta:{ui:promptsUi},
54
+ },async({route,method,body})=>{
55
+ if ((method !== "GET") !== write) throw new Error("Operation does not match the tool");
56
+ return {content:[],structuredContent:{result:await promptsClient.request(route,method,body)}};
57
+ });
58
+ let promptsHtml;
59
+ registerAppResource(server,"prompts",PROMPTS_URI,{},async()=>({contents:[{
60
+ uri:PROMPTS_URI,mimeType:RESOURCE_MIME_TYPE,text:promptsHtml ??= await createPromptsHtml(paths.packageRoot),
61
+ _meta:{ui:{csp:{connectDomains:[],resourceDomains:["data:","blob:"]}}},
62
+ }]}));
63
+ let html;
64
+ registerAppResource(server, "control-panel", APP_URI, {}, async () => ({ contents: [{
65
+ uri: APP_URI, mimeType: RESOURCE_MIME_TYPE, text: html ??= await createAppHtml(paths.packageRoot),
66
+ _meta: { ui: { csp: { connectDomains: [], resourceDomains: ["data:", "blob:"] } } },
67
+ }] }));
68
+ server.server.setRequestHandler(ListToolsRequestSchema, async () => ({tools}));
69
+ return server;
70
+ }
71
+ export async function servePlugin(options) {
72
+ // 只提供界面与通信。退出插件进程不会停止增强服务或 Codex。
73
+ await createPluginServer(options).connect(new StdioServerTransport());
74
+ }
@@ -0,0 +1,22 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const mime = { ".svg": "image/svg+xml", ".ttf": "font/ttf" };
4
+ export async function createAppHtml(packageRoot) {
5
+ const directory = path.join(packageRoot, "apps/dashboard");
6
+ let html = await readFile(path.join(directory, "index.html"), "utf8");
7
+ let css = await readFile(path.join(directory, "styles.css"), "utf8");
8
+ for (const match of [...css.matchAll(/url\("(\/[^"?]+)(?:\?[^" ]*)?"\)/g)]) {
9
+ const asset = await readFile(path.join(directory, match[1]));
10
+ css = css.replaceAll(match[0], `url("data:${mime[path.extname(match[1])]};base64,${asset.toString("base64")}")`);
11
+ }
12
+ const script = await readFile(path.join(packageRoot, "packages/plugin/assets/app.js"), "utf8");
13
+ html = html.replace(/\s*<link[^>]+>/g, "").replace(/\s*<script[\s\S]*?<\/script>/g, "");
14
+ return html.replace("</head>", `<style>${css}</style></head>`)
15
+ .replace("</body>", `<script type="module">${script.replaceAll("</script", "<\\/script")}</script></body>`);
16
+ }
17
+
18
+ export async function createPromptsHtml(packageRoot) {
19
+ const script = await readFile(path.join(packageRoot,'packages/plugin/assets/prompts.js'),'utf8');
20
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
21
+ <style>${await readFile(path.join(packageRoot,"packages/ui/src/native/mcp-theme.css"),"utf8")} </style></head><body><script type="module">${script.replaceAll('</script','<\\/script')}</script></body></html>`;
22
+ }
@@ -1,5 +1,4 @@
1
1
  import { createRendererSelfCheck, tokenRequirements } from "./renderer-self-check.mjs";
2
- import { readFileSync } from "node:fs";
3
2
  import { createCodexEnvironment } from "../../codex-adapter/src/renderer/environment.mjs";
4
3
  // 注入源生成:createInjectionSource 产出注入到 Codex Renderer 的自包含脚本字符串。
5
4
  // 该字符串经 CDP 注入后独立执行,因此 install 函数体必须自包含;
@@ -7,14 +6,11 @@ import { createCodexEnvironment } from "../../codex-adapter/src/renderer/environ
7
6
 
8
7
  import { createModelSliderInstallerSource } from "../../../features/model-slider/src/injection-source.mjs";
9
8
  import { createUIInstallerSource } from "../../ui/src/install.mjs";
9
+ import { MCP_NAVIGATION_ICONS } from "../../plugin/src/navigation-icons.mjs";
10
10
  import { randomBytes } from "node:crypto";
11
11
  import { CODEX_THEME_TOKENS, CODEX_THEME_TOKEN_ALIASES, MINECODEX_THEME_BINDINGS } from "../../ui/src/native/tokens.mjs";
12
12
 
13
- export const RUNTIME_VERSION = 109;
14
- const PROJECT_ICON = Object.fromEntries([16, 20].map(size => [size,
15
- readFileSync(new URL(`../../ui/assets/minecodex-menu-${size}.svg`, import.meta.url), "utf8"),
16
- ]));
17
- const EXTERNAL_ICON = readFileSync(new URL("../../ui/assets/arrow-up-right.svg", import.meta.url), "utf8");
13
+ export const RUNTIME_VERSION = 113;
18
14
  export const HOST_BINDING_NAME = "__codexPersonalHostAction";
19
15
  export const CSP_BOOTSTRAP_VERSION = 1;
20
16
  export const CSP_BOOTSTRAP_KEY = "__mineCodexCspBootstrapVersion";
@@ -148,7 +144,7 @@ export function createInjectionSource(features, {
148
144
 
149
145
  const lifetime = new AbortController();
150
146
  const codex = createCodexEnvironment();
151
- const featureUI = installUI(lifetime.signal, {codex,themeTokens:config.themeTokens,themeTokenAliases:config.themeTokenAliases,themeBindings:config.themeBindings});
147
+ const featureUI = installUI(lifetime.signal, {codex,mcpServer:"minecodex",mcpPluginId:"minecodex@minecodex-local",mcpNavigationIcons:config.mcpNavigationIcons,themeTokens:config.themeTokens,themeTokenAliases:config.themeTokenAliases,themeBindings:config.themeBindings});
152
148
  const rightPanelRouteKinds = new Set(config.rightPanelRouteKinds);
153
149
 
154
150
  const entryMarker = "data-codex-personal-feature";
@@ -210,8 +206,6 @@ export function createInjectionSource(features, {
210
206
  } : null,
211
207
  hostActions: new Set(feature.hostActions ?? []),
212
208
  }));
213
- featureUI.accountMenuEntry?.({ label:"MineCodex", icon:config.projectIcon, externalIcon:config.externalIcon,
214
- onOpen:() => requestPageHostAction("minecodex", "open-control-panel", {}) });
215
209
  const selfCheck = createRendererSelfCheck(features, config.sessionId, featureUI.theme);
216
210
  const featureById = new Map(features.map((feature) => [feature.id, feature]));
217
211
  const sidebarFeatures = features.filter((feature) => feature.entry?.kind === "sidebar");
@@ -2193,7 +2187,7 @@ export function createInjectionSource(features, {
2193
2187
  }
2194
2188
 
2195
2189
  function requestPageHostAction(featureId, action, payload) {
2196
- if (!(featureId === "minecodex" && action === "open-control-panel") && !featureById.get(featureId)?.hostActions.has(action)) return Promise.reject(new Error("Host action is not registered"));
2190
+ if (!featureById.get(featureId)?.hostActions.has(action)) return Promise.reject(new Error("Host action is not registered"));
2197
2191
  if (typeof globalThis[config.bindingName] !== "function") return Promise.reject(new Error("Host bridge is unavailable"));
2198
2192
  return new Promise((resolve, reject) => {
2199
2193
  const requestId = crypto.randomUUID();
@@ -2463,8 +2457,6 @@ export function createInjectionSource(features, {
2463
2457
  } : null,
2464
2458
  }));
2465
2459
  return `(${install.toString()})(${JSON.stringify({
2466
- projectIcon: PROJECT_ICON,
2467
- externalIcon: EXTERNAL_ICON,
2468
2460
  version: RUNTIME_VERSION,
2469
2461
  sessionId: runtimeSessionId,
2470
2462
  bindingName,
@@ -2474,6 +2466,7 @@ export function createInjectionSource(features, {
2474
2466
  themeTokens: CODEX_THEME_TOKENS,
2475
2467
  themeTokenAliases: CODEX_THEME_TOKEN_ALIASES,
2476
2468
  themeBindings: MINECODEX_THEME_BINDINGS,
2469
+ mcpNavigationIcons: MCP_NAVIGATION_ICONS,
2477
2470
  features: runtimeFeatures,
2478
2471
  })}, ${createUIInstallerSource()}, ${createModelSliderInstallerSource()}, ${createCodexEnvironment.toString()}, ${createRendererSelfCheck.toString()})`;
2479
2472
  }
@@ -1,6 +1,4 @@
1
- import { execFile as execFileCallback } from "node:child_process";
2
- import { promisify } from "node:util";
3
- import { fileURLToPath } from "node:url";
1
+ import { assertPromptsRequest } from "../../../features/notes/src/adapters/codex/requests.mjs";
4
2
  import { LocalImageSource } from "../../../features/images/src/backend/local-image-source.mjs";
5
3
  import { cloudImagesAction } from "../../../features/images/src/backend/cloud-library.mjs";
6
4
  // CodexRuntime:Codex Renderer 生命周期与 host action 执行。
@@ -81,9 +79,7 @@ export class CodexRuntime {
81
79
  onStatusChange = null,
82
80
  sitesService = null,
83
81
  onManagedCodexPidChange = null,
84
- openControlPanel = () => promisify(execFileCallback)(process.execPath, [fileURLToPath(new URL("../../cli/bin/mcx.mjs", import.meta.url)), "gui"], { timeout:20_000 }),
85
82
  }) {
86
- this.openControlPanel = openControlPanel;
87
83
  this.features = features;
88
84
  this.sitesService = sitesService ?? (features.some(feature => feature.id === "sites") ? new SitesService() : null);
89
85
  this.localImageSource = features.some(feature => feature.id === "images") ? new LocalImageSource({dataDir:path.join(profileDir,"minecodex-image-gen")}) : null;
@@ -539,11 +535,6 @@ export class CodexRuntime {
539
535
  return;
540
536
  }
541
537
  if (request.token !== this.bindingTokens.get(targetId) || typeof request.requestId !== "string") return;
542
- if (request.featureId === "minecodex" && request.action === "open-control-panel") {
543
- try { await this.openControlPanel(); await this.resolveHostAction(client, request.requestId, {ok:true,result:{opened:true}}); }
544
- catch (error) { await this.resolveHostAction(client, request.requestId, {ok:false,error:{code:"CONTROL_PANEL_FAILED",message:error.stderr?.trim() || error.message}}); }
545
- return;
546
- }
547
538
  const feature = this.features.find((candidate) => candidate.id === request.featureId);
548
539
  if (feature?.id === "session-status" && request.action === "refresh-session-status") {
549
540
  if (this.sessionStatusReader && this.clients.get(targetId) === client && !this.stopping) {
@@ -601,13 +592,9 @@ export class CodexRuntime {
601
592
  if (feature.id === "notes" && request.action === "quick-prompts") {
602
593
  try {
603
594
  const { route, method = "GET", body } = request.payload ?? {};
604
- const allowed = (route === "/api/state" && method === "GET")
605
- || (["/api/prompts", "/api/categories", "/api/reorder"].includes(route) && method === "POST")
606
- || (/^\/api\/categories\/[a-zA-Z0-9-]+$/.test(route) && ["PATCH", "DELETE"].includes(method))
607
- || (/^\/api\/prompts\/[a-zA-Z0-9-]+$/.test(route) && ["PATCH", "DELETE"].includes(method))
608
- || (/^\/api\/prompts\/[a-zA-Z0-9-]+\/use$/.test(route) && method === "POST");
595
+ assertPromptsRequest(route, method);
609
596
  const origin = pageScriptServiceOrigin(feature);
610
- if (!allowed || !origin) throw new Error("Prompt request is not allowed");
597
+ if (!origin) throw new Error("Prompt request is not allowed");
611
598
  const response = await this.fetchImpl(new URL(route, origin), {
612
599
  method, headers: { "Content-Type": "application/json", Origin: CODEX_APP_ORIGIN },
613
600
  body: body === undefined ? undefined : JSON.stringify(body),
@@ -1,5 +1,5 @@
1
- import { createNativeAccountMenuAdapter } from "./native/account-menu.mjs";
2
- import { installAccountMenuEntry } from "./components/account-menu.mjs";
1
+ import { installMcpAppChrome } from "./native/mcp-app.mjs";
2
+ import { installMcpNavigationIcons } from "./native/mcp-navigation.mjs";
3
3
  import { createNativeThemeAdapter } from "./native/theme.mjs";
4
4
  import { createNativePageAdapter } from "./native/page.mjs";
5
5
  import { createNativeRailAdapter } from "./native/navigation-rail.mjs";
@@ -21,7 +21,8 @@ export function createUIInstallerSource() {
21
21
  Object.assign(ui, (${installPage.toString()})(signal, ui, (${createNativePageAdapter.toString()})(context.codex)));
22
22
  Object.assign(ui, (${installNavigationRail.toString()})(signal, ui, (${createNativeRailAdapter.toString()})(context.codex)));
23
23
  Object.assign(ui, (${installFeatureTooltips.toString()})(signal));
24
- Object.assign(ui, (${installAccountMenuEntry.toString()})(signal, ui, (${createNativeAccountMenuAdapter.toString()})(context.codex)));
24
+ if (context.mcpServer) (${installMcpAppChrome.toString()})(signal, context.codex, {server:context.mcpServer,pluginId:context.mcpPluginId});
25
+ if (context.mcpNavigationIcons) (${installMcpNavigationIcons.toString()})(signal, (${createNativeRailAdapter.toString()})(context.codex), {server:context.mcpServer,pluginId:context.mcpPluginId,icons:context.mcpNavigationIcons});
25
26
  return ui;
26
27
  }`;
27
28
  }
@@ -0,0 +1,32 @@
1
+ // Codex 26.930:GlobalView 的重复标题由宿主生成,不能从 iframe 控制。
2
+ export function installMcpAppChrome(signal, codex, {server,pluginId}) {
3
+ const headers = new Map();
4
+ function restore(header, hidden) {
5
+ if (header.hasAttribute('data-mcx-hidden-app-header')) {
6
+ header.hidden = hidden;
7
+ header.removeAttribute('data-mcx-hidden-app-header');
8
+ }
9
+ }
10
+ function sync() {
11
+ const owned = new Set();
12
+ for (const surface of document.querySelectorAll('[data-mcp-extension-surface]')) {
13
+ const owner = codex.props(surface, props => props.source?.server != null);
14
+ if (!owner || !(owner.source.server === server || (pluginId != null && owner.source.pluginId === pluginId))) continue;
15
+ const header = surface.querySelector(':scope > [data-mcp-extension-header]');
16
+ if (!header) continue;
17
+ owned.add(header);
18
+ if (!headers.has(header)) headers.set(header, header.hidden);
19
+ header.setAttribute('data-mcx-hidden-app-header','');
20
+ header.hidden = true;
21
+ }
22
+ for (const [header,hidden] of headers) if (!owned.has(header)) {
23
+ restore(header,hidden);headers.delete(header);
24
+ }
25
+ }
26
+ const observer = new MutationObserver(sync);
27
+ observer.observe(document.documentElement,{childList:true,subtree:true,characterData:true});
28
+ sync();
29
+ const cleanup=()=>{observer.disconnect();for (const [header,hidden] of headers) restore(header,hidden);headers.clear();};
30
+ signal.addEventListener('abort',cleanup,{once:true});
31
+ return cleanup;
32
+ }
@@ -0,0 +1,52 @@
1
+ // MCP 原生 railIcons 默认与选中共用资源;只更换 mask 图片,颜色与交互仍归原生节点。
2
+ export function installMcpNavigationIcons(signal, nativeRail, {server, pluginId, icons}) {
3
+ const resources = new Map(icons.map(icon => [icon.tool,icon]));
4
+ const snapshots = new Map();
5
+ function restore(image, state) {
6
+ if (image.getAttribute('href') !== state.applied) return;
7
+ if (state.original === null) image.removeAttribute('href');
8
+ else image.setAttribute('href',state.original);
9
+ }
10
+ function identity(node) {
11
+ const item = nativeRail.item(node);
12
+ const entry = item?.railIcons?.default?.props?.entry;
13
+ if (!entry || !(entry.source?.server === server || (pluginId != null && entry.source?.pluginId === pluginId))) return null;
14
+ let tool;
15
+ try {tool = decodeURIComponent(entry.path.split('/').at(-1));} catch {return null;}
16
+ const resource = resources.get(tool);
17
+ if (!resource?.default || !resource.selected) return null;
18
+ return {key:entry.path,resource};
19
+ }
20
+ function sync() {
21
+ const owned = new Set();
22
+ for (const node of document.querySelectorAll('[data-app-navigation-rail] [data-sidebar-destination^="mcp:"]')) {
23
+ const owner = identity(node);
24
+ if (!owner) continue;
25
+ const image = node.querySelector('svg defs mask image');
26
+ if (!image) continue; // 原生刷新状态显示 spinner 时没有图标 mask。
27
+ owned.add(image);
28
+ let state = snapshots.get(image);
29
+ if (state && state.key !== owner.key) {
30
+ restore(image,state);snapshots.delete(image);state = null;
31
+ }
32
+ const current = image.getAttribute('href');
33
+ if (!state) {state = {key:owner.key,original:current,applied:null};snapshots.set(image,state);}
34
+ else if (current !== state.applied) state.original = current;
35
+ const selected = node.getAttribute('aria-current') === 'page'
36
+ || (node.hasAttribute('data-selected') && node.getAttribute('data-selected') !== 'false');
37
+ const next = selected ? owner.resource.selected : owner.resource.default;
38
+ state.applied = next;
39
+ if (current !== next) image.setAttribute('href',next);
40
+ }
41
+ for (const [image,state] of snapshots) if (!owned.has(image)) {
42
+ restore(image,state);snapshots.delete(image);
43
+ }
44
+ }
45
+ const observer = new MutationObserver(sync);
46
+ observer.observe(document.documentElement,{childList:true,subtree:true,attributes:true,
47
+ attributeFilter:['href','aria-current','data-selected','data-sidebar-destination']});
48
+ const cleanup = () => {observer.disconnect();for (const [image,state] of snapshots) restore(image,state);snapshots.clear();};
49
+ signal.addEventListener('abort',cleanup,{once:true});
50
+ if (signal.aborted) cleanup(); else sync();
51
+ return cleanup;
52
+ }
@@ -0,0 +1,4 @@
1
+ :root { --color-background-primary:light-dark(#fff,#212121); --color-background-secondary:light-dark(#f8f8f8,#303030); --color-background-tertiary:light-dark(#eee,#3c3c3c); --color-text-primary:light-dark(#202020,#eee); --color-text-secondary:light-dark(#626262,#aaa); --color-text-tertiary:light-dark(#777,#999); --color-border-primary:light-dark(#ddd,#555); --color-border-secondary:light-dark(#eee,#444); --color-border-info:#0285ff; --color-text-danger:light-dark(#ba2a2a,#ff6764); --font-sans:system-ui,sans-serif; --font-text-sm-size:13px; --font-text-md-size:14px; --font-text-xs-size:12px; --font-heading-lg-size:28px; --font-heading-md-size:20px; --border-radius-md:8px; --border-radius-lg:10px; --border-radius-xl:15px; --border-radius-full:999px;
2
+ --mcx-spacing:4px; --mcx-radius-2xl:20px; --mcx-radius-3xl:25px; --mcx-text-sm--line-height:1.43; --mcx-font-weight-semibold:600; --mcx-font-weight-normal:400; --font-weight-medium:500; --mcx-control-size-lg:36px; --mcx-control-font-size-md:13px; --mcx-control-gutter-md:16px; --mcx-control-gutter-pill-scaling:1; --mcx-button-gap-lg:8px; --mcx-modal-container-inner-padding:20px; --mcx-shadow-lg:0 8px 24px #0002; --mcx-shadow-xl-spread:0 12px 36px #0003; --mcx-color-border-overlay:var(--mcx-color-border); --mcx-blur-sm:4px; --mcx-blur-xl:24px; --mcx-color-dialog-overlay:#0002; --mcx-color-background-danger-soft-alpha:color-mix(in oklab,var(--color-text-danger) 12%,transparent); --mcx-color-background-danger-soft-alpha-hover:color-mix(in oklab,var(--color-text-danger) 20%,transparent); --color-background-primary-soft:var(--color-background-secondary); --cursor-interaction:pointer; }
3
+ html,body {margin:0;min-height:100%;height:100%;color:var(--color-text-primary);background:var(--color-background-primary);font-family:var(--font-sans);}
4
+ button,input,textarea {font:inherit;color:inherit;} button {border:0;background:transparent;padding:0;} .sr-only {position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);}
@@ -0,0 +1,20 @@
1
+ // MCP Apps 的宿主变量是 iframe 的主题来源;不读取父窗口 DOM。
2
+ export function applyMcpTheme(context) {
3
+ const root = document.documentElement;
4
+ root.dataset.theme = context.theme ?? root.dataset.theme ?? 'light';
5
+ root.style.colorScheme = root.dataset.theme;
6
+ for (const [name,value] of Object.entries(context.styles?.variables ?? {})) root.style.setProperty(name,value);
7
+ const aliases = {
8
+ 'color-surface':'--color-background-primary', 'color-surface-elevated-secondary':'--color-background-secondary',
9
+ 'color-text-primary':'--color-text-primary', 'color-text-secondary':'--color-text-secondary', 'color-text-tertiary':'--color-text-tertiary',
10
+ 'color-border':'--color-border-primary', 'color-border-subtle':'--color-border-secondary', 'color-border-primary-outline':'--color-border-primary',
11
+ 'color-background-primary-ghost-hover':'--color-background-tertiary', 'color-background-primary-outline-hover':'--color-background-tertiary',
12
+ 'color-background-primary-solid':'--color-text-primary', 'color-text-primary-solid':'--color-background-primary',
13
+ 'color-background-primary-solid-hover':'--color-text-secondary', 'color-ring':'--color-border-info', 'color-text-danger':'--color-text-danger',
14
+ 'font-sans-default':'--font-sans', 'text-sm':'--font-text-sm-size', 'text-base':'--font-text-md-size', 'text-xs':'--font-text-xs-size',
15
+ 'text-heading-lg':'--font-heading-lg-size', 'text-heading-md':'--font-heading-md-size',
16
+ 'radius-md':'--border-radius-md','radius-lg':'--border-radius-lg','radius-xl':'--border-radius-xl', 'radius-full':'--border-radius-full',
17
+ };
18
+ for (const [target,source] of Object.entries(aliases)) root.style.setProperty(`--mcx-${target}`,`var(${source})`);
19
+ if (context.locale) root.lang = context.locale;
20
+ }
@@ -1,13 +0,0 @@
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>
@@ -1,7 +0,0 @@
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>
@@ -1,7 +0,0 @@
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>