minecodex 2.0.4 → 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.
Files changed (100) hide show
  1. package/DESIGN.md +40 -1
  2. package/README.md +9 -3
  3. package/apps/dashboard/DESIGN.md +53 -31
  4. package/apps/dashboard/app.js +55 -39
  5. package/apps/dashboard/assets/OFL.txt +93 -0
  6. package/apps/dashboard/assets/info.svg +15 -0
  7. package/apps/dashboard/assets/moon.svg +13 -0
  8. package/apps/dashboard/assets/refresh-cw.svg +16 -0
  9. package/apps/dashboard/assets/silkscreen.ttf +0 -0
  10. package/apps/dashboard/assets/sun.svg +21 -0
  11. package/apps/dashboard/assets/terrain.js +89 -0
  12. package/apps/dashboard/assets/triangle-alert.svg +15 -0
  13. package/apps/dashboard/favicon.svg +12 -17
  14. package/apps/dashboard/index.html +24 -18
  15. package/apps/dashboard/styles.css +147 -181
  16. package/assets/brand/README.md +19 -0
  17. package/assets/brand/minecodex-icon-dark.svg +9 -0
  18. package/assets/brand/minecodex-icon-light.svg +9 -0
  19. package/assets/brand/minecodex-icon.svg +8 -0
  20. package/assets/brand/references/lucide-LICENSE.txt +43 -0
  21. package/assets/readme/hero.svg +93 -0
  22. package/features/hide-upsell-banner/README.md +7 -5
  23. package/features/hide-upsell-banner/codex-feature.json +2 -2
  24. package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +30 -12
  25. package/features/images/DESIGN.md +117 -41
  26. package/features/images/README.md +26 -16
  27. package/features/images/codex-feature.json +6 -3
  28. package/features/images/src/adapters/codex/image-prompt.mjs +24 -2
  29. package/features/images/src/adapters/codex/image-sources.mjs +19 -0
  30. package/features/images/src/adapters/codex/library-api.mjs +2 -0
  31. package/features/images/src/adapters/codex/library-viewer.mjs +26 -2
  32. package/features/images/src/adapters/codex/native-image-modules.mjs +211 -0
  33. package/features/images/src/adapters/codex/native-image-source.mjs +449 -0
  34. package/features/images/src/backend/local-image-source.mjs +138 -0
  35. package/features/images/src/frontend/cloud-image-actions.mjs +41 -0
  36. package/features/images/src/frontend/local-image-source.mjs +195 -0
  37. package/features/images/src/library-prompt-page.mjs +163 -53
  38. package/features/images/src/library-prompt-source.mjs +9 -3
  39. package/features/images/src/prompt-index.mjs +14 -2
  40. package/features/model-slider/DESIGN.md +2 -0
  41. package/features/model-slider/codex-feature.json +2 -2
  42. package/features/model-slider/src/frontend/install.mjs +78 -11
  43. package/features/notes/DESIGN.md +7 -3
  44. package/features/notes/codex-feature.json +2 -2
  45. package/features/notes/src/frontend/entry.mjs +26 -9
  46. package/features/session-status/DESIGN.md +4 -2
  47. package/features/session-status/codex-feature.json +2 -2
  48. package/features/session-status/src/adapters/codex/session-log.mjs +2 -1
  49. package/features/session-status/src/domain/session-stats.mjs +17 -9
  50. package/features/session-status/src/session-status.mjs +47 -11
  51. package/features/sidebar-focus/DESIGN.md +4 -4
  52. package/features/sidebar-focus/codex-feature.json +3 -2
  53. package/features/sidebar-focus/src/adapters/codex/renderer.mjs +1 -1
  54. package/features/sidebar-focus/src/sidebar-focus.mjs +9 -7
  55. package/features/sites/DESIGN.md +25 -3
  56. package/features/sites/README.md +6 -2
  57. package/features/sites/codex-feature.json +3 -2
  58. package/features/sites/src/adapters/codex/renderer.mjs +25 -4
  59. package/features/sites/src/backend/project-sites.mjs +18 -6
  60. package/features/sites/src/backend/sites-service.mjs +22 -10
  61. package/features/sites/src/domain/discovery-candidates.mjs +53 -0
  62. package/features/sites/src/frontend/entry.mjs +76 -38
  63. package/features/sites/src/frontend/styles.css +3 -3
  64. package/features/unread-inbox/DESIGN.md +24 -25
  65. package/features/unread-inbox/codex-feature.json +3 -2
  66. package/features/unread-inbox/src/adapters/codex/modules.mjs +52 -0
  67. package/features/unread-inbox/src/adapters/codex/renderer.mjs +90 -0
  68. package/features/unread-inbox/src/domain/notifications.mjs +21 -0
  69. package/features/unread-inbox/src/injection-source.mjs +5 -2
  70. package/features/unread-inbox/src/unread-inbox.mjs +212 -297
  71. package/features/usage-meter/codex-feature.json +2 -2
  72. package/package.json +11 -3
  73. package/packages/cli/native/MineCodex.app/Contents/Info.plist +4 -2
  74. package/packages/cli/native/MineCodex.app/Contents/MacOS/MineCodexServiceManager +0 -0
  75. package/packages/cli/native/MineCodex.app/Contents/Resources/MineCodex.icns +0 -0
  76. package/packages/cli/native/MineCodex.app/Contents/_CodeSignature/CodeResources +11 -0
  77. package/packages/cli/native/README.md +31 -0
  78. package/packages/cli/src/browser-control-panel.mjs +48 -0
  79. package/packages/cli/src/commands.mjs +3 -1
  80. package/packages/cli/src/control-server.mjs +6 -3
  81. package/packages/cli/src/platform.mjs +5 -0
  82. package/packages/codex-adapter/README.md +2 -1
  83. package/packages/codex-adapter/src/renderer/environment.mjs +24 -6
  84. package/packages/runtime-host/README.md +10 -5
  85. package/packages/runtime-host/src/codex-injection.mjs +13 -9
  86. package/packages/runtime-host/src/codex-runtime.mjs +26 -5
  87. package/packages/runtime-host/src/feature-registry.mjs +1 -0
  88. package/packages/ui/DESIGN.md +9 -2
  89. package/packages/ui/assets/arrow-up-right.svg +14 -0
  90. package/packages/ui/assets/lucide-LICENSE.txt +43 -0
  91. package/packages/ui/src/components/account-menu.mjs +74 -0
  92. package/packages/ui/src/components/button.mjs +2 -0
  93. package/packages/ui/src/components/navigation-rail.mjs +18 -10
  94. package/packages/ui/src/components/page.mjs +9 -2
  95. package/packages/ui/src/foundation/layers.mjs +1 -0
  96. package/packages/ui/src/install.mjs +6 -1
  97. package/packages/ui/src/native/account-menu.mjs +21 -0
  98. package/packages/ui/src/native/navigation-rail.mjs +6 -1
  99. package/packages/ui/src/native/page.mjs +38 -0
  100. package/packages/ui/src/native/tokens.mjs +4 -0
