fix(cli): collage presse-papiers image/fichier via clipboardData.files — #155 (partiel : logique+tests verts, e2e AppImage clipboard en attente)

(cherry picked from commit 2f39a5ce17b9895e683c0d48f0d9e034e701ed19)
This commit is contained in:
2026-08-06 19:09:25 +02:00
parent d1e6ae1a22
commit 31976d6ed8
4 changed files with 99 additions and 5 deletions

View File

@ -1080,6 +1080,80 @@ describe("CustomAgentChatView", () => {
expect(screen.getByText("Fichier: paste-image.png")).toBeTruthy();
});
it("accepts pasted images exposed only through clipboardData.files", async () => {
const agent = {
launchAgentChat: vi.fn(),
reattachAgentChat: vi.fn(async (sessionId: string) => ({
sessionId,
scrollback: [],
})),
sendAgentChat: vi.fn(async () => {}),
cancelAgentChat: vi.fn(async () => {}),
closeAgentChat: vi.fn(async () => {}),
};
render(
<DIProvider
gateways={{
agent,
system: { pickFile: vi.fn(async () => null) },
} as unknown as Gateways}
>
<CustomAgentChatView
projectId="project-1"
agentId="agent-1"
agentName="Worker"
profile={profile}
cwd="/repo"
nodeId="node-1"
sessionId="chat-session-1"
conversationId="conversation-1"
onSessionId={vi.fn()}
onConversationId={vi.fn()}
/>
</DIProvider>,
);
await waitFor(() =>
expect(agent.reattachAgentChat).toHaveBeenCalledWith(
"chat-session-1",
expect.any(Function),
),
);
const file = new File(["ignored"], "files-only.png", { type: "image/png" });
Object.defineProperty(file, "arrayBuffer", {
value: vi.fn(async () => new Uint8Array([4, 5, 6]).buffer),
});
fireEvent.paste(screen.getByLabelText(/message CLI custom/), {
clipboardData: {
items: [],
files: [file],
getData: () => "",
},
});
await screen.findByText("Fichier joint: files-only.png");
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledTimes(1));
expect(agent.sendAgentChat).toHaveBeenCalledWith(
"chat-session-1",
"",
expect.any(Function),
{
attachments: [
{
filename: "files-only.png",
contentBase64: "BAUG",
mime: "image/png",
sourceKind: "clipboard",
},
],
},
);
});
it("keeps the chat shell bounded with a scrollable message area and fixed composer", async () => {
const agent = {
launchAgentChat: vi.fn(() => new Promise<never>(() => {})),

View File

@ -268,10 +268,24 @@ async function clipboardImageToAttachment(file: File): Promise<AttachmentDraft>
}
function clipboardImageFiles(event: ClipboardEvent<HTMLTextAreaElement>): File[] {
return Array.from(event.clipboardData.items)
const clipboardItems = event.clipboardData.items
? Array.from(event.clipboardData.items)
: [];
const clipboardFiles = event.clipboardData.files
? Array.from(event.clipboardData.files)
: [];
const fromItems = clipboardItems
.filter((item) => item.kind === "file" && item.type.startsWith("image/"))
.map((item) => item.getAsFile())
.filter((file): file is File => Boolean(file));
const fromFiles = clipboardFiles.filter((file) => file.type.startsWith("image/"));
const seen = new Set<string>();
return [...fromItems, ...fromFiles].filter((file) => {
const key = `${file.name}\u0000${file.type}\u0000${file.size}\u0000${file.lastModified}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
function slashCommandPrefix(draft: string): string | null {