minecodex 2.0.4 → 2.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +25 -1
- package/README.md +4 -2
- package/features/images/DESIGN.md +106 -41
- package/features/images/README.md +25 -16
- package/features/images/codex-feature.json +6 -3
- package/features/images/src/adapters/codex/image-prompt.mjs +24 -2
- package/features/images/src/adapters/codex/image-sources.mjs +19 -0
- package/features/images/src/adapters/codex/library-api.mjs +2 -0
- package/features/images/src/adapters/codex/library-viewer.mjs +26 -2
- package/features/images/src/adapters/codex/native-image-source.mjs +556 -0
- package/features/images/src/backend/local-image-source.mjs +138 -0
- package/features/images/src/frontend/cloud-image-actions.mjs +39 -0
- package/features/images/src/frontend/local-image-source.mjs +194 -0
- package/features/images/src/library-prompt-page.mjs +158 -53
- package/features/images/src/library-prompt-source.mjs +11 -3
- package/features/images/src/prompt-index.mjs +14 -2
- package/features/model-slider/DESIGN.md +2 -0
- package/features/model-slider/src/frontend/install.mjs +78 -11
- package/features/notes/DESIGN.md +7 -3
- package/features/notes/src/frontend/entry.mjs +26 -9
- package/features/session-status/DESIGN.md +4 -2
- package/features/session-status/src/adapters/codex/session-log.mjs +2 -1
- package/features/session-status/src/domain/session-stats.mjs +17 -9
- package/features/session-status/src/session-status.mjs +47 -11
- package/package.json +1 -1
- package/packages/codex-adapter/README.md +2 -1
- package/packages/codex-adapter/src/renderer/environment.mjs +24 -6
- package/packages/runtime-host/src/codex-injection.mjs +3 -6
- package/packages/runtime-host/src/codex-runtime.mjs +16 -5
- package/packages/runtime-host/src/feature-registry.mjs +1 -0
- package/packages/ui/DESIGN.md +5 -2
- package/packages/ui/src/components/button.mjs +2 -0
- package/packages/ui/src/components/navigation-rail.mjs +18 -10
- package/packages/ui/src/components/page.mjs +9 -2
- package/packages/ui/src/install.mjs +3 -1
- package/packages/ui/src/native/navigation-rail.mjs +6 -1
- package/packages/ui/src/native/page.mjs +23 -0
- package/packages/ui/src/native/tokens.mjs +3 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
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;
|
|
@@ -7,7 +7,8 @@ const libraryApi=createLibraryApi();
|
|
|
7
7
|
let scheduled = false;
|
|
8
8
|
let candidate = null;
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const libraryViewer=createLibraryViewerAdapter(config.codex,{readViewer:header=>localImageSource?.readViewer(header)});
|
|
11
|
+
const libraryImage=libraryViewer.read;
|
|
11
12
|
function element(tag, className, text) {
|
|
12
13
|
const node = document.createElement(tag);
|
|
13
14
|
node.className = className;
|
|
@@ -26,79 +27,128 @@ function copyGlyph(copied = false) {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function closePanel(record, restoreFocus = false) {
|
|
29
|
-
record.request?.abort();
|
|
30
|
-
record.request = null;
|
|
31
30
|
record.panel.hidden = true;
|
|
32
|
-
|
|
31
|
+
|
|
33
32
|
if (restoreFocus && record.button.isConnected) record.button.focus();
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
function showPrompt(record) {
|
|
37
36
|
record.panel.hidden = false;
|
|
38
|
-
|
|
37
|
+
|
|
39
38
|
record.close.focus();
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
async function resolvePrompt(file, request) {
|
|
43
|
-
if (!file.origination_thread_id || !file.origination_message_id) return null;
|
|
44
42
|
request.signal.throwIfAborted();
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
if(file.kind === "local") return config.requestHostAction("local-image-source",{operation:"prompt",id:file.id});
|
|
44
|
+
if(!file.origination_thread_id || !file.origination_message_id)return null;
|
|
45
|
+
const conversation=await libraryApi.conversation(file.origination_thread_id,AbortSignal.any([signal,request.signal,AbortSignal.timeout(25_000)]));
|
|
46
|
+
return promptFromConversation(file,conversation);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
function unmount() {
|
|
50
50
|
if (!mounted) return;
|
|
51
51
|
closePanel(mounted);
|
|
52
52
|
clearTimeout(mounted.copyTimer);
|
|
53
|
-
mounted.
|
|
53
|
+
mounted.menuItem?.remove();
|
|
54
|
+
mounted.menuCleanup?.();
|
|
54
55
|
mounted.panel.remove();
|
|
55
56
|
mounted = null;
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
function syncMenu(record) {
|
|
60
|
+
const anchor = libraryViewer.promptMenu(record.file);
|
|
61
|
+
if (record.menuItem?.isConnected && record.menuItem.parentElement === anchor?.original.parentElement) return;
|
|
62
|
+
record.menuItem?.remove();
|
|
63
|
+
record.menuCleanup?.();
|
|
64
|
+
record.menuItem = null;
|
|
65
|
+
record.menuCleanup = null;
|
|
66
|
+
if (!anchor) return;
|
|
67
|
+
const {menu,original} = anchor;
|
|
68
|
+
const item = element("div", original.className);
|
|
69
|
+
item.setAttribute("role", "menuitem");
|
|
70
|
+
item.tabIndex = -1;
|
|
71
|
+
item.dataset.minecodexLibraryPrompt = "menuitem";
|
|
72
|
+
const row = original.firstElementChild.cloneNode(true);
|
|
73
|
+
row.querySelector(".truncate").textContent = label("View prompts", "查看提示词");
|
|
74
|
+
// 使用项目现有 Prompts 图形,保留原生菜单的紧凑/标准图标布局。
|
|
75
|
+
const glyph = document.querySelector('[aria-label="Prompts"] svg');
|
|
76
|
+
if (glyph) for (const svg of row.querySelectorAll("svg")) {
|
|
77
|
+
const icon = glyph.cloneNode(true);
|
|
78
|
+
icon.setAttribute("width", svg.getAttribute("width"));
|
|
79
|
+
icon.setAttribute("height", svg.getAttribute("height"));
|
|
80
|
+
svg.replaceWith(icon);
|
|
81
|
+
}
|
|
82
|
+
item.append(row);
|
|
83
|
+
original.before(item);
|
|
84
|
+
record.menuItem = item;
|
|
85
|
+
const open = event => {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
event.stopPropagation();
|
|
88
|
+
menu.addEventListener("focusScope.autoFocusOnUnmount", event => {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
if (mounted?.identity === record.identity) mounted.close.focus();
|
|
91
|
+
}, {once:true});
|
|
92
|
+
// 原生菜单在 document capture 处理 Escape;阻止它再冒泡关闭图片视口。
|
|
93
|
+
const stopEscape = event => event.stopPropagation();
|
|
94
|
+
menu.addEventListener("keydown", stopEscape, {once:true});
|
|
95
|
+
menu.dispatchEvent(new KeyboardEvent("keydown", {key:"Escape",bubbles:true,cancelable:true}));
|
|
96
|
+
menu.removeEventListener("keydown", stopEscape);
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
// 原生关闭菜单可能重建工具栏;把打开意图交给同一图片的新实例。
|
|
99
|
+
if (mounted?.identity === record.identity) showPrompt(mounted);
|
|
100
|
+
}, 0);
|
|
101
|
+
};
|
|
102
|
+
item.addEventListener("click", open);
|
|
103
|
+
item.addEventListener("pointermove", () => item.focus());
|
|
104
|
+
const keyboard = event => {
|
|
105
|
+
if (event.target === item && ["Enter", " "].includes(event.key)) return open(event);
|
|
106
|
+
if (!["ArrowUp", "ArrowDown"].includes(event.key)) return;
|
|
107
|
+
const items = [...menu.querySelectorAll('[role="menuitem"]')];
|
|
108
|
+
const index = items.indexOf(document.activeElement);
|
|
109
|
+
const next = items[(index + (event.key === "ArrowDown" ? 1 : -1) + items.length) % items.length];
|
|
110
|
+
// 仅补上未注册到原生 collection 的注入项,其他按键仍交给原生菜单。
|
|
111
|
+
if (index >= 0 && (document.activeElement === item || next === item)) {
|
|
112
|
+
event.preventDefault();event.stopImmediatePropagation();next.focus();
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
menu.addEventListener("keydown", keyboard, true);
|
|
116
|
+
record.menuCleanup = () => menu.removeEventListener("keydown", keyboard, true);
|
|
117
|
+
}
|
|
118
|
+
|
|
58
119
|
async function scan() {
|
|
59
120
|
scheduled = false;
|
|
60
121
|
if (signal.aborted) return;
|
|
61
122
|
const header = [...document.querySelectorAll('[data-testid="viewer-header"]')].find(header => header.getBoundingClientRect().width > 0 && libraryImage(header));
|
|
62
123
|
const file = header && libraryImage(header);
|
|
124
|
+
const toolbar = header && libraryViewer.toolbar(header);
|
|
63
125
|
const identity = file && JSON.stringify(file);
|
|
64
126
|
if (candidate?.header === header && candidate?.identity === identity
|
|
65
|
-
&&
|
|
127
|
+
&& candidate?.zoom === toolbar?.zoom && candidate?.host === toolbar?.host
|
|
128
|
+
&& (!mounted || (mounted.button.isConnected && mounted.panel.isConnected))) {
|
|
129
|
+
if (mounted) {syncMenu(mounted);openPendingLocalPrompt();}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
66
132
|
candidate?.request.abort();
|
|
67
133
|
unmount();
|
|
68
134
|
candidate = null;
|
|
69
|
-
|
|
70
|
-
|
|
135
|
+
// 等原生图片工具栏就绪再解析;否则早到的结果会把同图后续扫描短路。
|
|
136
|
+
if (!file || !toolbar || (file.kind !== "local" && (!file.origination_thread_id || !file.origination_message_id))) return;
|
|
137
|
+
const next = { header, identity, zoom:toolbar.zoom, host:toolbar.host, request: new AbortController() };
|
|
71
138
|
candidate = next;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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");
|
|
139
|
+
const {host,trigger:button} = toolbar;
|
|
140
|
+
// 对齐当前原生 File info 的实体材质、尺寸和内容区。
|
|
141
|
+
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
142
|
panel.id = PANEL_ID;
|
|
96
143
|
panel.setAttribute("aria-label", label("Image prompt", "图片提示词"));
|
|
97
144
|
panel.hidden = true;
|
|
98
145
|
panel.style.setProperty("-webkit-app-region", "no-drag");
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
146
|
+
// File info 的 pt-13 与包装层 pt-px 合计;窄视口保留两侧 gutter。
|
|
147
|
+
panel.style.top = "calc(var(--mcx-spacing) * 13 + 1px)";
|
|
148
|
+
panel.style.width = "calc(var(--mcx-spacing) * 74)";
|
|
149
|
+
panel.style.maxWidth = "calc(100% - var(--mcx-spacing) * 4)";
|
|
150
|
+
const heading = element("div", "flex h-11 shrink-0 items-center justify-between gap-2 py-1 ps-4 pe-2");
|
|
151
|
+
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
152
|
close.type = "button";
|
|
103
153
|
close.setAttribute("aria-label", label("Close prompt", "关闭提示词"));
|
|
104
154
|
// 直接复用原生关闭图标,避免手绘近似图形。
|
|
@@ -109,34 +159,82 @@ async function scan() {
|
|
|
109
159
|
const copy = element("button", close.className);
|
|
110
160
|
copy.type = "button";
|
|
111
161
|
function copyFeedback(copied = false, failed = false) {
|
|
112
|
-
const text = failed ? label("Try copying again", "重试复制") : copied ? label("Copied", "已复制") : label("Copy prompt", "复制提示词");
|
|
162
|
+
const text = failed ? label("Try copying again", "重试复制") : copied ? label("Copied", "已复制") : record?.prompt?.kind === "request" ? label("Copy original request", "复制原始请求") : label("Copy prompt", "复制提示词");
|
|
113
163
|
copy.setAttribute("data-minecodex-tooltip", text);
|
|
114
164
|
copy.setAttribute("aria-label", text);
|
|
115
165
|
copy.replaceChildren(copyGlyph(copied));
|
|
116
166
|
}
|
|
117
|
-
copyFeedback();
|
|
118
167
|
actions.append(copy, close);
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
);
|
|
168
|
+
const title = element("span", "text-base font-semibold text-default select-none", label("Prompt", "提示词"));
|
|
169
|
+
const titleHeading = element("h2", "min-w-0 flex-1 truncate");
|
|
170
|
+
titleHeading.append(title);
|
|
171
|
+
heading.append(titleHeading, actions);
|
|
172
|
+
const body = element("div", "min-h-0 flex-1 overflow-y-auto px-3 py-4");
|
|
125
173
|
panel.append(heading, body);
|
|
126
174
|
host.append(panel);
|
|
127
|
-
const record = { header, file, identity, button, panel, body, close,
|
|
175
|
+
const record = { header, file, identity, button, panel, body, close, copy, prompt: null, state: "loading", request: next.request,
|
|
128
176
|
refreshLocale() {
|
|
129
|
-
|
|
177
|
+
if (record.menuItem) record.menuItem.querySelector(".truncate").textContent = label("View prompts", "查看提示词");
|
|
130
178
|
panel.setAttribute("aria-label", label("Image prompt", "图片提示词"));
|
|
131
179
|
close.setAttribute("aria-label", label("Close prompt", "关闭提示词"));
|
|
132
|
-
|
|
180
|
+
title.textContent = label("Prompt", "提示词");
|
|
133
181
|
copyFeedback();
|
|
182
|
+
renderBody();
|
|
134
183
|
},
|
|
135
184
|
};
|
|
185
|
+
function renderBody() {
|
|
186
|
+
body.replaceChildren();
|
|
187
|
+
body.setAttribute("aria-busy", String(record.state === "loading"));
|
|
188
|
+
copy.disabled = record.state !== "ready";
|
|
189
|
+
copyFeedback();
|
|
190
|
+
if (record.state === "loading") {
|
|
191
|
+
const skeleton = element("div", "flex flex-col gap-2 px-1");
|
|
192
|
+
skeleton.setAttribute("aria-hidden", "true");
|
|
193
|
+
for (const width of ["100%", "92%", "100%", "68%"]) {
|
|
194
|
+
const line = element("div", "h-3 rounded-full bg-text/5");
|
|
195
|
+
line.style.width = width;
|
|
196
|
+
skeleton.append(line);
|
|
197
|
+
}
|
|
198
|
+
body.append(element("span", "sr-only", label("Loading prompt", "正在加载提示词")), skeleton);
|
|
199
|
+
} else if (record.state === "ready") {
|
|
200
|
+
if (record.prompt.kind === "request") {
|
|
201
|
+
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.", "未获取到生成提示词,以下提供用户原始请求。")));
|
|
202
|
+
body.append(element("h3", "px-1 pb-2 text-sm font-semibold text-default", label("Original request", "原始请求")));
|
|
203
|
+
}
|
|
204
|
+
body.append(element("p", "px-1 text-sm leading-5 text-default whitespace-pre-wrap break-words select-text", record.prompt.text));
|
|
205
|
+
} else {
|
|
206
|
+
const message = record.state === "empty" ? label("Native data does not provide the generation prompt for this image", "原生数据未提供这张图片的生成提示词") : label("Could not load the prompt", "提示词加载失败");
|
|
207
|
+
body.append(element("p", "px-1 text-sm leading-5 text-secondary", message));
|
|
208
|
+
if (record.state === "error") {
|
|
209
|
+
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", "重试"));
|
|
210
|
+
retry.type = "button";
|
|
211
|
+
retry.addEventListener("click", loadPrompt);
|
|
212
|
+
body.append(retry);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function loadPrompt() {
|
|
217
|
+
record.state = "loading";
|
|
218
|
+
renderBody();
|
|
219
|
+
try {
|
|
220
|
+
const prompt = await resolvePrompt(file, next.request);
|
|
221
|
+
// 换图或卸载后不写回;关闭面板仍可继续预取。
|
|
222
|
+
if (signal.aborted || candidate !== next || mounted !== record || !header.isConnected) return;
|
|
223
|
+
record.prompt = prompt;
|
|
224
|
+
record.state = prompt?.text ? "ready" : "empty";
|
|
225
|
+
} catch {
|
|
226
|
+
if (signal.aborted || candidate !== next || mounted !== record || next.request.signal.aborted) return;
|
|
227
|
+
record.state = "error";
|
|
228
|
+
}
|
|
229
|
+
renderBody();
|
|
230
|
+
}
|
|
136
231
|
mounted = record;
|
|
232
|
+
renderBody();
|
|
233
|
+
openPendingLocalPrompt();
|
|
137
234
|
copy.addEventListener("click", async () => {
|
|
235
|
+
if (record.state !== "ready") return;
|
|
138
236
|
try {
|
|
139
|
-
await navigator.clipboard.writeText(prompt.text);
|
|
237
|
+
await navigator.clipboard.writeText(record.prompt.text);
|
|
140
238
|
if (mounted !== record) return;
|
|
141
239
|
copyFeedback(true);
|
|
142
240
|
} catch {
|
|
@@ -146,10 +244,17 @@ async function scan() {
|
|
|
146
244
|
clearTimeout(record.copyTimer);
|
|
147
245
|
record.copyTimer = setTimeout(() => copyFeedback(), 2000);
|
|
148
246
|
});
|
|
149
|
-
|
|
247
|
+
syncMenu(record);
|
|
150
248
|
close.addEventListener("click", () => closePanel(record, true));
|
|
249
|
+
await loadPrompt();
|
|
151
250
|
}
|
|
152
251
|
|
|
252
|
+
function openPendingLocalPrompt(){
|
|
253
|
+
if(mounted?.file.kind!=="local" || localImageSource?.pendingPrompt()!==mounted.file.id)return;
|
|
254
|
+
localImageSource.consumePrompt();showPrompt(mounted);
|
|
255
|
+
}
|
|
256
|
+
function localPromptRequested(){openPendingLocalPrompt();void scan();}
|
|
257
|
+
document.addEventListener("minecodex:image-prompt",localPromptRequested);
|
|
153
258
|
const localeObserver = new MutationObserver(() => mounted?.refreshLocale());
|
|
154
259
|
localeObserver.observe(document.documentElement, { attributes:true, attributeFilter:["lang"] });
|
|
155
260
|
const observer = new MutationObserver(() => {
|
|
@@ -165,7 +270,7 @@ function escape(event) {
|
|
|
165
270
|
closePanel(mounted, true);
|
|
166
271
|
}
|
|
167
272
|
document.addEventListener("keydown", escape, true);
|
|
168
|
-
const cleanup = () => { candidate?.request.abort(); candidate = null; observer.disconnect(); localeObserver.disconnect(); document.removeEventListener("keydown", escape, true); unmount(); };
|
|
273
|
+
const cleanup = () => { document.removeEventListener("minecodex:image-prompt",localPromptRequested); candidate?.request.abort(); candidate = null; observer.disconnect(); localeObserver.disconnect(); document.removeEventListener("keydown", escape, true); unmount(); };
|
|
169
274
|
signal.addEventListener("abort", cleanup, { once: true });
|
|
170
275
|
scan();
|
|
171
276
|
return cleanup;
|
|
@@ -1,10 +1,18 @@
|
|
|
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 { installLocalImageSource } from "./frontend/local-image-source.mjs";
|
|
1
5
|
import { createLibraryApi } from "./adapters/codex/library-api.mjs";
|
|
2
6
|
import { createLibraryViewerAdapter } from "./adapters/codex/library-viewer.mjs";
|
|
3
7
|
import { readFile } from "node:fs/promises";
|
|
4
8
|
import { promptResolverSource } from "./adapters/codex/prompt-resolver-source.mjs";
|
|
5
9
|
|
|
6
|
-
// 两条 Renderer 路径共享纯规则,注入后不依赖 Node 模块闭包。
|
|
7
10
|
export async function createPageScriptSource() {
|
|
8
|
-
const source
|
|
9
|
-
|
|
11
|
+
const source=await readFile(new URL("./library-prompt-page.mjs",import.meta.url),"utf8");
|
|
12
|
+
const functions={createCloudImageActions,createImageSourcesAdapter,createNativeImageSource,installLocalImageSource,createLibraryApi,createLibraryViewerAdapter};
|
|
13
|
+
const declarations=Object.entries(functions).map(([name,fn])=>`const ${name}=${fn.toString()};`).join("\n");
|
|
14
|
+
return `${declarations}
|
|
15
|
+
let localImageSource=null;
|
|
16
|
+
const promptFromConversation=${promptResolverSource()};
|
|
17
|
+
${source.replace("const observer =", "localImageSource=installLocalImageSource(config,signal,createImageSourcesAdapter(config.codex),createNativeImageSource(config.codex,{requestHostAction:config.requestHostAction,libraryApi:createLibraryApi(),createCloudImageActions}));\nconst observer =").replace("return cleanup;", "return () => { cleanup(); localImageSource.cleanup(); };")}`;
|
|
10
18
|
}
|
|
@@ -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))
|
|
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
|
|
|
@@ -683,6 +683,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
683
683
|
|
|
684
684
|
function sizeModelMenuViewport(menu, maxVisibleItems) {
|
|
685
685
|
if (!menu.isConnected) return;
|
|
686
|
+
if (modelSelectorControllerMenu?.menu === menu && modelSelectorControllerMenu.animations?.length) return;
|
|
686
687
|
const items = modelMenuItems(menu).filter((item) => item.dataset.codexModelSliderRaw);
|
|
687
688
|
if (items.length <= maxVisibleItems) {
|
|
688
689
|
menu.removeAttribute("data-codex-model-slider-overflow");
|
|
@@ -959,9 +960,38 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
959
960
|
}
|
|
960
961
|
}
|
|
961
962
|
|
|
962
|
-
function
|
|
963
|
+
function animateControllerModelMenu(record, opening, onFinish) {
|
|
964
|
+
const height = record.menu.getBoundingClientRect().height;
|
|
965
|
+
clearTimeout(record.motionTimer);
|
|
966
|
+
record.animations?.forEach((animation) => animation.cancel());
|
|
967
|
+
record.animations = [];
|
|
968
|
+
if (matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
969
|
+
onFinish();
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
// 原生视图切换:底边保持锚定,高度 320ms,内容 200ms 淡变并位移 10px。
|
|
973
|
+
record.menu.style.overflowY = "hidden";
|
|
974
|
+
const size = record.menu.animate([
|
|
975
|
+
{ height: `${opening ? record.sliderHeight : height}px` },
|
|
976
|
+
{ height: `${opening ? record.listHeight : record.sliderHeight}px` },
|
|
977
|
+
], { duration: 320, easing: "cubic-bezier(.19,1,.22,1)", fill: "both" });
|
|
978
|
+
const content = record.content.animate(opening
|
|
979
|
+
? [{ opacity: 0, translate: "0 10px" }, { opacity: 1, translate: "0 0" }]
|
|
980
|
+
: [{ opacity: 1, translate: "0 0" }, { opacity: 0, translate: "0 10px" }],
|
|
981
|
+
{ duration: 200, delay: opening ? 40 : 0, easing: "cubic-bezier(.23,1,.32,1)", fill: "both" });
|
|
982
|
+
record.animations.push(size, content);
|
|
983
|
+
if (!opening) {
|
|
984
|
+
record.animations.push(record.menu.animate([{ opacity: 1 }, { opacity: 0 }],
|
|
985
|
+
{ duration: 200, delay: 40, easing: "cubic-bezier(.23,1,.32,1)", fill: "both" }));
|
|
986
|
+
}
|
|
987
|
+
// 后台窗口未必派发动画完成事件,清理按同一时长结束,避免残留菜单。
|
|
988
|
+
record.motionTimer = setTimeout(onFinish, 320);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function closeControllerModelMenu({ immediate = false } = {}) {
|
|
963
992
|
const record = modelSelectorControllerMenu;
|
|
964
993
|
if (!record) return false;
|
|
994
|
+
if (record.closing && !immediate) return true;
|
|
965
995
|
const placeholder = document.querySelector(
|
|
966
996
|
'[data-codex-intelligence-trigger] [class*="_ModelPickerTriggerPlaceholder_"]',
|
|
967
997
|
);
|
|
@@ -969,10 +999,18 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
969
999
|
placeholder.firstChild.nodeValue = record.placeholderText;
|
|
970
1000
|
}
|
|
971
1001
|
record.button.setAttribute("aria-expanded", "false");
|
|
972
|
-
record.
|
|
973
|
-
record.
|
|
974
|
-
|
|
975
|
-
|
|
1002
|
+
record.closing = true;
|
|
1003
|
+
record.menu.inert = true;
|
|
1004
|
+
const finish = () => {
|
|
1005
|
+
clearTimeout(record.motionTimer);
|
|
1006
|
+
record.animations?.forEach((animation) => animation.cancel());
|
|
1007
|
+
record.parentMenu.style.position = record.position;
|
|
1008
|
+
record.parentMenu.style.overflow = record.overflow;
|
|
1009
|
+
record.menu.remove();
|
|
1010
|
+
if (modelSelectorControllerMenu === record) modelSelectorControllerMenu = null;
|
|
1011
|
+
};
|
|
1012
|
+
if (immediate || !record.parentMenu.isConnected) finish();
|
|
1013
|
+
else animateControllerModelMenu(record, false, finish);
|
|
976
1014
|
return true;
|
|
977
1015
|
}
|
|
978
1016
|
|
|
@@ -988,8 +1026,11 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
988
1026
|
|
|
989
1027
|
function openControllerModelMenu(parentMenu, modelButton, selector) {
|
|
990
1028
|
if (modelSelectorControllerMenu) {
|
|
991
|
-
|
|
992
|
-
|
|
1029
|
+
if (!modelSelectorControllerMenu.closing) {
|
|
1030
|
+
closeControllerModelMenu();
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
closeControllerModelMenu({ immediate: true });
|
|
993
1034
|
}
|
|
994
1035
|
const controller = nativeComposerModelController();
|
|
995
1036
|
if (!Array.isArray(controller?.models) || typeof controller.onSelectModel !== "function") return;
|
|
@@ -1002,7 +1043,12 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
1002
1043
|
menu.setAttribute("data-state", "open");
|
|
1003
1044
|
menu.setAttribute("data-codex-model-slider-menu", "");
|
|
1004
1045
|
menu.setAttribute("data-codex-model-slider-controller-menu", "");
|
|
1005
|
-
featureUI.surface(menu, "dropdown");
|
|
1046
|
+
featureUI.surface(menu, "dropdown", { variant: "opaque" });
|
|
1047
|
+
// 切换同一个 picker 视图,不重复播放整个 Dropdown 的入场缩放。
|
|
1048
|
+
menu.style.animation = "none";
|
|
1049
|
+
const content = document.createElement("div");
|
|
1050
|
+
content.style.flexShrink = "0";
|
|
1051
|
+
menu.append(content);
|
|
1006
1052
|
models.forEach((model, index) => {
|
|
1007
1053
|
const item = document.createElement("div");
|
|
1008
1054
|
item.className = "no-drag outline-hidden rounded-lg px-[var(--padding-row-x)] py-[var(--padding-row-y)] text-sm text-default group hover:bg-primary-ghost-hover focus:bg-primary-ghost-hover cursor-interaction flex flex-col";
|
|
@@ -1014,7 +1060,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
1014
1060
|
scheduleReturn: false,
|
|
1015
1061
|
owned: true,
|
|
1016
1062
|
});
|
|
1017
|
-
|
|
1063
|
+
content.append(item);
|
|
1018
1064
|
});
|
|
1019
1065
|
menu.addEventListener("click", (event) => {
|
|
1020
1066
|
const star = event.target.closest?.("[data-codex-model-slider-star]");
|
|
@@ -1038,6 +1084,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
1038
1084
|
menu.addEventListener("keydown", (event) => {
|
|
1039
1085
|
if (event.key === "Escape") {
|
|
1040
1086
|
event.preventDefault();
|
|
1087
|
+
event.stopPropagation();
|
|
1041
1088
|
closeControllerModelMenu();
|
|
1042
1089
|
modelButton.focus();
|
|
1043
1090
|
return;
|
|
@@ -1063,11 +1110,13 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
1063
1110
|
|
|
1064
1111
|
modelSelectorControllerMenu = {
|
|
1065
1112
|
menu,
|
|
1113
|
+
content,
|
|
1066
1114
|
parentMenu,
|
|
1067
1115
|
button: modelButton,
|
|
1068
1116
|
placeholderText,
|
|
1069
1117
|
position: parentMenu.style.position,
|
|
1070
1118
|
overflow: parentMenu.style.overflow,
|
|
1119
|
+
sliderHeight: parentMenu.getBoundingClientRect().height,
|
|
1071
1120
|
};
|
|
1072
1121
|
parentMenu.style.position = "relative";
|
|
1073
1122
|
parentMenu.style.overflow = "visible";
|
|
@@ -1076,6 +1125,15 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
1076
1125
|
modelButton.setAttribute("aria-expanded", "true");
|
|
1077
1126
|
(menu.querySelector('[data-codex-model-slider-selected]')
|
|
1078
1127
|
?? menu.querySelector('[role="menuitem"]'))?.focus();
|
|
1128
|
+
const record = modelSelectorControllerMenu;
|
|
1129
|
+
record.listHeight = menu.getBoundingClientRect().height;
|
|
1130
|
+
animateControllerModelMenu(record, true, () => {
|
|
1131
|
+
if (modelSelectorControllerMenu !== record || record.closing) return;
|
|
1132
|
+
record.animations.forEach((animation) => animation.cancel());
|
|
1133
|
+
record.animations = [];
|
|
1134
|
+
menu.style.overflowY = "auto";
|
|
1135
|
+
sizeModelMenuViewport(menu, selector.maxVisibleItems);
|
|
1136
|
+
});
|
|
1079
1137
|
}
|
|
1080
1138
|
|
|
1081
1139
|
// ========================================================================
|
|
@@ -2005,7 +2063,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
2005
2063
|
enhanceComposerModelTrigger(selector);
|
|
2006
2064
|
const parentMenu = nativeModelPickerMenu();
|
|
2007
2065
|
if (modelSelectorControllerMenu && modelSelectorControllerMenu.parentMenu !== parentMenu) {
|
|
2008
|
-
closeControllerModelMenu();
|
|
2066
|
+
closeControllerModelMenu({ immediate: true });
|
|
2009
2067
|
}
|
|
2010
2068
|
if (!parentMenu) {
|
|
2011
2069
|
if (
|
|
@@ -2031,7 +2089,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
2031
2089
|
}
|
|
2032
2090
|
|
|
2033
2091
|
function restoreModelSelectorEnhancements() {
|
|
2034
|
-
closeControllerModelMenu();
|
|
2092
|
+
closeControllerModelMenu({ immediate: true });
|
|
2035
2093
|
clearTimeout(modelSelectorReturnTimer);
|
|
2036
2094
|
clearTimeout(modelSelectorEffortTransactionTimer);
|
|
2037
2095
|
modelSelectorTriggerObserver?.disconnect();
|
|
@@ -2091,6 +2149,15 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
2091
2149
|
modelSelectorNativeToggleHandlers.clear();
|
|
2092
2150
|
}
|
|
2093
2151
|
|
|
2152
|
+
window.addEventListener("keydown", event => {
|
|
2153
|
+
const record = modelSelectorControllerMenu;
|
|
2154
|
+
if (event.key !== "Escape" || !record) return;
|
|
2155
|
+
// 先退出插件模型视图,避免原生 document 监听同时关闭整个 picker。
|
|
2156
|
+
event.preventDefault();
|
|
2157
|
+
event.stopImmediatePropagation();
|
|
2158
|
+
closeControllerModelMenu();
|
|
2159
|
+
record.button.focus();
|
|
2160
|
+
}, { capture: true, signal });
|
|
2094
2161
|
document.addEventListener("click", event => {
|
|
2095
2162
|
if (modelSelectorControllerMenu
|
|
2096
2163
|
&& !modelSelectorControllerMenu.menu.contains(event.target)
|
package/features/notes/DESIGN.md
CHANGED
|
@@ -38,7 +38,9 @@ Header 为 Prompts;右侧依次为排序图标、分类调节图标、常驻
|
|
|
38
38
|
|
|
39
39
|
点击提示词只插入当前输入框,保留原草稿且不发送。Escape 关闭并回到入口;点击外部关闭;方向键移动列表焦点。原生 Add 或权限面板打开时关闭。切换输入框时关闭,异步结果不得插入另一会话。沿用原生焦点与 hover,无额外进场动画。
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
入口通过 Codex Adapter 的 `purpose:"insertion"` 挂载到可见的原生输入框,覆盖 Chat、Work、Codex、Temporary Chat、浮动聊天,以及 side panel 全屏非聊天页面的 chat bar。多输入框并存时优先当前焦点所在输入框;插入操作显式绑定打开面板的输入框,切换后关闭旧面板,不向隐藏输入框插入。此插入契约不扩展会话指标使用的 Codex thread 契约。
|
|
42
|
+
|
|
43
|
+
原生可见的 Add 或菜单按钮展开时关闭 Popover。Session Status 的 Context 展开状态不是弹出菜单,不作为冲突条件;隐藏输入框和隐藏控件不参与锚点定位或菜单判断。
|
|
42
44
|
|
|
43
45
|
## 统计
|
|
44
46
|
成功插入后原子增加 `use_count` 并更新 `called_at`,保存于现有 SQLite。创建、编辑、复制和排序不计数。历史次数未知,从 0 开始,不从旧 `last_used_at` 推断。删除分类、切换页面、重启不清零。
|
|
@@ -50,9 +52,9 @@ Header 为 Prompts;右侧依次为排序图标、分类调节图标、常驻
|
|
|
50
52
|
|
|
51
53
|
导航栏 book-open 单独采用 20px 容器、27×27 viewBox,实际轮廓约 16.15px,描边折算 1.33px,与原生导航一致。Lucide 两个描边子路径合为一个 path,一次绘制半透明 currentColor,避免书脊交叠变深。选中态使用 Phosphor 官方 book-open-fill 的20px资源;授权见 vendor/phosphor-LICENSE。输入框入口尺寸不变。
|
|
52
54
|
|
|
53
|
-
拖拽排序使用等高 Drop Placeholder
|
|
55
|
+
拖拽排序使用等高 Drop Placeholder,拖影跟随指针并显式按源行圆角裁切,不留下矩形外边界;同组条目以 180ms ease-out 让位,落位淡入 140ms。放到范围外或取消恢复原位,不写入排序。遵循 prefers-reduced-motion。
|
|
54
56
|
|
|
55
|
-
页面选择通用 Page
|
|
57
|
+
页面选择通用 Page 的 `sidebar:"none"` 分支,自身不带常驻二级侧栏,保留原生 Home、Space、Scheduled sidebar 的悬浮叠加与导航行为。关闭页面恢复原页面位置,不改变用户的侧栏展开偏好。
|
|
56
58
|
|
|
57
59
|
提示词编辑沿用原生 Project 的复合标题输入:40px 图标段位于标题前,细分隔线,共用圆角边框与 focus-within 状态。图标选择入口不再单独占一行;Title 通过 aria-label 标识。
|
|
58
60
|
|
|
@@ -68,3 +70,5 @@ Category 下拉面板与触发栏等宽、左右对齐,宽度包含内边距
|
|
|
68
70
|
内置 24 条来自 dsh-prompt 0.1.13 的 MIT 提示词,正文保持原样,按思考、学习、编程、效率分类并配 Emoji。来源与许可保存在 assets/dsh-prompt-LICENSE.txt。首次启动或旧库升级时单次导入;随后允许编辑和删除,重启不还原,也不重建被删除的分类。用户个人内容只保留在本机数据库,不纳入默认资源。
|
|
69
71
|
|
|
70
72
|
简短悬浮提示调用共享 Tooltip;具体文案由本功能维护。Quick Prompts 通过通用 ComposerMenu 使用原生浮动建议列表材质和输入框主体锚点。排序、分类与图标菜单显式选择 Dropdown。
|
|
73
|
+
|
|
74
|
+
Categories 管理弹窗的 New category 是有实际功能的次级操作,使用共享 Button 的 `secondary`(outline)分支;分类编辑的 Save 保留主按钮。
|