mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-24 16:54:43 +00:00
Dashboard Chat is an xterm mirror of a TUI inside the gateway, so server-side clipboard.paste never sees the browser clipboard. Upload pasted/dropped images to the profile's images/ dir (same place clipboard.paste / image.attach use), then drive /image over the PTY. Uses a dedicated /api/chat/image-upload endpoint (magic-byte check, 25MB cap, profile scope) instead of relative managed-files uploads that 400 on local dashboards without a locked root. Ctrl/Cmd+Shift+V also tries clipboard.read() for images before falling back to text, since preventDefault on that chord suppresses the DOM paste event. Salvages #57912 (client composition + /image PTY drive) and folds in #48563's upload endpoint + drop path. Co-authored-by: bird <6666242+bird@users.noreply.github.com> Co-authored-by: tt-a1i <53142663+tt-a1i@users.noreply.github.com>
99 lines
2.9 KiB
TypeScript
99 lines
2.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
firstImageFromClipboard,
|
|
imageFilesFromTransfer,
|
|
transferMayContainImage,
|
|
} from "./chatImagePaste";
|
|
|
|
// Minimal DataTransfer stand-ins. jsdom's DataTransfer doesn't let us seed
|
|
// items/files, so we hand-roll the shape the helpers read.
|
|
function makeItem(kind: string, type: string, file: File | null) {
|
|
return { kind, type, getAsFile: () => file } as unknown as DataTransferItem;
|
|
}
|
|
|
|
function makeData(opts: {
|
|
items?: DataTransferItem[];
|
|
files?: File[];
|
|
}): DataTransfer {
|
|
const items = opts.items ?? [];
|
|
const files = opts.files ?? [];
|
|
const itemList: Record<string | number, unknown> = { length: items.length };
|
|
items.forEach((it, i) => {
|
|
itemList[i] = it;
|
|
});
|
|
const fileList: Record<string | number, unknown> = { length: files.length };
|
|
files.forEach((f, i) => {
|
|
fileList[i] = f;
|
|
});
|
|
return {
|
|
items: itemList,
|
|
files: fileList,
|
|
} as unknown as DataTransfer;
|
|
}
|
|
|
|
const png = new File([new Uint8Array([1, 2, 3])], "x.png", {
|
|
type: "image/png",
|
|
});
|
|
const gif = new File([new Uint8Array([4, 5])], "y.gif", {
|
|
type: "image/gif",
|
|
});
|
|
|
|
describe("firstImageFromClipboard", () => {
|
|
it("returns null for null clipboard data", () => {
|
|
expect(firstImageFromClipboard(null)).toBeNull();
|
|
});
|
|
|
|
it("finds an image via items[].getAsFile()", () => {
|
|
const data = makeData({ items: [makeItem("file", "image/png", png)] });
|
|
expect(firstImageFromClipboard(data)).toBe(png);
|
|
});
|
|
|
|
it("ignores non-file and non-image items", () => {
|
|
const data = makeData({
|
|
items: [
|
|
makeItem("string", "text/plain", null),
|
|
makeItem("file", "application/pdf", new File([], "a.pdf")),
|
|
],
|
|
});
|
|
expect(firstImageFromClipboard(data)).toBeNull();
|
|
});
|
|
|
|
it("falls back to files[] when items are absent (Safari/Firefox)", () => {
|
|
const data = makeData({ files: [png] });
|
|
expect(firstImageFromClipboard(data)).toBe(png);
|
|
});
|
|
|
|
it("returns null when nothing image-like is present", () => {
|
|
const data = makeData({
|
|
files: [new File([], "notes.txt", { type: "text/plain" })],
|
|
});
|
|
expect(firstImageFromClipboard(data)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("imageFilesFromTransfer", () => {
|
|
it("dedupes the same file when present in both items and files", () => {
|
|
const data = makeData({
|
|
items: [makeItem("file", "image/png", png)],
|
|
files: [png, gif],
|
|
});
|
|
expect(imageFilesFromTransfer(data)).toEqual([png, gif]);
|
|
});
|
|
});
|
|
|
|
describe("transferMayContainImage", () => {
|
|
it("is true for image items even when type is empty (some browsers)", () => {
|
|
const data = makeData({
|
|
items: [makeItem("file", "", png)],
|
|
});
|
|
expect(transferMayContainImage(data)).toBe(true);
|
|
});
|
|
|
|
it("is false for text-only transfers", () => {
|
|
const data = makeData({
|
|
items: [makeItem("string", "text/plain", null)],
|
|
});
|
|
expect(transferMayContainImage(data)).toBe(false);
|
|
});
|
|
});
|