Skip to content

Commit 47d1f03

Browse files
committed
feat: enhance prompt attachment manager to support multiple image attachments and improve preview handling
1 parent e47ef58 commit 47d1f03

4 files changed

Lines changed: 307 additions & 26 deletions

File tree

packages/vscode-ide-companion/resources/prompt-attachments.js

Lines changed: 46 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@
4848
throw new Error("Prompt attachment manager requires promptInput, inputWrap, and toolsLine.");
4949
}
5050

51-
let attachment = null;
51+
let attachments = [];
52+
let nextAttachmentId = 0;
5253
let previewPopup = null;
5354
let previewImage = null;
55+
let previewAnchor = null;
5456

5557
function ensurePreviewPopup() {
5658
if (previewPopup) {
@@ -68,6 +70,7 @@
6870
if (!previewPopup) {
6971
return;
7072
}
73+
previewAnchor = null;
7174
previewPopup.classList.remove("show");
7275
}
7376

@@ -101,37 +104,49 @@
101104
previewPopup.style.top = top + "px";
102105
}
103106

104-
function showPreview(anchor) {
107+
function showPreview(anchor, attachment) {
105108
if (!attachment) {
106109
return;
107110
}
108111

109112
ensurePreviewPopup();
113+
previewAnchor = anchor;
110114
previewImage.src = attachment.dataUrl;
111115
previewPopup.classList.add("show");
112116
updatePreviewPosition(anchor);
113117
}
114118

115119
function emitChange() {
116120
onAttachmentChange({
117-
hasAttachments: Boolean(attachment),
118-
attachments: attachment ? [attachment] : [],
121+
hasAttachments: attachments.length > 0,
122+
attachments: attachments.slice(),
119123
});
120124
}
121125

122126
function clear() {
123-
attachment = null;
127+
attachments = [];
124128
toolsLine.innerHTML = "";
125129
toolsLine.classList.remove("has-attachment");
126130
hidePreview();
127131
emitChange();
128132
}
129133

130-
function createAttachmentNode() {
134+
function removeAttachment(id) {
135+
const nextAttachments = attachments.filter((attachment) => attachment.id !== id);
136+
if (nextAttachments.length === attachments.length) {
137+
return;
138+
}
139+
attachments = nextAttachments;
140+
render();
141+
emitChange();
142+
}
143+
144+
function createAttachmentNode(attachment) {
131145
const wrapper = createElement("div", "chat-attached-context-attachment show-file-icons");
132146
wrapper.tabIndex = 0;
133147
wrapper.setAttribute("role", "button");
134148
wrapper.setAttribute("aria-label", ATTACHMENT_LABEL + " (删除)");
149+
wrapper.dataset.attachmentId = String(attachment.id);
135150
wrapper.draggable = true;
136151

137152
const removeButton = createElement("a", "monaco-button codicon codicon-close");
@@ -143,7 +158,7 @@
143158
removeButton.addEventListener("click", (event) => {
144159
event.preventDefault();
145160
event.stopPropagation();
146-
clear();
161+
removeAttachment(attachment.id);
147162
});
148163

149164
const iconLabel = createElement("div", "monaco-icon-label");
@@ -166,7 +181,7 @@
166181
wrapper.appendChild(pill);
167182
wrapper.appendChild(text);
168183

169-
const show = () => showPreview(wrapper);
184+
const show = () => showPreview(wrapper, attachment);
170185
wrapper.addEventListener("mouseenter", show);
171186
wrapper.addEventListener("focus", show);
172187
wrapper.addEventListener("mouseleave", hidePreview);
@@ -177,7 +192,7 @@
177192
wrapper.addEventListener("keydown", (event) => {
178193
if (event.key === "Delete" || event.key === "Backspace") {
179194
event.preventDefault();
180-
clear();
195+
removeAttachment(attachment.id);
181196
}
182197
});
183198

@@ -186,37 +201,44 @@
186201

187202
function render() {
188203
toolsLine.innerHTML = "";
189-
toolsLine.classList.toggle("has-attachment", Boolean(attachment));
190-
if (!attachment) {
204+
toolsLine.classList.toggle("has-attachment", attachments.length > 0);
205+
if (attachments.length === 0) {
191206
hidePreview();
192207
return;
193208
}
194-
toolsLine.appendChild(createAttachmentNode());
209+
for (const attachment of attachments) {
210+
toolsLine.appendChild(createAttachmentNode(attachment));
211+
}
212+
if (previewAnchor && !toolsLine.contains(previewAnchor)) {
213+
hidePreview();
214+
}
195215
}
196216

197-
function setAttachmentData(data) {
217+
function addAttachmentData(data) {
198218
if (!data?.dataUrl) {
199219
return false;
200220
}
201221

202-
attachment = {
222+
nextAttachmentId += 1;
223+
attachments.push({
224+
id: nextAttachmentId,
203225
name: data.name || ATTACHMENT_LABEL,
204226
mimeType: data.mimeType || "image/png",
205227
dataUrl: data.dataUrl,
206228
label: ATTACHMENT_LABEL,
207-
};
229+
});
208230
render();
209231
emitChange();
210232
return true;
211233
}
212234

213-
async function setAttachmentFromFile(file) {
235+
async function addAttachmentFromFile(file) {
214236
if (!isImageFile(file)) {
215237
return false;
216238
}
217239

218240
const dataUrl = await readFileAsDataUrl(file);
219-
return setAttachmentData({
241+
return addAttachmentData({
220242
name: file.name || ATTACHMENT_LABEL,
221243
mimeType: file.type || "image/png",
222244
dataUrl,
@@ -232,7 +254,7 @@
232254

233255
event.preventDefault();
234256
try {
235-
await setAttachmentFromFile(file);
257+
await addAttachmentFromFile(file);
236258
} catch (error) {
237259
console.error("Failed to attach pasted image.", error);
238260
}
@@ -241,18 +263,16 @@
241263
promptInput.addEventListener("paste", handlePaste);
242264

243265
window.addEventListener("resize", () => {
244-
const attachmentNode = toolsLine.querySelector(".chat-attached-context-attachment");
245-
if (previewPopup?.classList.contains("show") && attachmentNode) {
246-
updatePreviewPosition(attachmentNode);
266+
if (previewPopup?.classList.contains("show") && previewAnchor) {
267+
updatePreviewPosition(previewAnchor);
247268
}
248269
});
249270

250271
window.addEventListener(
251272
"scroll",
252273
() => {
253-
const attachmentNode = toolsLine.querySelector(".chat-attached-context-attachment");
254-
if (previewPopup?.classList.contains("show") && attachmentNode) {
255-
updatePreviewPosition(attachmentNode);
274+
if (previewPopup?.classList.contains("show") && previewAnchor) {
275+
updatePreviewPosition(previewAnchor);
256276
}
257277
},
258278
true
@@ -261,10 +281,10 @@
261281
return {
262282
clear,
263283
hasAttachments() {
264-
return Boolean(attachment);
284+
return attachments.length > 0;
265285
},
266286
getImageUrls() {
267-
return attachment ? [attachment.dataUrl] : [];
287+
return attachments.map((attachment) => attachment.dataUrl);
268288
},
269289
};
270290
}

packages/vscode-ide-companion/resources/webview.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,6 +1121,7 @@ body {
11211121
.tools-line {
11221122
display: none;
11231123
align-items: center;
1124+
flex-wrap: wrap;
11241125
gap: 8px;
11251126
min-height: 0;
11261127
padding: 0 12px;

packages/vscode-ide-companion/src/tests/extension.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,26 @@ test("userPrompt with images sends userMessage with image placeholder", async ()
267267
assert.equal((userMsg as any).content, "粘贴的图像");
268268
});
269269

270+
test("userPrompt passes multiple image urls to the session manager", async () => {
271+
const deps = createDeps();
272+
let submittedPrompt: any = null;
273+
(deps.sessionManager as any).handleUserPrompt = (prompt: any) => {
274+
submittedPrompt = prompt;
275+
return Promise.resolve();
276+
};
277+
278+
await handleWebviewMessage(
279+
{
280+
type: "userPrompt",
281+
prompt: "",
282+
images: ["data:image/png;base64,abc", "data:image/jpeg;base64,def"],
283+
},
284+
deps
285+
);
286+
287+
assert.deepEqual(submittedPrompt?.imageUrls, ["data:image/png;base64,abc", "data:image/jpeg;base64,def"]);
288+
});
289+
270290
test("userPrompt with permissions (continue) does not send userMessage", async () => {
271291
const deps = createDeps();
272292
await handleWebviewMessage(

0 commit comments

Comments
 (0)