feat(workspace): composer like Claude Desktop, image thumbnails, tool icons in the picker
This commit is contained in:
@@ -2,17 +2,19 @@
|
||||
* @file RunSetup.test.tsx
|
||||
* @description The run-form pieces that outlived the pane's run form:
|
||||
* `PromptEditor`'s `/` and `@` autocomplete (Enter picks while the list is
|
||||
* open, `/` only where slash commands apply) and `ModelPicker` — a tool's own
|
||||
* open, `/` only where slash commands apply), Shift/Alt+Enter newlines, a
|
||||
* pasted image shown as a thumbnail and sent as its saved path, and `ModelPicker` — a tool's own
|
||||
* list from the server, a fixed list (an AI Router's models) when given one,
|
||||
* and Custom…, which is a sentinel, never a model id.
|
||||
*
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import i18n from "i18next";
|
||||
import { ModelPicker, PromptEditor } from "../RunSetup";
|
||||
import { ModelPicker, PromptEditor, promptWithImages, type PromptImage } from "../RunSetup";
|
||||
import { api } from "../../../lib/api";
|
||||
|
||||
vi.mock("../../../lib/api", async (importOriginal) => {
|
||||
@@ -34,7 +36,7 @@ vi.mock("../../../lib/api", async (importOriginal) => {
|
||||
return { flag: id in lists, models: lists[id] ?? [], defaultModel: null };
|
||||
}),
|
||||
},
|
||||
run: { files: r({ items: [] }) },
|
||||
run: { files: r({ items: [] }), dropImage: r({ path: "/tmp/agenthub-drops-1/a.png" }) },
|
||||
ccConfig: {
|
||||
commands: r({
|
||||
items: [
|
||||
@@ -67,6 +69,8 @@ function renderEditor(value: string, slash = true) {
|
||||
placeholder="Ask anything…"
|
||||
slash={slash}
|
||||
fileCwd="/Users/tester"
|
||||
images={[]}
|
||||
onImagesChange={() => {}}
|
||||
{...spies}
|
||||
/>
|
||||
);
|
||||
@@ -117,6 +121,65 @@ describe("PromptEditor — / and @ suggestions", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("PromptEditor — newlines and images", () => {
|
||||
it("Alt+Enter inserts a newline and does not send", () => {
|
||||
const spies = renderEditor("ab");
|
||||
(box() as HTMLTextAreaElement).setSelectionRange(1, 1);
|
||||
fireEvent.keyDown(box(), { key: "Enter", altKey: true });
|
||||
expect(spies.onChange).toHaveBeenLastCalledWith("a\nb");
|
||||
fireEvent.keyDown(box(), { key: "Enter", shiftKey: true });
|
||||
expect(spies.onSubmit).not.toHaveBeenCalled();
|
||||
fireEvent.keyDown(box(), { key: "Enter" });
|
||||
expect(spies.onSubmit).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("shows a pasted image as a thumbnail that opens full size and can be removed", async () => {
|
||||
URL.createObjectURL = vi.fn(() => "blob:shot");
|
||||
URL.revokeObjectURL = vi.fn();
|
||||
function Harness() {
|
||||
const [images, setImages] = useState<PromptImage[]>([]);
|
||||
return (
|
||||
<>
|
||||
<PromptEditor
|
||||
value="see"
|
||||
onChange={() => {}}
|
||||
onSubmit={() => {}}
|
||||
placeholder="Ask anything…"
|
||||
slash
|
||||
fileCwd="/Users/tester"
|
||||
images={images}
|
||||
onImagesChange={setImages}
|
||||
/>
|
||||
<output data-testid="sent">{promptWithImages("see", images)}</output>
|
||||
</>
|
||||
);
|
||||
}
|
||||
render(<Harness />);
|
||||
const img = new File(["x"], "shot.png", { type: "image/png" });
|
||||
fireEvent.paste(box(), { clipboardData: { files: [img] } });
|
||||
const thumb = await screen.findByRole("button", { name: "shot.png" });
|
||||
expect(api.run.dropImage).toHaveBeenCalledWith(img);
|
||||
// The path is not typed into the prompt; it goes with it on send.
|
||||
expect(screen.getByTestId("sent").textContent).toBe("see /tmp/agenthub-drops-1/a.png");
|
||||
|
||||
fireEvent.click(thumb);
|
||||
expect(screen.getByRole("dialog", { name: "shot.png" })).toBeTruthy();
|
||||
fireEvent.keyDown(document, { key: "Escape" });
|
||||
expect(screen.queryByRole("dialog")).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Remove image" }));
|
||||
expect(screen.queryByRole("button", { name: "shot.png" })).toBeNull();
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledWith("blob:shot");
|
||||
expect(screen.getByTestId("sent").textContent).toBe("see");
|
||||
});
|
||||
|
||||
it("leaves a text paste to the textarea", () => {
|
||||
renderEditor("");
|
||||
fireEvent.paste(box(), { clipboardData: { files: [] } });
|
||||
expect(api.run.dropImage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ModelPicker", () => {
|
||||
it("lists the tool's own models, and Custom… reports only the typed id", async () => {
|
||||
const onChange = vi.fn();
|
||||
|
||||
Reference in New Issue
Block a user