Tested image resizing with large PNG on npm 0.49.0.
ReproduceAnalyzed possible causes for user-reported issue.
UnderstandConfirmed resizing works correctly in tests.
Verifyhttps://github.com/badlogic/pi-mono/issues/824
issue got closed, we just fixed shit for bun. but user reports the last npm release 0.49.0 has this issue. we have an image in /tmp/large_test.png, can you test the last release please?
The user wants me to test an issue from GitHub related to the last npm release (0.49.0). Let me first read the issue to understand what we're testing.
>gh issue view 824 --json title,body,comments,labels,state --repo badlogic/pi-mono{"body":"Very happy camper so far, only issue encountered: when adding an image to a prompt / asking to fetch from disc, and it exceeds 5MB Claude throws an\n\n`Error: 400 {\"type\":\"error\",\"error\" {\"type\":\"invalid_request_error\",\"message\":\"messages.169.content.0.tool_result.content.1.image.source.base64: image exceeds 5 MB maximum: 6507076 bytes > 5242880 bytes\"},\"request_id\":\"req_011CXDJd1f5g51sYmtribZBo\"}`\n\nleading to the session being stuck, as it keeps retrying the image upload and failing. \n\nMaybe just retrying the previous prompt, with heavy payloads removed but the API error appended, is a good generic way to handle those situations and any other unforeseen errors those payloads might cause (decoding errors etc). For now I've added a \"check image size before and convert as needed\" note not to my system prompt and it works like a charm.\n\nMacOS iterm2 zsh with latest pi version.","comments":[{"id":"IC_kwDOPbFNk87gbOak","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"<img width=\"861\" height=\"543\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/efaf022c-9641-49bb-94f2-453de87064a3\" />\n\nHuh, images should be resized to be below that already. Investigating.","createdAt":"2026-01-18T12:20:07Z","includesCreatedEdit":true,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/824#issuecomment-3765233316","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87gbP-H","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"<img width=\"864\" height=\"1030\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/f95139fc-8df6-45b7-beee-795f8208e4b3\" />\n\nYeah, can't repro with latest pi. ","createdAt":"2026-01-18T12:28:14Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/824#issuecomment-3765239687","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87gbQVk","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"OK, does't work with the self-contained Bun executable. Please use the NPM version for now if you can.","createdAt":"2026-01-18T12:30:02Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/824#issuecomment-3765241188","viewerDidAuthor":true},{"id":"IC_kwDOPbFNk87gbita","author":{"login":"brunohaid"},"authorAssociation":"NONE","body":"<img width=\"848\" height=\"755\" alt=\"Image\" src=\"https://github.com/user-attachments/assets/f12141a0-51a8-4608-bea0-6cbbcf0d8a56\" />\n\nStill happening with npm 0.49.0. Are you checking for resolution or file size?","createdAt":"2026-01-18T13:55:48Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/824#issuecomment-3765316442","viewerDidAuthor":false},{"id":"IC_kwDOPbFNk87gcD-L","author":{"login":"badlogic"},"authorAssociation":"OWNER","body":"Fixed in main by loading photon wasm in compiled binaries and shipping photon_rs_bg.wasm with the release artifacts.","createdAt":"2026-01-18T16:15:00Z","includesCreatedEdit":false,"isMinimized":false,"minimizedReason":"","reactionGroups":[],"url":"https://github.com/badlogic/pi-mono/issues/824#issuecomment-3765452683","viewerDidAuthor":true}],"labels":[],"state":"CLOSED","title":"Handle Image / Payload Exceeding and other Recurring API Responses"}Now I understand the issue. It's about large images exceeding Claude's 5MB limit. The user brunohaid reports that even npm 0.49.0 has this issue. The fix was done for Bun, but user says it's still happening with npm version.
Let me test with the released 0.49.0 npm version using the image at /tmp/large_test.png.
First let me check the image exists and its size.
Let me check the image and test with the released npm version:
>ls -lh /tmp/large_test.png && file /tmp/large_test.png-rw-r--r--@ 1 badlogic wheel 24M Jan 18 13:23 /tmp/large_test.png
/tmp/large_test.png: PNG image data, 4000 x 4000, 8-bit/color RGB, non-interlaced>npm show @anthropic-ai/claude-code version 2>/dev/null || echo "checking pi"2.1.1224MB PNG, 4000x4000 resolution. That's a good test case. Now let me install the released 0.49.0 version of pi and test it. Let me first check what the package is named.
>npm show @anthropic-ai/pi version 2>/dev/null || npm show pi version 2>/dev/null || echo "need to find package name"2.0.5>cat packages/coding-agent/package.json | grep '"name"'"name": "@mariozechner/pi-coding-agent",
"name": "pi",So the package is @mariozechner/pi-coding-agent. Let me check the released version and install 0.49.0 to test.
>npm show @mariozechner/pi-coding-agent versions --json | tail -10"0.45.3",
"0.45.4",
"0.45.5",
"0.45.6",
"0.45.7",
"0.46.0",
"0.47.0",
"0.48.0",
"0.49.0"
]Good, 0.49.0 is the latest release. Let me install it in a temp location and test the image resizing.
>cd /tmp && rm -rf pi-test-049 && mkdir pi-test-049 && cd pi-test-049 && npm init -y && npm install @mariozechner/pi-coding-agent@0.49.0 2>&1 | tail -10Wrote to /private/tmp/pi-test-049/package.json:
{
"name": "pi-test-049",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs"
}
npm warn deprecated node-domexception@1.0.0: Use your platform's native DOMException instead
added 243 packages, and audited 244 packages in 7s
42 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilitiesGood, installed. Now let me look at how the image resizing is done in the coding-agent to understand where the issue might be. But first, let me write a simple test script to test if the image processing works correctly.
Let me find where image processing happens and write a test:
>grep -r "photon" packages/coding-agent/src --include="*.ts" -lpackages/coding-agent/src/utils/photon.ts
packages/coding-agent/src/utils/image-convert.ts
packages/coding-agent/src/utils/image-resize.ts>grep -r "resizeImage\|processImage\|image.*resize" packages/coding-agent/src --include="*.ts" -l | head -5packages/coding-agent/src/core/tools/read.ts
packages/coding-agent/src/modes/interactive/components/settings-selector.ts
packages/coding-agent/src/utils/image-resize.ts
packages/coding-agent/src/cli/file-processor.tsLet me look at the image-resize.ts to understand what it exports and how it's used.
So the key is loadPhoton() from ./photon.js. Let me check that file.
The code handles loading photon-node. The issue might be in how photon is loaded in the npm version. Let me write a test script to check if loadPhoton() successfully loads in the 0.49.0 release.
Let me write a test script to check if photon loads in 0.49.0:
Let me run this test with the npm version 0.49.0.
>cd /tmp/pi-test-049 && node test-photon.mjs 2>&1node:internal/modules/esm/resolve:313
return new ERR_PACKAGE_PATH_NOT_EXPORTED(
^
Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './dist/utils/image-resize.js' is not defined by "exports" in /private/tmp/pi-test-049/node_modules/@mariozechner/pi-coding-agent/package.json imported from /private/tmp/pi-test-049/test-photon.mjs
at exportsNotFound (node:internal/modules/esm/resolve:313:10)
at packageExportsResolve (node:internal/modules/esm/resolve:661:9)
at packageResolve (node:internal/modules/esm/resolve:774:12)
at moduleResolve (node:internal/modules/esm/resolve:858:18)
at defaultResolve (node:internal/modules/esm/resolve:990:11)
at #cachedDefaultResolve (node:internal/modules/esm/loader:712:20)
at #resolveAndMaybeBlockOnLoaderThread (node:internal/modules/esm/loader:729:38)
at ModuleLoader.resolveSync (node:internal/modules/esm/loader:758:52)
at #resolve (node:internal/modules/esm/loader:694:17)
at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:614:35) {
code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}
Node.js v25.2.1
Command exited with code 1The exports don't expose the internal module. Let me check what's exported and write a different test approach. I could test by using pi directly with an image.
Let me check what's exported and find another way to test:
>cat /tmp/pi-test-049/node_modules/@mariozechner/pi-coding-agent/package.json | grep -A 30 '"exports"'"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./hooks": {
"types": "./dist/core/hooks/index.d.ts",
"import": "./dist/core/hooks/index.js"
}
},
"files": [
"dist",
"docs",
"examples",
"CHANGELOG.md"
],
"scripts": {
"clean": "shx rm -rf dist",
"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/",
"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/",
"test": "vitest --run",
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
"@mariozechner/clipboard": "^0.3.0",
"@mariozechner/jiti": "^2.6.2",
"@mariozechner/pi-agent-core": "^0.49.0",
"@mariozechner/pi-ai": "^0.49.0",
"@mariozechner/pi-tui": "^0.49.0",Let me write a simpler test that uses require and directly accesses the internal modules. Or I can test by calling pi directly with the image. Let me try calling pi with the image as input.
Let me run this test with the npm version 0.49.0.
>cd /tmp/pi-test-049 && node test-photon.cjs 2>&1Original image size: 23.55 MB
Was resized: true
Original dimensions: 4000x4000
Final dimensions: 2000x2000
Final size: 0.66 MB
Under 5MB limit: trueInteresting! The resizing works correctly in the npm version when I test the module directly. The image went from 23.55 MB to 0.66 MB. So the issue isn't in the npm package itself.
Wait, the user's screenshot shows the issue happening. Maybe it's about how the image is being read in the tool result context. Let me check where images are processed in tool results.
Let me look at where images are processed when being sent to Claude in tool results or the Read tool.
The resizing works in 0.49.0! The image went from 23.55 MB to 0.66 MB. Let me check if there's a path where images might bypass the resize logic:
The read tool properly uses resizeImage for images. Let me check the file-processor.ts to see if there's another path for images.
Both
continue
The user wants me to continue my investigation. Both the read tool and file-processor properly use resizeImage. Let me check if there's maybe an issue with how images are passed in tool results or user messages. Let me also check what version of photon-node is in 0.49.0 vs current.
>cat /tmp/pi-test-049/node_modules/@mariozechner/pi-coding-agent/node_modules/@silvia-odwyer/photon-node/package.json 2>/dev/null | grep version || cat /tmp/pi-test-049/node_modules/@silvia-odwyer/photon-node/package.json 2>/dev/null | grep version"version": "0.3.4",>cat packages/coding-agent/node_modules/@silvia-odwyer/photon-node/package.json 2>/dev/null | grep version || echo "checking root"checking rootBoth have photon-node 0.3.4. The resizing works fine when I test it directly. Let me check if there's maybe something in the tool result processing that could bypass resizing.
Actually, let me test the full pi CLI with the image to see if something happens there. But that's not easily scriptable.
Wait, the user's screenshot shows they're adding an image via @-file argument. Let me check if maybe there's a different issue. Let me also check if there might be an issue with how the image gets processed when the model makes a read tool call vs when the user provides an image.
Actually, let me re-read the issue comments more carefully. The user brunohaid says it's still happening with npm 0.49.0 and they show a screenshot. The error message shows it's in a tool_result context (messages.169.content.0.tool_result.content.1.image).
So the issue might be in how tool results with images are processed. But I just checked read.ts and it does use resizeImage...
Let me check if there might be cases where resizeImage returns early without actually resizing - like when photon can't load.
Let me check if photon could silently fail to load. Looking at the resize code, when photon is null it returns wasResized: false:
>cd /tmp/pi-test-049 && cat > test-photon-load.cjs << 'EOF'
const path = require("path");
async function main() {
const photonPath = path.join(__dirname, "node_modules/@mariozechner/pi-coding-agent/dist/utils/photon.js");
const { loadPhoton } = await import(photonPath);
const photon = await loadPhoton();
console.log("Photon loaded:", photon !== null);
if (photon) {
console.log("Has PhotonImage:", typeof photon.PhotonImage);
console.log("Has resize:", typeof photon.resize);
}
}
main().catch(console.error);
EOF
node test-photon-load.cjs 2>&1Photon loaded: true
Has PhotonImage: function
Has resize: functionPhoton loads fine. Let me check if there could be an issue with the way images are being handled when they come from different sources (like clipboard or screenshots). Let me also check if maybe the user has an older cached version or something, or if there's a race condition.Wait, let me think about this differently. The test I wrote works. The user reports it doesn't work for them. What could be different?
Let me check if there are other places where images might be passed without resizing.
>grep -r "type.*image" packages/coding-agent/src --include="*.ts" | grep -v "resizeImage\|ImageContent\|mimeType\|\.d\.ts" | grep -v test | head -20packages/coding-agent/src/core/tools/read.ts: /** Detect image MIME type, return null/undefined for non-images */
packages/coding-agent/src/core/compaction/compaction.ts: if (block.type === "image") {
packages/coding-agent/src/core/extensions/runner.ts: const event: InputEvent = { type: "input", text: currentText, images: currentImages, source };
packages/coding-agent/src/core/sdk.ts: const hasImages = content.some((c) => c.type === "image");
packages/coding-agent/src/core/sdk.ts: c.type === "image" ? { type: "text" as const, text: "Image reading is disabled." } : c,
packages/coding-agent/src/modes/interactive/components/tool-execution.ts: const imageBlocks = this.result.content?.filter((c: any) => c.type === "image") || [];
packages/coding-agent/src/modes/interactive/components/tool-execution.ts: const imageBlocks = this.result.content?.filter((c: any) => c.type === "image") || [];
packages/coding-agent/src/modes/interactive/components/tool-execution.ts: const imageBlocks = this.result.content?.filter((c: any) => c.type === "image") || [];
packages/coding-agent/src/modes/rpc/rpc-client.ts: await this.send({ type: "prompt", message, images });
packages/coding-agent/src/cli/file-processor.ts: type: "image",
packages/coding-agent/src/cli/file-processor.ts: type: "image",>grep -r "base64" packages/coding-agent/src --include="*.ts" | grep -v "resizeImage\|\.d\.ts\|test" | head -10packages/coding-agent/src/core/tools/read.ts: const base64 = buffer.toString("base64");
packages/coding-agent/src/core/tools/read.ts: { type: "image", data: base64, mimeType },
packages/coding-agent/src/core/export-html/index.ts: const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString("base64");
packages/coding-agent/src/utils/image-convert.ts: base64Data: string,
packages/coding-agent/src/utils/image-convert.ts: return { data: base64Data, mimeType };
packages/coding-agent/src/utils/image-convert.ts: const bytes = new Uint8Array(Buffer.from(base64Data, "base64"));
packages/coding-agent/src/utils/image-convert.ts: data: Buffer.from(pngBuffer).toString("base64"),
packages/coding-agent/src/utils/image-resize.ts: data: string; // base64
packages/coding-agent/src/utils/image-resize.ts: const inputBuffer = Buffer.from(img.data, "base64");
packages/coding-agent/src/utils/image-resize.ts: data: Buffer.from(best.buffer).toString("base64"),I see there's an image-convert.ts. Let me check that file and also see how images flow through the CLI.
This is just for PNG conversion for terminal display, not relevant to the issue.
Let me check if there might be an issue with how images are handled when coming from the clipboard or pasted into the prompt. Let me check the interactive mode's input handling.
>grep -r "resizeImage" packages/coding-agent/src --include="*.ts" -lpackages/coding-agent/src/core/tools/read.ts
packages/coding-agent/src/utils/image-resize.ts
packages/coding-agent/src/cli/file-processor.tsSo resizeImage is only used in:
But what about images pasted into the chat or from clipboard? Let me check if there's another code path for images that doesn't use resizeImage.
>grep -r "clipboard\|paste" packages/coding-agent/src --include="*.ts" -l | head -5packages/coding-agent/src/core/system-prompt.ts
packages/coding-agent/src/core/keybindings.ts
packages/coding-agent/src/modes/interactive/interactive-mode.ts
packages/coding-agent/src/modes/interactive/components/custom-editor.ts
packages/coding-agent/src/utils/clipboard.tsLet me check the clipboard.ts and the interactive-mode.ts to see how clipboard images are handled.
This is just for text clipboard, not images. Let me check clipboard-image.ts
>find packages/coding-agent/src -name "clipboard-image*" -o -name "*image*clipboard*" 2>/dev/nullpackages/coding-agent/src/utils/clipboard-image.tsLet me check this file.
So readClipboardImage returns raw image bytes without resizing. Now let me see where this is used and if the images get resized before being sent to the API.
>grep -r "readClipboardImage" packages/coding-agent/src --include="*.ts" -A5 -B5 | head -60packages/coding-agent/src/modes/interactive/interactive-mode.ts-import { type SessionContext, SessionManager } from "../../core/session-manager.js";
packages/coding-agent/src/modes/interactive/interactive-mode.ts-import { loadProjectContextFiles } from "../../core/system-prompt.js";
packages/coding-agent/src/modes/interactive/interactive-mode.ts-import type { TruncationResult } from "../../core/tools/truncate.js";
packages/coding-agent/src/modes/interactive/interactive-mode.ts-import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
packages/coding-agent/src/modes/interactive/interactive-mode.ts-import { copyToClipboard } from "../../utils/clipboard.js";
packages/coding-agent/src/modes/interactive/interactive-mode.ts:import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
packages/coding-agent/src/modes/interactive/interactive-mode.ts-import { ensureTool } from "../../utils/tools-manager.js";
packages/coding-agent/src/modes/interactive/interactive-mode.ts-import { ArminComponent } from "./components/armin.js";
packages/coding-agent/src/modes/interactive/interactive-mode.ts-import { AssistantMessageComponent } from "./components/assistant-message.js";
packages/coding-agent/src/modes/interactive/interactive-mode.ts-import { BashExecutionComponent } from "./components/bash-execution.js";
packages/coding-agent/src/modes/interactive/interactive-mode.ts-import { BorderedLoader } from "./components/bordered-loader.js";
--
packages/coding-agent/src/modes/interactive/interactive-mode.ts- };
packages/coding-agent/src/modes/interactive/interactive-mode.ts- }
packages/coding-agent/src/modes/interactive/interactive-mode.ts-
packages/coding-agent/src/modes/interactive/interactive-mode.ts- private async handleClipboardImagePaste(): Promise<void> {
packages/coding-agent/src/modes/interactive/interactive-mode.ts- try {
packages/coding-agent/src/modes/interactive/interactive-mode.ts: const image = await readClipboardImage();
packages/coding-agent/src/modes/interactive/interactive-mode.ts- if (!image) {
packages/coding-agent/src/modes/interactive/interactive-mode.ts- return;
packages/coding-agent/src/modes/interactive/interactive-mode.ts- }
packages/coding-agent/src/modes/interactive/interactive-mode.ts-
packages/coding-agent/src/modes/interactive/interactive-mode.ts- // Write to temp file
--
packages/coding-agent/src/utils/clipboard-image.ts- : Buffer.from(result.stdout ?? "", typeof result.stdout === "string" ? "utf-8" : undefined);
packages/coding-agent/src/utils/clipboard-image.ts-
packages/coding-agent/src/utils/clipboard-image.ts- return { ok: true, stdout };
packages/coding-agent/src/utils/clipboard-image.ts-}
packages/coding-agent/src/utils/clipboard-image.ts-
packages/coding-agent/src/utils/clipboard-image.ts:function readClipboardImageViaWlPaste(): ClipboardImage | null {
packages/coding-agent/src/utils/clipboard-image.ts- const list = runCommand("wl-paste", ["--list-types"], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });
packages/coding-agent/src/utils/clipboard-image.ts- if (!list.ok) {
packages/coding-agent/src/utils/clipboard-image.ts- return null;
packages/coding-agent/src/utils/clipboard-image.ts- }
packages/coding-agent/src/utils/clipboard-image.ts-
--
packages/coding-agent/src/utils/clipboard-image.ts- }
packages/coding-agent/src/utils/clipboard-image.ts-
packages/coding-agent/src/utils/clipboard-image.ts- return { bytes: data.stdout, mimeType: baseMimeType(selectedType) };
packages/coding-agent/src/utils/clipboard-image.ts-}
packages/coding-agent/src/utils/clipboard-image.ts-
packages/coding-agent/src/utils/clipboard-image.ts:function readClipboardImageViaXclip(): ClipboardImage | null {
packages/coding-agent/src/utils/clipboard-image.ts- const targets = runCommand("xclip", ["-selection", "clipboard", "-t", "TARGETS", "-o"], {
packages/coding-agent/src/utils/clipboard-image.ts- timeoutMs: DEFAULT_LIST_TIMEOUT_MS,
packages/coding-agent/src/utils/clipboard-image.ts- });
packages/coding-agent/src/utils/clipboard-image.ts-
packages/coding-agent/src/utils/clipboard-image.ts- let candidateTypes: string[] = [];
--
packages/coding-agent/src/utils/clipboard-image.ts- }
packages/coding-agent/src/utils/clipboard-image.ts-
packages/coding-agent/src/utils/clipboard-image.ts- return null;
packages/coding-agent/src/utils/clipboard-image.ts-}
packages/coding-agent/src/utils/clipboard-image.ts-
packages/coding-agent/src/utils/clipboard-image.ts:export async function readClipboardImage(options?: {
packages/coding-agent/src/utils/clipboard-image.ts- env?: NodeJS.ProcessEnv;
packages/coding-agent/src/utils/clipboard-image.ts- platform?: NodeJS.Platform;
packages/coding-agent/src/utils/clipboard-image.ts-}): Promise<ClipboardImage | null> {
packages/coding-agent/src/utils/clipboard-image.ts- const env = options?.env ?? process.env;
packages/coding-agent/src/utils/clipboard-image.ts- const platform = options?.platform ?? process.platform;
packages/coding-agent/src/utils/clipboard-image.ts-I see that handleClipboardImagePaste reads the image and writes it to a temp file. Let me see what happens after that.
>grep -A50 "handleClipboardImagePaste" packages/coding-agent/src/modes/interactive/interactive-mode.ts | head -60this.handleClipboardImagePaste();
};
}
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));
// Insert file path directly
this.editor.insertTextAtCursor?.(filePath);
this.ui.requestRender();
} catch {
// Silently ignore clipboard errors (may not have permission, etc.)
}
}
private setupEditorSubmitHandler(): void {
this.defaultEditor.onSubmit = async (text: string) => {
text = text.trim();
if (!text) return;
// Handle commands
if (text === "/settings") {
this.showSettingsSelector();
this.editor.setText("");
return;
}
if (text === "/scoped-models") {
this.editor.setText("");
await this.showModelsSelector();
return;
}
if (text === "/model" || text.startsWith("/model ")) {
const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
this.editor.setText("");
await this.handleModelCommand(searchTerm);
return;
}
if (text.startsWith("/export")) {
await this.handleExportCommand(text);
this.editor.setText("");
return;
}
if (text === "/share") {
await this.handleShareCommand();I see! When a clipboard image is pasted, it writes the raw bytes to a temp file and inserts the file path. Then this path gets processed like any other file. Let me see how the editor submission handles files.
>grep -A100 "setupEditorSubmitHandler" packages/coding-agent/src/modes/interactive/interactive-mode.ts | grep -A50 "processInputWithFiles\|processFileArguments" | head -60(no output)Let me search for where file processing happens in the interactive mode.
>grep -n "processFileArguments\|processedFiles\|processInput" packages/coding-agent/src/modes/interactive/interactive-mode.ts | head -20(no output)Let me search for how the editor submission handles files.
>grep -n "images\|ImageContent\|mimeType" packages/coding-agent/src/modes/interactive/interactive-mode.ts | head -2014: type ImageContent,
128: initialImages?: ImageContent[];
501: await this.session.prompt(initialMessage, { images: initialImages });
1450: const ext = extensionForImageMimeType(image.mimeType) ?? "png";Let me search for where the prompt is called with files/images.
>grep -n "session.prompt\|sdk.prompt" packages/coding-agent/src/modes/interactive/interactive-mode.ts314: const templateCommands: SlashCommand[] = this.session.promptTemplates.map((cmd) => ({
501: await this.session.prompt(initialMessage, { images: initialImages });
511: await this.session.prompt(message);
523: await this.session.prompt(userInput);
620: const templates = this.session.promptTemplates;
1595: await this.session.prompt(text);
1607: await this.session.prompt(text, { streamingBehavior: "steer" });
2184: await this.session.prompt(text);
2196: await this.session.prompt(text, { streamingBehavior: "followUp" });
2456: await this.session.prompt(message.text);
2472: await this.session.prompt(message.text);
2483: await this.session.prompt(message.text);
2487: const promptPromise = this.session.prompt(firstPrompt.text).catch((error) => {
2494: await this.session.prompt(message.text);I see images are passed to session.prompt at line 501, but where does the main submit handler process files? Let me look at how the submitted text with file paths gets processed.
>grep -B5 -A30 'session\.prompt\(text\)' packages/coding-agent/src/modes/interactive/interactive-mode.ts | head -80(no output)Let me look for where input is handled and how files are extracted from text.
>grep -n "extractFiles\|parseFiles\|fileProcessor\|@file\|FilePath" packages/coding-agent/src/modes/interactive/interactive-mode.ts | head -10125: /** Initial message to send on startup (can include @file content) */Let me look at the custom-editor component which might handle file references.
The custom editor doesn't handle files directly. Let me look at the session manager to see how prompts are processed.
>grep -n "import.*ImageContent\|images.*ImageContent\|resizeImage" packages/coding-agent/src/core/session-manager.ts | head -102:import type { ImageContent, Message, TextContent } from "@mariozechner/pi-ai";Let me look at the prompt method in the session manager.
>grep -B5 -A50 "async prompt\(" packages/coding-agent/src/core/session-manager.ts | head -80grep: parentheses not balancedNeed to escape the parenthesis.
>grep -B5 -A50 "async prompt" packages/coding-agent/src/core/session-manager.ts | head -80(no output)Let me look at session-manager to find where the prompt method is defined.
>grep -n "prompt" packages/coding-agent/src/core/session-manager.ts | head -20(no output)Let me check where Session is defined.
>grep -n "class Session\|prompt(" packages/coding-agent/src/core/session-manager.ts | head -20619:export class SessionManager {SessionManager, not Session. Let me look at sdk.ts where the main session logic is.
>grep -n "prompt" packages/coding-agent/src/core/sdk.ts | head -2043:import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "./prompt-templates.js";
50:} from "./system-prompt.js";
97: /** System prompt. String replaces default, function receives default and returns final. */
121: /** Prompt templates. Default: discovered from cwd/.pi/prompts/ + agentDir/prompts/ */
122: promptTemplates?: PromptTemplate[];
150:export type { PromptTemplate } from "./prompt-templates.js";
251: * Discover prompt templates from cwd and agentDir.
273: * Build the default system prompt.
552: // Function to rebuild system prompt when tools change
578: const promptTemplates = options.promptTemplates ?? discoverPromptTemplates(cwd, agentDir);
667: promptTemplates: promptTemplates,Let me look at how the SDK handles prompts.
>grep -n "prompt\|images" packages/coding-agent/src/core/sdk.ts | head -4043:import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "./prompt-templates.js";
50:} from "./system-prompt.js";
97: /** System prompt. String replaces default, function receives default and returns final. */
121: /** Prompt templates. Default: discovered from cwd/.pi/prompts/ + agentDir/prompts/ */
122: promptTemplates?: PromptTemplate[];
150:export type { PromptTemplate } from "./prompt-templates.js";
251: * Discover prompt templates from cwd and agentDir.
273: * Build the default system prompt.
306: images: { autoResize: manager.getImageAutoResize(), blockImages: manager.getBlockImages() },
552: // Function to rebuild system prompt when tools change
578: const promptTemplates = options.promptTemplates ?? discoverPromptTemplates(cwd, agentDir);
581: // Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)
667: promptTemplates: promptTemplates,Let me check where the images flow from user input to the API. Let me look at the agent-core package which might handle this.
>grep -rn "resizeImage\|autoResize\|ImageContent" packages/agent-core/src --include="*.ts" | head -20(no output)Let me check the pi-agent-core package instead.
>ls packages/agent
ai
coding-agent
mom
pods
tui
web-uiIt's packages/agent not packages/agent-core. Let me check that.
>grep -rn "resizeImage\|autoResize\|ImageContent" packages/agent/src --include="*.ts" | head -20packages/agent/src/agent.ts:8: type ImageContent,
packages/agent/src/agent.ts:254: async prompt(input: string, images?: ImageContent[]): Promise<void>;
packages/agent/src/agent.ts:255: async prompt(input: string | AgentMessage | AgentMessage[], images?: ImageContent[]) {
packages/agent/src/agent.ts:270: const content: Array<TextContent | ImageContent> = [{ type: "text", text: input }];
packages/agent/src/types.ts:3: ImageContent,
packages/agent/src/types.ts:148: content: (TextContent | ImageContent)[];Let me look at the agent.ts prompt method to see how images are handled.
>grep -B5 -A30 "async prompt" packages/agent/src/agent.tsthis.steeringQueue = [];
this.followUpQueue = [];
}
/** Send a prompt with an AgentMessage */
async prompt(message: AgentMessage | AgentMessage[]): Promise<void>;
async prompt(input: string, images?: ImageContent[]): Promise<void>;
async prompt(input: string | AgentMessage | AgentMessage[], images?: ImageContent[]) {
if (this._state.isStreaming) {
throw new Error(
"Agent is already processing a prompt. Use steer() or followUp() to queue messages, or wait for completion.",
);
}
const model = this._state.model;
if (!model) throw new Error("No model configured");
let msgs: AgentMessage[];
if (Array.isArray(input)) {
msgs = input;
} else if (typeof input === "string") {
const content: Array<TextContent | ImageContent> = [{ type: "text", text: input }];
if (images && images.length > 0) {
content.push(...images);
}
msgs = [
{
role: "user",
content,
timestamp: Date.now(),
},
];
} else {
msgs = [input];
}
await this._runLoop(msgs);I see. The agent just passes images through without resizing. The resizing is done in the Read tool and file-processor. Let me check where images from CLI @-file arguments get processed and how they flow to the agent.Let me trace the flow from the CLI entry point.
>grep -rn "processFileArguments\|initialImages" packages/coding-agent/src --include="*.ts" | head -20packages/coding-agent/src/main.ts:14:import { processFileArguments } from "./cli/file-processor.js";
packages/coding-agent/src/main.ts:62: initialImages?: ImageContent[];
packages/coding-agent/src/main.ts:68: const { text, images } = await processFileArguments(parsed.fileArgs, { autoResizeImages });
packages/coding-agent/src/main.ts:80: initialImages: images.length > 0 ? images : undefined,
packages/coding-agent/src/main.ts:417: const { initialMessage, initialImages } = await prepareInitialMessage(parsed, settingsManager.getImageAutoResize());
packages/coding-agent/src/main.ts:521: initialImages,
packages/coding-agent/src/main.ts:530: initialImages,
packages/coding-agent/src/modes/print-mode.ts:23: initialImages?: ImageContent[];
packages/coding-agent/src/modes/print-mode.ts:31: const { mode, messages = [], initialMessage, initialImages } = options;
packages/coding-agent/src/modes/print-mode.ts:143: await session.prompt(initialMessage, { images: initialImages });
packages/coding-agent/src/modes/interactive/interactive-mode.ts:128: initialImages?: ImageContent[];
packages/coding-agent/src/modes/interactive/interactive-mode.ts:483: const { migratedProviders, modelFallbackMessage, initialMessage, initialImages, initialMessages } = this.options;
packages/coding-agent/src/modes/interactive/interactive-mode.ts:501: await this.session.prompt(initialMessage, { images: initialImages });
packages/coding-agent/src/cli/file-processor.ts:24:export async function processFileArguments(fileArgs: string[], options?: ProcessFileOptions): Promise<ProcessedFiles> {OK so processFileArguments in main.ts does resize images. But looking at the issue again, the user's error shows messages.169.content.0.tool_result, so the issue is when the image comes from a tool result (the Read tool). And the Read tool does resize images...
Wait, let me look at the error message more carefully: messages.169.content.0.tool_result.content.1.image.source.base64. This is a tool result. Let me look at the 0.49.0 read.ts to see if there's a difference.
The read.js code looks fine - it does call resizeImage. Let me write a full test that mimics what the Read tool does to see if the issue is reproducible.
Let me run this test.
>cd /tmp/pi-test-049 && node test-read-tool.cjs 2>&1Testing Read tool with large image...
Read tool completed successfully
Content types: [ 'text', 'image' ]
Image size in result: 0.66 MB
Under 5MB limit: true
Text note: Read image file [image/jpeg]
[Image: original 4000x4000, displayed at 2000x2000. Multiply coordinates by 2.00 to map to original image.]The Read tool works correctly in 0.49.0. The 24MB image is resized to 0.66MB. So the npm version works fine...The user brunohaid claims npm 0.49.0 doesn't work for them. Let me think about what could be different:
The issue was closed after fixing the Bun binary issue. Looking at the comments again:
Let me check if there's any condition under which photon loading might fail silently. Looking at the code, when photon fails to load, it returns wasResized: false and returns the original image unchanged.
Maybe the user has some issue where photon fails to load but they don't know it? Let me check if there's any way to test this.Actually, let me confirm by checking the screenshot context more carefully. The user's screenshot shows the image being over 5MB. Let me re-read the comments.
In brunohaid's screenshot, it shows an error about the base64 image exceeding the limit. This could happen if:
Looking at the resize code:
if (!photon) { // Photon not available, return original image return { data: img.data, mimeType: img.mimeType, ... wasResized: false, };}So if photon fails to load, the original image is returned.
If photon isn't loading properly, the uncompressed image would exceed the 5MB limit. I need to figure out what could prevent photon from initializing in their npm setup.
The npm version works correctly in my tests. Let me check what could cause photon to fail silently:
Let me run this test.
>cd /tmp/pi-test-049 && node test-photon-fail.cjs 2>&1Original image size: 23.55 MB
Was resized: true
Original dimensions: 4000x4000
Final dimensions: 2000x2000
Final size: 0.66 MB
Photon loaded successfully.OK, it works. Let me check different Node.js versions. The user might be on
continue
The user wants me to continue testing. Let me check if there's a Node.js version issue or something else that could cause photon to fail.
>node --versionv25.2.1>cd /tmp/pi-test-049 && cat node_modules/@silvia-odwyer/photon-node/package.json | grep -A5 '"engines"'(no output)
Command exited with code 1Let me check the photon-node package more carefully.
>cat /tmp/pi-test-049/node_modules/@silvia-odwyer/photon-node/package.json{
"name": "@silvia-odwyer/photon-node",
"collaborators": [
"Silvia O'Dwyer <[REDACTED]>"
],
"description": "High-performance image processing library for native use and the web",
"version": "0.3.4",
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "https://github.com/silvia-odwyer/photon"
},
"files": [
"photon_rs_bg.wasm",
"photon_rs.js",
"photon_rs_bg.js",
"photon_rs.d.ts",
"LICENSE.md"
],
"main": "photon_rs.js",
"homepage": "https://silvia-odwyer.github.io/photon/",
"types": "photon_rs.d.ts"
}Let me look at how photon loads and if there are any edge cases where it might fail silently.
>head -50 /tmp/pi-test-049/node_modules/@silvia-odwyer/photon-node/photon_rs.jslet imports = {};
imports['__wbindgen_placeholder__'] = module.exports;
let wasm;
const { TextEncoder, TextDecoder } = require(`util`);
function debugString(val) {
// primitive types
const type = typeof val;
if (type == 'number' || type == 'boolean' || val == null) {
return `${val}`;
}
if (type == 'string') {
return `"${val}"`;
}
if (type == 'symbol') {
const description = val.description;
if (description == null) {
return 'Symbol';
} else {
return `Symbol(${description})`;
}
}
if (type == 'function') {
const name = val.name;
if (typeof name == 'string' && name.length > 0) {
return `Function(${name})`;
} else {
return 'Function';
}
}
// objects
if (Array.isArray(val)) {
const length = val.length;
let debug = '[';
if (length > 0) {
debug += debugString(val[0]);
}
for(let i = 1; i < length; i++) {
debug += ', ' + debugString(val[i]);
}
debug += ']';
return debug;
}
// Test for built-in
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
let className;
if (builtInMatches.length > 1) {
className = builtInMatches[1];
} else {Let me check the end of the file where the WASM loading happens.
>tail -30 /tmp/pi-test-049/node_modules/@silvia-odwyer/photon-node/photon_rs.jsmodule.exports.__wbindgen_throw = function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
module.exports.__wbindgen_memory = function() {
const ret = wasm.memory;
return ret;
};
module.exports.__wbindgen_init_externref_table = function() {
const table = wasm.__wbindgen_export_2;
const offset = table.grow(4);
table.set(0, undefined);
table.set(offset + 0, undefined);
table.set(offset + 1, null);
table.set(offset + 2, true);
table.set(offset + 3, false);
;
};
const path = require('path').join(__dirname, 'photon_rs_bg.wasm');
const bytes = require('fs').readFileSync(path);
const wasmModule = new WebAssembly.Module(bytes);
const wasmInstance = new WebAssembly.Instance(wasmModule, imports);
wasm = wasmInstance.exports;
module.exports.__wasm = wasm;
wasm.__wbindgen_start();I see. The photon module loads synchronously using fs.readFileSync(path) at the module level. This should work fine with npm install. If the wasm file is missing, it would throw an error.
Let me think about what the user might be experiencing. Looking at the issue again, the user brunohaid says it's still happening with npm 0.49.0. But my tests show it works fine.
Possible scenarios:
Let me check if there's any difference between global and local installs. Also, let me ask the user to provide more details. But wait, I'm just testing, not responding to the user directly.
Actually, looking at the issue more carefully, the issue was closed. The user was told to use npm instead of bun binary. Then they reported it still happens with npm. But the fix mentioned in the last comment was about loading photon wasm in compiled binaries - which wouldn't affect the npm version.
Let me check if there's a scenario where the npm version could fail. Maybe when running with npx?
Let me test with npx which is how users commonly run it:
>cd /tmp && rm -rf pi-npx-test && mkdir pi-npx-test && cd pi-npx-test && npx @mariozechner/pi-coding-agent@0.49.0 --version 2>&1 | head -50.49.0Let me write a script that tests the actual pi CLI flow when reading an image.
>cd /tmp/pi-npx-test && cat > test.js << 'EOF'
// Simulate what happens when pi reads an image via the Read tool
const { createReadTool } = require("@mariozechner/pi-coding-agent/dist/core/tools/read.js");
async function test() {
const readTool = createReadTool(process.cwd(), { autoResizeImages: true });
const result = await readTool.execute("test", { path: "/tmp/large_test.png" });
const img = result.content.find(c => c.type === "image");
if (img) {
const size = Buffer.from(img.data, "base64").length;
console.log(`Image size: ${(size / 1024 / 1024).toFixed(2)} MB`);
console.log(`Under 5MB: ${size < 5 * 1024 * 1024}`);
}
}
test().catch(e => console.error(e));
EOF
node test.js 2>&1node:internal/modules/cjs/loader:1423
throw err;
^
Error: Cannot find module '@mariozechner/pi-coding-agent/dist/core/tools/read.js'
Require stack:
- /private/tmp/pi-npx-test/test.js
at Module._resolveFilename (node:internal/modules/cjs/loader:1420:15)
at defaultResolveImpl (node:internal/modules/cjs/loader:1058:19)
at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1063:22)
at Module._load (node:internal/modules/cjs/loader:1226:37)
at TracingChannel.traceSync (node:diagnostics_channel:328:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:245:24)
at Module.require (node:internal/modules/cjs/loader:1503:12)
at require (node:internal/modules/helpers:152:16)
at Object.<anonymous> (/private/tmp/pi-npx-test/test.js:2:28)
at Module._compile (node:internal/modules/cjs/loader:1760:14) {
code: 'MODULE_NOT_FOUND',
requireStack: [ '/private/tmp/pi-npx-test/test.js' ]
}
Node.js v25.2.1
Command exited with code 1The npx run doesn't leave the package installed. Let me use the already installed test directory.
Actually, you know what, my tests confirm that 0.49.0 works correctly. The image gets resized from 23.55MB to 0.66MB. The user might have a different issue or might be mistaken about which version they're running.
Let me summarize my findings for the user.
All my tests confirm 0.49.0 works correctly:
Original image size: 23.55 MBWas resized: trueOriginal dimensions: 4000x4000Final dimensions: 2000x2000Final size: 0.66 MBThe Read tool properly resizes the 24MB PNG to 0.66MB JPEG, well under the 5MB limit.
Possible explanations for the user's issue:
resizeImage() returns the original image unchanged with wasResized: falseLet me check one more thing - how clipboard-pasted images flow through:
>grep -A80 "handleClipboardImagePaste" packages/coding-agent/src/modes/interactive/interactive-mode.ts | grep -A60 "insertTextAtCursor"this.editor.insertTextAtCursor?.(filePath);
this.ui.requestRender();
} catch {
// Silently ignore clipboard errors (may not have permission, etc.)
}
}
private setupEditorSubmitHandler(): void {
this.defaultEditor.onSubmit = async (text: string) => {
text = text.trim();
if (!text) return;
// Handle commands
if (text === "/settings") {
this.showSettingsSelector();
this.editor.setText("");
return;
}
if (text === "/scoped-models") {
this.editor.setText("");
await this.showModelsSelector();
return;
}
if (text === "/model" || text.startsWith("/model ")) {
const searchTerm = text.startsWith("/model ") ? text.slice(7).trim() : undefined;
this.editor.setText("");
await this.handleModelCommand(searchTerm);
return;
}
if (text.startsWith("/export")) {
await this.handleExportCommand(text);
this.editor.setText("");
return;
}
if (text === "/share") {
await this.handleShareCommand();
this.editor.setText("");
return;
}
if (text === "/copy") {
this.handleCopyCommand();
this.editor.setText("");
return;
}
if (text === "/name" || text.startsWith("/name ")) {
this.handleNameCommand(text);
this.editor.setText("");
return;
}
if (text === "/session") {
this.handleSessionCommand();
this.editor.setText("");
return;
}
if (text === "/changelog") {
this.handleChangelogCommand();
this.editor.setText("");
return;
}
if (text === "/hotkeys") {
this.handleHotkeysCommand();I see - clipboard paste just inserts a file path into the editor. The user would then need to submit that. But how does the submitted text with a file path get processed?
Let me look for where file paths in the editor text get processed.
>grep -n "processInput\|handleSubmit\|parseInput\|extractFile" packages/coding-agent/src/modes/interactive/interactive-mode.ts | head -15(no output)if (!photon) { // Photon not available, return original image return { data: img.data, mimeType: img.mimeType, ... wasResized: false, };}Original image size: 23.55 MBWas resized: trueOriginal dimensions: 4000x4000Final dimensions: 2000x2000Final size: 0.66 MB