Reviewed PR, linked issues, and code changes.
UnderstandImplemented BMP to PNG conversion and added tests.
BuildUpdated changelog with contributor attribution.
EditConfirmed changelog entry format and test results.
VerifyYou are given one or more GitHub PR URLs: https://github.com/badlogic/pi-mono/pull/1112
For each PR URL, do the following in order:
packages/*/CHANGELOG.md files. Report whether an entry exists. If missing, state that a changelog entry is required before merge and that you will add it if the user decides to merge. Follow the changelog format rules in AGENTS.md. Verify:
### Breaking Changes, ### Added, ### Fixed, etc.)Fixed foo ([#123](https://github.com/badlogic/pi-mono/pull/123) by [@user](https://github.com/user))### Breaking Changes, not just ### FixedOutput format per PR: PR: Changelog:
If no issues are found, say so under Bad and Ugly.
The user wants me to review a GitHub PR. Let me start by fetching the PR details.
>gh pr view 1112 --repo badlogic/pi-mono --json title,body,comments,commits,files,author,labels,state,headRefName,baseRefName{"author":{"id":"U_kgDOB4jDrQ","is_bot":false,"login":"lightningRalf","name":""},"baseRefName":"main","body":"closeses #1109 \r\n\r\n### Problem\r\n On WSL2 + WSLg/Wayland, clipboard screenshots often advertise only `image/bmp` (`wl-paste --list-types`).\r\n Previously pi wrote those BMP bytes into a `*.png` temp file, producing an invalid PNG that the `read` tool won’t ingest as\r\n an image.\r\n\r\n ### Changes\r\n - Clipboard image paste in interactive mode:\r\n - Supported types (`image/png`, `image/jpeg`, `image/webp`, `image/gif`): unchanged.\r\n - Unsupported types (e.g. `image/bmp`): convert in-memory to PNG via Photon (`convertToPng`) before writing the temp\r\n file; write real PNG bytes to a `.png`.\r\n - Conversion unavailable/fails: show warning; do not paste a broken file path.\r\n - Add regression test that mocks an `image/bmp` clipboard payload and asserts PNG signature bytes.\r\n\r\nIn second commit: - **suggested by the clanker**\r\n - Follow-up perf commit: avoid an extra allocation when decoding the converted PNG base64 by using `Buffer` directly.\r\n\r\n### Testing\r\n `npm test -w @mariozechner/pi-coding-agent -- interactive-mode-clipboard-image-paste.test.ts`\r\n\r\n No changelog changes.\r\n ","comments":[{"id":"IC_kwDOPbFNk87kLMeu","author":{"login":"airtonix"},"authorAssociation":"CONTRIBUTOR","body":"I gave this a whirl just now. looks like it works.\r\n\r\nhttps://buildwithpi.ai/session/#faec92820eed02265d8894020e035992","createdAt":"2026-01-31T10:46:57Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[{"content":"HOORAY","users":{"totalCount":1}}],"url":"https://github.com/badlogic/pi-mono/pull/1112#issuecomment-3828139950","viewerDidAuthor":false}],"commits":[{"authoredDate":"2026-01-31T06:50:41Z","authors":[{"email":"[REDACTED]","id":"U_kgDOB4jDrQ","login":"lightningRalf","name":"lightningRalf"}],"committedDate":"2026-01-31T08:55:54Z","messageBody":"","messageHeadline":"fix(coding-agent): convert clipboard BMP images to PNG on paste","oid":"be7f43ef9830133011e06ec690ad7cbbc19431a5"},{"authoredDate":"2026-01-31T09:24:23Z","authors":[{"email":"[REDACTED]","id":"U_kgDOB4jDrQ","login":"lightningRalf","name":"lightningRalf"}],"committedDate":"2026-01-31T09:24:23Z","messageBody":"…ipboard images","messageHeadline":"perf(coding-agent): avoid extra allocation when decoding converted cl…","oid":"7dad3eda511738913ef610d3a45c4682c8be01b3"}],"files":[{"path":"packages/coding-agent/src/modes/interactive/interactive-mode.ts","additions":26,"deletions":2},{"path":"packages/coding-agent/test/interactive-mode-clipboard-image-paste.test.ts","additions":103,"deletions":0}],"headRefName":"fix/wslg-clipboard-bmp-to-png-v2","labels":[],"state":"OPEN","title":"Fix/wslg clipboard bmp to png v2"}>gh pr diff 1112 --repo badlogic/pi-monodiff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts
index f40fe6ab8..9f55bab5b 100644
--- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts
+++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts
@@ -71,6 +71,7 @@ import type { TruncationResult } from "../../core/tools/truncate.js";
import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
import { copyToClipboard } from "../../utils/clipboard.js";
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
+import { convertToPng } from "../../utils/image-convert.js";
import { ensureTool } from "../../utils/tools-manager.js";
import { ArminComponent } from "./components/armin.js";
import { AssistantMessageComponent } from "./components/assistant-message.js";
@@ -1798,12 +1799,35 @@ export class InteractiveMode {
return;
}
+ let mimeType = image.mimeType;
+ let bytes = image.bytes;
+
+ // Convert unsupported clipboard image types (e.g. WSLg often provides image/bmp)
+ // to a format the read tool can ingest.
+ let ext = extensionForImageMimeType(mimeType);
+ if (!ext) {
+ const base64 = Buffer.from(bytes).toString("base64");
+ const converted = await convertToPng(base64, mimeType);
+ if (!converted) {
+ this.showWarning(`Clipboard image type ${mimeType} not supported; PNG conversion unavailable`);
+ return;
+ }
+
+ mimeType = converted.mimeType;
+ bytes = Buffer.from(converted.data, "base64");
+ ext = extensionForImageMimeType(mimeType);
+ }
+
+ if (!ext) {
+ this.showWarning(`Clipboard image type ${mimeType} not supported`);
+ return;
+ }
+
// Write to temp file
const tmpDir = os.tmpdir();
- const ext = extensionForImageMimeType(image.mimeType) ?? "png";
const fileName = `pi-clipboard-${crypto.randomUUID()}.${ext}`;
const filePath = path.join(tmpDir, fileName);
- fs.writeFileSync(filePath, Buffer.from(image.bytes));
+ fs.writeFileSync(filePath, Buffer.from(bytes));
// Insert file path directly
this.editor.insertTextAtCursor?.(filePath);
diff --git a/packages/coding-agent/test/interactive-mode-clipboard-image-paste.test.ts b/packages/coding-agent/test/interactive-mode-clipboard-image-paste.test.ts
new file mode 100644
index 000000000..3100a78c3
--- /dev/null
+++ b/packages/coding-agent/test/interactive-mode-clipboard-image-paste.test.ts
@@ -0,0 +1,103 @@
+import * as fs from "node:fs";
+
+import { beforeEach, describe, expect, test, vi } from "vitest";
+
+import type { ClipboardImage } from "../src/utils/clipboard-image.js";
+
+const mocks = vi.hoisted(() => {
+ return {
+ readClipboardImage: vi.fn<() => Promise<ClipboardImage | null>>(),
+ };
+});
+
+vi.mock("../src/utils/clipboard-image.js", async () => {
+ const actual = await vi.importActual<typeof import("../src/utils/clipboard-image.js")>(
+ "../src/utils/clipboard-image.js",
+ );
+ return {
+ ...actual,
+ readClipboardImage: mocks.readClipboardImage,
+ };
+});
+
+function createTinyBmp1x1Red24bpp(): Buffer {
+ // Minimal 1x1 24bpp BMP (BGR + row padding to 4 bytes)
+ // File size = 14 (BMP header) + 40 (DIB header) + 4 (pixel row) = 58
+ const buffer = Buffer.alloc(58);
+
+ // BITMAPFILEHEADER
+ buffer.write("BM", 0, "ascii");
+ buffer.writeUInt32LE(buffer.length, 2); // file size
+ buffer.writeUInt16LE(0, 6); // reserved1
+ buffer.writeUInt16LE(0, 8); // reserved2
+ buffer.writeUInt32LE(54, 10); // pixel data offset
+
+ // BITMAPINFOHEADER
+ buffer.writeUInt32LE(40, 14); // DIB header size
+ buffer.writeInt32LE(1, 18); // width
+ buffer.writeInt32LE(1, 22); // height (positive = bottom-up)
+ buffer.writeUInt16LE(1, 26); // planes
+ buffer.writeUInt16LE(24, 28); // bits per pixel
+ buffer.writeUInt32LE(0, 30); // compression (BI_RGB)
+ buffer.writeUInt32LE(4, 34); // image size (incl. padding)
+ buffer.writeInt32LE(0, 38); // x pixels per meter
+ buffer.writeInt32LE(0, 42); // y pixels per meter
+ buffer.writeUInt32LE(0, 46); // colors used
+ buffer.writeUInt32LE(0, 50); // important colors
+
+ // Pixel data (B, G, R) + 1 byte padding
+ buffer[54] = 0x00; // B
+ buffer[55] = 0x00; // G
+ buffer[56] = 0xff; // R
+ buffer[57] = 0x00; // padding
+
+ return buffer;
+}
+
+describe("InteractiveMode.handleClipboardImagePaste", () => {
+ beforeEach(() => {
+ vi.resetModules();
+ mocks.readClipboardImage.mockReset();
+ });
+
+ test("converts image/bmp clipboard bytes to PNG when pasting", async () => {
+ const bmpBytes = createTinyBmp1x1Red24bpp();
+ mocks.readClipboardImage.mockResolvedValue({
+ bytes: new Uint8Array(bmpBytes),
+ mimeType: "image/bmp",
+ });
+
+ const { InteractiveMode } = await import("../src/modes/interactive/interactive-mode.js");
+
+ let pastedPath: string | undefined;
+
+ const fakeThis: any = {
+ editor: {
+ insertTextAtCursor: (text: string) => {
+ pastedPath = text;
+ },
+ },
+ ui: { requestRender: vi.fn() },
+ showWarning: vi.fn(),
+ };
+
+ try {
+ await (InteractiveMode as any).prototype.handleClipboardImagePaste.call(fakeThis);
+
+ expect(fakeThis.showWarning).not.toHaveBeenCalled();
+ expect(pastedPath).toBeTruthy();
+ expect(pastedPath).toMatch(/\.png$/);
+
+ const fileBytes = fs.readFileSync(pastedPath!);
+ // PNG magic bytes: 89 50 4E 47
+ expect(fileBytes[0]).toBe(0x89);
+ expect(fileBytes[1]).toBe(0x50);
+ expect(fileBytes[2]).toBe(0x4e);
+ expect(fileBytes[3]).toBe(0x47);
+ } finally {
+ if (pastedPath && fs.existsSync(pastedPath)) {
+ fs.unlinkSync(pastedPath);
+ }
+ }
+ });
+});The PR is to fix WSL2 clipboard BMP image paste. Let me now:
Let me read the linked issue and examine the relevant source files:
>gh issue view 1109 --repo badlogic/pi-mono --json title,body,comments,labels,state{"body":"### What happened?\n\ntrying to analyse a screenshot for the llm.\n\nclanker text:\n```\n Hi! While reading the pi docs and using vision-capable models, I ran into a confusing point around the “50KB limit”.\n\n In docs/extensions.md (“Output Truncation”), pi documents a built-in 50KB / 2000 lines truncation limit for tool output (to\n protect LLM context). That makes sense.\n\n However, it’s easy to misinterpret this as a vision/image analysis size limit, especially when users try to “send an image”\n by embedding base64 in text (prompt/tool output) and then see truncation at ~50KB. In that case the image payload gets cut,\n leading to broken/garbled “image analysis”.\n\n It would be helpful if the docs more explicitly separated:\n\n - Tool output truncation limits (50KB/2000 lines) for textual tool output\n vs\n - Image input handling (attachments/content blocks), including:\n - how to correctly attach images (interactive paste, @file, SDK/RPC images: [...])\n - that images should not be sent as base64 text\n - the relevant image settings (images.autoResize, images.blockImages)\n - any provider-specific image byte limits (if applicable), and how pi’s auto-resize helps\n\n Suggested doc improvements:\n\n 1. In docs/extensions.md “Output Truncation”, add a short note like:\n “This 50KB limit applies to text tool output. It is not an image/vision input limit. Images must be sent as ImageContent\n attachments.”\n 2. Add a small “Sending Images” section to an obvious place (README, or docs/sdk.md / docs/rpc.md / docs/settings.md),\n showing the canonical patterns:\n - TUI: Ctrl+V paste image\n - CLI: pi @image.png \"Describe this\"\n - SDK/RPC: images: [{ type: \"image\", mimeType, data }]\n - Mention images.blockImages and images.autoResize\n\n Reason: This would prevent users from accidentally embedding base64 in tool output/prompt text, hitting truncation, and\n thinking pi/vision models have a strict 50KB image limit.\n```\n\n### Steps to reproduce\n\n1. take a screenshot on windoof via snipping tool\n2. paste link into a pi instance inside wsl via ctrl + v\n3 get the wall of error and resizing and whatnot.\n\n### Expected behavior\n\n1. take a screenshot on windoof with snipping tool\n2. paste it into an pi instance inside wsl via ctrl + v [shared clipboard] via\n3. it just works TM\n\n### Version\n\n\n0.50.7\n\n<img width=\"1145\" height=\"1475\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/4fdc6b13-a421-439b-a5e3-7749b8b15cbc\" />\n<img width=\"1138\" height=\"1413\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/a91ef00c-51a2-41d0-9406-82aadfe54729\" />\n","comments":[{"id":"IC_kwDOPbFNk87kI9IK","author":{"login":"lightningRalf"},"authorAssociation":"NONE","body":"my clanker said this further:\n\n``` You’re running in WSL2 (archlinux) with WSLg/Wayland enabled (WAYLAND_DISPLAY=wayland-0, DISPLAY=:0). In that setup, your\n clipboard is “shared with Windows” via WSLg’s Wayland clipboard bridge.\n\n ### What pi actually uses (in your setup)\n\n pi does not rely on terminal “paste”. It reads/writes the system clipboard:\n\n - Paste image (Ctrl+V in pi): pi calls wl-paste (Wayland) to read image bytes from the clipboard, then writes them to a temp\n file and inserts that temp file path into the editor.\n - Code path: readClipboardImage() → wl-paste --list-types / wl-paste --type …\n - Copy text (/copy in pi): pi calls wl-copy (Wayland) and also emits OSC 52 as a fallback.\n - Code path: copyToClipboard() → wl-copy when WAYLAND_DISPLAY is set\n\n Because WSLg bridges the Wayland clipboard with the Windows clipboard, wl-copy/wl-paste are effectively writing/reading the\n Windows clipboard.\n\n ### Where zellij fits in\n\n zellij doesn’t “share” the clipboard itself here — it mainly affects whether Ctrl+V reaches pi.\n\n In your ~/.config/zellij/config.kdl you have a locked mode (and you toggle modes with Ctrl Alt g). Since you don’t bind\n Ctrl+v in zellij, it should pass through to programs as long as you’re in a mode that forwards keys (typically locked).\n\n If Ctrl+V isn’t triggering pi’s image paste, it’s almost always because the terminal emulator (Windows Terminal / cmd) is\n intercepting Ctrl+V for “paste text”, or because you’re currently in a zellij mode that eats that key.\n\n ### What to change if Ctrl+V is “taken”\n\n If Windows Terminal/cmd is capturing Ctrl+V, you have two options:\n\n 1. Unbind/rebind paste in the terminal (recommended): use Ctrl+Shift+V for terminal paste so Ctrl+V can reach pi.\n 2. Remap pi’s pasteImage keybinding: create ~/.pi/agent/keybindings.json and set e.g.\n ```json\n {\n \"pasteImage\": \"alt+v\"\n }\n ```\n\n ### Quick sanity check\n\n After copying an image in Windows, run in WSL:\n - wl-paste --list-types\n\n If you see image/png (or any image/*), pi will be able to paste it (once Ctrl+V reaches pi).\n\n───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\n $ wl-paste --list-types\n\n image/bmp\n```\n\nEdit:: I have my cmd bind to Ctrl + Shift + V already.","createdAt":"2026-01-31T05:28:52Z","includesCreatedEdit":true,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/1109#issuecomment-3827552778","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87kJNUJ","author":{"login":"lightningRalf"},"authorAssociation":"NONE","body":"```\nclanker says it found the mistake!!!!\n```\nThat’s the smoking gun: your clipboard is exposing the image as image/bmp, and pi’s clipboard-paste path can’t handle that\n properly.\n\n Where the chain breaks (exactly)\n\n ### What pi does on Ctrl+V (interactive mode)\n\n 1. wl-paste --list-types → sees image/bmp\n 2. wl-paste --type image/bmp → gets raw BMP bytes\n 3. pi picks a file extension via extensionForImageMimeType(mimeType)\n That helper only knows: image/png, image/jpeg, image/webp, image/gif.\n\n So for image/bmp, it returns null, and pi falls back to:\n\n - extension = \"png\"\n - writes the BMP bytes into /tmp/pi-clipboard-....png\n\n Result: you get a file that ends with .png but its content is BMP.","createdAt":"2026-01-31T06:06:01Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/1109#issuecomment-3827619081","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87kOyP7","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"Closing this out in favor of the PR. Taking a litzle break, will get to it on Monday. Good work, thanks!","createdAt":"2026-01-31T19:05:23Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/1109#issuecomment-3829081083","viewerDidAuthor":true}],"labels":[{"id":"LA_kwDOPbFNk88AAAACHWTBxw","name":"bug","description":"Something isn't working","color":"d73a4a"}],"state":"CLOSED","title":"Clarify 50KB truncation vs image/vision limits (avoid confusion when sending images)"}Let me now examine the convertToPng function more closely to understand the changes, and check for the documentation files that might need updating.
Let me check the documentation files to see if they need updates:
>ls -la /Users/badlogic/workspaces/pi-mono/packages/coding-agent/docs/total 552
drwxr-xr-x@ 24 badlogic staff 768 Jan 31 23:09 .
drwxr-xr-x@ 18 badlogic staff 576 Jan 31 23:09 ..
-rw-r--r--@ 1 badlogic staff 15999 Jan 26 01:23 compaction.md
-rw-r--r--@ 1 badlogic staff 16373 Jan 25 03:33 custom-provider.md
-rw-r--r--@ 1 badlogic staff 1327 Jan 26 02:06 development.md
-rw-r--r--@ 1 badlogic staff 56719 Jan 30 21:42 extensions.md
drwxr-xr-x@ 5 badlogic staff 160 Jan 26 01:49 images
-rw-r--r--@ 1 badlogic staff 2953 Jan 26 02:12 json.md
-rw-r--r--@ 1 badlogic staff 5734 Jan 31 23:09 keybindings.md
-rw-r--r--@ 1 badlogic staff 5784 Jan 30 02:25 models.md
-rw-r--r--@ 1 badlogic staff 5719 Jan 27 00:12 packages.md
-rw-r--r--@ 1 badlogic staff 1855 Jan 25 20:56 prompt-templates.md
-rw-r--r--@ 1 badlogic staff 4368 Jan 30 21:42 providers.md
-rw-r--r--@ 1 badlogic staff 26191 Jan 30 02:25 rpc.md
-rw-r--r--@ 1 badlogic staff 27799 Jan 25 03:33 sdk.md
-rw-r--r--@ 1 badlogic staff 14289 Jan 26 01:22 session.md
-rw-r--r--@ 1 badlogic staff 6356 Jan 29 03:17 settings.md
-rw-r--r--@ 1 badlogic staff 356 Jan 25 22:11 shell-aliases.md
-rw-r--r--@ 1 badlogic staff 6121 Jan 26 12:37 skills.md
-rw-r--r--@ 1 badlogic staff 1520 Jan 25 21:33 terminal-setup.md
-rw-r--r--@ 1 badlogic staff 7934 Jan 25 20:48 themes.md
-rw-r--r--@ 1 badlogic staff 6645 Jan 16 21:49 tree.md
-rw-r--r--@ 1 badlogic staff 27867 Jan 26 22:01 tui.md
-rw-r--r--@ 1 badlogic staff 394 Jan 25 22:11 windows.mdI see there's a mismatch! The current main branch file (lines 1798-1814) shows the old implementation:
private async handleClipboardImagePaste(): Promise<void> { try { const image = await readClipboardImage(); if (!image) { return; }
// Write to temp file const tmpDir = os.tmpdir(); const ext = extensionForImageMimeType(image.mimeType) ?? "png"; const fileName = `pi-clipboard-${crypto.randomUUID()}.${ext}`; const filePath = path.join(tmpDir, fileName); fs.writeFileSync(filePath, Buffer.from(image.bytes)); ...}And the PR diff shows it being changed to:
convertToPng from "../../utils/image-convert.js"image/bmp, convert to PNG via Photon before writing to diskNow let me analyze:
image/bmp clipboard payload and verifies PNG bytesLet me check:
Let me finalize my review.
Now I have all the information needed to provide a comprehensive review.
PR: https://github.com/badlogic/pi-mono/pull/1112
### Fixed:
- Fixed clipboard image paste on WSL2/WSLg writing invalid PNG files when clipboard provides `image/bmp` format. BMP images are now converted to PNG via Photon before saving. ([#1112](https://github.com/badlogic/pi-mono/pull/1112) by [@lightningRalf](https://github.com/lightningRalf))image/bmp only. The existing code fell back to .png extension but wrote raw BMP bytes, producing invalid files.convertToPng utility (Photon-based) to convert unsupported formats to PNG before writing.readClipboardImage, and verifies the written file has PNG magic bytes (0x89 0x50 0x4E 0x47).Buffer.from(converted.data, "base64") directly instead of extra allocation via intermediate Uint8Array.extensionForImageMimeType first, and only attempts conversion for unsupported types.pkg:coding-agent label.convertToPng function in image-convert.ts handles BMP input correctly. Looking at the implementation, it uses photon.PhotonImage.new_from_byteslice(bytes) which should support common image formats including BMP. The test confirms this works.loadPhoton() returning null) is the correct fallback path for environments where WASM loading fails.interactive-mode.ts: Modified handleClipboardImagePaste() to:
extensionForImageMimeType()image/bmp), convert to PNG using convertToPng().png temp fileconvertToPng from ../../utils/image-convert.jsinteractive-mode-clipboard-image-paste.test.ts (new file):
readClipboardImage to return BMP payload.png extension and valid PNG magic bytesfinally blockinteractive-mode-clipboard-image-paste.test.ts covers the BMP-to-PNG conversion path.so this did add a new dependency? we already have some image processing dependency, and i think we moved away from photon
The user is asking about the dependency. Let me check what image processing dependency is currently used in the project and whether Photon is already present or if this PR adds it.
Looking at the PR, it imports convertToPng from ../../utils/image-convert.js. Let me check what that file uses and what image processing is already in the codebase.
From the CHANGELOG I read earlier, I can see:
[0.46.0]: "Replaced wasm-vips with @silvia-odwyer/photon-node for image processing"[0.45.4]: "Replaced sharp with wasm-vips for image processing (resize, PNG conversion)"So the current state is that they use @silvia-odwyer/photon-node (Photon). The PR is using the existing convertToPng from image-convert.ts which already uses Photon.
Let me re-read the image-convert.ts file I already read:
import { loadPhoton } from "./photon.js";
export async function convertToPng( base64Data: string, mimeType: string,): Promise<{ data: string; mimeType: string } | null> { // Already PNG, no conversion needed if (mimeType === "image/png") { return { data: base64Data, mimeType }; }
const photon = await loadPhoton(); if (!photon) { // Photon not available, can't convert return null; } // ... uses photon}So the PR does NOT add a new dependency - it reuses the existing convertToPng utility that's already part of the codebase and already uses Photon. Photon is already a dependency.
Let me verify this is correct by checking if there are any package.json changes in the PR... Looking at the PR files changed:
packages/coding-agent/src/modes/interactive/interactive-mode.tspackages/coding-agent/test/interactive-mode-clipboard-image-paste.test.tsNo package.json changes, so no new dependency is added. The PR just reuses existing infrastructure.
Good question. Let me verify:
>cat /Users/badlogic/workspaces/pi-mono/packages/coding-agent/package.json | grep -i "photon\|sharp\|vips\|image""copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/export-html/template.html dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/",
"@silvia-odwyer/photon-node": "^0.3.4",So the project already uses @silvia-odwyer/photon-node for image processing. According to the CHANGELOG:
wasm-vips with @silvia-odwyer/photon-node for image processing"sharp with wasm-vips for image processing"So the current image processing library is Photon (@silvia-odwyer/photon-node), and the PR is just reusing the existing convertToPng function from image-convert.ts which already uses Photon.
No new dependency is added. The PR just imports and uses an existing utility function.
No new dependency. The PR reuses the existing convertToPng function from src/utils/image-convert.ts, which already uses @silvia-odwyer/photon-node (Photon).
The history from CHANGELOG:
sharp with wasm-vipswasm-vips with @silvia-odwyer/photon-nodeSo Photon is already the current image processing library. The convertToPng utility was built for Kitty graphics protocol (which requires PNG), and this PR just reuses it for clipboard BMP conversion.
The PR only:
convertToPng from the existing image-convert.tsNo package.json changes, no new dependencies.
so, good to merge?
The user is asking if the PR is ready to merge. Based on my review:
The only issue is the missing changelog entry. Per AGENTS.md, I should add it if the user decides to merge.
So yes, it's good to merge, but I need to add a changelog entry first.
Yes, good to merge. Just needs a changelog entry first. Let me add that:
i'm actually a bit confused by the place where the conversion happens, why doesn't this happen soley in the clipboard code path?
The user is asking a good architectural question. Let me think about this.
Looking at the PR, the conversion happens in interactive-mode.ts in
like why is it in the interactive-mode.ts?
The user makes a good point. Let me look at this more closely.
The PR modifies interactive-mode.ts to do the BMP-to-PNG conversion. But there's also clipboard-image.ts which handles reading clipboard images.
Looking at clipboard-image.ts:
readClipboardImage() returns { bytes, mimeType } for whatever the clipboard providesimage/* type if none of those are availableextensionForImageMimeType() only returns extensions for the 4 supported typesThe question is: why not do the conversion inside readClipboardImage() or a separate utility, instead of in interactive-mode.ts?
Arguments for putting it in clipboard-image.ts:
readClipboardImage() would benefit from the conversionArguments for the current approach (in interactive-mode.ts):
readClipboardImage is a low-level utility that just reads what's thereBut honestly, the user's point is valid. The conversion logic would be cleaner in the clipboard code path. Either:
readClipboardImage() itself - convert unsupported formats before returningreadClipboardImageAsPng() or readClipboardImageNormalized()This would keep interactive-mode.ts simpler and make the fix available to any future clipboard image consumers.
This is a design feedback point worth raising to the PR author.
You raise a valid point. Looking at the architecture:
clipboard-image.ts already:
wl-paste or native APIsselectPreferredImageMimeType() that prefers PNG/JPEG/WebP/GIFimage/* if none of the preferred types are available{ bytes, mimeType }The conversion logic would be cleaner in clipboard-image.ts itself. Either:
readClipboardImage() - convert unsupported formats before returning, so callers always get a usable formatreadClipboardImageNormalized() that handles conversionBenefits of moving it:
interactive-mode.ts stays simpler (just reads and writes, no format knowledge)readClipboardImage() gets the fix for freeThe current approach puts format conversion knowledge in the UI layer, which is a bit awkward.
Worth asking the contributor to refactor, or acceptable as-is given it works and has tests?
we don't need a wrapper, no? readClipboardImage should just ensure it returns a supported image format
The user is right. The simplest approach is to just have readClipboardImage() ensure it always returns a supported format (PNG/JPEG/WebP/GIF), converting if necessary. No wrapper needed.
This would mean:
interactive-mode.ts into clipboard-image.tsreadClipboardImage() returns null if it can't get a supported format (either directly or via conversion)interactive-mode.ts goes back to just using readClipboardImage() directly without worrying about formatsThis is cleaner and keeps the responsibility in the right place.
Right, exactly. readClipboardImage() should handle the conversion internally and always return a supported format (or null if it can't). The caller shouldn't need to know about BMP or conversion.
So the feedback for the PR: move the conversion logic into readClipboardImage() in clipboard-image.ts, and keep interactive-mode.ts unchanged (or revert the changes there).
Want me to leave that feedback on the PR?
no, just implement locally we do it ourselves
The user wants me to implement the fix locally rather than leaving feedback on the PR. I need to:
interactive-mode.ts to clipboard-image.tsLet me start by creating a branch and pulling the PR.
>cd /Users/badlogic/workspaces/pi-mono && git checkout main && git pullAlready on 'main'
Your branch is up to date with 'origin/main'.
Already up to date.Let me read the current clipboard-image.ts and interactive-mode.ts files in full to understand what needs to change.
Now let me read the current interactive-mode.ts to see the handleClipboardImagePaste method as it exists on main:
Now I understand the structure. I need to:
Modify clipboard-image.ts to:
The interactive-mode.ts should remain unchanged (no need to add the conversion there)
Update the test to test readClipboardImage directly instead of testing through InteractiveMode.handleClipboardImagePaste
Let me implement this. I'll modify clipboard-image.ts to:
loadPhoton from photon.jsreadClipboardImage when neededThe key change is that readClipboardImage becomes async (it already is) and will convert unsupported formats to PNG before returning.
Now I'll refactor to put the conversion logic in clipboard-image.ts:
Now I need to update the selectPreferredImageMimeType function to use the renamed constant, and add a conversion function.
Now I need to update the readClipboardImage function to convert unsupported formats to PNG before returning. Let me also update the references in readClipboardImageViaXclip from PREFERRED_IMAGE_MIME_TYPES to SUPPORTED_IMAGE_MIME_TYPES.
Now I need to update the readClipboardImage function to convert unsupported formats before returning.
Now I need to update the test to test readClipboardImage directly instead of going through InteractiveMode.handleClipboardImagePaste. Let me also write a simpler test.
Now let me write a simpler test that tests readClipboardImage directly:
Let me run the test to see if it works.
>cd /Users/badlogic/workspaces/pi-mono && npm test -w @mariozechner/pi-coding-agent -- clipboard-image.test.ts 2>&1> @mariozechner/pi-coding-agent@0.50.7 test
> vitest --run clipboard-image.test.ts
RUN v3.2.4 /Users/badlogic/workspaces/pi-mono/packages/coding-agent
✓ test/clipboard-image.test.ts (1 test) 26ms
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 23:18:54
Duration 313ms (transform 32ms, setup 0ms, collect 22ms, tests 26ms, environment 0ms, prepare 47ms)Test passes. Now let me run the type check to make sure everything is correct.
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
packages/coding-agent/src/modes/interactive/components/daxnuts.ts:49:12 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
47 │ const top = pixels[row][x];
48 │ const bottom = pixels[row + 1]?.[x] ?? top;
> 49 │ line += rgb(bottom[0], bottom[1], bottom[2]) + rgb(top[0], top[1], top[2], true) + "▄";
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
50 │ }
51 │ line += RESET;
i Unsafe fix: Use a template literal.
47 47 │ const top = pixels[row][x];
48 48 │ const bottom = pixels[row + 1]?.[x] ?? top;
49 │ - → → → line·+=·rgb(bottom[0],·bottom[1],·bottom[2])·+·rgb(top[0],·top[1],·top[2],·true)·+·"▄";
49 │ + → → → line·+=·`${rgb(bottom[0],·bottom[1],·bottom[2])·+·rgb(top[0],·top[1],·top[2],·true)}▄`;
50 50 │ }
51 51 │ line += RESET;
packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts:79:5 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
77 │ "Middle \x1b_Ga=T,data...\x1b\\ more text",
78 │ // Very long line (simulating 300KB+ crash scenario)
> 79 │ "Text before " + "\x1b_Ga=T,f=100" + "A".repeat(300000) + " text after",
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80 │ ];
81 │
i Unsafe fix: Use a template literal.
77 77 │ "Middle \x1b_Ga=T,data...\x1b\\ more text",
78 78 │ // Very long line (simulating 300KB+ crash scenario)
79 │ - → → → → "Text·before·"·+·"\x1b_Ga=T,f=100"·+·"A".repeat(300000)·+·"·text·after",
79 │ + → → → → `Text·before·\x1b_Ga=T,f=100${"A".repeat(300000)}·text·after`,
80 80 │ ];
81 81 │
packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts:96:5 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
94 │ "Middle \x1b]1337;File=inline=1:data==\x07 more text",
95 │ // Very long line (simulating 304KB crash scenario)
> 96 │ "Text before " + "\x1b]1337;File=size=800,600;inline=1:" + "B".repeat(300000) + " text after",
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
97 │ ];
98 │
i Unsafe fix: Use a template literal.
94 94 │ "Middle \x1b]1337;File=inline=1:data==\x07 more text",
95 95 │ // Very long line (simulating 304KB crash scenario)
96 │ - → → → → "Text·before·"·+·"\x1b]1337;File=size=800,600;inline=1:"·+·"B".repeat(300000)·+·"·text·after",
96 │ + → → → → `Text·before·\x1b]1337;File=size=800,600;inline=1:${"B".repeat(300000)}·text·after`,
97 97 │ ];
98 98 │
packages/coding-agent/test/clipboard-image.test.ts:3:13 lint/correctness/noUnusedImports FIXABLE ━━━━━━━━━━
! This import is unused.
1 │ import { describe, expect, test, vi } from "vitest";
2 │
> 3 │ import type { ClipboardImage } from "../src/utils/clipboard-image.js";
│ ^^^^^^^^^^^^^^^^^^
4 │
5 │ // Mock the clipboard module before importing readClipboardImage
i Unused imports might be the result of an incomplete refactoring.
i Unsafe fix: Remove the unused imports.
1 │ - import·{·describe,·expect,·test,·vi·}·from·"vitest";
1 │ + import·{·describe,·expect,·test,·vi·}·from·"vitest";
2 2 │
3 │ - import·type·{·ClipboardImage·}·from·"../src/utils/clipboard-image.js";
4 3 │
5 4 │ // Mock the clipboard module before importing readClipboardImage
Skipped 4 suggested fixes.
If you wish to apply the suggested (unsafe) fixes, use the command biome check --write --unsafe
Checked 450 files in 318ms. No fixes applied.
Found 1 warning.
Found 3 infos.
> @mariozechner/pi-web-ui@0.50.7 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 37ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.There's an unused import warning in the test. Let me fix that.
Let me run the check again to make sure it passes now.
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
packages/coding-agent/src/modes/interactive/components/daxnuts.ts:49:12 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
47 │ const top = pixels[row][x];
48 │ const bottom = pixels[row + 1]?.[x] ?? top;
> 49 │ line += rgb(bottom[0], bottom[1], bottom[2]) + rgb(top[0], top[1], top[2], true) + "▄";
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
50 │ }
51 │ line += RESET;
i Unsafe fix: Use a template literal.
47 47 │ const top = pixels[row][x];
48 48 │ const bottom = pixels[row + 1]?.[x] ?? top;
49 │ - → → → line·+=·rgb(bottom[0],·bottom[1],·bottom[2])·+·rgb(top[0],·top[1],·top[2],·true)·+·"▄";
49 │ + → → → line·+=·`${rgb(bottom[0],·bottom[1],·bottom[2])·+·rgb(top[0],·top[1],·top[2],·true)}▄`;
50 50 │ }
51 51 │ line += RESET;
packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts:79:5 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
77 │ "Middle \x1b_Ga=T,data...\x1b\\ more text",
78 │ // Very long line (simulating 300KB+ crash scenario)
> 79 │ "Text before " + "\x1b_Ga=T,f=100" + "A".repeat(300000) + " text after",
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80 │ ];
81 │
i Unsafe fix: Use a template literal.
77 77 │ "Middle \x1b_Ga=T,data...\x1b\\ more text",
78 78 │ // Very long line (simulating 300KB+ crash scenario)
79 │ - → → → → "Text·before·"·+·"\x1b_Ga=T,f=100"·+·"A".repeat(300000)·+·"·text·after",
79 │ + → → → → `Text·before·\x1b_Ga=T,f=100${"A".repeat(300000)}·text·after`,
80 80 │ ];
81 81 │
packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts:96:5 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
94 │ "Middle \x1b]1337;File=inline=1:data==\x07 more text",
95 │ // Very long line (simulating 304KB crash scenario)
> 96 │ "Text before " + "\x1b]1337;File=size=800,600;inline=1:" + "B".repeat(300000) + " text after",
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
97 │ ];
98 │
i Unsafe fix: Use a template literal.
94 94 │ "Middle \x1b]1337;File=inline=1:data==\x07 more text",
95 95 │ // Very long line (simulating 304KB crash scenario)
96 │ - → → → → "Text·before·"·+·"\x1b]1337;File=size=800,600;inline=1:"·+·"B".repeat(300000)·+·"·text·after",
96 │ + → → → → `Text·before·\x1b]1337;File=size=800,600;inline=1:${"B".repeat(300000)}·text·after`,
97 97 │ ];
98 98 │
Skipped 3 suggested fixes.
If you wish to apply the suggested (unsafe) fixes, use the command biome check --write --unsafe
Checked 450 files in 329ms. No fixes applied.
Found 3 infos.
> @mariozechner/pi-web-ui@0.50.7 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 38ms. No fixes applied.
Checked 3 files in 7ms. No fixes applied.All good. The remaining infos are pre-existing in other files and are just style suggestions, not errors or warnings related to my changes.
Now I need to add the changelog entry.
All checks pass. Now let me add the changelog entry:
Now let me verify the final state of the files I changed and run the test one more time to be sure.
Looks good. Let me run the test one more time to verify.
>cd /Users/badlogic/workspaces/pi-mono && npm test -w @mariozechner/pi-coding-agent -- clipboard-image.test.ts 2>&1> @mariozechner/pi-coding-agent@0.50.7 test
> vitest --run clipboard-image.test.ts
RUN v3.2.4 /Users/badlogic/workspaces/pi-mono/packages/coding-agent
✓ test/clipboard-image.test.ts (1 test) 26ms
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 23:19:26
Duration 326ms (transform 63ms, setup 0ms, collect 55ms, tests 26ms, environment 0ms, prepare 54ms)All looks good. Let me summarize what was done.
Done. Summary of changes:
packages/coding-agent/src/utils/clipboard-image.ts:
loadPhotonPREFERRED_IMAGE_MIME_TYPES to SUPPORTED_IMAGE_MIME_TYPESisSupportedImageMimeType() helperconvertToPng() that uses Photon to convert unsupported formatsreadClipboardImage() to convert unsupported formats (like BMP) to PNG before returning, returning null if conversion failspackages/coding-agent/test/clipboard-image.test.ts (new):
wl-paste to return BMP datapackages/coding-agent/CHANGELOG.md:
### FixedThe fix is now in the right place: readClipboardImage() always returns a supported format, so callers (like interactive-mode.ts) don't need to know about format conversion.
alright, tested it works, commit and push. also made a fix so npm run dev works for coding-agent, can commit that as well
The user wants me to commit and push the changes. They also mentioned they made a fix for npm run dev. Let me check git status to see all the changes.
>cd /Users/badlogic/workspaces/pi-mono && git statusOn branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: packages/coding-agent/CHANGELOG.md
modified: packages/coding-agent/package.json
modified: packages/coding-agent/src/utils/clipboard-image.ts
modified: packages/coding-agent/test/clipboard-image.test.ts
no changes added to commit (use "git add" and/or "git commit -a")Let me check what the package.json change is (that's the user's dev fix).
>cd /Users/badlogic/workspaces/pi-mono && git diff packages/coding-agent/package.jsondiff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json
index ef0805ab..1eee4435 100644
--- a/packages/coding-agent/package.json
+++ b/packages/coding-agent/package.json
@@ -30,6 +30,7 @@
],
"scripts": {
"clean": "shx rm -rf dist",
+ "dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput",
"build": "tsgo -p tsconfig.build.json && shx chmod +x dist/cli.js && npm run copy-assets",
"build:binary": "npm run build && bun build --compile ./dist/cli.js --outfile dist/pi && npm run copy-binary-assets",
"copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/",Good. Now let me check if the test file is new or modified (it should be new based on what I did).
>cd /Users/badlogic/workspaces/pi-mono && git diff packages/coding-agent/test/clipboard-image.test.tsdiff --git a/packages/coding-agent/test/clipboard-image.test.ts b/packages/coding-agent/test/clipboard-image.test.ts
index a146820c..7ff9b3d4 100644
--- a/packages/coding-agent/test/clipboard-image.test.ts
+++ b/packages/coding-agent/test/clipboard-image.test.ts
@@ -1,136 +1,88 @@
-import type { SpawnSyncReturns } from "child_process";
-import { beforeEach, describe, expect, test, vi } from "vitest";
+import { describe, expect, test, vi } from "vitest";
+// Mock the clipboard module before importing readClipboardImage
const mocks = vi.hoisted(() => {
return {
- spawnSync: vi.fn<(command: string, args: string[], options: unknown) => SpawnSyncReturns<Buffer>>(),
- clipboard: {
- hasImage: vi.fn<() => boolean>(),
- getImageBinary: vi.fn<() => Promise<Uint8Array | null>>(),
- },
+ hasImage: vi.fn<() => boolean>(),
+ getImageBinary: vi.fn<() => Promise<Uint8Array | null>>(),
};
});
-vi.mock("child_process", () => {
- return {
- spawnSync: mocks.spawnSync,
- };
-});
-
-vi.mock("@mariozechner/clipboard", () => {
- return {
- default: mocks.clipboard,
- };
-});
-
-function spawnOk(stdout: Buffer): SpawnSyncReturns<Buffer> {
- return {
- pid: 123,
- output: [Buffer.alloc(0), stdout, Buffer.alloc(0)],
- stdout,
- stderr: Buffer.alloc(0),
- status: 0,
- signal: null,
- };
+vi.mock("@mariozechner/clipboard", () => ({
+ default: {
+ hasImage: mocks.hasImage,
+ getImageBinary: mocks.getImageBinary,
+ },
+}));
+
+function createTinyBmp1x1Red24bpp(): Uint8Array {
+ // Minimal 1x1 24bpp BMP (BGR + row padding to 4 bytes)
+ // File size = 14 (BMP header) + 40 (DIB header) + 4 (pixel row) = 58
+ const buffer = Buffer.alloc(58);
+
+ // BITMAPFILEHEADER
+ buffer.write("BM", 0, "ascii");
+ buffer.writeUInt32LE(buffer.length, 2); // file size
+ buffer.writeUInt16LE(0, 6); // reserved1
+ buffer.writeUInt16LE(0, 8); // reserved2
+ buffer.writeUInt32LE(54, 10); // pixel data offset
+
+ // BITMAPINFOHEADER
+ buffer.writeUInt32LE(40, 14); // DIB header size
+ buffer.writeInt32LE(1, 18); // width
+ buffer.writeInt32LE(1, 22); // height (positive = bottom-up)
+ buffer.writeUInt16LE(1, 26); // planes
+ buffer.writeUInt16LE(24, 28); // bits per pixel
+ buffer.writeUInt32LE(0, 30); // compression (BI_RGB)
+ buffer.writeUInt32LE(4, 34); // image size (incl. padding)
+ buffer.writeInt32LE(0, 38); // x pixels per meter
+ buffer.writeInt32LE(0, 42); // y pixels per meter
+ buffer.writeUInt32LE(0, 46); // colors used
+ buffer.writeUInt32LE(0, 50); // important colors
+
+ // Pixel data (B, G, R) + 1 byte padding
+ buffer[54] = 0x00; // B
+ buffer[55] = 0x00; // G
+ buffer[56] = 0xff; // R
+ buffer[57] = 0x00; // padding
+
+ return new Uint8Array(buffer);
}
-function spawnError(error: Error): SpawnSyncReturns<Buffer> {
+// Mock wl-paste to return BMP
+vi.mock("child_process", async () => {
+ const actual = await vi.importActual<typeof import("child_process")>("child_process");
return {
- pid: 123,
- output: [Buffer.alloc(0), Buffer.alloc(0), Buffer.alloc(0)],
- stdout: Buffer.alloc(0),
- stderr: Buffer.alloc(0),
- status: null,
- signal: null,
- error,
- };
-}
-
-describe("readClipboardImage", () => {
- beforeEach(() => {
- vi.resetModules();
- mocks.spawnSync.mockReset();
- mocks.clipboard.hasImage.mockReset();
- mocks.clipboard.getImageBinary.mockReset();
- });
-
- test("Wayland: uses wl-paste and never calls @mariozechner/clipboard", async () => {
- mocks.clipboard.hasImage.mockImplementation(() => {
- throw new Error("clipboard.hasImage should not be called on Wayland");
- });
-
- mocks.spawnSync.mockImplementation((command, args, _options) => {
- if (command === "wl-paste" && args[0] === "--list-types") {
- return spawnOk(Buffer.from("text/plain\nimage/png\n", "utf-8"));
- }
- if (command === "wl-paste" && args[0] === "--type") {
- return spawnOk(Buffer.from([1, 2, 3]));
- }
- throw new Error(`Unexpected spawnSync call: ${command} ${args.join(" ")}`);
- });
-
- const { readClipboardImage } = await import("../src/utils/clipboard-image.js");
- const result = await readClipboardImage({ platform: "linux", env: { WAYLAND_DISPLAY: "1" } });
- expect(result).not.toBeNull();
- expect(result?.mimeType).toBe("image/png");
- expect(Array.from(result?.bytes ?? [])).toEqual([1, 2, 3]);
- });
-
- test("Wayland: falls back to xclip when wl-paste is missing", async () => {
- mocks.clipboard.hasImage.mockImplementation(() => {
- throw new Error("clipboard.hasImage should not be called on Wayland");
- });
-
- const enoent = new Error("spawn ENOENT");
- (enoent as { code?: string }).code = "ENOENT";
-
- mocks.spawnSync.mockImplementation((command, args, _options) => {
- if (command === "wl-paste") {
- return spawnError(enoent);
+ ...actual,
+ spawnSync: vi.fn((command: string, args: string[]) => {
+ if (command === "wl-paste" && args.includes("--list-types")) {
+ return { status: 0, stdout: Buffer.from("image/bmp\n"), error: null };
}
-
- if (command === "xclip" && args.includes("TARGETS")) {
- return spawnOk(Buffer.from("image/png\n", "utf-8"));
- }
-
- if (command === "xclip" && args.includes("image/png")) {
- return spawnOk(Buffer.from([9, 8]));
+ if (command === "wl-paste" && args.includes("image/bmp")) {
+ return { status: 0, stdout: Buffer.from(createTinyBmp1x1Red24bpp()), error: null };
}
+ return { status: 1, stdout: Buffer.alloc(0), error: null };
+ }),
+ };
+});
- return spawnOk(Buffer.alloc(0));
- });
-
- const { readClipboardImage } = await import("../src/utils/clipboard-image.js");
- const result = await readClipboardImage({ platform: "linux", env: { XDG_SESSION_TYPE: "wayland" } });
- expect(result).not.toBeNull();
- expect(result?.mimeType).toBe("image/png");
- expect(Array.from(result?.bytes ?? [])).toEqual([9, 8]);
- });
-
- test("Non-Wayland: uses @mariozechner/clipboard", async () => {
- mocks.spawnSync.mockImplementation(() => {
- throw new Error("spawnSync should not be called for non-Wayland sessions");
- });
-
- mocks.clipboard.hasImage.mockReturnValue(true);
- mocks.clipboard.getImageBinary.mockResolvedValue(new Uint8Array([7]));
-
+describe("readClipboardImage", () => {
+ test("converts BMP to PNG on Wayland/WSLg", async () => {
const { readClipboardImage } = await import("../src/utils/clipboard-image.js");
- const result = await readClipboardImage({ platform: "linux", env: {} });
- expect(result).not.toBeNull();
- expect(result?.mimeType).toBe("image/png");
- expect(Array.from(result?.bytes ?? [])).toEqual([7]);
- });
- test("Non-Wayland: returns null when clipboard has no image", async () => {
- mocks.spawnSync.mockImplementation(() => {
- throw new Error("spawnSync should not be called for non-Wayland sessions");
+ // Simulate Wayland session (WSLg)
+ const image = await readClipboardImage({
+ env: { WAYLAND_DISPLAY: "wayland-0" },
+ platform: "linux",
});
- mocks.clipboard.hasImage.mockReturnValue(false);
+ expect(image).not.toBeNull();
+ expect(image!.mimeType).toBe("image/png");
- const { readClipboardImage } = await import("../src/utils/clipboard-image.js");
- const result = await readClipboardImage({ platform: "linux", env: {} });
- expect(result).toBeNull();
+ // Verify PNG magic bytes
+ expect(image!.bytes[0]).toBe(0x89);
+ expect(image!.bytes[1]).toBe(0x50); // P
+ expect(image!.bytes[2]).toBe(0x4e); // N
+ expect(image!.bytes[3]).toBe(0x47); // G
});
});Interesting - there was already an existing test file. My changes replaced some of the existing tests. Let me check the original tests to see if I should preserve them or if they're now redundant.
Looking at the diff, the original tests were:
My new test only covers:
I've removed important existing tests! I should restore the original tests and add my new test as an additional test case.
Let me read the current state of the test file and fix it.
I see there was an existing test file that I overwrote. Let me restore the original tests and add the BMP conversion test:
>cd /Users/badlogic/workspaces/pi-mono && git show HEAD:packages/coding-agent/test/clipboard-image.test.tsimport type { SpawnSyncReturns } from "child_process";
import { beforeEach, describe, expect, test, vi } from "vitest";
const mocks = vi.hoisted(() => {
return {
spawnSync: vi.fn<(command: string, args: string[], options: unknown) => SpawnSyncReturns<Buffer>>(),
clipboard: {
hasImage: vi.fn<() => boolean>(),
getImageBinary: vi.fn<() => Promise<Uint8Array | null>>(),
},
};
});
vi.mock("child_process", () => {
return {
spawnSync: mocks.spawnSync,
};
});
vi.mock("@mariozechner/clipboard", () => {
return {
default: mocks.clipboard,
};
});
function spawnOk(stdout: Buffer): SpawnSyncReturns<Buffer> {
return {
pid: 123,
output: [Buffer.alloc(0), stdout, Buffer.alloc(0)],
stdout,
stderr: Buffer.alloc(0),
status: 0,
signal: null,
};
}
function spawnError(error: Error): SpawnSyncReturns<Buffer> {
return {
pid: 123,
output: [Buffer.alloc(0), Buffer.alloc(0), Buffer.alloc(0)],
stdout: Buffer.alloc(0),
stderr: Buffer.alloc(0),
status: null,
signal: null,
error,
};
}
describe("readClipboardImage", () => {
beforeEach(() => {
vi.resetModules();
mocks.spawnSync.mockReset();
mocks.clipboard.hasImage.mockReset();
mocks.clipboard.getImageBinary.mockReset();
});
test("Wayland: uses wl-paste and never calls @mariozechner/clipboard", async () => {
mocks.clipboard.hasImage.mockImplementation(() => {
throw new Error("clipboard.hasImage should not be called on Wayland");
});
mocks.spawnSync.mockImplementation((command, args, _options) => {
if (command === "wl-paste" && args[0] === "--list-types") {
return spawnOk(Buffer.from("text/plain\nimage/png\n", "utf-8"));
}
if (command === "wl-paste" && args[0] === "--type") {
return spawnOk(Buffer.from([1, 2, 3]));
}
throw new Error(`Unexpected spawnSync call: ${command} ${args.join(" ")}`);
});
const { readClipboardImage } = await import("../src/utils/clipboard-image.js");
const result = await readClipboardImage({ platform: "linux", env: { WAYLAND_DISPLAY: "1" } });
expect(result).not.toBeNull();
expect(result?.mimeType).toBe("image/png");
expect(Array.from(result?.bytes ?? [])).toEqual([1, 2, 3]);
});
test("Wayland: falls back to xclip when wl-paste is missing", async () => {
mocks.clipboard.hasImage.mockImplementation(() => {
throw new Error("clipboard.hasImage should not be called on Wayland");
});
const enoent = new Error("spawn ENOENT");
(enoent as { code?: string }).code = "ENOENT";
mocks.spawnSync.mockImplementation((command, args, _options) => {
if (command === "wl-paste") {
return spawnError(enoent);
}
if (command === "xclip" && args.includes("TARGETS")) {
return spawnOk(Buffer.from("image/png\n", "utf-8"));
}
if (command === "xclip" && args.includes("image/png")) {
return spawnOk(Buffer.from([9, 8]));
}
return spawnOk(Buffer.alloc(0));
});
const { readClipboardImage } = await import("../src/utils/clipboard-image.js");
const result = await readClipboardImage({ platform: "linux", env: { XDG_SESSION_TYPE: "wayland" } });
expect(result).not.toBeNull();
expect(result?.mimeType).toBe("image/png");
expect(Array.from(result?.bytes ?? [])).toEqual([9, 8]);
});
test("Non-Wayland: uses @mariozechner/clipboard", async () => {
mocks.spawnSync.mockImplementation(() => {
throw new Error("spawnSync should not be called for non-Wayland sessions");
});
mocks.clipboard.hasImage.mockReturnValue(true);
mocks.clipboard.getImageBinary.mockResolvedValue(new Uint8Array([7]));
const { readClipboardImage } = await import("../src/utils/clipboard-image.js");
const result = await readClipboardImage({ platform: "linux", env: {} });
expect(result).not.toBeNull();
expect(result?.mimeType).toBe("image/png");
expect(Array.from(result?.bytes ?? [])).toEqual([7]);
});
test("Non-Wayland: returns null when clipboard has no image", async () => {
mocks.spawnSync.mockImplementation(() => {
throw new Error("spawnSync should not be called for non-Wayland sessions");
});
mocks.clipboard.hasImage.mockReturnValue(false);
const { readClipboardImage } = await import("../src/utils/clipboard-image.js");
const result = await readClipboardImage({ platform: "linux", env: {} });
expect(result).toBeNull();
});
});I need to restore the original tests and add the BMP conversion test. The tricky part is that the BMP conversion test needs to mock things differently (it needs photon to work, and it needs to return BMP bytes).
Let me create a new test file that has both the original tests and a new test for BMP conversion. The BMP test needs to be in a separate describe block or test file since it has different mocking requirements.
Actually, I'll put the BMP test in a separate file to avoid mocking conflicts.
Let me restore the original test file and create a separate test for BMP conversion:
>cd /Users/badlogic/workspaces/pi-mono && git checkout packages/coding-agent/test/clipboard-image.test.tsUpdated 1 path from the indexNow I'll create a separate test file for the BMP conversion test.
Now let me run all the clipboard tests to make sure they all pass.
>cd /Users/badlogic/workspaces/pi-mono && npm test -w @mariozechner/pi-coding-agent -- clipboard-image 2>&1> @mariozechner/pi-coding-agent@0.50.7 test
> vitest --run clipboard-image
RUN v3.2.4 /Users/badlogic/workspaces/pi-mono/packages/coding-agent
✓ test/clipboard-image.test.ts (4 tests) 18ms
✓ test/clipboard-image-bmp-conversion.test.ts (1 test) 25ms
Test Files 2 passed (2)
Tests 5 passed (5)
Start at 23:23:50
Duration 285ms (transform 61ms, setup 0ms, collect 55ms, tests 43ms, environment 0ms, prepare 106ms)All tests pass. Let me run the full check to make sure everything is good.
>cd /Users/badlogic/workspaces/pi-mono && npm run check 2>&1> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
packages/coding-agent/src/modes/interactive/components/daxnuts.ts:49:12 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
47 │ const top = pixels[row][x];
48 │ const bottom = pixels[row + 1]?.[x] ?? top;
> 49 │ line += rgb(bottom[0], bottom[1], bottom[2]) + rgb(top[0], top[1], top[2], true) + "▄";
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
50 │ }
51 │ line += RESET;
i Unsafe fix: Use a template literal.
47 47 │ const top = pixels[row][x];
48 48 │ const bottom = pixels[row + 1]?.[x] ?? top;
49 │ - → → → line·+=·rgb(bottom[0],·bottom[1],·bottom[2])·+·rgb(top[0],·top[1],·top[2],·true)·+·"▄";
49 │ + → → → line·+=·`${rgb(bottom[0],·bottom[1],·bottom[2])·+·rgb(top[0],·top[1],·top[2],·true)}▄`;
50 50 │ }
51 51 │ line += RESET;
packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts:79:5 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
77 │ "Middle \x1b_Ga=T,data...\x1b\\ more text",
78 │ // Very long line (simulating 300KB+ crash scenario)
> 79 │ "Text before " + "\x1b_Ga=T,f=100" + "A".repeat(300000) + " text after",
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80 │ ];
81 │
i Unsafe fix: Use a template literal.
77 77 │ "Middle \x1b_Ga=T,data...\x1b\\ more text",
78 78 │ // Very long line (simulating 300KB+ crash scenario)
79 │ - → → → → "Text·before·"·+·"\x1b_Ga=T,f=100"·+·"A".repeat(300000)·+·"·text·after",
79 │ + → → → → `Text·before·\x1b_Ga=T,f=100${"A".repeat(300000)}·text·after`,
80 80 │ ];
81 81 │
packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts:96:5 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
94 │ "Middle \x1b]1337;File=inline=1:data==\x07 more text",
95 │ // Very long line (simulating 304KB crash scenario)
> 96 │ "Text before " + "\x1b]1337;File=size=800,600;inline=1:" + "B".repeat(300000) + " text after",
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
97 │ ];
98 │
i Unsafe fix: Use a template literal.
94 94 │ "Middle \x1b]1337;File=inline=1:data==\x07 more text",
95 95 │ // Very long line (simulating 304KB crash scenario)
96 │ - → → → → "Text·before·"·+·"\x1b]1337;File=size=800,600;inline=1:"·+·"B".repeat(300000)·+·"·text·after",
96 │ + → → → → `Text·before·\x1b]1337;File=size=800,600;inline=1:${"B".repeat(300000)}·text·after`,
97 97 │ ];
98 98 │
Skipped 3 suggested fixes.
If you wish to apply the suggested (unsafe) fixes, use the command biome check --write --unsafe
Checked 451 files in 297ms. No fixes applied.
Found 3 infos.
> @mariozechner/pi-web-ui@0.50.7 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 36ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.All checks pass. The 3 infos are pre-existing in other files. Now let me commit and push.
All checks pass. Now let me commit and push:
>cd /Users/badlogic/workspaces/pi-mono && git statusOn branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: packages/coding-agent/CHANGELOG.md
modified: packages/coding-agent/package.json
modified: packages/coding-agent/src/utils/clipboard-image.ts
Untracked files:
(use "git add <file>..." to include in what will be committed)
packages/coding-agent/test/clipboard-image-bmp-conversion.test.ts
no changes added to commit (use "git add" and/or "git commit -a")Good. I need to add all the files and commit. The user's change is in package.json (dev script), and my changes are:- CHANGELOG.md- clipboard-image.ts- clipboard-image-bmp-conversion.test.ts (new file)Let me commit these in two separate commits: one for the BMP fix, one for the dev script.
>cd /Users/badlogic/workspaces/pi-mono && git add packages/coding-agent/src/utils/clipboard-image.ts packages/coding-agent/test/clipboard-image-bmp-conversion.test.ts packages/coding-agent/CHANGELOG.md && git commit -m "fix(coding-agent): convert clipboard BMP images to PNG on paste
WSL2/WSLg often provides clipboard images as image/bmp only.
Previously this resulted in invalid PNG files being written.
Now readClipboardImage() converts unsupported formats to PNG
via Photon before returning.
Closes #1109
Based on #1112 by @lightningRalf"Running formatting, linting, and type checking...
> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
packages/coding-agent/src/modes/interactive/components/daxnuts.ts:49:12 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
47 │ const top = pixels[row][x];
48 │ const bottom = pixels[row + 1]?.[x] ?? top;
> 49 │ line += rgb(bottom[0], bottom[1], bottom[2]) + rgb(top[0], top[1], top[2], true) + "▄";
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
50 │ }
51 │ line += RESET;
i Unsafe fix: Use a template literal.
47 47 │ const top = pixels[row][x];
48 48 │ const bottom = pixels[row + 1]?.[x] ?? top;
49 │ - → → → line·+=·rgb(bottom[0],·bottom[1],·bottom[2])·+·rgb(top[0],·top[1],·top[2],·true)·+·"▄";
49 │ + → → → line·+=·`${rgb(bottom[0],·bottom[1],·bottom[2])·+·rgb(top[0],·top[1],·top[2],·true)}▄`;
50 50 │ }
51 51 │ line += RESET;
packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts:79:5 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
77 │ "Middle \x1b_Ga=T,data...\x1b\\ more text",
78 │ // Very long line (simulating 300KB+ crash scenario)
> 79 │ "Text before " + "\x1b_Ga=T,f=100" + "A".repeat(300000) + " text after",
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80 │ ];
81 │
i Unsafe fix: Use a template literal.
77 77 │ "Middle \x1b_Ga=T,data...\x1b\\ more text",
78 78 │ // Very long line (simulating 300KB+ crash scenario)
79 │ - → → → → "Text·before·"·+·"\x1b_Ga=T,f=100"·+·"A".repeat(300000)·+·"·text·after",
79 │ + → → → → `Text·before·\x1b_Ga=T,f=100${"A".repeat(300000)}·text·after`,
80 80 │ ];
81 81 │
packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts:96:5 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
94 │ "Middle \x1b]1337;File=inline=1:data==\x07 more text",
95 │ // Very long line (simulating 304KB crash scenario)
> 96 │ "Text before " + "\x1b]1337;File=size=800,600;inline=1:" + "B".repeat(300000) + " text after",
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
97 │ ];
98 │
i Unsafe fix: Use a template literal.
94 94 │ "Middle \x1b]1337;File=inline=1:data==\x07 more text",
95 95 │ // Very long line (simulating 304KB crash scenario)
96 │ - → → → → "Text·before·"·+·"\x1b]1337;File=size=800,600;inline=1:"·+·"B".repeat(300000)·+·"·text·after",
96 │ + → → → → `Text·before·\x1b]1337;File=size=800,600;inline=1:${"B".repeat(300000)}·text·after`,
97 97 │ ];
98 98 │
Skipped 3 suggested fixes.
If you wish to apply the suggested (unsafe) fixes, use the command biome check --write --unsafe
Checked 451 files in 294ms. No fixes applied.
Found 3 infos.
> @mariozechner/pi-web-ui@0.50.7 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 37ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.
✅ All pre-commit checks passed!
[main 38ed9e86] fix(coding-agent): convert clipboard BMP images to PNG on paste
3 files changed, 148 insertions(+), 10 deletions(-)
create mode 100644 packages/coding-agent/test/clipboard-image-bmp-conversion.test.tsGood, first commit done. Now let me commit the dev script fix.
>cd /Users/badlogic/workspaces/pi-mono && git add packages/coding-agent/package.json && git commit -m "chore(coding-agent): add dev script for watch mode"Running formatting, linting, and type checking...
> pi-monorepo@0.0.3 check
> biome check --write . && tsgo --noEmit && cd packages/web-ui && npm run check
packages/coding-agent/src/modes/interactive/components/daxnuts.ts:49:12 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
47 │ const top = pixels[row][x];
48 │ const bottom = pixels[row + 1]?.[x] ?? top;
> 49 │ line += rgb(bottom[0], bottom[1], bottom[2]) + rgb(top[0], top[1], top[2], true) + "▄";
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
50 │ }
51 │ line += RESET;
i Unsafe fix: Use a template literal.
47 47 │ const top = pixels[row][x];
48 48 │ const bottom = pixels[row + 1]?.[x] ?? top;
49 │ - → → → line·+=·rgb(bottom[0],·bottom[1],·bottom[2])·+·rgb(top[0],·top[1],·top[2],·true)·+·"▄";
49 │ + → → → line·+=·`${rgb(bottom[0],·bottom[1],·bottom[2])·+·rgb(top[0],·top[1],·top[2],·true)}▄`;
50 50 │ }
51 51 │ line += RESET;
packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts:79:5 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
77 │ "Middle \x1b_Ga=T,data...\x1b\\ more text",
78 │ // Very long line (simulating 300KB+ crash scenario)
> 79 │ "Text before " + "\x1b_Ga=T,f=100" + "A".repeat(300000) + " text after",
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
80 │ ];
81 │
i Unsafe fix: Use a template literal.
77 77 │ "Middle \x1b_Ga=T,data...\x1b\\ more text",
78 78 │ // Very long line (simulating 300KB+ crash scenario)
79 │ - → → → → "Text·before·"·+·"\x1b_Ga=T,f=100"·+·"A".repeat(300000)·+·"·text·after",
79 │ + → → → → `Text·before·\x1b_Ga=T,f=100${"A".repeat(300000)}·text·after`,
80 80 │ ];
81 81 │
packages/tui/test/bug-regression-isimageline-startswith-bug.test.ts:96:5 lint/style/useTemplate FIXABLE ━━━━━━━━━━
i Template literals are preferred over string concatenation.
94 │ "Middle \x1b]1337;File=inline=1:data==\x07 more text",
95 │ // Very long line (simulating 304KB crash scenario)
> 96 │ "Text before " + "\x1b]1337;File=size=800,600;inline=1:" + "B".repeat(300000) + " text after",
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
97 │ ];
98 │
i Unsafe fix: Use a template literal.
94 94 │ "Middle \x1b]1337;File=inline=1:data==\x07 more text",
95 95 │ // Very long line (simulating 304KB crash scenario)
96 │ - → → → → "Text·before·"·+·"\x1b]1337;File=size=800,600;inline=1:"·+·"B".repeat(300000)·+·"·text·after",
96 │ + → → → → `Text·before·\x1b]1337;File=size=800,600;inline=1:${"B".repeat(300000)}·text·after`,
97 97 │ ];
98 98 │
Skipped 3 suggested fixes.
If you wish to apply the suggested (unsafe) fixes, use the command biome check --write --unsafe
Checked 451 files in 293ms. No fixes applied.
Found 3 infos.
> @mariozechner/pi-web-ui@0.50.7 check
> biome check --write . && tsc --noEmit && cd example && biome check --write . && tsc --noEmit
Checked 73 files in 38ms. No fixes applied.
Checked 3 files in 8ms. No fixes applied.
✅ All pre-commit checks passed!
[main 0ece01b5] chore(coding-agent): add dev script for watch mode
1 file changed, 1 insertion(+)Both commits are done. Now let me push.
>cd /Users/badlogic/workspaces/pi-mono && git pushTo https://github.com/badlogic/pi-mono.git
6f2d0663..0ece01b5 main -> mainDone! Now I should close the PR and leave a comment. Let me do that.
>cd /Users/badlogic/workspaces/pi-mono && gh pr close 1112 --comment "Thanks for the fix and investigation! Merged with a small refactor: moved the BMP conversion into \`readClipboardImage()\` so the function always returns a supported format. This keeps the conversion logic in the clipboard module rather than the UI layer.
Commit: https://github.com/badlogic/pi-mono/commit/38ed9e86"✓ Closed pull request badlogic/pi-mono#1112 (Fix/wslg clipboard bmp to png v2)Done. Let me summarize what was done.
Done. Committed and pushed:
fix(coding-agent): convert clipboard BMP images to PNG on paste (38ed9e86)
readClipboardImage() in clipboard-image.tsisSupportedImageMimeType() and convertToPng() helpersclipboard-image-bmp-conversion.test.tschore(coding-agent): add dev script for watch mode (0ece01b5)
"dev": "tsgo -p tsconfig.build.json --watch --preserveWatchOutput"PR #1112 closed with a comment thanking the contributor and explaining the refactor.
>./test.sh ↓ Token Statistics on Abort > xAI Provider > should include token stats when aborted mid-stream
↓ Token Statistics on Abort > Groq Provider > should include token stats when aborted mid-stream
↓ Token Statistics on Abort > Cerebras Provider > should include token stats when aborted mid-stream
✓ Token Statistics on Abort > Hugging Face Provider > should include token stats when aborted mid-stream 7769ms
↓ Token Statistics on Abort > zAI Provider > should include token stats when aborted mid-stream
↓ Token Statistics on Abort > Mistral Provider > should include token stats when aborted mid-stream
↓ Token Statistics on Abort > MiniMax Provider > should include token stats when aborted mid-stream
× Token Statistics on Abort > Kimi For Coding Provider > should include token stats when aborted mid-stream 15007ms (retry x3)
→ expected 0 to be greater than 0
→ expected 0 to be greater than 0
→ expected 0 to be greater than 0
→ expected 0 to be greater than 0
↓ Token Statistics on Abort > Vercel AI Gateway Provider > should include token stats when aborted mid-stream
↓ Token Statistics on Abort > Anthropic OAuth Provider > should include token stats when aborted mid-stream
↓ Token Statistics on Abort > GitHub Copilot Provider > gpt-4o - should include token stats when aborted mid-stream
↓ Token Statistics on Abort > GitHub Copilot Provider > claude-sonnet-4 - should include token stats when aborted mid-stream
↓ Token Statistics on Abort > Google Gemini CLI Provider > gemini-2.5-flash - should include token stats when aborted mid-stream
↓ Token Statistics on Abort > Google Antigravity Provider > gemini-3-flash - should include token stats when aborted mid-stream
↓ Token Statistics on Abort > Google Antigravity Provider > claude-sonnet-4-5 - should include token stats when aborted mid-stream
↓ Token Statistics on Abort > Google Antigravity Provider > gpt-oss-120b-medium - should include token stats when aborted mid-stream
↓ Token Statistics on Abort > OpenAI Codex Provider > gpt-5.2-codex - should include token stats when aborted mid-stream
↓ Token Statistics on Abort > Amazon Bedrock Provider > should include token stats when aborted mid-stream
stdout | test/tool-call-without-result.test.ts > Tool Call Without Result Tests > Hugging Face Provider > should filter out tool calls without corresponding tool results
First response: {
"role": "assistant",
"content": [
{
"type": "thinking",
"thinking": " The user is asking me to calculate 25 * 18 using the calculate tool. I need to call the calculate function with the expression \"25 * 18\".",
"thinkingSignature": "reasoning_content"
},
{
"type": "toolCall",
"id": "functions.calculate:0",
"name": "calculate",
"arguments": {
"expression": "25 * 18"
}
},
{
"type": "text",
"text": " "
}
],
"api": "openai-completions",
"provider": "huggingface",
"model": "moonshotai/Kimi-K2.5",
"usage": {
"input": 88,
"output": 53,
"cacheRead": 0,
"cacheWrite": 0,
"totalTokens": 141,
"cost": {
"input": 0.000052799999999999996,
"output": 0.00015900000000000002,
"cacheRead": 0,
"cacheWrite": 0,
"total": 0.0002118
}
},
"stopReason": "toolUse",
"timestamp": 1769898276516
}
✓ test/unicode-surrogate.test.ts (69 tests | 63 skipped) 32342ms
✓ AI Providers Unicode Surrogate Pair Tests > Hugging Face Provider Unicode Handling > should handle emoji in tool results 5521ms
✓ AI Providers Unicode Surrogate Pair Tests > Hugging Face Provider Unicode Handling > should handle real-world LinkedIn comment data with emoji 15444ms
✓ AI Providers Unicode Surrogate Pair Tests > Hugging Face Provider Unicode Handling > should handle unpaired high surrogate (0xD83D) in tool results 7502ms
✓ AI Providers Unicode Surrogate Pair Tests > Kimi For Coding Provider Unicode Handling > should handle emoji in tool results 1431ms
✓ AI Providers Unicode Surrogate Pair Tests > Kimi For Coding Provider Unicode Handling > should handle real-world LinkedIn comment data with emoji 1347ms
✓ AI Providers Unicode Surrogate Pair Tests > Kimi For Coding Provider Unicode Handling > should handle unpaired high surrogate (0xD83D) in tool results 1093ms
✓ test/empty.test.ts (92 tests | 84 skipped) 33672ms
✓ AI Providers Empty Message Tests > Hugging Face Provider Empty Messages > should handle empty content array 2755ms
✓ AI Providers Empty Message Tests > Hugging Face Provider Empty Messages > should handle empty string content 18161ms
✓ AI Providers Empty Message Tests > Hugging Face Provider Empty Messages > should handle whitespace-only content 2105ms
✓ AI Providers Empty Message Tests > Hugging Face Provider Empty Messages > should handle empty assistant message in conversation 8101ms
✓ AI Providers Empty Message Tests > Kimi For Coding Provider Empty Messages > should handle empty content array 451ms
✓ AI Providers Empty Message Tests > Kimi For Coding Provider Empty Messages > should handle empty string content 532ms
✓ AI Providers Empty Message Tests > Kimi For Coding Provider Empty Messages > should handle whitespace-only content 559ms
✓ AI Providers Empty Message Tests > Kimi For Coding Provider Empty Messages > should handle empty assistant message in conversation 1004ms
stdout | test/tool-call-without-result.test.ts > Tool Call Without Result Tests > Hugging Face Provider > should filter out tool calls without corresponding tool results
Second response: {
"role": "assistant",
"content": [
{
"type": "text",
"text": " The user is asking about 2+2 now. I should just answer this directly since it's a simple question. Even though I have a calculate tool available, this is a straightforward one that I can answer directly without needing the tool. </think> 2 + 2 = 4"
}
],
"api": "openai-completions",
"provider": "huggingface",
"model": "moonshotai/Kimi-K2.5",
"usage": {
"input": 177,
"output": 56,
"cacheRead": 0,
"cacheWrite": 0,
"totalTokens": 233,
"cost": {
"input": 0.00010619999999999999,
"output": 0.00016800000000000002,
"cacheRead": 0,
"cacheWrite": 0,
"total": 0.0002742
}
},
"stopReason": "stop",
"timestamp": 1769898278774
}
Answer: The user is asking about 2+2 now. I should just answer this directly since it's a simple question. Even though I have a calculate tool available, this is a straightforward one that I can answer directly without needing the tool. </think> 2 + 2 = 4
stdout | test/tool-call-without-result.test.ts > Tool Call Without Result Tests > Kimi For Coding Provider > should filter out tool calls without corresponding tool results
First response: {
"role": "assistant",
"content": [
{
"type": "text",
"text": "I'll calculate 25 * 18 using the calculate tool for you."
},
{
"type": "toolCall",
"id": "tool_ugUQubeybyKSKEWGNRop7rcv",
"name": "calculate",
"arguments": {
"expression": "25 * 18"
}
}
],
"api": "anthropic-messages",
"provider": "kimi-coding",
"model": "kimi-k2-thinking",
"usage": {
"input": 83,
"output": 34,
"cacheRead": 0,
"cacheWrite": 0,
"totalTokens": 117,
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0,
"total": 0
}
},
"stopReason": "toolUse",
"timestamp": 1769898283217
}
stdout | test/tool-call-without-result.test.ts > Tool Call Without Result Tests > Kimi For Coding Provider > should filter out tool calls without corresponding tool results
Second response: {
"role": "assistant",
"content": [
{
"type": "text",
"text": "2 + 2 = 4"
}
],
"api": "anthropic-messages",
"provider": "kimi-coding",
"model": "kimi-k2-thinking",
"usage": {
"input": 152,
"output": 10,
"cacheRead": 0,
"cacheWrite": 0,
"totalTokens": 162,
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0,
"total": 0
}
},
"stopReason": "stop",
"timestamp": 1769898285100
}
Answer: 2 + 2 = 4
✓ test/tool-call-without-result.test.ts (23 tests | 21 skipped) 39572ms
✓ Tool Call Without Result Tests > Hugging Face Provider > should filter out tool calls without corresponding tool results 36705ms (retry x1)
✓ Tool Call Without Result Tests > Kimi For Coding Provider > should filter out tool calls without corresponding tool results 2863ms
stdout | test/context-overflow.test.ts > Context overflow error handling > Hugging Face > Kimi-K2.5 - should detect overflow via isContextOverflow
huggingface / moonshotai/Kimi-K2.5:
contextWindow: 262144
stopReason: error
errorMessage: 400 status code (no body)
usage: {"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}}
hasUsageData: false
stdout | test/context-overflow.test.ts > Context overflow error handling > Kimi For Coding > kimi-k2-thinking - should detect overflow via isContextOverflow
kimi-coding / kimi-k2-thinking:
contextWindow: 262144
stopReason: error
errorMessage: 400 {"error":{"type":"invalid_request_error","message":"error, status code: 400, message: Invalid request: Your request exceeded model token limit: 262144 (requested: 516362)"},"type":"error"}
usage: {"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}}
hasUsageData: false
✓ test/context-overflow.test.ts (30 tests | 28 skipped) 58883ms
✓ Context overflow error handling > Hugging Face > Kimi-K2.5 - should detect overflow via isContextOverflow 55377ms
✓ Context overflow error handling > Kimi For Coding > kimi-k2-thinking - should detect overflow via isContextOverflow 3505ms
✓ test/stream.test.ts (155 tests | 145 skipped) 71052ms
✓ Generate E2E Tests > Hugging Face Provider (Kimi-K2.5 via OpenAI Completions) > should complete basic text generation 16928ms
✓ Generate E2E Tests > Hugging Face Provider (Kimi-K2.5 via OpenAI Completions) > should handle tool calling 4016ms
✓ Generate E2E Tests > Hugging Face Provider (Kimi-K2.5 via OpenAI Completions) > should handle streaming 3896ms
✓ Generate E2E Tests > Hugging Face Provider (Kimi-K2.5 via OpenAI Completions) > should handle thinking mode 9834ms (retry x1)
✓ Generate E2E Tests > Hugging Face Provider (Kimi-K2.5 via OpenAI Completions) > should handle multi-turn with thinking and tools 21312ms
✓ Generate E2E Tests > Kimi For Coding Provider (kimi-k2-thinking via Anthropic Messages) > should complete basic text generation 2294ms
✓ Generate E2E Tests > Kimi For Coding Provider (kimi-k2-thinking via Anthropic Messages) > should handle tool calling 1698ms
✓ Generate E2E Tests > Kimi For Coding Provider (kimi-k2-thinking via Anthropic Messages) > should handle streaming 964ms
✓ Generate E2E Tests > Kimi For Coding Provider (kimi-k2-thinking via Anthropic Messages) > should handle thinking mode 5432ms
✓ Generate E2E Tests > Kimi For Coding Provider (kimi-k2-thinking via Anthropic Messages) > should handle multi-turn with thinking and tools 4671ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
FAIL test/tokens.test.ts > Token Statistics on Abort > Kimi For Coding Provider > should include token stats when aborted mid-stream
FAIL test/tokens.test.ts > Token Statistics on Abort > Kimi For Coding Provider > should include token stats when aborted mid-stream
FAIL test/tokens.test.ts > Token Statistics on Abort > Kimi For Coding Provider > should include token stats when aborted mid-stream
FAIL test/tokens.test.ts > Token Statistics on Abort > Kimi For Coding Provider > should include token stats when aborted mid-stream
AssertionError: expected 0 to be greater than 0
❯ testTokensOnAbort test/tokens.test.ts:75:28
73| } else {
74| expect(msg.usage.input).toBeGreaterThan(0);
75| expect(msg.usage.output).toBeGreaterThan(0);
| ^
76|
77| // Antigravity Gemini and Claude models report token usage, but no c…
❯ test/tokens.test.ts:193:4
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/4]⎯
Test Files 1 failed | 20 passed | 5 skipped (26)
Tests 1 failed | 59 passed | 488 skipped (548)
Start at 23:24:05
Duration 71.89s (transform 838ms, setup 0ms, collect 6.17s, tests 304.82s, environment 3ms, prepare 1.29s)
npm error Lifecycle script `test` failed with error:
npm error code 1
npm error path /Users/badlogic/workspaces/pi-mono/packages/ai
npm error workspace @mariozechner/pi-ai@0.50.7
npm error location /Users/badlogic/workspaces/pi-mono/packages/ai
npm error command failed
npm error command sh -c vitest --run
> @mariozechner/pi-coding-agent@0.50.7 test
> vitest --run
RUN v3.2.4 /Users/badlogic/workspaces/pi-mono/packages/coding-agent
✓ test/prompt-templates.test.ts (73 tests) 5ms
✓ test/skills.test.ts (27 tests) 20ms
✓ test/compaction.test.ts (21 tests | 2 skipped) 28ms
npm error code E404
npm error 404 Not Found - GET https://registry.npmjs.org/nonexistent-package - Not found
npm error 404
npm error 404 The requested resource 'nonexistent-package@1.0.0' could not be found or you do not have permission to access it.
npm error 404
npm error 404 Note that you can also install from a
npm error 404 tarball, folder, http url, or git url.
npm error A complete log of this run can be found in: /Users/badlogic/.npm/_logs/2026-01-31T22_25_18_720Z-debug-0.log
Cloning into '/var/folders/49/l4171l3n219_xwq30fmyzr8w0000gn/T/pm-test-1769898319350-6vc4qacffbh/agent/git/github.com/nonexistent/repo'...
↓ test/rpc.test.ts (14 tests | 14 skipped)
✓ test/model-registry.test.ts (26 tests) 415ms
✓ test/session-manager/build-context.test.ts (14 tests) 4ms
remote: Repository not found.
fatal: repository 'https://github.com/nonexistent/repo/' not found
✓ test/package-manager.test.ts (41 tests) 1311ms
✓ DefaultPackageManager > source parsing > should emit progress events on install attempt 767ms
✓ DefaultPackageManager > source parsing > should recognize github URLs without git: prefix 460ms
↓ test/agent-session-tree-navigation.test.ts (10 tests | 10 skipped)
↓ test/compaction-extensions.test.ts (8 tests | 8 skipped)
✓ test/session-manager/tree-traversal.test.ts (28 tests) 12ms
✓ test/tools.test.ts (42 tests) 1265ms
✓ Coding Agent Tools > bash tool > should respect timeout 1003ms
✓ test/extensions-runner.test.ts (15 tests) 258ms
✓ test/plan-mode-utils.test.ts (33 tests) 10ms
✓ test/extensions-discovery.test.ts (27 tests) 372ms
✓ test/model-resolver.test.ts (19 tests) 13ms
✓ test/session-manager/file-operations.test.ts (17 tests) 45ms
✓ test/args.test.ts (45 tests) 8ms
✓ test/image-processing.test.ts (8 tests) 116ms
✓ test/settings-manager.test.ts (8 tests) 17ms
✓ test/session-manager/labels.test.ts (8 tests) 4ms
✓ test/path-utils.test.ts (10 tests) 12ms
✓ test/resource-loader.test.ts (13 tests) 232ms
From /var/folders/49/l4171l3n219_xwq30fmyzr8w0000gn/T/git-update-test-1769898320280-tcf3k6t2po/remote
c84b948..a516f38 main -> origin/main
✓ test/clipboard-image.test.ts (4 tests) 40ms
HEAD is now at a516f38 Second commit
✓ test/session-selector-path-delete.test.ts (5 tests) 13ms
✓ test/session-selector-search.test.ts (5 tests) 3ms
✓ test/block-images.test.ts (8 tests) 32ms
↓ test/compaction-thinking-model.test.ts (3 tests | 3 skipped)
↓ test/agent-session-compaction.test.ts (5 tests | 5 skipped)
✓ test/agent-session-concurrent.test.ts (4 tests) 48ms
✓ test/settings-manager-bug.test.ts (2 tests) 9ms
✓ test/clipboard-image-bmp-conversion.test.ts (1 test) 29ms
✓ test/truncate-to-width.test.ts (6 tests) 21ms
↓ test/agent-session-branching.test.ts (3 tests | 3 skipped)
✓ test/frontmatter.test.ts (8 tests) 11ms
✓ test/compaction-extensions-example.test.ts (2 tests) 2ms
✓ test/extensions-input-event.test.ts (8 tests) 132ms
From /var/folders/49/l4171l3n219_xwq30fmyzr8w0000gn/T/git-update-test-1769898321101-smbulne2m3a/remote
3b5a0dd..1bfd520 main -> origin/main
✓ test/session-info-modified-timestamp.test.ts (1 test) 23ms
✓ test/session-selector-rename.test.ts (3 tests) 17ms
HEAD is now at 1bfd520 Fourth commit
✓ test/session-manager/migration.test.ts (2 tests) 2ms
✓ test/session-manager/save-entry.test.ts (1 test) 5ms
✓ test/system-prompt.test.ts (3 tests) 16ms
✓ test/sdk-skills.test.ts (3 tests) 20ms
From /var/folders/49/l4171l3n219_xwq30fmyzr8w0000gn/T/git-update-test-1769898322115-xl9xh9il59/remote
120dfb0..f4355e8 main -> origin/main
HEAD is now at f4355e8 Commit to keep
From /var/folders/49/l4171l3n219_xwq30fmyzr8w0000gn/T/git-update-test-1769898322115-xl9xh9il59/remote
+ f4355e8...28eae50 main -> origin/main (forced update)
HEAD is now at 28eae50 Rewritten commit
✓ test/interactive-mode-status.test.ts (2 tests) 4ms
From /var/folders/49/l4171l3n219_xwq30fmyzr8w0000gn/T/git-update-test-1769898322780-b2qua9n7sdc/remote
120dfb0..b628ef4 main -> origin/main
HEAD is now at b628ef4 Commit B
From /var/folders/49/l4171l3n219_xwq30fmyzr8w0000gn/T/git-update-test-1769898322780-b2qua9n7sdc/remote
+ b628ef4...c744b1c main -> origin/main (forced update)
HEAD is now at c744b1c New commit replacing A and B
From /var/folders/49/l4171l3n219_xwq30fmyzr8w0000gn/T/git-update-test-1769898323390-5510lrlj5ie/remote
1db9c91..0c39e17 main -> origin/main
HEAD is now at 0c39e17 v3
From /var/folders/49/l4171l3n219_xwq30fmyzr8w0000gn/T/git-update-test-1769898323390-5510lrlj5ie/remote
+ 0c39e17...c1ab283 main -> origin/main (forced update)
HEAD is now at c1ab283 Rewrite B
✓ test/git-update.test.ts (6 tests) 4047ms
✓ DefaultPackageManager git update > normal updates (no force-push) > should update to latest commit when remote has new commits 821ms
✓ DefaultPackageManager git update > normal updates (no force-push) > should handle multiple commits ahead 1014ms
✓ DefaultPackageManager git update > force-push scenarios > should recover when remote history is rewritten 665ms
✓ DefaultPackageManager git update > force-push scenarios > should recover when local commit no longer exists in remote 610ms
✓ DefaultPackageManager git update > force-push scenarios > should handle complete history rewrite 666ms
Test Files 38 passed | 6 skipped (44)
Tests 547 passed | 45 skipped (592)
Start at 23:25:18
Duration 6.32s (transform 1.74s, setup 0ms, collect 19.06s, tests 8.62s, environment 5ms, prepare 2.69s)
> @mariozechner/pi-tui@0.50.7 test
> node --test --import tsx test/*.test.ts
Result: null
Result: null
Result: {
items: [
{ value: '/.nofollow/', label: '.nofollow/' },
{ value: '/.resolve/', label: '.resolve/' },
{ value: '/.vol/', label: '.vol/' },
{ value: '/Applications/', label: 'Applications/' },
{ value: '/bin/', label: 'bin/' },
{ value: '/cores/', label: 'cores/' },
{ value: '/dev/', label: 'dev/' },
{ value: '/etc/', label: 'etc/' },
{ value: '/home/', label: 'home/' },
{ value: '/Library/', label: 'Library/' },
{ value: '/opt/', label: 'opt/' },
{ value: '/private/', label: 'private/' },
{ value: '/sbin/', label: 'sbin/' },
{ value: '/System/', label: 'System/' },
{ value: '/tmp/', label: 'tmp/' },
{ value: '/Users/', label: 'Users/' },
{ value: '/usr/', label: 'usr/' },
{ value: '/var/', label: 'var/' },
{ value: '/Volumes/', label: 'Volumes/' },
{ value: '/.file', label: '.file' },
{ value: '/.VolumeIcon.icns', label: '.VolumeIcon.icns' }
],
prefix: '/'
}
▶ CombinedAutocompleteProvider
▶ extractPathPrefix
✔ extracts / from 'hey /' when forced (10.935334ms)
✔ extracts /A from '/A' when forced (0.727125ms)
✔ does not trigger for slash commands (0.121292ms)
✔ triggers for absolute paths after slash command argument (1.279708ms)
✔ extractPathPrefix (13.49325ms)
▶ fd @ file suggestions
✔ returns all files and folders for empty @ query (8.224834ms)
✔ matches file with extension in query (13.888125ms)
✔ filters are case insensitive (12.870708ms)
✔ ranks directories before files (8.804167ms)
✔ returns nested file paths (13.502083ms)
✔ matches deeply nested paths (21.844375ms)
✔ matches directory in middle of path with --full-path (12.604208ms)
✔ quotes paths with spaces for @ suggestions (8.204542ms)
✔ continues autocomplete inside quoted @ paths (10.881292ms)
✔ applies quoted @ completion without duplicating closing quote (8.187333ms)
✔ fd @ file suggestions (119.452208ms)
▶ quoted path completion
✔ quotes paths with spaces for direct completion (2.581292ms)
✔ continues completion inside quoted paths (1.405792ms)
✔ applies quoted completion without duplicating closing quote (1.537792ms)
✔ quoted path completion (5.66475ms)
✔ CombinedAutocompleteProvider (138.973334ms)
▶ Bug regression: isImageLine() crash with image escape sequences
▶ Bug scenario: Terminal without image support
✔ old implementation would return false, causing crash (0.557667ms)
✔ new implementation returns true correctly (4.766708ms)
✔ new implementation detects Kitty sequences in any position (0.7925ms)
✔ new implementation detects iTerm2 sequences in any position (2.394167ms)
✔ Bug scenario: Terminal without image support (8.966833ms)
▶ Integration: Tool execution scenario
✔ detects image sequences in read tool output (0.964834ms)
✔ detects Kitty sequences from Image component (1.321417ms)
✔ handles ANSI codes before image sequences (1.169ms)
✔ Integration: Tool execution scenario (3.646583ms)
▶ Crash scenario simulation
✔ does NOT crash on very long lines with image sequences (1.116333ms)
✔ handles lines exactly matching crash log dimensions (0.618666ms)
✔ Crash scenario simulation (1.900667ms)
▶ Negative cases: Don't false positive
✔ does not detect images in regular long text (1.389084ms)
✔ does not detect images in lines with file paths (0.924083ms)
✔ Negative cases: Don't false positive (2.391541ms)
✔ Bug regression: isImageLine() crash with image escape sequences (17.2785ms)
▶ Editor component
▶ Prompt history navigation
✔ does nothing on Up arrow when history is empty (5.144708ms)
✔ shows most recent history entry on Up arrow when editor is empty (0.844625ms)
✔ cycles through history entries on repeated Up arrow (1.186ms)
✔ returns to empty editor on Down arrow after browsing history (7.655375ms)
✔ navigates forward through history with Down arrow (12.072625ms)
✔ exits history mode when typing a character (7.711667ms)
✔ exits history mode on setText (0.421875ms)
✔ does not add empty strings to history (2.134458ms)
✔ does not add consecutive duplicates to history (5.329791ms)
✔ allows non-consecutive duplicates in history (2.674ms)
✔ uses cursor movement instead of history when editor has content (0.808417ms)
✔ limits history to 100 entries (3.628666ms)
✔ allows cursor movement within multi-line history entry with Down (0.528833ms)
✔ allows cursor movement within multi-line history entry with Up (0.3285ms)
✔ navigates from multi-line entry back to newer via Down after cursor movement (0.319333ms)
✔ Prompt history navigation (52.707667ms)
▶ public state accessors
✔ returns cursor position (1.155625ms)
✔ returns lines as a defensive copy (0.280833ms)
✔ public state accessors (1.516208ms)
▶ Backslash+Enter newline workaround
✔ inserts backslash immediately (no buffering) (0.293584ms)
✔ converts standalone backslash to newline on Enter (1.935166ms)
✔ inserts backslash normally when followed by other characters (0.348583ms)
✔ does not trigger newline when backslash is not immediately before cursor (0.374708ms)
✔ only removes one backslash when multiple are present (1.009584ms)
✔ Backslash+Enter newline workaround (4.064708ms)
▶ Unicode text editing behavior
✔ inserts mixed ASCII, umlauts, and emojis as literal text (0.684334ms)
✔ deletes single-code-unit unicode characters (umlauts) with Backspace (0.314542ms)
✔ deletes multi-code-unit emojis with single Backspace (0.327041ms)
✔ inserts characters at the correct position after cursor movement over umlauts (1.085166ms)
✔ moves cursor across multi-code-unit emojis with single arrow key (29.022708ms)
✔ preserves umlauts across line breaks (0.492333ms)
✔ replaces the entire document with unicode text via setText (paste simulation) (2.693583ms)
✔ moves cursor to document start on Ctrl+A and inserts at the beginning (2.371084ms)
✔ deletes words correctly with Ctrl+W and Alt+Backspace (2.177083ms)
✔ navigates words correctly with Ctrl+Left/Right (0.5955ms)
✔ Unicode text editing behavior (40.019875ms)
▶ Grapheme-aware text wrapping
✔ wraps lines correctly when text contains wide emojis (0.713583ms)
✔ wraps long text with emojis at correct positions (0.538875ms)
✔ wraps CJK characters correctly (each is 2 columns wide) (0.76525ms)
✔ handles mixed ASCII and wide characters in wrapping (0.357417ms)
✔ renders cursor correctly on wide characters (0.322958ms)
✔ does not exceed terminal width with emoji at wrap boundary (0.287292ms)
✔ shows cursor at end of line before wrap, wraps on next char (0.888208ms)
✔ Grapheme-aware text wrapping (4.001959ms)
▶ Word wrapping
✔ wraps at word boundaries instead of mid-word (2.316167ms)
✔ does not start lines with leading whitespace after word wrap (1.185583ms)
✔ breaks long words (URLs) at character level (1.350292ms)
✔ preserves multiple spaces within words on same line (0.549458ms)
✔ handles empty string (1.319125ms)
✔ handles single word that fits exactly (0.48025ms)
✔ wraps word to next line when it ends exactly at terminal width (0.098958ms)
✔ keeps whitespace at terminal width boundary on same line (0.047041ms)
✔ handles unbreakable word filling width exactly followed by space (0.047875ms)
✔ wraps word to next line when it fits width but not remaining space (0.040375ms)
✔ keeps word with multi-space and following word together when they fit (0.05025ms)
✔ keeps word with multi-space and following word when they fill width exactly (0.057459ms)
✔ splits when word plus multi-space plus word exceeds width (0.051959ms)
✔ breaks long whitespace at line boundary (0.06125ms)
✔ breaks long whitespace at line boundary 2 (3.291666ms)
✔ breaks whitespace spanning full lines (0.119625ms)
✔ Word wrapping (11.375542ms)
▶ Kill ring
✔ Ctrl+W saves deleted text to kill ring and Ctrl+Y yanks it (0.787417ms)
✔ Ctrl+U saves deleted text to kill ring (0.423125ms)
✔ Ctrl+K saves deleted text to kill ring (1.131167ms)
✔ Ctrl+Y does nothing when kill ring is empty (0.267541ms)
✔ Alt+Y cycles through kill ring after Ctrl+Y (0.377208ms)
✔ Alt+Y does nothing if not preceded by yank (0.2585ms)
✔ Alt+Y does nothing if kill ring has ≤1 entry (0.209875ms)
✔ consecutive Ctrl+W accumulates into one kill ring entry (0.215792ms)
✔ Ctrl+U accumulates multiline deletes including newlines (0.215375ms)
✔ backward deletions prepend, forward deletions append during accumulation (0.278458ms)
✔ non-delete actions break kill accumulation (0.249917ms)
✔ non-yank actions break Alt+Y chain (0.276625ms)
✔ kill ring rotation persists after cycling (0.319583ms)
✔ consecutive deletions across lines coalesce into one entry (0.21775ms)
✔ Ctrl+K at line end deletes newline and coalesces (0.279ms)
✔ handles yank in middle of text (0.255833ms)
✔ handles yank-pop in middle of text (0.271708ms)
✔ multiline yank and yank-pop in middle of text (0.308208ms)
✔ Alt+D deletes word forward and saves to kill ring (0.264083ms)
✔ Alt+D at end of line deletes newline (0.266ms)
✔ Kill ring (7.123167ms)
▶ Undo
✔ does nothing when undo stack is empty (0.330333ms)
✔ coalesces consecutive word characters into one undo unit (0.39175ms)
✔ undoes spaces one at a time (0.347292ms)
✔ undoes newlines and signals next word to capture state (0.367584ms)
✔ undoes backspace (0.310083ms)
✔ undoes forward delete (0.376625ms)
✔ undoes Ctrl+W (delete word backward) (0.389167ms)
✔ undoes Ctrl+K (delete to line end) (0.512958ms)
✔ undoes Ctrl+U (delete to line start) (0.5075ms)
✔ undoes yank (0.349584ms)
✔ undoes single-line paste atomically (0.449375ms)
✔ undoes multi-line paste atomically (0.388125ms)
✔ undoes insertTextAtCursor atomically (0.326041ms)
✔ insertTextAtCursor handles multiline text (0.332458ms)
✔ insertTextAtCursor normalizes CRLF and CR line endings (0.205208ms)
✔ undoes setText to empty string (0.342208ms)
✔ clears undo stack on submit (1.80875ms)
✔ exits history browsing mode on undo (0.426459ms)
✔ undo restores to pre-history state even after multiple history navigations (0.428792ms)
✔ cursor movement starts new undo unit (0.617917ms)
✔ no-op delete operations do not push undo snapshots (0.346667ms)
✔ undoes autocomplete (5.145917ms)
✔ Undo (15.039875ms)
▶ Autocomplete
✔ auto-applies single force-file suggestion without showing menu (1.273ms)
✔ shows menu when force-file has multiple suggestions (0.369542ms)
✔ keeps suggestions open when typing in force mode (Tab-triggered) (0.380084ms)
✔ hides autocomplete when backspacing slash command to empty (0.292541ms)
✔ Autocomplete (2.397375ms)
▶ Character jump (Ctrl+])
✔ jumps forward to first occurrence of character on same line (0.335ms)
✔ jumps forward to next occurrence after cursor (0.28875ms)
✔ jumps forward across multiple lines (0.282333ms)
✔ jumps backward to first occurrence before cursor on same line (0.205541ms)
✔ jumps backward across multiple lines (0.311917ms)
✔ does nothing when character is not found (forward) (0.476958ms)
✔ does nothing when character is not found (backward) (0.2855ms)
✔ is case-sensitive (0.906875ms)
✔ cancels jump mode when Ctrl+] is pressed again (0.395084ms)
✔ cancels jump mode on Escape and processes the Escape (0.342292ms)
✔ cancels backward jump mode when Ctrl+Alt+] is pressed again (0.2905ms)
✔ searches for special characters (0.302125ms)
✔ handles empty text gracefully (0.204875ms)
✔ resets lastAction when jumping (0.276542ms)
✔ Character jump (Ctrl+]) (5.088208ms)
▶ Sticky column
✔ preserves target column when moving up through a shorter line (0.516ms)
✔ preserves target column when moving down through a shorter line (0.430167ms)
✔ resets sticky column on horizontal movement (left arrow) (0.372958ms)
✔ resets sticky column on horizontal movement (right arrow) (0.358875ms)
✔ resets sticky column on typing (0.376ms)
✔ resets sticky column on backspace (0.380791ms)
✔ resets sticky column on Ctrl+A (move to line start) (0.374458ms)
✔ resets sticky column on Ctrl+E (move to line end) (0.292292ms)
✔ resets sticky column on word movement (Ctrl+Left) (0.242208ms)
✔ resets sticky column on word movement (Ctrl+Right) (0.28025ms)
✔ resets sticky column on undo (1.638ms)
✔ handles multiple consecutive up/down movements (0.849125ms)
✔ moves correctly through wrapped visual lines without getting stuck (0.929ms)
✔ handles setText resetting sticky column (0.457416ms)
✔ sets preferredVisualCol when pressing right at end of prompt (last line) (0.333292ms)
✔ handles editor resizes when preferredVisualCol is on the same line (3.863625ms)
✔ handles editor resizes when preferredVisualCol is on a different line (0.494417ms)
✔ Sticky column (12.465958ms)
✔ Editor component (156.355666ms)
▶ fuzzyMatch
✔ empty query matches everything with score 0 (0.682125ms)
✔ query longer than text does not match (0.099375ms)
✔ exact match has good score (0.135917ms)
✔ characters must appear in order (0.097291ms)
✔ case insensitive matching (0.084167ms)
✔ consecutive matches score better than scattered matches (0.072291ms)
✔ word boundary matches score better (0.054416ms)
✔ matches swapped alpha numeric tokens (0.069542ms)
✔ fuzzyMatch (1.934709ms)
▶ fuzzyFilter
✔ empty query returns all items unchanged (0.697375ms)
✔ filters out non-matching items (0.135166ms)
✔ sorts results by match quality (0.099916ms)
✔ works with custom getText function (0.074333ms)
✔ fuzzyFilter (1.127125ms)
▶ Input component
✔ submits value including backslash on Enter (1.427416ms)
✔ inserts backslash as regular character (0.123125ms)
✔ Input component (2.088666ms)
▶ matchesKey
▶ Kitty protocol with alternate keys (non-Latin layouts)
✔ should match Ctrl+c when pressing Ctrl+С (Cyrillic) with base layout key (0.940625ms)
✔ should match Ctrl+d when pressing Ctrl+В (Cyrillic) with base layout key (0.147208ms)
✔ should match Ctrl+z when pressing Ctrl+Я (Cyrillic) with base layout key (0.061417ms)
✔ should match Ctrl+Shift+p with base layout key (0.06425ms)
✔ should still match direct codepoint when no base layout key (0.058375ms)
✔ should handle shifted key in format (0.060666ms)
✔ should handle event type in format (0.061708ms)
✔ should handle full format with shifted key, base key, and event type (0.055375ms)
✔ should not match wrong key even with base layout (0.060583ms)
✔ should not match wrong modifiers even with base layout (0.100209ms)
✔ Kitty protocol with alternate keys (non-Latin layouts) (2.151ms)
▶ Legacy key matching
✔ should match legacy Ctrl+c (0.137541ms)
✔ should match legacy Ctrl+d (0.045542ms)
✔ should match escape key (0.055875ms)
✔ should match legacy linefeed as enter (0.217041ms)
✔ should treat linefeed as shift+enter when kitty active (0.19425ms)
✔ should parse ctrl+space (0.072292ms)
✔ should match legacy Ctrl+symbol (0.068958ms)
✔ should match legacy Ctrl+Alt+symbol (0.051709ms)
✔ should parse legacy alt-prefixed sequences when kitty inactive (0.100375ms)
✔ should match arrow keys (0.050583ms)
✔ should match SS3 arrows and home/end (0.041833ms)
✔ should match legacy function keys and clear (0.037583ms)
✔ should match alt+arrows (0.033625ms)
✔ should match rxvt modifier sequences (0.049292ms)
✔ Legacy key matching (1.333459ms)
✔ matchesKey (3.763ms)
▶ parseKey
▶ Kitty protocol with alternate keys
✔ should return Latin key name when base layout key is present (0.073708ms)
✔ should return key name from codepoint when no base layout (0.0375ms)
✔ Kitty protocol with alternate keys (0.152416ms)
▶ Legacy key parsing
✔ should parse legacy Ctrl+letter (0.053875ms)
✔ should parse special keys (0.037125ms)
✔ should parse arrow keys (0.03525ms)
✔ should parse SS3 arrows and home/end (0.038833ms)
✔ should parse legacy function and modifier sequences (0.049208ms)
✔ should parse double bracket pageUp (0.0275ms)
✔ Legacy key parsing (0.309334ms)
✔ parseKey (0.528ms)
▶ Markdown component
▶ Nested lists
✔ should render simple nested list (13.617625ms)
✔ should render deeply nested list (0.447667ms)
✔ should render ordered nested list (10.358ms)
✔ should render mixed ordered and unordered nested lists (0.466916ms)
✔ should maintain numbering when code blocks are not indented (LLM output) (0.608167ms)
✔ Nested lists (26.018833ms)
▶ Tables
✔ should render simple table (20.511834ms)
✔ should render row dividers between data rows (0.728333ms)
✔ should keep column width at least the longest word (4.644ms)
✔ should render table with alignment (3.228625ms)
✔ should handle tables with varying column widths (2.101ms)
✔ should wrap table cells when table exceeds available width (0.443709ms)
✔ should wrap long cell content to multiple lines (0.481292ms)
✔ should wrap long unbroken tokens inside table cells (not only at line start) (0.778333ms)
✔ should wrap styled inline code inside table cells without breaking borders (0.307542ms)
✔ should handle extremely narrow width gracefully (0.158167ms)
✔ should render table correctly when it fits naturally (0.156875ms)
✔ should respect paddingX when calculating table width (0.208291ms)
✔ Tables (36.571625ms)
▶ Combined features
✔ should render lists and tables together (0.304625ms)
✔ Combined features (0.337833ms)
▶ Pre-styled text (thinking traces)
✔ should preserve gray italic styling after inline code (1.268958ms)
✔ should preserve gray italic styling after bold text (3.015667ms)
✔ should not leak styles into following lines when rendered in TUI (19.767167ms)
✔ Pre-styled text (thinking traces) (24.386334ms)
▶ Spacing after code blocks
✔ should have only one blank line between code block and following paragraph (0.294625ms)
✔ Spacing after code blocks (0.34375ms)
▶ Spacing after dividers
✔ should have only one blank line between divider and following paragraph (0.209625ms)
✔ Spacing after dividers (0.238542ms)
▶ Spacing after headings
✔ should have only one blank line between heading and following paragraph (0.1605ms)
✔ Spacing after headings (0.184375ms)
▶ Spacing after blockquotes
✔ should have only one blank line between blockquote and following paragraph (0.304834ms)
✔ Spacing after blockquotes (0.328958ms)
▶ Blockquotes with multiline content
✔ should apply consistent styling to all lines in lazy continuation blockquote (0.225125ms)
✔ should apply consistent styling to explicit multiline blockquote (0.120125ms)
✔ should wrap long blockquote lines and add border to each wrapped line (0.201583ms)
✔ should properly indent wrapped blockquote lines with styling (0.161042ms)
✔ should render inline formatting inside blockquotes and reapply quote styling after (0.16225ms)
✔ Blockquotes with multiline content (0.930166ms)
▶ Links
✔ should not duplicate URL for autolinked emails (0.103291ms)
✔ should not duplicate URL for bare URLs (1.623375ms)
✔ should show URL for explicit markdown links with different text (1.213583ms)
✔ should show URL for explicit mailto links with different text (0.207291ms)
✔ Links (3.899917ms)
▶ HTML-like tags in text
✔ should render content with HTML-like tags as text (0.211125ms)
✔ should render HTML tags in code blocks correctly (0.101625ms)
✔ HTML-like tags in text (0.370417ms)
✔ Markdown component (94.135208ms)
▶ TUI overlay options
▶ width overflow protection
✔ should truncate overlay lines that exceed declared width (9.016708ms)
✔ should handle overlay with complex ANSI sequences without crashing (4.163208ms)
✔ should handle overlay composited on styled base content (70.49325ms)
✔ should handle wide characters at overlay boundary (1.658ms)
✔ should handle overlay positioned at terminal edge (1.329333ms)
✔ should handle overlay on base content with OSC sequences (3.313291ms)
✔ width overflow protection (90.607625ms)
▶ width percentage
✔ should render overlay at percentage of terminal width (13.193167ms)
✔ should respect minWidth when widthPercent results in smaller width (9.688459ms)
✔ width percentage (23.061875ms)
▶ anchor positioning
✔ should position overlay at top-left (2.382334ms)
✔ should position overlay at bottom-right (19.869666ms)
✔ should position overlay at top-center (1.497167ms)
✔ anchor positioning (23.932458ms)
▶ margin
✔ should clamp negative margins to zero (2.43525ms)
✔ should respect margin as number (2.275667ms)
✔ should respect margin object (1.171125ms)
✔ margin (6.04875ms)
▶ offset
✔ should apply offsetX and offsetY from anchor position (1.782333ms)
✔ offset (1.865667ms)
▶ percentage positioning
✔ should position with rowPercent and colPercent (1.461084ms)
✔ rowPercent 0 should position at top (1.207917ms)
✔ rowPercent 100 should position at bottom (1.757083ms)
✔ percentage positioning (4.616708ms)
▶ maxHeight
✔ should truncate overlay to maxHeight (2.042375ms)
✔ should truncate overlay to maxHeightPercent (0.951459ms)
✔ maxHeight (3.184ms)
▶ absolute positioning
✔ row and col should override anchor (2.348666ms)
✔ absolute positioning (2.440875ms)
▶ stacked overlays
✔ should render multiple overlays with later ones on top (1.479875ms)
✔ should handle overlays at different positions without interference (10.909583ms)
✔ should properly hide overlays in stack order (5.440042ms)
✔ stacked overlays (18.222042ms)
✔ TUI overlay options (174.600375ms)
Terminal rows: 24
Content lines: 3
Overlay visible: true
▶ TUI overlay with short content
✔ should render overlay when content is shorter than terminal height (64.727667ms)
✔ TUI overlay with short content (65.298958ms)
▶ SelectList
✔ normalizes multiline descriptions to single line (1.40325ms)
✔ SelectList (2.889459ms)
▶ StdinBuffer
▶ Regular Characters
✔ should pass through regular characters immediately (1.718584ms)
✔ should pass through multiple regular characters (0.143125ms)
✔ should handle unicode characters (0.1095ms)
✔ Regular Characters (2.407375ms)
▶ Complete Escape Sequences
✔ should pass through complete mouse SGR sequences (0.35075ms)
✔ should pass through complete arrow key sequences (0.148792ms)
✔ should pass through complete function key sequences (0.098375ms)
✔ should pass through meta key sequences (0.113666ms)
✔ should pass through SS3 sequences (0.076208ms)
✔ Complete Escape Sequences (1.076209ms)
▶ Partial Escape Sequences
✔ should buffer incomplete mouse SGR sequence (0.613042ms)
✔ should buffer incomplete CSI sequence (0.136833ms)
✔ should buffer split across many chunks (0.165459ms)
✔ should flush incomplete sequence after timeout (14.791875ms)
✔ Partial Escape Sequences (15.988375ms)
▶ Mixed Content
✔ should handle characters followed by escape sequence (0.236709ms)
✔ should handle escape sequence followed by characters (0.1585ms)
✔ should handle multiple complete sequences (0.132834ms)
✔ should handle partial sequence with preceding characters (0.197125ms)
✔ Mixed Content (0.920666ms)
▶ Kitty Keyboard Protocol
✔ should handle Kitty CSI u press events (0.202917ms)
✔ should handle Kitty CSI u release events (0.08775ms)
✔ should handle batched Kitty press and release (0.059791ms)
✔ should handle multiple batched Kitty events (0.072625ms)
✔ should handle Kitty arrow keys with event type (0.046584ms)
✔ should handle Kitty functional keys with event type (0.115125ms)
✔ should handle plain characters mixed with Kitty sequences (0.15475ms)
✔ should handle Kitty sequence followed by plain characters (0.058791ms)
✔ should handle rapid typing simulation with Kitty protocol (0.076792ms)
✔ Kitty Keyboard Protocol (1.131792ms)
▶ Mouse Events
✔ should handle mouse press event (0.154416ms)
✔ should handle mouse release event (0.072084ms)
✔ should handle mouse move event (0.052833ms)
✔ should handle split mouse events (0.078375ms)
✔ should handle multiple mouse events (0.071333ms)
✔ should handle old-style mouse sequence (ESC[M + 3 bytes) (0.045708ms)
✔ should buffer incomplete old-style mouse sequence (0.068125ms)
✔ Mouse Events (0.64625ms)
▶ Edge Cases
✔ should handle empty input (0.067625ms)
✔ should handle lone escape character with timeout (17.32075ms)
✔ should handle lone escape character with explicit flush (0.169875ms)
✔ should handle buffer input (0.071ms)
✔ should handle very long sequences (0.086166ms)
✔ Edge Cases (17.812458ms)
▶ Flush
✔ should flush incomplete sequences (0.074084ms)
✔ should return empty array if nothing to flush (0.040333ms)
✔ should emit flushed data via timeout (17.711ms)
✔ Flush (18.105334ms)
▶ Clear
✔ should clear buffered content without emitting (0.346416ms)
✔ Clear (0.486708ms)
▶ Bracketed Paste
✔ should emit paste event for complete bracketed paste (0.165917ms)
✔ should handle paste arriving in chunks (0.100125ms)
✔ should handle paste with input before and after (0.069667ms)
✔ should handle paste with newlines (0.05125ms)
✔ should handle paste with unicode (0.056334ms)
✔ Bracketed Paste (0.579292ms)
▶ Destroy
✔ should clear buffer on destroy (0.067542ms)
✔ should clear pending timeouts on destroy (16.073667ms)
✔ Destroy (16.231042ms)
✔ StdinBuffer (76.962167ms)
▶ isImageLine
▶ iTerm2 image protocol
✔ should detect iTerm2 image escape sequence at start of line (0.690458ms)
✔ should detect iTerm2 image escape sequence with text before it (0.071666ms)
✔ should detect iTerm2 image escape sequence in middle of long line (0.064125ms)
✔ should detect iTerm2 image escape sequence at end of line (0.075167ms)
✔ should detect minimal iTerm2 image escape sequence (0.103417ms)
✔ iTerm2 image protocol (1.61425ms)
▶ Kitty image protocol
✔ should detect Kitty image escape sequence at start of line (0.101208ms)
✔ should detect Kitty image escape sequence with text before it (0.07425ms)
✔ should detect Kitty image escape sequence with padding (0.057167ms)
✔ Kitty image protocol (0.365667ms)
▶ Bug regression tests
✔ should detect image sequences in very long lines (304k+ chars) (0.170458ms)
✔ should detect image sequences when terminal doesn't support images (0.09375ms)
✔ should detect image sequences with ANSI codes before them (0.055958ms)
✔ should detect image sequences with ANSI codes after them (0.154958ms)
✔ Bug regression tests (0.58875ms)
▶ Negative cases - lines without images
✔ should not detect images in plain text lines (0.100583ms)
✔ should not detect images in lines with only ANSI codes (0.050834ms)
✔ should not detect images in lines with cursor movement codes (0.086792ms)
✔ should not detect images in lines with partial iTerm2 sequences (0.052166ms)
✔ should not detect images in lines with partial Kitty sequences (0.051458ms)
✔ should not detect images in empty lines (0.031875ms)
✔ should not detect images in lines with newlines only (0.033375ms)
✔ Negative cases - lines without images (0.53175ms)
▶ Mixed content scenarios
✔ should detect images when line has both Kitty and iTerm2 sequences (0.070292ms)
✔ should detect image in line with multiple text and image segments (0.037125ms)
✔ should not falsely detect image in line with file path containing keywords (0.03375ms)
✔ Mixed content scenarios (0.188791ms)
✔ isImageLine (3.762083ms)
▶ TruncatedText component
✔ pads output lines to exactly match width (0.767708ms)
✔ pads output with vertical padding lines to width (0.085875ms)
✔ truncates long text and pads to width (1.083292ms)
✔ preserves ANSI codes in output and pads correctly (0.261791ms)
✔ truncates styled text and adds reset code before ellipsis (0.227583ms)
✔ handles text that fits exactly (0.07775ms)
✔ handles empty text (0.096584ms)
✔ stops at newline and only shows first line (0.064541ms)
✔ truncates first line even with newlines in text (0.136417ms)
✔ TruncatedText component (3.482667ms)
▶ TUI overlay compositing
✔ should not leak styles when a trailing reset sits beyond the last visible column (no overlay) (7.656667ms)
✔ should not leak styles when overlay slicing drops trailing SGR resets (4.048209ms)
✔ TUI overlay compositing (12.286292ms)
▶ TUI resize handling
✔ triggers full re-render when terminal height changes (10.270208ms)
✔ triggers full re-render when terminal width changes (4.276208ms)
✔ TUI resize handling (15.188708ms)
▶ TUI content shrinkage
✔ clears empty rows when content shrinks significantly (2.765333ms)
✔ handles shrink to single line (3.34825ms)
✔ handles shrink to empty (3.375625ms)
✔ TUI content shrinkage (9.729958ms)
▶ TUI differential rendering
✔ tracks cursor correctly when content shrinks with unchanged remaining lines (3.652333ms)
✔ renders correctly when only a middle line changes (spinner case) (7.170042ms)
✔ resets styles after each rendered line (1.873834ms)
✔ renders correctly when first line changes but rest stays same (3.077125ms)
✔ renders correctly when last line changes but rest stays same (3.424042ms)
✔ renders correctly when multiple non-adjacent lines change (2.128916ms)
✔ handles transition from content to empty and back to content (4.320041ms)
✔ TUI differential rendering (26.032125ms)
▶ wrapTextWithAnsi
▶ underline styling
✔ should not apply underline style before the styled text (2.009167ms)
✔ should not have whitespace before underline reset code (0.314209ms)
✔ should not bleed underline to padding - each line should end with reset for underline only (0.354416ms)
✔ underline styling (3.171584ms)
▶ background color preservation
✔ should preserve background color across wrapped lines without full reset (0.221917ms)
✔ should reset underline but preserve background when wrapping underlined text inside background (0.27425ms)
✔ background color preservation (0.573ms)
▶ basic wrapping
✔ should wrap plain text correctly (0.110042ms)
✔ should truncate trailing whitespace that exceeds width (0.0695ms)
✔ should preserve color codes across wraps (0.128458ms)
✔ basic wrapping (0.42825ms)
✔ wrapTextWithAnsi (4.504333ms)
ℹ tests 369
ℹ suites 81
ℹ pass 369
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 914.510084
Restored auth.json
>test.sh/bin/bash: test.sh: command not found
did you add a proper changelog entry attributed to the contributor? you should
The user is asking about the changelog entry. Let me check the current changelog to see if I properly attributed it to the contributor.
>cd /Users/badlogic/workspaces/pi-mono && head -20 packages/coding-agent/CHANGELOG.md# Changelog
## [Unreleased]
### Added
- Added `newSession`, `tree`, and `fork` keybinding actions for `/new`, `/tree`, and `/fork` commands. All unbound by default. ([#1114](https://github.com/badlogic/pi-mono/pull/1114) by [@juanibiapina](https://github.com/juanibiapina))
### Fixed
- Fixed clipboard image paste on WSL2/WSLg writing invalid PNG files when clipboard provides `image/bmp` format. BMP images are now converted to PNG before saving. ([#1112](https://github.com/badlogic/pi-mono/pull/1112) by [@lightningRalf](https://github.com/lightningRalf))
## [0.50.7] - 2026-01-31
### Fixed
- Multi-file extensions in packages now work correctly. Package resolution now uses the same discovery logic as local extensions: only `index.ts` (or manifest-declared entries) are loaded from subdirectories, not helper modules. ([#1102](https://github.com/badlogic/pi-mono/issues/1102))
## [0.50.6] - 2026-01-30The changelog entry is correct. It links to PR #1112 and attributes it to @lightningRalf with proper format:
- Fixed clipboard image paste on WSL2/WSLg writing invalid PNG files when clipboard provides `image/bmp` format. BMP images are now converted to PNG before saving. ([#1112](https://github.com/badlogic/pi-mono/pull/1112) by [@lightningRalf](https://github.com/lightningRalf))This follows the AGENTS.md format for external contributions:
External contributions:
Fixed foo ([#456](https://github.com/badlogic/pi-mono/pull/456) by [@username](https://github.com/username))
The test failure is unrelated to our changes - it's a flaky Kimi For Coding provider test about token statistics on abort. That's a pre-existing issue.
Yes, the changelog entry is properly attributed:
- Fixed clipboard image paste on WSL2/WSLg writing invalid PNG files when clipboard provides `image/bmp` format. BMP images are now converted to PNG before saving. ([#1112](https://github.com/badlogic/pi-mono/pull/1112) by [@lightningRalf](https://github.com/lightningRalf))This follows the AGENTS.md format for external contributions with the PR link and author link.
The test failure is unrelated to our changes. It's a flaky Kimi For Coding Provider test for token statistics on abort (the provider returns 0 tokens when aborted mid-stream). That's a pre-existing issue with that provider's API behavior.
private async handleClipboardImagePaste(): Promise<void> { try { const image = await readClipboardImage(); if (!image) { return; }
// Write to temp file const tmpDir = os.tmpdir(); const ext = extensionForImageMimeType(image.mimeType) ?? "png"; const fileName = `pi-clipboard-${crypto.randomUUID()}.${ext}`; const filePath = path.join(tmpDir, fileName); fs.writeFileSync(filePath, Buffer.from(image.bytes)); ...}- Fixed clipboard image paste on WSL2/WSLg writing invalid PNG files when clipboard provides `image/bmp` format. BMP images are now converted to PNG via Photon before saving. ([#1112](https://github.com/badlogic/pi-mono/pull/1112) by [@lightningRalf](https://github.com/lightningRalf))import { loadPhoton } from "./photon.js";
export async function convertToPng( base64Data: string, mimeType: string,): Promise<{ data: string; mimeType: string } | null> { // Already PNG, no conversion needed if (mimeType === "image/png") { return { data: base64Data, mimeType }; }
const photon = await loadPhoton(); if (!photon) { // Photon not available, can't convert return null; } // ... uses photon}- Fixed clipboard image paste on WSL2/WSLg writing invalid PNG files when clipboard provides `image/bmp` format. BMP images are now converted to PNG before saving. ([#1112](https://github.com/badlogic/pi-mono/pull/1112) by [@lightningRalf](https://github.com/lightningRalf))- Fixed clipboard image paste on WSL2/WSLg writing invalid PNG files when clipboard provides `image/bmp` format. BMP images are now converted to PNG before saving. ([#1112](https://github.com/badlogic/pi-mono/pull/1112) by [@lightningRalf](https://github.com/lightningRalf))