minecodex 2.0.15 → 2.0.17

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 (39) hide show
  1. package/README.md +9 -0
  2. package/apps/dashboard/app.js +9 -6
  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/notes/assets/icons/prompt-entry-selected.svg +1 -0
  7. package/features/notes/assets/icons/prompt-entry.svg +16 -0
  8. package/features/notes/codex-feature.json +3 -2
  9. package/features/notes/src/adapters/codex/insertion.mjs +92 -0
  10. package/features/notes/src/adapters/codex/requests.mjs +7 -0
  11. package/features/notes/src/frontend/entry.mjs +39 -137
  12. package/features/notes/src/frontend/icons.json +0 -2
  13. package/features/notes/src/injection-source.mjs +5 -0
  14. package/package.json +18 -5
  15. package/packages/cli/src/commands.mjs +10 -0
  16. package/packages/codex-adapter/src/renderer/environment.mjs +30 -1
  17. package/packages/plugin/assets/app.js +116 -0
  18. package/packages/plugin/assets/prompts.js +437 -0
  19. package/packages/plugin/plugin.json +16 -0
  20. package/packages/plugin/src/app.mjs +17 -0
  21. package/packages/plugin/src/control-client.mjs +41 -0
  22. package/packages/plugin/src/install.mjs +26 -0
  23. package/packages/plugin/src/navigation-icons.mjs +8 -0
  24. package/packages/plugin/src/prompts-app.mjs +45 -0
  25. package/packages/plugin/src/prompts-client.mjs +16 -0
  26. package/packages/plugin/src/server.mjs +74 -0
  27. package/packages/plugin/src/ui-resource.mjs +23 -0
  28. package/packages/runtime-host/src/codex-injection.mjs +5 -12
  29. package/packages/runtime-host/src/codex-runtime.mjs +3 -16
  30. package/packages/ui/src/install.mjs +4 -3
  31. package/packages/ui/src/native/mcp-app.mjs +32 -0
  32. package/packages/ui/src/native/mcp-navigation.mjs +52 -0
  33. package/packages/ui/src/native/mcp-theme.css +4 -0
  34. package/packages/ui/src/native/mcp-theme.mjs +20 -0
  35. package/packages/ui/assets/arrow-up-right.svg +0 -13
  36. package/packages/ui/assets/minecodex-menu-16.svg +0 -7
  37. package/packages/ui/assets/minecodex-menu-20.svg +0 -7
  38. package/packages/ui/src/components/account-menu.mjs +0 -88
  39. package/packages/ui/src/native/account-menu.mjs +0 -21
package/README.md CHANGED
@@ -50,11 +50,18 @@ mcx open
50
50
  | `mcx uninstall [--purge]` | 移除扩展;默认保留个人数据,带 `--purge` 参数才会清理 |
51
51
  | `mcx update` | 更新到最新的稳定版本 |
52
52
  | `mcx open` | 打开 MineCodex 控制台 |
53
+ | `mcx plugin install` | 通过官方插件机制注册 Codex 内的 MineCodex 控制台入口 |
53
54
  | `mcx status [--json]` | 查看安装与插件运行状态 |
54
55
  | `mcx restart` | 重启 Codex 并应用插件开启/关闭状态 |
55
56
 
56
57
  关闭某项功能仅停用对应的扩展服务,不会删除任何个人数据;修改设置后运行 `mcx restart` 即可应用更改。
57
58
 
59
+ 运行 `mcx plugin install` 后,在 Codex 的 Explore/探索中打开 MineCodex,也可按原生
60
+ 导航设置固定到侧栏。该本地插件使用官方 MCP Apps 入口与面板,复用原有控制台。
61
+ 关闭面板不会停止增强;各项功能继续由 MineCodex 本地服务和注入运行时提供。
62
+ 当前正在运行的窗口若未刷新插件目录,下次正常启动后查看。安装命令不会重启 Codex。
63
+ 插件卸载使用 `codex plugin remove minecodex@minecodex-local`,不删除增强配置或数据。
64
+
58
65
  运行 `mcx uninstall` 会移除名为 MineCodex 的后台项目,并默认保留配置设置、Notes 笔记和 Images 图片索引;只有显式加上 `--purge` 参数,才会清理 MineCodex 产生的本地配置。MineCodex 绝不会删除 `~/.codex/generated_images` 中的原始图片或 Notes 引用的本地文件。
59
66
 
60
67
  ## 数据与图片操作
@@ -106,3 +113,5 @@ MineCodex 是非官方独立扩展项目,与 OpenAI 无关联。项目自有
106
113
  支持 Prompts 与分类、公网 Sites、模型收藏。模型旧收藏在新版模型滑条首次启动后收录,
107
114
  导入收藏在下次启动 Codex 后载入。图片文件、本地站点、账号信息与设备设置不包含在文件中;
108
115
  可迁移功能的特殊说明可通过 info 查看。这是手动迁移,不是自动云同步。文件包含提示词正文,请妥善保管。
116
+
117
+ 官方插件提供 MineCodex 控制台与 **Prompt** 两个入口,分别使用 MCX 品牌图标与 Prompt 图标。Prompt 仍由 MCX 管理,沿用原有提示词数据;输入框里的快捷提示词继续只填入、不发送。
@@ -1,4 +1,5 @@
1
1
  import {diagnosticReport} from "./assets/diagnostics.js";
