feat(chat): colle les fichiers du presse-papiers via Ctrl+V dans le composer (#181)

This commit is contained in:
2026-08-11 15:57:30 +02:00
parent ce2640c9af
commit 419987d890
4 changed files with 296 additions and 3 deletions

View File

@ -1562,6 +1562,162 @@ describe("CustomAgentChatView", () => {
);
});
it("keeps pasted text insertion when clipboard files are attached", 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 composer = screen.getByLabelText(/message CLI custom/) as HTMLTextAreaElement;
fireEvent.change(composer, { target: { value: "prefix suffix" } });
composer.setSelectionRange(7, 13);
const file = new File(["ignored"], "mixed.txt", { type: "text/plain" });
Object.defineProperty(file, "arrayBuffer", {
value: vi.fn(async () => new Uint8Array([7, 8, 9]).buffer),
});
fireEvent.paste(composer, {
clipboardData: {
items: [
{
kind: "file",
type: "text/plain",
getAsFile: () => file,
},
],
getData: () => "inserted",
},
});
await screen.findByText("Fichier joint: mixed.txt");
await waitFor(() => expect(composer.value).toBe("prefix inserted"));
fireEvent.click(screen.getByRole("button", { name: "Envoyer" }));
await waitFor(() => expect(agent.sendAgentChat).toHaveBeenCalledTimes(1));
expect(agent.sendAgentChat).toHaveBeenCalledWith(
"chat-session-1",
"prefix inserted",
expect.any(Function),
{
attachments: [
{
filename: "mixed.txt",
contentBase64: "BwgJ",
mime: "text/plain",
sourceKind: "clipboard",
},
],
},
);
expect(screen.getByText("prefix inserted")).toBeTruthy();
expect(screen.getByText("Fichier: mixed.txt")).toBeTruthy();
});
it("deduplicates clipboard files by metadata across clipboard sources and repeated paste", 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"], "duplicate.pdf", {
type: "application/pdf",
lastModified: 123,
});
Object.defineProperty(file, "arrayBuffer", {
value: vi.fn(async () => new Uint8Array([10, 11, 12]).buffer),
});
const clipboardData = {
items: [
{
kind: "file",
type: "application/pdf",
getAsFile: () => file,
},
],
files: [file],
getData: () => "",
};
const composer = screen.getByLabelText(/message CLI custom/);
fireEvent.paste(composer, { clipboardData });
await screen.findByText("Fichier joint: duplicate.pdf");
fireEvent.paste(composer, { clipboardData });
await waitFor(() =>
expect(screen.getAllByText("Fichier joint: duplicate.pdf")).toHaveLength(1),
);
});
it("keeps the chat shell bounded with a scrollable message area and fixed composer", async () => {
const agent = {
launchAgentChat: vi.fn(() => new Promise<never>(() => {})),