@oh-my-pi/pi-coding-agent 5.0.0 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/package.json +6 -6
- package/src/cli/file-processor.ts +16 -7
- package/src/core/system-prompt.ts +4 -0
- package/src/core/tools/read.ts +20 -12
- package/src/modes/interactive/components/tool-execution.ts +12 -8
- package/src/utils/image-convert.ts +8 -8
- package/src/utils/image-resize.ts +23 -21
- package/src/utils/photon.ts +25 -0
- package/src/utils/vips.ts +0 -23
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.0.1] - 2026-01-12
|
|
6
|
+
### Changed
|
|
7
|
+
|
|
8
|
+
- Replaced wasm-vips with Photon for more stable WASM image processing
|
|
9
|
+
- Added graceful fallback to original images when image resizing fails
|
|
10
|
+
- Added error handling for image conversion failures in interactive mode to prevent crashes
|
|
11
|
+
- Replace wasm-vips with Photon for more stable WASM image processing (fixes worker thread crashes)
|
|
12
|
+
|
|
5
13
|
## [5.0.0] - 2026-01-12
|
|
6
14
|
|
|
7
15
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -39,11 +39,12 @@
|
|
|
39
39
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-agent-core": "5.0.
|
|
43
|
-
"@oh-my-pi/pi-ai": "5.0.
|
|
44
|
-
"@oh-my-pi/pi-git-tool": "5.0.
|
|
45
|
-
"@oh-my-pi/pi-tui": "5.0.
|
|
42
|
+
"@oh-my-pi/pi-agent-core": "5.0.1",
|
|
43
|
+
"@oh-my-pi/pi-ai": "5.0.1",
|
|
44
|
+
"@oh-my-pi/pi-git-tool": "5.0.1",
|
|
45
|
+
"@oh-my-pi/pi-tui": "5.0.1",
|
|
46
46
|
"@openai/agents": "^0.3.7",
|
|
47
|
+
"@silvia-odwyer/photon": "^0.3.3",
|
|
47
48
|
"@sinclair/typebox": "^0.34.46",
|
|
48
49
|
"ajv": "^8.17.1",
|
|
49
50
|
"chalk": "^5.5.0",
|
|
@@ -60,7 +61,6 @@
|
|
|
60
61
|
"node-html-parser": "^6.1.13",
|
|
61
62
|
"smol-toml": "^1.6.0",
|
|
62
63
|
"strip-ansi": "^7.1.2",
|
|
63
|
-
"wasm-vips": "^0.0.16",
|
|
64
64
|
"winston": "^3.17.0",
|
|
65
65
|
"winston-daily-rotate-file": "^5.0.0",
|
|
66
66
|
"zod": "^4.3.5"
|
|
@@ -52,13 +52,22 @@ export async function processFileArguments(fileArgs: string[], options?: Process
|
|
|
52
52
|
let dimensionNote: string | undefined;
|
|
53
53
|
|
|
54
54
|
if (_autoResizeImages) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
try {
|
|
56
|
+
const resized = await resizeImage({ type: "image", data: base64Content, mimeType });
|
|
57
|
+
dimensionNote = formatDimensionNote(resized);
|
|
58
|
+
attachment = {
|
|
59
|
+
type: "image",
|
|
60
|
+
mimeType: resized.mimeType,
|
|
61
|
+
data: resized.data,
|
|
62
|
+
};
|
|
63
|
+
} catch {
|
|
64
|
+
// Fall back to original image on resize failure
|
|
65
|
+
attachment = {
|
|
66
|
+
type: "image",
|
|
67
|
+
mimeType,
|
|
68
|
+
data: base64Content,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
62
71
|
} else {
|
|
63
72
|
attachment = {
|
|
64
73
|
type: "image",
|
|
@@ -632,6 +632,10 @@ export interface BuildSystemPromptOptions {
|
|
|
632
632
|
|
|
633
633
|
/** Build the system prompt with tools, guidelines, and context */
|
|
634
634
|
export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): Promise<string> {
|
|
635
|
+
if (process.env.NULL_PROMPT === "true") {
|
|
636
|
+
return "";
|
|
637
|
+
}
|
|
638
|
+
|
|
635
639
|
const {
|
|
636
640
|
customPrompt,
|
|
637
641
|
tools,
|
package/src/core/tools/read.ts
CHANGED
|
@@ -518,19 +518,27 @@ export function createReadTool(session: ToolSession): AgentTool<typeof readSchem
|
|
|
518
518
|
const base64 = Buffer.from(buffer).toString("base64");
|
|
519
519
|
|
|
520
520
|
if (autoResizeImages) {
|
|
521
|
-
// Resize image if needed
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
521
|
+
// Resize image if needed - catch errors from WASM
|
|
522
|
+
try {
|
|
523
|
+
const resized = await resizeImage({ type: "image", data: base64, mimeType });
|
|
524
|
+
const dimensionNote = formatDimensionNote(resized);
|
|
525
|
+
|
|
526
|
+
let textNote = `Read image file [${resized.mimeType}]`;
|
|
527
|
+
if (dimensionNote) {
|
|
528
|
+
textNote += `\n${dimensionNote}`;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
content = [
|
|
532
|
+
{ type: "text", text: textNote },
|
|
533
|
+
{ type: "image", data: resized.data, mimeType: resized.mimeType },
|
|
534
|
+
];
|
|
535
|
+
} catch {
|
|
536
|
+
// Fall back to original image on resize failure
|
|
537
|
+
content = [
|
|
538
|
+
{ type: "text", text: `Read image file [${mimeType}]` },
|
|
539
|
+
{ type: "image", data: base64, mimeType },
|
|
540
|
+
];
|
|
528
541
|
}
|
|
529
|
-
|
|
530
|
-
content = [
|
|
531
|
-
{ type: "text", text: textNote },
|
|
532
|
-
{ type: "image", data: resized.data, mimeType: resized.mimeType },
|
|
533
|
-
];
|
|
534
542
|
} else {
|
|
535
543
|
content = [
|
|
536
544
|
{ type: "text", text: `Read image file [${mimeType}]` },
|
|
@@ -261,15 +261,19 @@ export class ToolExecutionComponent extends Container {
|
|
|
261
261
|
if (img.mimeType === "image/png") continue;
|
|
262
262
|
if (this.convertedImages.has(i)) continue;
|
|
263
263
|
|
|
264
|
-
// Convert async
|
|
264
|
+
// Convert async - catch errors from WASM processing
|
|
265
265
|
const index = i;
|
|
266
|
-
convertToPng(img.data, img.mimeType)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
266
|
+
convertToPng(img.data, img.mimeType)
|
|
267
|
+
.then((converted) => {
|
|
268
|
+
if (converted) {
|
|
269
|
+
this.convertedImages.set(index, converted);
|
|
270
|
+
this.updateDisplay();
|
|
271
|
+
this.ui.requestRender();
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
.catch(() => {
|
|
275
|
+
// Ignore conversion failures - display will use original image format
|
|
276
|
+
});
|
|
273
277
|
}
|
|
274
278
|
}
|
|
275
279
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { logger } from "../core/logger";
|
|
2
2
|
import { convertToPngWithImageMagick } from "./image-magick";
|
|
3
|
-
import {
|
|
3
|
+
import { getPhoton } from "./photon";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Convert image to PNG format for terminal display.
|
|
7
7
|
* Kitty graphics protocol requires PNG format (f=100).
|
|
8
|
-
* Uses
|
|
8
|
+
* Uses Photon (Rust/WASM) if available, falls back to ImageMagick.
|
|
9
9
|
*/
|
|
10
10
|
export async function convertToPng(
|
|
11
11
|
base64Data: string,
|
|
@@ -17,20 +17,20 @@ export async function convertToPng(
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
try {
|
|
20
|
-
const
|
|
21
|
-
const image =
|
|
20
|
+
const photon = await getPhoton();
|
|
21
|
+
const image = photon.PhotonImage.new_from_byteslice(new Uint8Array(Buffer.from(base64Data, "base64")));
|
|
22
22
|
try {
|
|
23
|
-
const pngBuffer = image.
|
|
23
|
+
const pngBuffer = image.get_bytes();
|
|
24
24
|
return {
|
|
25
25
|
data: Buffer.from(pngBuffer).toString("base64"),
|
|
26
26
|
mimeType: "image/png",
|
|
27
27
|
};
|
|
28
28
|
} finally {
|
|
29
|
-
image.
|
|
29
|
+
image.free();
|
|
30
30
|
}
|
|
31
31
|
} catch (error) {
|
|
32
|
-
//
|
|
33
|
-
logger.error("Failed to convert image to PNG with
|
|
32
|
+
// Photon failed, try ImageMagick fallback
|
|
33
|
+
logger.error("Failed to convert image to PNG with Photon", {
|
|
34
34
|
error: error instanceof Error ? error.message : String(error),
|
|
35
35
|
});
|
|
36
36
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { logger } from "../core/logger";
|
|
3
3
|
import { getImageDimensionsWithImageMagick, resizeWithImageMagick } from "./image-magick";
|
|
4
|
-
import {
|
|
4
|
+
import { getPhoton } from "./photon";
|
|
5
5
|
|
|
6
6
|
export interface ImageResizeOptions {
|
|
7
7
|
maxWidth?: number; // Default: 2000
|
|
@@ -31,7 +31,7 @@ const DEFAULT_OPTIONS: Required<ImageResizeOptions> = {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Fallback resize using ImageMagick when
|
|
34
|
+
* Fallback resize using ImageMagick when Photon is unavailable.
|
|
35
35
|
*/
|
|
36
36
|
async function resizeImageWithImageMagick(
|
|
37
37
|
img: ImageContent,
|
|
@@ -85,7 +85,7 @@ function pickSmaller(
|
|
|
85
85
|
* Resize an image to fit within the specified max dimensions and file size.
|
|
86
86
|
* Returns the original image if it already fits within the limits.
|
|
87
87
|
*
|
|
88
|
-
* Uses
|
|
88
|
+
* Uses Photon (Rust/WASM) for image processing. Falls back to ImageMagick if unavailable.
|
|
89
89
|
*
|
|
90
90
|
* Strategy for staying under maxBytes:
|
|
91
91
|
* 1. First resize to maxWidth/maxHeight
|
|
@@ -95,18 +95,19 @@ function pickSmaller(
|
|
|
95
95
|
*/
|
|
96
96
|
export async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage> {
|
|
97
97
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
98
|
-
const
|
|
98
|
+
const inputBuffer = Buffer.from(img.data, "base64");
|
|
99
99
|
|
|
100
100
|
try {
|
|
101
|
-
const
|
|
102
|
-
const image =
|
|
101
|
+
const photon = await getPhoton();
|
|
102
|
+
const image = photon.PhotonImage.new_from_byteslice(new Uint8Array(inputBuffer));
|
|
103
|
+
|
|
103
104
|
try {
|
|
104
|
-
const originalWidth = image.
|
|
105
|
-
const originalHeight = image.
|
|
105
|
+
const originalWidth = image.get_width();
|
|
106
|
+
const originalHeight = image.get_height();
|
|
106
107
|
const format = img.mimeType?.split("/")[1] ?? "png";
|
|
107
108
|
|
|
108
109
|
// Check if already within all limits (dimensions AND size)
|
|
109
|
-
const originalSize =
|
|
110
|
+
const originalSize = inputBuffer.length;
|
|
110
111
|
if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {
|
|
111
112
|
return {
|
|
112
113
|
data: img.data,
|
|
@@ -138,18 +139,19 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
138
139
|
height: number,
|
|
139
140
|
jpegQuality: number,
|
|
140
141
|
): { buffer: Uint8Array; mimeType: string } {
|
|
141
|
-
const
|
|
142
|
-
const resized = image!.resize(scale);
|
|
143
|
-
|
|
144
|
-
const pngBuffer = resized.writeToBuffer(".png");
|
|
145
|
-
const jpegBuffer = resized.writeToBuffer(".jpg", { Q: jpegQuality });
|
|
142
|
+
const resized = photon.resize(image, width, height, photon.SamplingFilter.Lanczos3);
|
|
146
143
|
|
|
147
|
-
|
|
144
|
+
try {
|
|
145
|
+
const pngBuffer = resized.get_bytes();
|
|
146
|
+
const jpegBuffer = resized.get_bytes_jpeg(jpegQuality);
|
|
148
147
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
return pickSmaller(
|
|
149
|
+
{ buffer: pngBuffer, mimeType: "image/png" },
|
|
150
|
+
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
|
151
|
+
);
|
|
152
|
+
} finally {
|
|
153
|
+
resized.free();
|
|
154
|
+
}
|
|
153
155
|
}
|
|
154
156
|
|
|
155
157
|
// Try to produce an image under maxBytes
|
|
@@ -229,10 +231,10 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
229
231
|
wasResized: true,
|
|
230
232
|
};
|
|
231
233
|
} finally {
|
|
232
|
-
image.
|
|
234
|
+
image.free();
|
|
233
235
|
}
|
|
234
236
|
} catch (error) {
|
|
235
|
-
logger.error("Failed to resize image with
|
|
237
|
+
logger.error("Failed to resize image with Photon", {
|
|
236
238
|
error: error instanceof Error ? error.message : String(error),
|
|
237
239
|
});
|
|
238
240
|
return resizeImageWithImageMagick(img, opts);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { base64_to_image, PhotonImage, resize as photonResize, SamplingFilter } from "@silvia-odwyer/photon";
|
|
2
|
+
|
|
3
|
+
let _photon: typeof import("@silvia-odwyer/photon") | undefined;
|
|
4
|
+
let _initialized = false;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get the initialized Photon module.
|
|
8
|
+
* Lazily imports and initializes the WASM module on first use.
|
|
9
|
+
*/
|
|
10
|
+
export async function getPhoton(): Promise<typeof import("@silvia-odwyer/photon")> {
|
|
11
|
+
if (_photon && _initialized) return _photon;
|
|
12
|
+
|
|
13
|
+
const photon = await import("@silvia-odwyer/photon");
|
|
14
|
+
|
|
15
|
+
// Initialize the WASM module (default export is the init function)
|
|
16
|
+
if (!_initialized) {
|
|
17
|
+
await photon.default();
|
|
18
|
+
_initialized = true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_photon = photon;
|
|
22
|
+
return _photon;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type { PhotonImage, SamplingFilter, photonResize, base64_to_image };
|
package/src/utils/vips.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type realVips from "wasm-vips";
|
|
2
|
-
import { logger } from "../core/logger";
|
|
3
|
-
|
|
4
|
-
// Cached vips instance
|
|
5
|
-
let _vips: Promise<typeof realVips> | undefined;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Get the vips instance.
|
|
9
|
-
* @returns The vips instance.
|
|
10
|
-
*/
|
|
11
|
-
export function Vips(): Promise<typeof realVips> {
|
|
12
|
-
if (_vips) return _vips;
|
|
13
|
-
|
|
14
|
-
let instance: Promise<typeof realVips> | undefined;
|
|
15
|
-
try {
|
|
16
|
-
instance = import("wasm-vips").then((mod) => (mod.default ?? mod)());
|
|
17
|
-
} catch (error) {
|
|
18
|
-
logger.error("Failed to import wasm-vips", { error: error instanceof Error ? error.message : String(error) });
|
|
19
|
-
instance = Promise.reject(error);
|
|
20
|
-
}
|
|
21
|
-
_vips = instance;
|
|
22
|
-
return instance;
|
|
23
|
-
}
|