@@ -0,0 +1,195 @@
1
+ // 只编排来源与加载状态;图库、标签、菜单及详情由原生 Adapter 承载。
2
+ export function installLocalImageSource(config, signal, sources, native) {
3
+ let mounted;
4
+ let revision = 0;
5
+ let pendingPrompt;
6
+ const text = (en, zh) => (document.documentElement.lang || navigator.language).startsWith("zh") ? zh : en;
7
+
8
+ function requestPrompt(image) {
9
+ pendingPrompt = image.id;
10
+ document.dispatchEvent(new CustomEvent("minecodex:image-prompt", {detail:{imageId:image.id}}));
11
+ }
12
+
13
+ function unmount() {
14
+ revision++;
15
+ if (!mounted) return;
16
+ const record = mounted;
17
+ mounted = null;
18
+ record.observer?.disconnect();
19
+ record.view?.dispose();
20
+ record.nav?.dispose();
21
+ record.entry.tabs.hidden = record.tabsHidden;
22
+ record.entry.content.hidden = record.contentHidden;
23
+ record.entry.search?.removeEventListener("input", record.onSearch, true);
24
+ record.entry.search?.removeEventListener("keydown", record.onSearchKey, true);
25
+ if (record.entry.search) record.entry.search.value = record.searchValue;
26
+ record.navigation.remove();
27
+ record.area.remove();
28
+ }
29
+
30
+ function error(record) {
31
+ record.message.textContent = text("Could not load local images.", "本地图片加载失败。");
32
+ record.retry.hidden = false;
33
+ }
34
+
35
+ async function fetchPage(record, reset = false) {
36
+ if (record.fetching && !reset) return;
37
+ const current = ++revision;
38
+ record.fetching = true;
39
+ record.retry.hidden = true;
40
+ if (reset) {
41
+ record.images = [];
42
+ record.offset = 0;
43
+ record.total = 0;
44
+ record.view.render({images:[], loading:true});
45
+ }
46
+ try {
47
+ const page = await config.requestHostAction("local-image-source", {
48
+ operation:"list", offset:record.offset, query:record.query,
49
+ });
50
+ if (signal.aborted || mounted !== record || revision !== current || !record.active) return;
51
+ record.images.push(...page.images);
52
+ record.offset += page.images.length;
53
+ record.total = page.total;
54
+ record.view.render({images:record.images, loading:false});
55
+ record.message.textContent = page.total ? "" : text("No local images found.", "没有找到本地图片。");
56
+ record.sentinel.hidden = record.offset >= record.total;
57
+ } catch {
58
+ if (mounted === record && revision === current) {
59
+ record.view.render({images:record.images, loading:false});
60
+ error(record);
61
+ }
62
+ } finally {
63
+ if (mounted === record && revision === current) record.fetching = false;
64
+ }
65
+ }
66
+
67
+ function activate(record, key) {
68
+ revision++;
69
+ record.view?.clearSelection();
70
+ record.active = key === "imagegen";
71
+ record.entry.content.hidden = record.active || record.contentHidden;
72
+ record.area.hidden = !record.active;
73
+ if (record.entry.search) record.entry.search.value = record.active ? record.query : record.searchValue;
74
+ record.nav.render(key);
75
+ if (record.active) void fetchPage(record, true);
76
+ }
77
+
78
+ async function scan() {
79
+ if (signal.aborted) return;
80
+ const entry = sources.read();
81
+ if (mounted && mounted.entry.tabs === entry?.tabs) {
82
+ if (!mounted.active) mounted.nav?.render(entry.selectedKey);
83
+ return;
84
+ }
85
+ unmount();
86
+ if (!entry) return;
87
+ const navigation = document.createElement("div");
88
+ navigation.dataset.minecodexImageNavigation = "true";
89
+ entry.tabs.parentElement.append(navigation);
90
+ const area = document.createElement("section");
91
+ area.className = entry.content.className;
92
+ area.id = "minecodex-image-gen-content";
93
+ area.hidden = true;
94
+ const container = document.createElement("div");
95
+ const message = document.createElement("p");
96
+ message.className = "text-sm text-secondary";
97
+ const retry = document.createElement("button");
98
+ retry.type = "button";
99
+ retry.className = "rounded-full bg-text/5 px-4 py-2 text-sm text-default";
100
+ retry.textContent = text("Try again", "重试");
101
+ retry.hidden = true;
102
+ const sentinel = document.createElement("div");
103
+ sentinel.style.height = "1px";
104
+ sentinel.hidden = true;
105
+ sentinel.setAttribute("aria-hidden", "true");
106
+ area.append(container, message, retry, sentinel);
107
+ entry.content.after(area);
108
+ const record = {
109
+ entry, navigation, area, message, retry, sentinel,
110
+ tabsHidden:entry.tabs.hidden, contentHidden:entry.content.hidden,
111
+ searchValue:entry.search?.value ?? "", images:[], offset:0, total:0,
112
+ query:"", fetching:false, active:false,
113
+ };
114
+ mounted = record;
115
+ try {
116
+ const results = await Promise.allSettled([
117
+ native.mountNavigation(navigation, entry.tabs, key => activate(record, key)),
118
+ native.mount(container, entry.tabs, {
119
+ onOpen:(image, open) => open(image), onPrompt:requestPrompt,
120
+ onHidden:ids=>{
121
+ const hidden=new Set(ids),removed=record.images.filter(image=>hidden.has(image.id)).length;
122
+ record.images=record.images.filter(image=>!hidden.has(image.id));
123
+ record.offset=Math.max(0,record.offset-removed);record.total=Math.max(0,record.total-removed);
124
+ record.sentinel.hidden=record.offset>=record.total;
125
+ },
126
+ onRetry:error => {
127
+ console.error("MineCodex native Images rendering",error);
128
+ if (mounted !== record) return;
129
+ unmount();
130
+ config.codex.report("images.local-source", false, "Native image rendering failed");
131
+ },
132
+ }),
133
+ ]);
134
+ // 任一原生能力失败时也释放已建立的另一棵 root。
135
+ if (results.some(result => result.status === "rejected")) {
136
+ for (const result of results) if (result.status === "fulfilled") result.value.dispose();
137
+ throw results.find(result => result.status === "rejected").reason;
138
+ }
139
+ const [nav, view] = results.map(result => result.value);
140
+ if (signal.aborted || mounted !== record) {
141
+ nav.dispose();
142
+ view.dispose();
143
+ return;
144
+ }
145
+ record.nav = nav;
146
+ record.view = view;
147
+ entry.tabs.hidden = true;
148
+ nav.render(entry.selectedKey);
149
+ retry.onclick = () => fetchPage(record, true);
150
+ record.observer = new IntersectionObserver(entries => {
151
+ if (entries.some(entry => entry.isIntersecting) && record.active && !record.fetching && record.offset < record.total) {
152
+ void fetchPage(record);
153
+ }
154
+ }, {rootMargin:"400px"});
155
+ record.observer.observe(sentinel);
156
+ record.onSearch = event => {
157
+ if (!record.active) return;
158
+ event.stopImmediatePropagation();
159
+ record.query = entry.search.value.slice(0, 256);
160
+ void fetchPage(record, true);
161
+ };
162
+ record.onSearchKey = event => {
163
+ if (record.active && event.key === "Enter") {
164
+ event.preventDefault();
165
+ event.stopImmediatePropagation();
166
+ }
167
+ };
168
+ entry.search?.addEventListener("input", record.onSearch, true);
169
+ entry.search?.addEventListener("keydown", record.onSearchKey, true);
170
+ } catch {
171
+ if (mounted === record) {
172
+ unmount();
173
+ config.codex.report("images.local-source", false, "Native image capability unavailable");
174
+ }
175
+ }
176
+ }
177
+
178
+ let scheduled = false;
179
+ const observer = new MutationObserver(() => {
180
+ if (scheduled) return;
181
+ scheduled = true;
182
+ queueMicrotask(() => {scheduled = false; void scan();});
183
+ });
184
+ observer.observe(document.body, {childList:true, subtree:true, attributes:true, attributeFilter:["aria-selected"]});
185
+ void scan();
186
+ let cleaned = false;
187
+ const cleanup = () => {if(cleaned)return;cleaned=true;observer.disconnect();unmount();pendingPrompt=null;};
188
+ signal.addEventListener("abort", cleanup, {once:true});
189
+ return {
190
+ cleanup,
191
+ readViewer:header => native.readViewer(header),
192
+ pendingPrompt:() => pendingPrompt,
193
+ consumePrompt:() => {pendingPrompt = null;},
194
+ };
195
+ }
@@ -1,13 +1,15 @@
1
- // 仅增强原生 Library 图片详情;不创建第二个图库或侧栏入口。
1
+ // 原生 Library 图片详情的提示词增强;Image Gen 由同功能的独立安装器承载。
2
2
  const PANEL_ID = "minecodex-library-prompt";
