Files
oh-my-pi/packages/coding-agent/test/utils/enhanced-paste.test.ts
T
roboomp 43477234e2 fix(tui): handle kitty osc 5522 dot-listing paste responses
Kitty implements the OSC 5522 "list available MIME types" reply by
sending one DATA packet whose `mime` field decodes to the literal `.`
and whose payload carries the available types as a whitespace-separated
list (see `fulfill_read_request` in kovidgoyal/kitty:kitty/clipboard.py
when the requested MIME is `TARGETS_MIME = '.'`). The 5522-mode
ancillary spec (rockorager.dev/misc/bracketed-paste-mime) instead
encodes each available type as its own DATA packet with an empty
payload.

The TUI's EnhancedPasteController only honored the ancillary form. When
Kitty delivered the dot-listing form for a plain-text paste, the parser
pushed `.` into the candidate list, `choosePasteMime(["."])` matched
nothing, and the editor surfaced "Clipboard paste has no supported text
or image data" instead of inserting the text.

Decode the payload as a UTF-8 whitespace-separated MIME list when the
DATA packet's mime is the dot sentinel, then fall through to the
existing per-type behavior. Add two regression tests covering the
single-type and multi-type Kitty payload shapes.

Fixes #2051
2026-06-07 12:10:52 +00:00

181 lines
6.9 KiB
TypeScript

