Files
oh-my-pi/packages/coding-agent/test/utils/enhanced-paste.test.ts
T
roboomp 29730eb643 fix(tui): handle kitty osc 5522 dot-listing paste responses
Kitty implements OSC 5522 paste-event listings by sending one DATA
packet whose mime field decodes to the literal dot sentinel and whose
payload carries the available types as a whitespace-separated list. The
TUI previously only honored the ancillary per-type DATA-packet form, so
plain-text Kitty pastes were rejected as unsupported.

Decode the dot-listing payload during the listing phase and keep the
per-type packet path as a fallback. Also emit the follow-up OSC 5522 read
request using the Kitty protocol shape: the selected MIME list belongs in
the request payload, not in mime metadata. Sending it as metadata makes
Kitty parse an empty requested MIME list and returns no clipboard bytes,
which surfaces as an empty paste.

Add regression coverage for Kitty's text-only and multi-type listings,
and assert the payload-form read requests for default and primary
selection paste events.

Fixes #2051
2026-06-07 13:14:38 +00:00

177 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:pw=${password}:name=${pasteEventName};${imageMime}${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");
const pasteEventName = Buffer.from("Paste event", "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).toEqual([`${OSC}type=read:loc=primary:pw=${password}:name=${pasteEventName};${textMime}${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("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:pw=${password}:name=${pasteEventName};${textMime}${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)).toBe(`${OSC}type=read;${imageMime}${ST}`);
});
});