@oh-my-pi/pi-coding-agent 4.9.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 CHANGED
@@ -2,6 +2,20 @@
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
+
13
+ ## [5.0.0] - 2026-01-12
14
+
15
+ ### Added
16
+
17
+ - Implemented `xhigh` thinking level for Anthropic models with increased reasoning limits
18
+
5
19
  ## [4.9.0] - 2026-01-12
6
20
 
7
21
  ## [4.8.3] - 2026-01-12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "4.9.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": "4.9.0",
43
- "@oh-my-pi/pi-ai": "4.9.0",
44
- "@oh-my-pi/pi-git-tool": "4.9.0",
45
- "@oh-my-pi/pi-tui": "4.9.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
- const resized = await resizeImage({ type: "image", data: base64Content, mimeType });
56
- dimensionNote = formatDimensionNote(resized);
57
- attachment = {
58
- type: "image",
59
- mimeType: resized.mimeType,
60
- data: resized.data,
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,
@@ -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
- const resized = await resizeImage({ type: "image", data: base64, mimeType });
523
- const dimensionNote = formatDimensionNote(resized);
524
-
525
- let textNote = `Read image file [${resized.mimeType}]`;
526
- if (dimensionNote) {
527
- textNote += `\n${dimensionNote}`;
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).then((converted) => {
267
- if (converted) {
268
- this.convertedImages.set(index, converted);
269
- this.updateDisplay();
270
- this.ui.requestRender();
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 { Vips } from "./vips";
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 wasm-vips if available, falls back to ImageMagick (magick/convert).
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 { Image } = await Vips();
21
- const image = Image.newFromBuffer(Buffer.from(base64Data, "base64"));
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.writeToBuffer(".png");
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.delete();
29
+ image.free();
30
30
  }
31
31
  } catch (error) {
32
- // wasm-vips failed, try ImageMagick fallback
33
- logger.error("Failed to convert image to PNG with wasm-vips", {
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 { Vips } from "./vips";
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 wasm-vips is unavailable.
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 wasm-vips for image processing. Falls back to ImageMagick if unavailable.
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 buffer = Buffer.from(img.data, "base64");
98
+ const inputBuffer = Buffer.from(img.data, "base64");
99
99
 
100
100
  try {
101
- const { Image } = await Vips();
102
- const image = Image.newFromBuffer(buffer);
101
+ const photon = await getPhoton();
102
+ const image = photon.PhotonImage.new_from_byteslice(new Uint8Array(inputBuffer));
103
+
103
104
  try {
104
- const originalWidth = image.width;
105
- const originalHeight = image.height;
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 = buffer.length;
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 scale = Math.min(width / originalWidth, height / originalHeight);
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
- resized.delete();
144
+ try {
145
+ const pngBuffer = resized.get_bytes();
146
+ const jpegBuffer = resized.get_bytes_jpeg(jpegQuality);
148
147
 
149
- return pickSmaller(
150
- { buffer: pngBuffer, mimeType: "image/png" },
151
- { buffer: jpegBuffer, mimeType: "image/jpeg" },
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.delete();
234
+ image.free();
233
235
  }
234
236
  } catch (error) {
235
- logger.error("Failed to resize image with wasm-vips", {
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
- }