3
3
  const zh = () => (document.documentElement.lang || navigator.language || "en").toLowerCase().startsWith("zh");
4
4
  const label = (en, cn) => zh() ? cn : en;
5
5
  let mounted = null;
6
+ let localImageSource = null;
6
7
  const libraryApi=createLibraryApi();
7
8
  let scheduled = false;
8
9
  let candidate = null;
9
10
 
10
- const libraryImage=createLibraryViewerAdapter(config.codex).read;
11
+ const libraryViewer=createLibraryViewerAdapter(config.codex,{readViewer:header=>localImageSource?.readViewer(header)});
12
+ const libraryImage=libraryViewer.read;
11
13
  function element(tag, className, text) {
12
14
  const node = document.createElement(tag);
13
15
  node.className = className;
@@ -26,79 +28,128 @@ function copyGlyph(copied = false) {
26
28
  }
27
29
 
28
30
  function closePanel(record, restoreFocus = false) {
29
- record.request?.abort();
30
- record.request = null;
31
31
  record.panel.hidden = true;
32
- record.button.setAttribute("aria-expanded", "false");
32
+
33
33
  if (restoreFocus && record.button.isConnected) record.button.focus();
34
34
  }
35
35
 
36
36
  function showPrompt(record) {
37
37
  record.panel.hidden = false;
38
- record.button.setAttribute("aria-expanded", "true");
38
+
39
39
  record.close.focus();
40
40
  }
41
41
 
42
42
  async function resolvePrompt(file, request) {
43
- if (!file.origination_thread_id || !file.origination_message_id) return null;
44
43
  request.signal.throwIfAborted();
45
- const conversation = await libraryApi.conversation(file.origination_thread_id,AbortSignal.any([signal,request.signal,AbortSignal.timeout(25_000)]));
46
- return promptFromConversation(file, conversation);
44
+ if(file.kind === "local") return config.requestHostAction("local-image-source",{operation:"prompt",id:file.id});
45
+ if(!file.origination_thread_id || !file.origination_message_id)return null;
46
+ const conversation=await libraryApi.conversation(file.origination_thread_id,AbortSignal.any([signal,request.signal,AbortSignal.timeout(25_000)]));
47
+ return promptFromConversation(file,conversation);
47
48
  }
48
49
 
49
50
  function unmount() {
50
51
  if (!mounted) return;
51
52
  closePanel(mounted);
52
53
  clearTimeout(mounted.copyTimer);
53
- mounted.button.remove();
54
+ mounted.menuItem?.remove();
55
+ mounted.menuCleanup?.();
54
56
  mounted.panel.remove();
55
57
  mounted = null;
56
58
  }
57
59
 
60
+ function syncMenu(record) {
61
+ const anchor = libraryViewer.promptMenu(record.file);
62
+ if (record.menuItem?.isConnected && record.menuItem.parentElement === anchor?.original.parentElement) return;
63
+ record.menuItem?.remove();
64
+ record.menuCleanup?.();
65
+ record.menuItem = null;
66
+ record.menuCleanup = null;
67
+ if (!anchor) return;
68
+ const {menu,original} = anchor;
69
+ const item = element("div", original.className);
70
+ item.setAttribute("role", "menuitem");
71
+ item.tabIndex = -1;
72
+ item.dataset.minecodexLibraryPrompt = "menuitem";
73
+ const row = original.firstElementChild.cloneNode(true);
74
+ row.querySelector(".truncate").textContent = label("View prompts", "查看提示词");
75
+ // 使用项目现有 Prompts 图形,保留原生菜单的紧凑/标准图标布局。
76
+ const glyph = document.querySelector('[aria-label="Prompts"] svg');
77
+ if (glyph) for (const svg of row.querySelectorAll("svg")) {
78
+ const icon = glyph.cloneNode(true);
79
+ icon.setAttribute("width", svg.getAttribute("width"));
80
+ icon.setAttribute("height", svg.getAttribute("height"));
81
+ svg.replaceWith(icon);
82
+ }
83
+ item.append(row);
84
+ original.before(item);
85
+ record.menuItem = item;
86
+ const open = event => {
87
+ event.preventDefault();
88
+ event.stopPropagation();
89
+ menu.addEventListener("focusScope.autoFocusOnUnmount", event => {
90
+ event.preventDefault();
91
+ if (mounted?.identity === record.identity) mounted.close.focus();
92
+ }, {once:true});
93
+ // 原生菜单在 document capture 处理 Escape;阻止它再冒泡关闭图片视口。
94
+ const stopEscape = event => event.stopPropagation();
95
+ menu.addEventListener("keydown", stopEscape, {once:true});
96
+ menu.dispatchEvent(new KeyboardEvent("keydown", {key:"Escape",bubbles:true,cancelable:true}));
97
+ menu.removeEventListener("keydown", stopEscape);
98
+ setTimeout(() => {
99
+ // 原生关闭菜单可能重建工具栏;把打开意图交给同一图片的新实例。
100
+ if (mounted?.identity === record.identity) showPrompt(mounted);
101
+ }, 0);
102
+ };
103
+ item.addEventListener("click", open);
104
+ item.addEventListener("pointermove", () => item.focus());
105
+ const keyboard = event => {
106
+ if (event.target === item && ["Enter", " "].includes(event.key)) return open(event);
107
+ if (!["ArrowUp", "ArrowDown"].includes(event.key)) return;
108
+ const items = [...menu.querySelectorAll('[role="menuitem"]')];
109
+ const index = items.indexOf(document.activeElement);
110
+ const next = items[(index + (event.key === "ArrowDown" ? 1 : -1) + items.length) % items.length];
111
+ // 仅补上未注册到原生 collection 的注入项,其他按键仍交给原生菜单。
112
+ if (index >= 0 && (document.activeElement === item || next === item)) {
113
+ event.preventDefault();event.stopImmediatePropagation();next.focus();
114
+ }
115
+ };
116
+ menu.addEventListener("keydown", keyboard, true);
117
+ record.menuCleanup = () => menu.removeEventListener("keydown", keyboard, true);
118
+ }
119
+
58
120
  async function scan() {
59
121
  scheduled = false;
60
122
  if (signal.aborted) return;
61
123
  const header = [...document.querySelectorAll('[data-testid="viewer-header"]')].find(header => header.getBoundingClientRect().width > 0 && libraryImage(header));
62
124
  const file = header && libraryImage(header);
125
+ const toolbar = header && libraryViewer.toolbar(header);
63
126
  const identity = file && JSON.stringify(file);
64
127
  if (candidate?.header === header && candidate?.identity === identity
65
- && (!mounted || (mounted.button.isConnected && mounted.panel.isConnected))) return;
128
+ && candidate?.zoom === toolbar?.zoom && candidate?.host === toolbar?.host
129
+ && (!mounted || (mounted.button.isConnected && mounted.panel.isConnected))) {
130
+ if (mounted) {syncMenu(mounted);openPendingLocalPrompt();}
131
+ return;
132
+ }
66
133
  candidate?.request.abort();
67
134
  unmount();
68
135
  candidate = null;
69
- if (!file) return;
70
- const next = { header, identity, request: new AbortController() };
136
+ // 等原生图片工具栏就绪再解析;否则早到的结果会把同图后续扫描短路。
137
+ if (!file || !toolbar || (file.kind !== "local" && (!file.origination_thread_id || !file.origination_message_id))) return;
138
+ const next = { header, identity, zoom:toolbar.zoom, host:toolbar.host, request: new AbortController() };
71
139
  candidate = next;
72
- let prompt;
73
- try { prompt = await resolvePrompt(file, next.request); }
74
- catch { return; }
75
- // 异步结果只能写回当前图片;没有提示词时不展示入口。
76
- if (!prompt?.text || signal.aborted || candidate !== next || !header.isConnected) return;
77
- // 缩放项在图片详情中是百分比菜单;沿用它的胶囊类,不固定构建 hash。
78
- const zoom = [...header.querySelectorAll('button[aria-haspopup="menu"]')].find(button => /^\d+(?:\.\d+)?%$/.test(button.textContent.trim()));
79
- const group = zoom?.parentElement?.parentElement;
80
- const host = header.closest('[role="presentation"]');
81
- if (!zoom || !group || !host) return;
82
- const capsuleClass = zoom.className.split(/\s+/).filter(name => !/^_(?:dropdown|zoom)_/.test(name)).join(" ");
83
- const button = element("button", capsuleClass, label("Prompt", "提示词"));
84
- button.type = "button";
85
- button.dataset.minecodexLibraryPrompt = "trigger";
86
- button.setAttribute("aria-expanded", "false");
87
- button.setAttribute("aria-controls", PANEL_ID);
88
- button.style.width = "max-content";
89
- button.style.minWidth = "0";
90
- button.style.flex = "0 0 auto";
91
- button.style.paddingInline = "calc(var(--mcx-spacing) * 3)";
92
- group.prepend(button);
93
- // 对齐原生 Library Version history 的面板、标题和内容区 token。
94
- const panel = element("aside", "absolute inset-x-2 top-toolbar bottom-2 z-30 ms-auto mt-2 flex max-w-75 flex-col overflow-hidden rounded-2xl border border-default bg-surface shadow-lg select-none");
140
+ const {host,trigger:button} = toolbar;
141
+ // 对齐当前原生 File info 的实体材质、尺寸和内容区。
142
+ const panel = element("aside", "absolute end-2 bottom-2 z-40 flex min-h-0 flex-col overflow-hidden rounded-2xl bg-surface-elevated-secondary electron:elevation-prominent text-default select-none");
95
143
  panel.id = PANEL_ID;
96
144
  panel.setAttribute("aria-label", label("Image prompt", "图片提示词"));
97
145
  panel.hidden = true;
98
146
  panel.style.setProperty("-webkit-app-region", "no-drag");
99
- panel.style.paddingBottom = "calc(var(--mcx-spacing) * 3)";
100
- const heading = element("div", "flex items-center justify-between py-2 ps-4 pe-2");
101
- const close = element("button", "flex size-8 shrink-0 items-center justify-center rounded-full text-default hover:bg-primary-ghost-hover focus-visible:outline-ring");
147
+ // File info 的 pt-13 与包装层 pt-px 合计;窄视口保留两侧 gutter。
148
+ panel.style.top = "calc(var(--mcx-spacing) * 13 + 1px)";
149
+ panel.style.width = "calc(var(--mcx-spacing) * 74)";
150
+ panel.style.maxWidth = "calc(100% - var(--mcx-spacing) * 4)";
151
+ const heading = element("div", "flex h-11 shrink-0 items-center justify-between gap-2 py-1 ps-4 pe-2");
152
+ const close = element("button", "no-drag cursor-interaction select-none disabled:cursor-default disabled:opacity-40 focus:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0 flex border gap-1 rounded-full text-default bg-text/5 not-disabled:not-aria-disabled:hover:bg-text/10 border-transparent button-toolbar py-0 text-sm leading-[18px] aspect-square shrink-0 items-center justify-center !px-0");
102
153
  close.type = "button";
103
154
  close.setAttribute("aria-label", label("Close prompt", "关闭提示词"));
104
155
  // 直接复用原生关闭图标,避免手绘近似图形。
@@ -109,34 +160,82 @@ async function scan() {
109
160
  const copy = element("button", close.className);
110
161
  copy.type = "button";
111
162
  function copyFeedback(copied = false, failed = false) {
112
- const text = failed ? label("Try copying again", "重试复制") : copied ? label("Copied", "已复制") : label("Copy prompt", "复制提示词");
163
+ const text = failed ? label("Try copying again", "重试复制") : copied ? label("Copied", "已复制") : record?.prompt?.kind === "request" ? label("Copy original request", "复制原始请求") : label("Copy prompt", "复制提示词");
113
164
  copy.setAttribute("data-minecodex-tooltip", text);
114
165
  copy.setAttribute("aria-label", text);
115
166
  copy.replaceChildren(copyGlyph(copied));
116
167
  }
117
- copyFeedback();
118
168
  actions.append(copy, close);
119
- heading.append(element("span", "text-base font-semibold text-default", label("Prompt", "提示词")), actions);
120
- const body = element("div", "min-h-0 flex-1 overflow-y-auto px-3");
121
- body.style.paddingTop = "var(--mcx-spacing)";
122
- body.append(
123
- element("p", "px-1 text-sm leading-5 text-default whitespace-pre-wrap break-words select-text", prompt.text),
124
- );
169
+ const title = element("span", "text-base font-semibold text-default select-none", label("Prompt", "提示词"));
170
+ const titleHeading = element("h2", "min-w-0 flex-1 truncate");
171
+ titleHeading.append(title);
172
+ heading.append(titleHeading, actions);
173
+ const body = element("div", "min-h-0 flex-1 overflow-y-auto px-3 py-4");
125
174
  panel.append(heading, body);
126
175
  host.append(panel);
127
- const record = { header, file, identity, button, panel, body, close, request: null,
176
+ const record = { header, file, identity, button, panel, body, close, copy, prompt: null, state: "loading", request: next.request,
128
177
  refreshLocale() {
129
- button.textContent = label("Prompt", "提示词");
178
+ if (record.menuItem) record.menuItem.querySelector(".truncate").textContent = label("View prompts", "查看提示词");
130
179
  panel.setAttribute("aria-label", label("Image prompt", "图片提示词"));
131
180
  close.setAttribute("aria-label", label("Close prompt", "关闭提示词"));
132
- heading.firstElementChild.textContent = label("Prompt", "提示词");
181
+ title.textContent = label("Prompt", "提示词");
133
182
  copyFeedback();
183
+ renderBody();
134
184
  },
135
185
  };
186
+ function renderBody() {
187
+ body.replaceChildren();
188
+ body.setAttribute("aria-busy", String(record.state === "loading"));
189
+ copy.disabled = record.state !== "ready";
190
+ copyFeedback();
191
+ if (record.state === "loading") {
192
+ const skeleton = element("div", "flex flex-col gap-2 px-1");
193
+ skeleton.setAttribute("aria-hidden", "true");
194
+ for (const width of ["100%", "92%", "100%", "68%"]) {
195
+ const line = element("div", "h-3 rounded-full bg-text/5");
196
+ line.style.width = width;
197
+ skeleton.append(line);
198
+ }
199
+ body.append(element("span", "sr-only", label("Loading prompt", "正在加载提示词")), skeleton);
200
+ } else if (record.state === "ready") {
201
+ if (record.prompt.kind === "request") {
202
+ body.append(element("p", "px-1 pb-4 text-sm leading-5 text-secondary", label("The generation prompt is unavailable. The original request is shown below.", "未获取到生成提示词,以下提供用户原始请求。")));
203
+ body.append(element("h3", "px-1 pb-2 text-sm font-semibold text-default", label("Original request", "原始请求")));
204
+ }
205
+ body.append(element("p", "px-1 text-sm leading-5 text-default whitespace-pre-wrap break-words select-text", record.prompt.text));
206
+ } else {
207
+ const message = record.state === "empty" ? label("Native data does not provide the generation prompt for this image", "原生数据未提供这张图片的生成提示词") : label("Could not load the prompt", "提示词加载失败");
208
+ body.append(element("p", "px-1 text-sm leading-5 text-secondary", message));
209
+ if (record.state === "error") {
210
+ const retry = element("button", "mt-3 rounded-full border border-default px-3 py-1 text-sm text-default hover:bg-primary-ghost-hover", label("Try again", "重试"));
211
+ retry.type = "button";
212
+ retry.addEventListener("click", loadPrompt);
213
+ body.append(retry);
214
+ }
215
+ }
216
+ }
217
+ async function loadPrompt() {
218
+ record.state = "loading";
219
+ renderBody();
220
+ try {
221
+ const prompt = await resolvePrompt(file, next.request);
222
+ // 换图或卸载后不写回;关闭面板仍可继续预取。
223
+ if (signal.aborted || candidate !== next || mounted !== record || !header.isConnected) return;
224
+ record.prompt = prompt;
225
+ record.state = prompt?.text ? "ready" : "empty";
226
+ } catch {
227
+ if (signal.aborted || candidate !== next || mounted !== record || next.request.signal.aborted) return;
228
+ record.state = "error";
229
+ }
230
+ renderBody();
231
+ }
136
232
  mounted = record;
233
+ renderBody();
234
+ openPendingLocalPrompt();
137
235
  copy.addEventListener("click", async () => {
236
+ if (record.state !== "ready") return;
138
237
  try {
139
- await navigator.clipboard.writeText(prompt.text);
238
+ await navigator.clipboard.writeText(record.prompt.text);
140
239
  if (mounted !== record) return;
141
240
  copyFeedback(true);
142
241
  } catch {
@@ -146,12 +245,22 @@ async function scan() {
146
245
  clearTimeout(record.copyTimer);
147
246
  record.copyTimer = setTimeout(() => copyFeedback(), 2000);
148
247
  });
149
- button.addEventListener("click", () => panel.hidden ? showPrompt(record) : closePanel(record));
248
+ syncMenu(record);
150
249
  close.addEventListener("click", () => closePanel(record, true));
250
+ await loadPrompt();
151
251
  }
152
252
 
253
+ function openPendingLocalPrompt(){
254
+ if(mounted?.file.kind!=="local" || localImageSource?.pendingPrompt()!==mounted.file.id)return;
255
+ localImageSource.consumePrompt();showPrompt(mounted);
256
+ }
257
+ function localPromptRequested(){openPendingLocalPrompt();void scan();}
258
+ document.addEventListener("minecodex:image-prompt",localPromptRequested);
153
259
  const localeObserver = new MutationObserver(() => mounted?.refreshLocale());
154
260
  localeObserver.observe(document.documentElement, { attributes:true, attributeFilter:["lang"] });
261
+ localImageSource = installLocalImageSource(config, signal, createImageSourcesAdapter(config.codex), createNativeImageSource(config.codex, {
262
+ requestHostAction:config.requestHostAction, libraryApi, createCloudImageActions, signal,
263
+ }));
155
264
  const observer = new MutationObserver(() => {
156
265
  if (scheduled) return;
157
266
  scheduled = true;
@@ -165,7 +274,8 @@ function escape(event) {
165
274
  closePanel(mounted, true);
166
275
  }
167
276
  document.addEventListener("keydown", escape, true);
168
- const cleanup = () => { candidate?.request.abort(); candidate = null; observer.disconnect(); localeObserver.disconnect(); document.removeEventListener("keydown", escape, true); unmount(); };
277
+ let cleaned = false;
278
+ const cleanup = () => {if(cleaned)return;cleaned=true;document.removeEventListener("minecodex:image-prompt",localPromptRequested);candidate?.request.abort();candidate=null;observer.disconnect();localeObserver.disconnect();document.removeEventListener("keydown",escape,true);unmount();localImageSource.cleanup();};
169
279
  signal.addEventListener("abort", cleanup, { once: true });
170
280
  scan();
171
281
  return cleanup;
@@ -1,10 +1,16 @@
1
+ import { createCloudImageActions } from "./frontend/cloud-image-actions.mjs";
2
+ import { createImageSourcesAdapter } from "./adapters/codex/image-sources.mjs";
3
+ import { createNativeImageSource } from "./adapters/codex/native-image-source.mjs";
4
+ import {createNativeImageModules, discoverFilePreviewComposerBindings} from "./adapters/codex/native-image-modules.mjs";
5
+ import { installLocalImageSource } from "./frontend/local-image-source.mjs";
1
6
  import { createLibraryApi } from "./adapters/codex/library-api.mjs";
2
7
  import { createLibraryViewerAdapter } from "./adapters/codex/library-viewer.mjs";
3
8
  import { readFile } from "node:fs/promises";
4
9
  import { promptResolverSource } from "./adapters/codex/prompt-resolver-source.mjs";
5
10
 
6
- // 两条 Renderer 路径共享纯规则,注入后不依赖 Node 模块闭包。
7
11
  export async function createPageScriptSource() {
8
- const source = await readFile(new URL("./library-prompt-page.mjs", import.meta.url), "utf8");
9
- return `const createLibraryApi=${createLibraryApi.toString()};\nconst createLibraryViewerAdapter=${createLibraryViewerAdapter.toString()};\nconst promptFromConversation = ${promptResolverSource()};\n${source}`;
12
+ const source=await readFile(new URL("./library-prompt-page.mjs",import.meta.url),"utf8");
13
+ const functions={createCloudImageActions,createImageSourcesAdapter,discoverFilePreviewComposerBindings,createNativeImageModules,createNativeImageSource,installLocalImageSource,createLibraryApi,createLibraryViewerAdapter};
14
+ const declarations=Object.entries(functions).map(([name,fn])=>`const ${name}=${fn.toString()};`).join("\n");
15
+ return `${declarations}\nconst promptFromConversation=${promptResolverSource()};\n${source}`;
10
16
  }
@@ -49,10 +49,17 @@ async function readPrompts(logPath, prompts) {
49
49
  const stored = new Map();
50
50
  const pendingSources = new Map();
51
51
  for await (const line of lines) {
52
- if (line.includes('"image_generation_end"') || /"type"\s*:\s*"custom_tool_call"/.test(line) || /"name"\s*:\s*"wait"/.test(line) || [...pendingSources.keys()].some(id => line.includes(id))) {
52
+ if (line.includes('"image_generation_end"') || line.includes('"item_completed"') || /"type"\s*:\s*"custom_tool_call"/.test(line) || /"name"\s*:\s*"wait"/.test(line) || [...pendingSources.keys()].some(id => line.includes(id))) {
53
53
  let event;
54
54
  try { event = JSON.parse(line); } catch { continue; }
55
55
  const payload = event?.payload ?? event;
56
+ // 以完成事件为准;旧版内部状态可能仍是 generating,不能据此丢弃提示词。
57
+ const item = payload?.type === "item_completed" ? payload.item : null;
58
+ if (item?.type === "Extension" && item.kind === "image_gen.generation"
59
+ && typeof item.id === "string"
60
+ && typeof item.revisedPrompt === "string" && item.revisedPrompt.trim()) {
61
+ prompts.set(item.id, item.revisedPrompt);
62
+ }
56
63
  const legacy = legacyPromptFromPayload(payload);
57
64
  if (legacy && !prompts.has(legacy.imageGenerationItemId)) {
58
65
  prompts.set(legacy.imageGenerationItemId, legacy.prompt);
@@ -160,7 +167,12 @@ export class ThreadPromptIndex {
160
167
  async find(threadId, imageGenerationItemId) {
161
168
  const key = cacheKey(threadId, imageGenerationItemId);
162
169
  const cached = this.threadPrompts.get(threadId);
163
- if (cached?.has(imageGenerationItemId)) return cached.get(imageGenerationItemId);
170
+ if (cached?.has(imageGenerationItemId)) {
171
+ // 工具请求先到、完成事件后到时,重新读取更准确的 revisedPrompt。
172
+ if (!await this.threadLogsChanged(threadId)) return cached.get(imageGenerationItemId);
173
+ this.threadPrompts.delete(threadId);
174
+ this.clearNegative(threadId);
175
+ }
164
176
 
165
177
  const negative = this.negativeCache.get(key);
166
178
  if (negative) {
@@ -22,6 +22,8 @@ Composer 触发器区分两种状态:占位态("Select model" / "Select effo
22
22
  - **Composer Trigger:** 占位态恢复原生 160px Content 宽度;真实模型态 HUG 完整名称,头部保留原生可点击 chevron。
23
23
  - **Slider Header:** 模型名最多两行,容器高度随内容自然撑开;effort 与 chevron 不收缩。无 Fast 时移除原生 12px 预留占位,让按钮与面板严格居中。
24
24
  - **Model List:** 保持 264px 原生宽度、Provider 副文本、收藏与最多六项可视高度;收藏与未收藏之间保留分隔线,弹窗内容四周保留约 6px 内边距;模型弹窗与原生 Slider 一致地水平居中于触发器。列表打开期间,Composer 占位文案从 `Select effort` 切为原生语义 `Select model`,关闭后恢复。
25
+ - 模型列表使用共享 Dropdown 的 `opaque` 分支:不透明主题填充、无背景模糊,保留 Dropdown 圆角、边缘和阴影,避免后方聊天文字透入列表。此选择仅适用于模型列表。
26
+ - Slider 与模型列表切换沿用 Codex 26.930.21537 的动效节奏:底边锚定,高度 320ms(`cubic-bezier(.19,1,.22,1)`);列表内容 200ms 淡变与 10px 位移(`cubic-bezier(.23,1,.32,1)`),进入延迟 40ms。返回时收缩并淡出,Escape 先返回 Slider;减少动态效果时立即切换。切换与卸载会取消旧动画和清理定时器,不改原生 React 节点。
25
27
  - OCX 自动发现的 GPT 路由若与原生模型同名且没有独立推理档位,列表只显示原生项;当前已选中的路由仍显示,避免失去切换入口。有独立档位的 Provider 路由继续显示并标注 Provider。
26
28
  - Slider 预览档位时同步 effort 标签与 Fast 控件的 Ultra 状态;离开 Ultra 后立即恢复普通档位颜色。
27
29
 
@@ -7,8 +7,8 @@
7
7
  "zh-CN": "模型滑条"
8
8
  },
9
9
  "description": {
10
- "en": "A clearer, faster model picker with provider identity, favorites, and smoother interaction.",
11
- "zh-CN": "让模型选择更清楚、更快,提供品牌识别、常用收藏和更顺畅的交互。"
10
+ "en": "Switch models and thinking levels. Keep favorites within reach.",
11
+ "zh-CN": "切换模型与思考强度,快速找到常用收藏。"
12
12
  },
13
13
  "entry": {
14
14
  "kind": "composer-model-selector"