import { describe, expect, it } from "bun:test";
import { EnhancedPasteController } from "../../src/utils/enhanced-paste";
const ST = "\x1b\\";
const OSC = "\x1b]5522;";
function packet(metadata: string, payload?: string): string {
return `${OSC}${metadata}${payload === undefined ? "" : `;${payload}`}${ST}`;
}
describe("EnhancedPasteController", () => {
it("requests image data from an OSC 5522 paste event and preserves chunk boundaries", () => {
const writes: string[] = [];
const pastedImages: Array<{ data: string; mimeType: string }> = [];
const statuses: string[] = [];
const controller = new EnhancedPasteController({
write: data => writes.push(data),
pasteText: () => statuses.push("unexpected text paste"),
pasteImage: image => {
pastedImages.push({ data: image.data, mimeType: image.mimeType });
},
showStatus: message => statuses.push(message),
});
controller.enable();
expect(writes).toEqual(["\x1b[?5522h"]);
const imageMime = Buffer.from("image/png", "utf8").toString("base64");
const textMime = Buffer.from("text/plain", "utf8").toString("base64");
const password = Buffer.from("secret123", "utf8").toString("base64");
controller.handleInput(packet(`type=read:status=OK:pw=${password}`));
controller.handleInput(packet(`type=read:status=DATA:mime=${textMime}`));
controller.handleInput(packet(`type=read:status=DATA:mime=${imageMime}`));
controller.handleInput(packet("type=read:status=DONE"));
const pasteEventName = Buffer.from("Paste event", "utf8").toString("base64");
expect(writes.at(-1)).toBe(`${OSC}type=read:mime=${imageMime}:pw=${password}:name=${pasteEventName}${ST}`);
controller.handleInput(packet("type=read:status=OK"));
controller.handleInput(
packet(`type=read:status=DATA:mime=${imageMime}`, Buffer.from("image-", "utf8").toString("base64")),
);
controller.handleInput(
packet(`type=read:status=DATA:mime=${imageMime}`, Buffer.from("bytes", "utf8").toString("base64")),
);
controller.handleInput(packet("type=read:status=DONE"));
expect(pastedImages).toEqual([
{
data: Buffer.from("image-bytes", "utf8").toString("base64"),
mimeType: "image/png",
},
]);
expect(statuses).toEqual([]);
});
it("falls back to text/plain and carries primary-selection location into the read request", () => {
const writes: string[] = [];
const pastedText: string[] = [];
const controller = new EnhancedPasteController({
write: data => writes.push(data),
pasteText: text => pastedText.push(text),
pasteImage: () => {
throw new Error("unexpected image paste");
},
showStatus: message => pastedText.push(`status:${message}`),
});
const textMime = Buffer.from("text/plain", "utf8").toString("base64");
const password = Buffer.from("secret456", "utf8").toString("base64");
expect(controller.handleInput("plain text")).toBe(false);
controller.handleInput(packet(`type=read:status=OK:loc=primary:pw=${password}`));
controller.handleInput(packet(`type=read:status=DATA:mime=${textMime}`));
controller.handleInput(packet("type=read:status=DONE"));
expect(writes).toHaveLength(1);
expect(writes[0]).toContain(`mime=${textMime}`);
expect(writes[0]).toContain("loc=primary");
expect(writes[0]).toContain(`pw=${password}`);
controller.handleInput(packet("type=read:status=OK"));
controller.handleInput(
packet(`type=read:status=DATA:mime=${textMime}`, Buffer.from("hello ", "utf8").toString("base64")),
);
controller.handleInput(
packet(`type=read:status=DATA:mime=${textMime}`, Buffer.from("world", "utf8").toString("base64")),
);
controller.handleInput(packet("type=read:status=DONE"));
expect(pastedText).toEqual(["hello world"]);
});
it("reports unsupported paste events instead of leaking OSC packets to the editor", () => {
const statuses: string[] = [];
const controller = new EnhancedPasteController({
write: () => {},
pasteText: () => {},
pasteImage: () => {},
showStatus: message => statuses.push(message),
});
const htmlMime = Buffer.from("text/html", "utf8").toString("base64");
expect(controller.handleInput(packet("type=read:status=OK"))).toBe(true);
expect(controller.handleInput(packet(`type=read:status=DATA:mime=${htmlMime}`))).toBe(true);
expect(controller.handleInput(packet("type=read:status=DONE"))).toBe(true);
expect(statuses).toEqual(["Clipboard paste has no supported text or image data"]);
});
it("decodes Kitty's dot-listing DATA payload to discover plain-text and request it", () => {
const writes: string[] = [];
const pastedText: string[] = [];
const controller = new EnhancedPasteController({
write: data => writes.push(data),
pasteText: text => pastedText.push(text),
pasteImage: () => {
throw new Error("unexpected image paste");
},
showStatus: message => pastedText.push(`status:${message}`),
});
const dot = Buffer.from(".", "utf8").toString("base64");
const textMime = Buffer.from("text/plain", "utf8").toString("base64");
const password = Buffer.from("secret-token-123", "utf8").toString("base64");
const pasteEventName = Buffer.from("Paste event", "utf8").toString("base64");
// Kitty bundles the available MIME types into a single DATA packet
// whose `mime` field is the literal `.` and whose payload carries a
// whitespace-separated, base64-encoded list (e.g. "text/plain\n").
controller.handleInput(packet(`type=read:status=OK:pw=${password}`));
controller.handleInput(
packet(
`type=read:status=DATA:mime=${dot}:pw=${password}`,
Buffer.from("text/plain\n", "utf8").toString("base64"),
),
);
controller.handleInput(packet(`type=read:status=DONE:pw=${password}`));
expect(writes.at(-1)).toBe(
`${OSC}type=read:mime=${textMime}:pw=${password}:name=${pasteEventName}${ST}`,
);
controller.handleInput(packet("type=read:status=OK"));
controller.handleInput(
packet(`type=read:status=DATA:mime=${textMime}`, Buffer.from("hello", "utf8").toString("base64")),
);
controller.handleInput(
packet(`type=read:status=DATA:mime=${textMime}`, Buffer.from(" world", "utf8").toString("base64")),
);
controller.handleInput(packet("type=read:status=DONE"));
expect(pastedText).toEqual(["hello world"]);
});
it("prefers images when Kitty's dot-listing payload advertises multiple MIME types", () => {
const writes: string[] = [];
const controller = new EnhancedPasteController({
write: data => writes.push(data),
pasteText: () => {
throw new Error("unexpected text paste");
},
pasteImage: () => {},
showStatus: () => {},
});
const dot = Buffer.from(".", "utf8").toString("base64");
const imageMime = Buffer.from("image/png", "utf8").toString("base64");
controller.handleInput(packet("type=read:status=OK"));
controller.handleInput(
packet(
`type=read:status=DATA:mime=${dot}`,
Buffer.from("text/plain image/png text/html\n", "utf8").toString("base64"),
),
);
controller.handleInput(packet("type=read:status=DONE"));
expect(writes.at(-1)).toContain(`mime=${imageMime}`);
});
});