2
+ import {dashboardFetch} from "./transport.js";
2
3
  let savedLocale;
3
4
  try { savedLocale = localStorage.getItem("minecodex-language"); } catch { /* 存储不可用时跟随浏览器语言。 */ }
4
5
  const locale = ["en", "zh-CN"].includes(savedLocale) ? savedLocale
@@ -221,7 +222,7 @@ function updatePluginCard(card, feature) {
221
222
  const dot = card.querySelector(".plugin-state .status-dot");
222
223
  const stateLabel = card.querySelector(".plugin-state-label");
223
224
  card.dataset.featureId = feature.id;
224
- image.src = `${feature.iconUrl}?v=resource-1`;
225
+ image.src = feature.iconUrl.startsWith("data:") ? feature.iconUrl : `${feature.iconUrl}?v=resource-1`;
225
226
  name.textContent = feature.label;
226
227
  description.textContent = feature.description;
227
228
  input.checked = Boolean(feature.enabled);
@@ -310,7 +311,7 @@ function replaceFeatureInState(featureId, patch) {
310
311
  async function load() {
311
312
  elements.loadError.hidden = true;
312
313
  try {
313
- const response = await fetch("/api/status", { headers: { "Accept-Language": locale } });
314
+ const response = await dashboardFetch("/api/status", { headers: { "Accept-Language": locale } });
314
315
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
315
316
  const fresh = await response.json();
316
317
  if (pendingFeatureIds.size && state) fresh.features = fresh.features.map(feature =>
@@ -339,7 +340,7 @@ elements.pluginList.addEventListener("change", async (event) => {
339
340
  updatePluginCard(card, state.features.find((feature) => feature.id === featureId));
340
341
  elements.saveState.textContent = copy.saving;
341
342
  try {
342
- const response = await fetch(`/api/features/${encodeURIComponent(featureId)}`, {
343
+ const response = await dashboardFetch(`/api/features/${encodeURIComponent(featureId)}`, {
343
344
  method: "PATCH",
344
345
  headers: { "Content-Type": "application/json", "Accept-Language": locale },
345
346
  body: JSON.stringify({ enabled: input.checked }),
@@ -369,7 +370,7 @@ elements.restartButton.addEventListener("click", async () => {
369
370
  elements.restartButton.disabled = true;
370
371
  elements.restartButton.querySelector(".button-label").textContent = copy.restarting;
371
372
  try {
372
- const response = await fetch("/api/restart", {
373
+ const response = await dashboardFetch("/api/restart", {
373
374
  method: "POST",
374
375
  headers: { "Content-Type": "application/json", "Accept-Language": locale },
375
376
  body: "{}",
@@ -404,7 +405,7 @@ elements.updateButton.addEventListener("click", async () => {
404
405
  elements.updateButton.querySelector(".button-label").textContent = copy.checkingUpdates;
405
406
  elements.updateResult.hidden = true;
406
407
  try {
407
- const response = await fetch("/api/update");
408
+ const response = await dashboardFetch("/api/update");
408
409
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
409
410
  const update = await response.json();
410
411
  elements.updateResult.textContent = update.available ? copy.updateAvailable(update.latest) : copy.current;
@@ -422,7 +423,8 @@ const systemTheme = matchMedia("(prefers-color-scheme: dark)");
422
423
  let selectedTheme = null;
423
424
  try { selectedTheme = localStorage.getItem("minecodex-appearance"); } catch { /* 存储不可用时仍允许本次切换。 */ }
424
425
  function applyTheme() {
425
- const dark = selectedTheme === "dark" || (selectedTheme !== "light" && systemTheme.matches);
426
+ const hostDark = window.minecodexHostTheme ? window.minecodexHostTheme === "dark" : systemTheme.matches;
427
+ const dark = selectedTheme === "dark" || (selectedTheme !== "light" && hostDark);
426
428
  document.documentElement.dataset.theme = dark ? "dark" : "light";
427
429
  elements.themeToggle.setAttribute("aria-label", locale === "zh-CN" ? "外观" : "Appearance");
428
430
  for (const button of elements.themeButtons) {
@@ -447,6 +449,7 @@ elements.languageToggle.addEventListener("click", (event) => {
447
449
  location.reload();
448
450
  });
449
451
  systemTheme.addEventListener("change", applyTheme);
452
+ window.addEventListener("minecodex-host-theme", applyTheme);
450
453
  document.addEventListener("keydown", () => { document.documentElement.dataset.input = "keyboard"; });
451
454
  document.addEventListener("pointerdown", () => { document.documentElement.dataset.input = "pointer"; });
452
455
  applyStaticCopy();
@@ -1,4 +1,5 @@
1
1
  import { installFeatureTooltips } from './ui-tooltip.js';
2
+ import {dashboardFetch} from '../transport.js';
2
3
  const tooltipLifetime = new AbortController();
3
4
  const { tooltipSurface } = installFeatureTooltips(tooltipLifetime.signal);
4
5
  window.addEventListener('pagehide', event => { if (!event.persisted) tooltipLifetime.abort(); });
@@ -20,7 +21,7 @@ document.querySelector('#data-close').setAttribute('aria-label', t('关闭', 'Cl
20
21
  exportButton.querySelector('.button-label').textContent = t('导出', 'Export');
21
22
  importButton.querySelector('.button-label').textContent = t('导入', 'Import');
22
23
  async function request(url, data) {
23
- const response = await fetch(url, data ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) } : {});
24
+ const response = await dashboardFetch(url, data ? { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) } : {});
24
25
  if (!response.ok) throw new Error(String(response.status));
25
26
  return response.json();
26
27
  }
@@ -0,0 +1,18 @@
1
+ // 浏览器继续走同源 HTTP;官方插件面板通过 MCP 调用同一套本地服务。
2
+ export async function dashboardFetch(url, options = {}) {
3
+ const app = window.minecodexMcpApp;
4
+ if (!app) return fetch(url, options);
5
+ const method = options.method ?? "GET";
6
+ const result = await app.callServerTool({
7
+ name: method === "GET" ? "minecodex_read" : "minecodex_write",
8
+ arguments: {
9
+ route: url,
10
+ method,
11
+ locale: new Headers(options.headers).get("Accept-Language") ?? document.documentElement.lang,
12
+ ...(options.body ? { body: JSON.parse(options.body) } : {}),
13
+ },
14
+ });
15
+ if (result.isError) throw new Error(result.content?.find(item => item.type === "text")?.text ?? "MineCodex request failed");
16
+ const { status, body } = result.structuredContent;
17
+ return new Response(JSON.stringify(body), { status, headers: { "Content-Type": "application/json" } });
18
+ }
@@ -0,0 +1,4 @@
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 24 24"><defs><mask id="pickaxe-cutout" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24" style="mask-type:luminance"><rect width="24" height="24" fill="white"/><g id="pickaxe" transform="translate(6.24 6.24) scale(.48)" fill="black">
3
+ <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"/>
4
+ </g></mask></defs><path fill="currentColor" mask="url(#pickaxe-cutout)" d="M9.3316 2.03768C11.1644 1.54688 13.0268 2.04668 14.356 3.20174C16.0859 2.86512 17.9511 3.36418 19.2935 4.70663C20.6356 6.04882 21.1326 7.91288 20.7964 9.64217C21.9527 10.9716 22.4534 12.8349 21.9625 14.6685C21.4711 16.5025 20.1051 17.8657 18.439 18.439C17.8658 20.1047 16.503 21.47 14.6695 21.9615C12.8356 22.4528 10.9727 21.9519 9.64312 20.7955C7.91371 21.1319 6.04993 20.635 4.70757 19.2926C3.36558 17.9505 2.86659 16.0866 3.20269 14.357C2.04693 13.0273 1.54757 11.1638 2.03863 9.33065C2.52997 7.49759 3.89436 6.13282 5.56011 5.55917C6.13382 3.8935 7.49837 2.52891 9.3316 2.03768Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg width="20" height="20" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" fill="currentColor"><path d="M240,56V200a8,8,0,0,1-8,8H160a24,24,0,0,0-24,23.94,7.9,7.9,0,0,1-5.12,7.55A8,8,0,0,1,120,232a24,24,0,0,0-24-24H24a8,8,0,0,1-8-8V56a8,8,0,0,1,8-8H88a32,32,0,0,1,32,32v87.73a8.17,8.17,0,0,0,7.47,8.25,8,8,0,0,0,8.53-8V80a32,32,0,0,1,32-32h64A8,8,0,0,1,240,56Z"/></svg>
@@ -0,0 +1,16 @@
1
+ <!-- @license lucide-static v0.577.0 - ISC -->
2
+ <svg
3
+ class="lucide lucide-book-open"
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ width="20"
6
+ height="20"
7
+ viewBox="-1.5 -1.5 27 27"
8
+ fill="none"
9
+ stroke="currentColor"
10
+ stroke-width="1.8"
11
+ stroke-linecap="round"
12
+ stroke-linejoin="round"
13
+ >
14
+ <path d="M12 7v14" />
15
+ <path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
16
+ </svg>
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "id": "notes",
4
- "version": "0.1.11",
4
+ "version": "0.1.12",
5
5
  "label": {
6
6
  "en": "Prompt Library",
7
7
  "zh-CN": "提示词库"
@@ -19,7 +19,8 @@
19
19
  "icons": "src/frontend/icons.json",
20
20
  "styles": "src/frontend/styles.css",
21
21
  "previews": "assets/prompt-previews.json"
22
- }
22
+ },
23
+ "sourceFactory": "src/injection-source.mjs"
23
24
  },
24
25
  "placement": {
25
26
  "order": 20
@@ -0,0 +1,92 @@
1
+ // 请求只到达发起 App 所在窗口,不经共享服务队列寻找任意聚焦窗口。
2
+ export function installPromptInsertion(signal, {codex, api, insertText}) {
3
+ const navigation = codex.navigation();
4
+ let remembered = null, busy = false;
5
+ function remember(composer) {
6
+ const location = navigation?.location();
7
+ if (!location || location.path.startsWith('/mcp-app/') || !composer) return;
8
+ remembered = {location, editable:composer.editable, threadId:composer.threadId,
9
+ placement:composer.root.getAttribute('data-composer-placement')};
10
+ }
11
+ function available() {
12
+ const target = remembered?.editable;
13
+ return codex.composer(target ?? null, {purpose:'insertion'});
14
+ }
15
+ function waitForComposer(valid, original, deadline) {
16
+ return new Promise((resolve,reject) => {
17
+ let observer, timer;
18
+ const finish = (error, value) => {observer?.disconnect();clearTimeout(timer);error ? reject(error) : resolve(value);};
19
+ const check = () => {
20
+ if (!valid()) return finish(new Error('Input changed before insertion'));
21
+ const candidates = [...document.querySelectorAll('[contenteditable="true"]')]
22
+ .map(editable => codex.composer(editable,{purpose:'insertion'}))
23
+ .filter(composer => composer && composer.threadId === original.threadId
24
+ && composer.root.getAttribute('data-composer-placement') === original.placement);
25
+ if (candidates.length > 1) return finish(new Error('Input changed before insertion'));
26
+ if (candidates.length === 1) finish(null,candidates[0]);
27
+ };
28
+ observer = new MutationObserver(check);
29
+ observer.observe(document.documentElement,{childList:true,subtree:true,attributes:true,attributeFilter:['hidden','inert','aria-hidden']});
30
+ timer = setTimeout(() => finish(new Error('No available input in the focused window')),Math.max(0,Math.min(2000,deadline-Date.now())));
31
+ check();
32
+ });
33
+ }
34
+ async function receive(event) {
35
+ if (event.data?.type !== 'minecodex:insert-prompt' || typeof event.data.promptId !== 'string') return;
36
+ const port = event.ports?.[0];
37
+ const source = codex.mcpAppSource(event.source,{server:'minecodex',pluginId:'minecodex@minecodex-local',resourceUri:'ui://minecodex/prompts.html'});
38
+ if (!source || !port) return;
39
+ if (busy || !document.hasFocus()) {port.postMessage({error:'Input changed before insertion'});port.close();return;}
40
+ busy = true;
41
+ const deadline = Math.min(Date.now() + 4000, event.data.expiresAt ?? Infinity);
42
+ let cancelled = false, navigating = false, restoring = false, expected = navigation?.location(), inserted = false;
43
+ const target = available();
44
+ const original = target ? {editable:target.editable,threadId:target.threadId,placement:target.root.getAttribute('data-composer-placement')} : remembered;
45
+ const onBlur = () => {queueMicrotask(() => {if (!document.hasFocus()) cancelled = true;});};
46
+ const onFocus = event => {
47
+ const editable = event.target.closest?.('[contenteditable="true"]');
48
+ if (editable && editable !== original?.editable) {
49
+ const focused = codex.composer(editable,{purpose:'insertion'});
50
+ if (!restoring || navigation.location().path !== remembered?.location.path
51
+ || focused?.threadId !== original?.threadId
52
+ || focused?.root.getAttribute('data-composer-placement') !== original?.placement) cancelled = true;
53
+ }
54
+ };
55
+ window.addEventListener('blur',onBlur);
56
+ document.addEventListener('focusin',onFocus);
57
+ const unsubscribe = navigation?.subscribe(location => {
58
+ if (navigating && location.path === remembered?.location.path) {expected = location;navigating = false;}
59
+ else if (location.key !== expected?.key) cancelled = true;
60
+ });
61
+ const valid = () => !cancelled && !signal.aborted && Date.now() < deadline && document.hasFocus();
62
+ try {
63
+ const state = await api('/api/state');
64
+ const prompt = state.prompts.find(item => item.id === event.data.promptId);
65
+ if (!prompt) throw new Error('Prompt was not found');
66
+ if (!valid() || !source.connected()) throw new Error('Input changed before insertion');
67
+ let composer = target && codex.composer(target.editable,{purpose:'insertion'});
68
+ if (target && !composer) throw new Error('Input changed before insertion');
69
+ if (!composer) {
70
+ if (!original || !remembered || !navigation) throw new Error('No available input in the focused window');
71
+ navigating = restoring = true;
72
+ let timer;
73
+ try {await Promise.race([navigation.navigate(remembered.location.path),new Promise((_,reject) => {
74
+ timer = setTimeout(() => {cancelled = true;reject(new Error('No available input in the focused window'));},2000);
75
+ })]);} finally {clearTimeout(timer);}
76
+ composer = await waitForComposer(valid,original,deadline);
77
+ }
78
+ if (!valid() || composer.threadId !== original?.threadId
79
+ || composer.root.getAttribute('data-composer-placement') !== original?.placement
80
+ || (!navigating && navigation && navigation.location().key !== expected?.key)) throw new Error('Input changed before insertion');
81
+ // 校验与同步填入之间不再等待;成功后计数失败不报告为可重试的插入失败。
82
+ insertText(prompt.body,composer.editable);
83
+ inserted = true;
84
+ port.postMessage({ok:true});
85
+ try {await api(`/api/prompts/${encodeURIComponent(prompt.id)}/use`,{method:'POST',body:{action:'insert',success:true}});}
86
+ catch (error) {console.warn('[Prompts] Usage could not be saved',error);}
87
+ } catch (error) {if (!inserted) port.postMessage({error:error.message});}
88
+ finally {unsubscribe?.();window.removeEventListener('blur',onBlur);document.removeEventListener('focusin',onFocus);port.close();busy=false;}
89
+ }
90
+ window.addEventListener('message',receive,{signal});
91
+ return {remember};
92
+ }
@@ -0,0 +1,7 @@
1
+ export function assertPromptsRequest(route, method = 'GET') {
2
+ const allowed = (route === '/api/state' && method === 'GET')
3
+ || (['/api/prompts', '/api/categories', '/api/reorder'].includes(route) && method === 'POST')
4
+ || (/^\/api\/(prompts|categories)\/[a-zA-Z0-9-]+$/.test(route) && ['PATCH','DELETE'].includes(method))
5
+ || (/^\/api\/prompts\/[a-zA-Z0-9-]+\/use$/.test(route) && method === 'POST');
6
+ if (!allowed) throw new Error('Unsupported Prompts operation');
7
+ }
@@ -10,6 +10,7 @@ let addButton = null;
10
10
  let composerRoot = null;
11
11
  let composerThread = null;
12
12
  let composerEditable = null;
13
+ let focusedEditable = null;
13
14
  let panel = null;
14
15
  let items = [];
15
16
  let generation = 0;
@@ -25,6 +26,9 @@ function appearanceLabel(key) {
25
26
  }
26
27
  function errorMessage(error) {
27
28
  const messages = {
29
+ "No available input in the focused window": "当前窗口没有可用输入框,请先打开聊天并确认 MCX 增强服务已开启。",
30
+ "Input changed before insertion": "输入框已切换,请重新选择提示词。",
31
+ "Enable Prompts in MineCodex first": "请先在 MineCodex 中开启提示词。",
28
32
  "Open a chat before using a prompt.": "请先打开一个对话,再使用提示词。",
29
33
  "Category was not found": "未找到该分类",
30
34
  "Prompt was not found": "未找到该提示词",
@@ -281,6 +285,8 @@ function renderList() {
281
285
  async () => {
282
286
  const current = generation;
283
287
  try {
288
+ if (!composerEditable?.isConnected || !config.codex.composer(composerEditable,{purpose:"insertion"}))
289
+ throw new Error("Input changed before insertion");
284
290
  window.__codexPersonalRuntime.insertText(item.body, composerEditable);
285
291
  // 插入同步成功后再记一次调用;失败和单纯浏览不计数。
286
292
  close();
@@ -338,10 +344,15 @@ function isOpenComposerMenu(node) {
338
344
  && (node.getAttribute("data-composer-navigation-target") === "add-context"
339
345
  || node.getAttribute("aria-haspopup") === "menu");
340
346
  }
347
+ function insertionComposer() {
348
+ // 官方 App 获得 iframe 焦点时,保留用户最后操作的同窗口输入框。
349
+ const target = document.activeElement?.tagName === "IFRAME" ? focusedEditable : null;
350
+ return config.codex.composer(target,{purpose:"insertion"});
351
+ }
341
352
  function sync() {
342
353
  if (disposed || signal.aborted) return;
343
- syncNavigation();
344
- const nativeComposer = config.codex.composer(null,{purpose:"insertion"});
354
+ if (config.management) return;
355
+ const nativeComposer = panel ? config.codex.composer(composerEditable,{purpose:"insertion"}) : insertionComposer();
345
356
  const add = nativeComposer?.add;
346
357
  const card = nativeComposer?.root;
347
358
  const thread = nativeComposer?.threadId ?? null;
@@ -357,6 +368,7 @@ function sync() {
357
368
  composerRoot = card;
358
369
  }
359
370
  composerEditable = nativeComposer?.editable ?? null;
371
+ insertionBridge?.remember(nativeComposer);
360
372
  if (!add) return;
361
373
  if (
362
374
  panel &&
@@ -402,8 +414,6 @@ function sync() {
402
414
  }
403
415
  // 页面与快捷调用面板共享数据;所有编辑只存在于页面。
404
416
  // 管理页面与编辑弹窗
405
- let navButton = null;
406
- let ownNavGroup = null;
407
417
  let page = null;
408
418
  let pageGeneration = 0;
409
419
  let activeCategory = "all";
@@ -414,8 +424,6 @@ let cardResizeObserver = null;
414
424
  let searchText = "";
415
425
  let editor = null;
416
426
  let pageFocus = null;
417
- const hiddenSurfaces = new Map();
418
- let navigationRail = null;
419
427
 
420
428
  function pageControl(label, handler, primary = false, tone = "secondary") {
421
429
  return config.ui.action(control(label, handler, `pm-command${primary ? " pm-primary" : ""}`), primary ? "primary" : tone);
@@ -428,23 +436,7 @@ function pageError(error) {
428
436
  (editor ?? page.querySelector(".pm-wrap") ?? page).append(message);
429
437
  }
430
438
  function positionPage() {
431
- if (!page) return;
432
- const rail = config.codex.rail();
433
- if (!rail) {
434
- closePage();
435
- return;
436
- }
437
- const rect = rail.getBoundingClientRect();
438
- page.style.left = `${rect.right}px`;
439
- page.style.top = `${rect.top}px`;
440
- page.style.right = "4px";
441
- page.style.bottom = "4px";
442
- }
443
- function syncNavigationSelection() {
444
- navigationRail?.select(Boolean(page));
445
- }
446
- function restoreNavigationSelection() {
447
- navigationRail?.select(false);
439
+ if (page) Object.assign(page.style, {inset:"0",borderRadius:"0"});
448
440
  }
449
441
  function closePage(restoreFocus = false, restoreNavigation = true) {
450
442
  dragState?.cancel();
@@ -456,12 +448,7 @@ function closePage(restoreFocus = false, restoreNavigation = true) {
456
448
  if (page) config.ui.releasePage(page, { restoreNavigation });
457
449
  page?.remove();
458
450
  page = null;
459
- restoreNavigationSelection();
460
451
  editor = null;
461
- for (const [surface, inert] of hiddenSurfaces) surface.inert = inert;
462
- hiddenSurfaces.clear();
463
- navButton?.removeAttribute("data-selected");
464
- navButton?.removeAttribute("aria-current");
465
452
  document.documentElement.removeAttribute("data-mcx-prompts-open");
466
453
  if (restoreFocus && pageFocus?.isConnected)
467
454
  pageFocus.focus({ preventScroll: true });
@@ -487,19 +474,9 @@ function openPage() {
487
474
  page = element("section");
488
475
  page.setAttribute(MARKER, "page");
489
476
  config.ui.page(page, { sidebar:"none", onNavigate:() => closePage(false, false) });
490
- page.setAttribute("aria-label", text("Prompts", "提示词"));
477
+ page.setAttribute("aria-label", text("Prompt", "提示词"));
491
478
  document.body.append(page);
492
- // 只隔离主内容的焦点,保留导航栏与原生窗口控件。
493
- for (const surface of document.querySelectorAll(
494
- '[class*="_PageSurface_"], [class*="_Workspace_"]',
495
- )) {
496
- hiddenSurfaces.set(surface, surface.inert);
497
- surface.inert = true;
498
- }
499
479
  document.documentElement.setAttribute("data-mcx-prompts-open", "");
500
- navButton?.setAttribute("data-selected", "");
501
- navButton?.setAttribute("aria-current", "page");
502
- syncNavigationSelection();
503
480
  positionPage();
504
481
  page.append(element("div", "pm-empty", text("Loading…", "加载中…")));
505
482
  return loadPage();
@@ -666,7 +643,7 @@ function renderPage() {
666
643
  searchField,
667
644
  pageControl(text("New prompt", "新建提示词"), () => editPrompt(), true),
668
645
  );
669
- heading.append(element("h1", "", text("Prompts", "提示词")), actions);
646
+ heading.append(element("h1", "", text("Prompt", "提示词")), actions);
670
647
  const tabs = element("div", "pm-tabs");
671
648
  tabs.setAttribute("role", "tablist");
672
649
  tabs.setAttribute("aria-label", text("Categories", "分类"));
@@ -818,31 +795,9 @@ function cardPreview(item) {
818
795
  }
819
796
  async function usePagePrompt(item, trigger) {
820
797
  trigger.disabled = true;
821
- closePage();
822
- try {
823
- // Page 归还原路由后等待输入框挂载;不自动发送消息。
824
- let editable = null;
825
- for (let attempt = 0; attempt < 40 && !signal.aborted; attempt++) {
826
- editable = config.codex.composer(null, { purpose: "insertion" })?.editable;
827
- if (editable) break;
828
- await new Promise((resolve) => setTimeout(resolve, 50));
829
- }
830
- if (signal.aborted) return;
831
- if (!editable) throw new Error("Open a chat before using a prompt.");
832
- window.__codexPersonalRuntime.insertText(item.body, editable);
833
- } catch (error) {
834
- await openPage();
835
- pageError(error);
836
- return;
837
- }
838
- try {
839
- await api(`/api/prompts/${encodeURIComponent(item.id)}/use`, {
840
- method: "POST", body: { action: "insert", success: true },
841
- });
842
- } catch (error) {
843
- // 正文已插入,计数失败不能再次插入。
844
- console.warn("[Prompts] Usage could not be saved", error);
845
- }
798
+ try { await config.insertPrompt(item.id); if (!signal.aborted) await loadPage(); }
799
+ catch (error) { if (!signal.aborted) pageError(error); }
800
+ finally { trigger.disabled = false; }
846
801
  }
847
802
  let imagePreview = null;
848
803
  let imagePreviewAnchor = null;
@@ -1031,7 +986,7 @@ function emptyState(searching) {
1031
986
  const empty = element("div", "pm-empty-state");
1032
987
  const art = element("span", "pm-empty-art", searching ? "🔎" : "📖");
1033
988
  art.setAttribute("role", "img");
1034
- art.setAttribute("aria-label", text("Prompts", "提示词"));
989
+ art.setAttribute("aria-label", text("Prompt", "提示词"));
1035
990
  empty.append(
1036
991
  art,
1037
992
  element(
@@ -1620,70 +1575,16 @@ function editCategory(category = null) {
1620
1575
  name.focus();
1621
1576
  }
1622
1577
  // 原生导航挂载与卸载
1623
- function syncNavigation() {
1624
- const rail = config.codex.rail();
1625
- if (!rail) {
1626
- navButton?.remove();
1627
- navButton = null;
1628
- if (page) closePage();
1629
- return;
1630
- }
1631
- const slot = config.codex.railSlot(ownNavGroup);
1632
- if (!slot) return;
1633
- const { template:source, scroll } = slot;
1634
- let group = slot.group;
1635
- if (!group) {
1636
- if (!ownNavGroup) {
1637
- ownNavGroup = element("div", "flex shrink-0 flex-col items-center gap-2");
1638
- ownNavGroup.setAttribute(MARKER, "nav-group");
1639
- }
1640
- if (ownNavGroup.parentElement !== scroll) scroll.append(ownNavGroup);
1641
- group = ownNavGroup;
1642
- }
1643
- if (!navButton?.isConnected) {
1644
- navigationRail?.restore();
1645
- navButton = source.cloneNode(false);
1646
- navButton.removeAttribute("id");
1647
- navButton.removeAttribute("title");
1648
- navButton.removeAttribute("aria-describedby");
1649
- navButton.removeAttribute("data-sidebar-destination");
1650
- navButton.removeAttribute("data-selected");
1651
- navButton.removeAttribute("aria-current");
1652
- navButton.removeAttribute("data-suppress-active-style");
1653
- navButton.setAttribute(MARKER, "nav");
1654
- navButton.setAttribute("aria-label", text("Prompts", "提示词"));
1655
- navButton.setAttribute("data-minecodex-tooltip", text("Prompts", "提示词"));
1656
- const inner = element("span", source.firstElementChild?.className ?? "");
1657
- const glyph = icon("prompt-nav");
1658
- glyph.setAttribute("aria-hidden", "true");
1659
- inner.append(glyph, element("span", "sr-only", text("Prompts", "提示词")));
1660
- navButton.append(inner);
1661
- navigationRail = config.ui.navigationRail(navButton, {
1662
- defaultIcon:glyph, selectedIcon:icon("prompt-nav-selected"),
1663
- });
1664
- navButton.addEventListener("click", openPage);
1665
- }
1666
- if (navButton.parentElement !== group || navButton !== group.lastElementChild)
1667
- group.append(navButton);
1668
- if (group !== ownNavGroup) ownNavGroup?.remove();
1669
- syncNavigationSelection();
1670
- positionPage();
1671
- }
1672
- document.addEventListener(
1673
- "click",
1674
- (event) => {
1675
- if (
1676
- page &&
1677
- event.target.closest("[data-app-navigation-rail] button") &&
1678
- !navButton?.contains(event.target)
1679
- )
1680
- closePage(false, false);
1681
- },
1682
- { signal, capture: true },
1683
- );
1684
- window.addEventListener("popstate", () => closePage(false, false), { signal });
1685
1578
  window.addEventListener("resize", positionPage, { signal });
1686
1579
 
1580
+ document.addEventListener("focusin", event => {
1581
+ if (config.management) return;
1582
+ const target = event.target.closest?.('[contenteditable="true"]');
1583
+ const focused = target ? config.codex.composer(target,{purpose:"insertion"}) : null;
1584
+ if (focused) focusedEditable = focused.editable;
1585
+ sync();
1586
+ }, {signal});
1587
+
1687
1588
  const observer = new MutationObserver(() => {
1688
1589
  if (syncQueued) return;
1689
1590
  syncQueued = true;
@@ -1692,7 +1593,7 @@ const observer = new MutationObserver(() => {
1692
1593
  sync();
1693
1594
  });
1694
1595
  });
1695
- observer.observe(document.documentElement, {
1596
+ if (!config.management) observer.observe(document.documentElement, {
1696
1597
  childList: true,
1697
1598
  subtree: true,
1698
1599
  attributes: true,
@@ -1745,24 +1646,25 @@ document.addEventListener(
1745
1646
  );
1746
1647
  window.addEventListener("resize", () => { clearQuickPreview(true); position(); }, { signal });
1747
1648
  window.addEventListener("scroll", position, { signal, capture: true });
1748
- sync();
1649
+ const insertionBridge = config.management ? null : installPromptInsertion(signal, {
1650
+ codex:config.codex, api, insertText:(body,editable) => window.__codexPersonalRuntime.insertText(body,editable),
1651
+ });
1652
+ if (config.management) openPage(); else sync();
1749
1653
 
1750
1654
  config.registerSelfCheck?.(() => {
1751
- const rail=config.codex.railSlot(ownNavGroup), composer=config.codex.composer(null,{purpose:"insertion"});
1752
- const checks=[{name:"prompts-navigation-anchor",available:Boolean(rail && navButton?.isConnected)}];
1753
- if (composer) checks.push({name:"quick-prompts-anchor",available:Boolean(composer.add && button?.isConnected && composerRoot === composer.root)});
1655
+ const composer=config.codex.composer(null,{purpose:"insertion"});
1656
+ const checks=composer ? [{name:"quick-prompts-anchor",available:Boolean(composer.add && button?.isConnected && composerEditable === composer.editable)}] : [];
1754
1657
  const available=checks.every(check=>check.available);
1755
- return {state:available ? "available" : "unavailable",reason:available ? null : "prompts-anchor-unavailable",checks,
1756
- scopes:[{name:"navigation",element:navButton},{name:"composer",element:composer?.root},{name:"prompts-page",element:page},{name:"prompts-popup",element:pagePopup},{name:"quick-prompts",element:panel}]};
1658
+ return {state:composer ? available ? "available" : "unavailable" : "not-applicable",reason:available ? null : "prompts-anchor-unavailable",checks,
1659
+ scopes:[{name:"composer",element:composer?.root},{name:"quick-prompts",element:panel}]};
1757
1660
  });
1758
1661
 
1759
1662
  return () => {
1760
1663
  disposed = true;
1664
+
1761
1665
  observer.disconnect();
1762
1666
  close();
1763
1667
  button?.remove();
1764
1668
  closePage();
1765
- navButton?.remove();
1766
- ownNavGroup?.remove();
1767
1669
  style.remove();
1768
1670
  };
@@ -8,8 +8,6 @@
8
8
  "back": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M9.87117 11.6289C10.0762 11.8339 10.0762 12.166 9.87117 12.3711C9.66614 12.5761 9.334 12.5761 9.12898 12.3711L5.36433 8.60641C5.0296 8.27129 5.0296 7.72863 5.36433 7.39351L9.12898 3.62887C9.334 3.42384 9.66614 3.42384 9.87117 3.62887C10.0762 3.83389 10.0762 4.16603 9.87117 4.37105L6.24226 7.99996L9.87117 11.6289Z\" fill=\"currentColor\"/></svg>",
9
9
  "settings": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M6.33374 8.97461C7.54786 8.97484 8.56163 9.83236 8.80347 10.9746H13.3337C13.6235 10.9749 13.8591 11.2102 13.8591 11.5C13.8591 11.7898 13.6235 12.0251 13.3337 12.0254H8.80347C8.56163 13.1676 7.54786 14.0252 6.33374 14.0254C5.11907 14.0254 4.10455 13.1673 3.86304 12.0244L2.66675 12.0254C2.37691 12.0253 2.14138 11.7899 2.14136 11.5C2.14136 11.2102 2.37693 10.9748 2.66675 10.9746H3.86304C4.10491 9.83221 5.11941 8.97461 6.33374 8.97461ZM6.33374 10.0254C5.51912 10.0254 4.85913 10.6854 4.85913 11.5C4.85913 12.3146 5.51912 12.9746 6.33374 12.9746C7.14814 12.9743 7.80835 12.3145 7.80835 11.5C7.80835 10.6855 7.14814 10.0257 6.33374 10.0254Z\" fill=\"currentColor\"/> <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M9.66675 1.97461C10.881 1.97461 11.8935 2.83236 12.1355 3.97461H13.3328C13.6227 3.97459 13.8581 4.21005 13.8582 4.5C13.8581 4.78993 13.6227 5.02537 13.3328 5.02539H12.1365C11.8946 6.16774 10.881 7.02539 9.66675 7.02539C8.45245 7.02539 7.43893 6.16774 7.19702 5.02539H2.66675C2.3768 5.02539 2.14136 4.78995 2.14136 4.5C2.14136 4.21005 2.3768 3.97461 2.66675 3.97461H7.19702C7.43893 2.83226 8.45245 1.97461 9.66675 1.97461ZM9.66675 3.02539C8.85213 3.02539 8.19214 3.68538 8.19214 4.5C8.19214 5.31462 8.85213 5.97461 9.66675 5.97461C10.4814 5.97461 11.1414 5.31462 11.1414 4.5C11.1414 3.68538 10.4814 3.02539 9.66675 3.02539Z\" fill=\"currentColor\"/></svg>",
10
10
  "drag": "<svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n>\n <path d=\"M4 5h16\" />\n <path d=\"M4 12h16\" />\n <path d=\"M4 19h16\" />\n</svg>\n",
11
- "prompt-nav": "<svg\n class=\"lucide lucide-book-open\"\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"20\"\n height=\"20\"\n viewBox=\"-1.5 -1.5 27 27\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"1.8\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n>\n <path d=\"M12 7v14 M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z\" />\n</svg>",
12
- "prompt-nav-selected": "<svg width=\"20\" height=\"20\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 256 256\" fill=\"currentColor\"><path d=\"M240,56V200a8,8,0,0,1-8,8H160a24,24,0,0,0-24,23.94,7.9,7.9,0,0,1-5.12,7.55A8,8,0,0,1,120,232a24,24,0,0,0-24-24H24a8,8,0,0,1-8-8V56a8,8,0,0,1,8-8H88a32,32,0,0,1,32,32v87.73a8.17,8.17,0,0,0,7.47,8.25,8,8,0,0,0,8.53-8V80a32,32,0,0,1,32-32h64A8,8,0,0,1,240,56Z\"/></svg>",
13
11
  "layout-grid": "<!-- Lucide, ISC license; see assets/brand/references/lucide-LICENSE.txt -->\n<svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n>\n <rect width=\"7\" height=\"7\" x=\"3\" y=\"3\" rx=\"1\" />\n <rect width=\"7\" height=\"7\" x=\"14\" y=\"3\" rx=\"1\" />\n <rect width=\"7\" height=\"7\" x=\"14\" y=\"14\" rx=\"1\" />\n <rect width=\"7\" height=\"7\" x=\"3\" y=\"14\" rx=\"1\" />\n</svg>\n",
14
12
  "list": "<!-- Lucide, ISC license; see assets/brand/references/lucide-LICENSE.txt -->\n<svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n>\n <path d=\"M3 5h.01\" />\n <path d=\"M3 12h.01\" />\n <path d=\"M3 19h.01\" />\n <path d=\"M8 5h13\" />\n <path d=\"M8 12h13\" />\n <path d=\"M8 19h13\" />\n</svg>\n",
15
13
  "square-pen": "<!-- Lucide, ISC license; see assets/brand/references/lucide-LICENSE.txt -->\n<svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n>\n <path d=\"M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7\" />\n <path d=\"M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z\" />\n</svg>\n"
@@ -0,0 +1,5 @@
1
+ import {readFile} from 'node:fs/promises';
2
+ import {installPromptInsertion} from './adapters/codex/insertion.mjs';
3
+ export async function createPageScriptSource() {
4
+ return `const installPromptInsertion = ${installPromptInsertion.toString()};\n${await readFile(new URL('./frontend/entry.mjs',import.meta.url),'utf8')}`;
5
+ }