From af07faec11525ec7fcc1337e180c7bfdfba6d245 Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 13 May 2026 13:16:43 +0200 Subject: [PATCH] feat: Bun 1.3.14 - Raised the Bun minimum version to >=1.3.14 across package metadata, install scripts, and changelog notes. - Removed the Photon native image pipeline and added SIXEL-based `sixel` support in pi-natives. - Migrated coding-agent image handling and resizing to `Bun.Image`, including updated tests and a JPEG quality bump to 80. - Added HTTP/2 fetch bootstrap with HTTPS-only fallback and updated Bun build flags for autoload suppression/`--keep-names`. --- Cargo.lock | 33 --- Cargo.toml | 1 - bun.lock | 8 +- bunfig.toml | 1 + crates/pi-natives/Cargo.toml | 1 - crates/pi-natives/src/image.rs | 190 ------------------ crates/pi-natives/src/lib.rs | 6 +- crates/pi-natives/src/sixel.rs | 54 +++++ package.json | 4 +- packages/agent/CHANGELOG.md | 3 + packages/agent/package.json | 2 +- packages/ai/CHANGELOG.md | 6 +- packages/ai/package.json | 2 +- packages/ai/src/index.ts | 1 + packages/ai/src/utils/h2-fetch.ts | 47 +++++ packages/coding-agent/CHANGELOG.md | 6 + packages/coding-agent/package.json | 2 +- packages/coding-agent/scripts/build-binary.ts | 5 + packages/coding-agent/src/cli.ts | 8 + .../src/modes/components/assistant-message.ts | 12 +- .../src/modes/components/tool-execution.ts | 15 +- .../coding-agent/src/utils/image-convert.ts | 27 --- .../coding-agent/src/utils/image-loading.ts | 10 +- .../coding-agent/src/utils/image-resize.ts | 76 +++---- .../test/image-input-normalization.test.ts | 45 ++++- .../test/utils/image-resize.test.ts | 10 +- packages/natives/CHANGELOG.md | 5 + packages/natives/README.md | 17 +- packages/natives/native/index.d.ts | 54 ----- packages/natives/native/index.js | 14 -- packages/natives/package.json | 2 +- packages/stats/CHANGELOG.md | 3 + packages/stats/package.json | 2 +- packages/swarm-extension/package.json | 4 +- packages/tui/CHANGELOG.md | 3 + packages/tui/package.json | 2 +- .../typescript-edit-benchmark/package.json | 2 +- packages/utils/package.json | 2 +- scripts/ci-release-build-binaries.ts | 5 +- scripts/install.ps1 | 2 +- scripts/install.sh | 2 +- 41 files changed, 266 insertions(+), 428 deletions(-) delete mode 100644 crates/pi-natives/src/image.rs create mode 100644 crates/pi-natives/src/sixel.rs create mode 100644 packages/ai/src/utils/h2-fetch.ts delete mode 100644 packages/coding-agent/src/utils/image-convert.ts diff --git a/Cargo.lock b/Cargo.lock index 8b7d4f5fe..bcdd6e17a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -506,8 +506,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", - "jobserver", - "libc", "shlex", ] @@ -1672,16 +1670,6 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - [[package]] name = "js-sys" version = "0.3.98" @@ -1746,16 +1734,6 @@ dependencies = [ "libc", ] -[[package]] -name = "libwebp-sys" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cd30df7c7165ce74a456e4ca9732c603e8dc5e60784558c1c6dc047f876733" -dependencies = [ - "cc", - "glob", -] - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2492,7 +2470,6 @@ dependencies = [ "toml", "unicode-segmentation", "unicode-width", - "webp", "winreg 0.56.0", "xxhash-rust", ] @@ -4428,16 +4405,6 @@ dependencies = [ "string_cache_codegen", ] -[[package]] -name = "webp" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c071456adef4aca59bf6a583c46b90ff5eb0b4f758fc347cea81290288f37ce1" -dependencies = [ - "image", - "libwebp-sys", -] - [[package]] name = "weezl" version = "0.1.12" diff --git a/Cargo.toml b/Cargo.toml index 5eecfc304..dea14ee96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -280,7 +280,6 @@ syntect = { version = "5.3", default-features = false, features = [ "default-themes", "regex-fancy", ] } -webp = "0.3" # ────────────────────────────────────────────────────────────────────────────── # Markup Conversion diff --git a/bun.lock b/bun.lock index 29d384676..a7dfc8ae6 100644 --- a/bun.lock +++ b/bun.lock @@ -134,7 +134,7 @@ "@oh-my-pi/pi-utils": "workspace:*", }, "devDependencies": { - "@types/bun": "^1.3.13", + "@types/bun": "^1.3.14", }, "peerDependencies": { "@oh-my-pi/pi-coding-agent": "^13", @@ -226,7 +226,7 @@ "@tailwindcss/node": "^4.2.4", "@types/babel__generator": "^7.27.0", "@types/babel__traverse": "^7.28.0", - "@types/bun": "^1.3.13", + "@types/bun": "^1.3.14", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/turndown": "5.0.6", @@ -714,7 +714,7 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="], + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], "@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], @@ -804,7 +804,7 @@ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - "bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], diff --git a/bunfig.toml b/bunfig.toml index 6bfbeeffa..89db0a5ef 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -2,6 +2,7 @@ telemetry = false [install] minimumReleaseAge = 259200 # 3 days in seconds +minimumReleaseAgeExcludes = ["@types/bun"] linker = "hoisted" exact = true saveTextLockfile = true diff --git a/crates/pi-natives/Cargo.toml b/crates/pi-natives/Cargo.toml index caff04feb..d21570380 100644 --- a/crates/pi-natives/Cargo.toml +++ b/crates/pi-natives/Cargo.toml @@ -50,7 +50,6 @@ tokio-util.workspace = true toml.workspace = true unicode-segmentation.workspace = true unicode-width.workspace = true -webp.workspace = true xxhash-rust.workspace = true [target.'cfg(unix)'.dependencies] diff --git a/crates/pi-natives/src/image.rs b/crates/pi-natives/src/image.rs deleted file mode 100644 index f279b8934..000000000 --- a/crates/pi-natives/src/image.rs +++ /dev/null @@ -1,190 +0,0 @@ -//! Minimal image processing API for resizing and format conversion. -//! -//! Provides only the subset of functionality needed: -//! - Load image from bytes (PNG, JPEG, WebP, GIF) -//! - Get dimensions -//! - Resize with configurable filter -//! - Export as PNG, JPEG, WebP, or GIF - -use std::{io::Cursor, sync::Arc}; - -use icy_sixel::{EncodeOptions, sixel_encode}; -use image::{ - DynamicImage, ImageFormat as StdImageFormat, ImageReader, codecs::jpeg::JpegEncoder, - imageops::FilterType, -}; -use napi::bindgen_prelude::*; -use napi_derive::napi; - -use crate::task; - -/// Output format for [`PhotonImage::encode`]. -#[napi] -pub enum ImageFormat { - /// PNG encoded bytes. - PNG = 0, - /// JPEG encoded bytes. - JPEG = 1, - /// WebP encoded bytes. - WEBP = 2, - /// GIF encoded bytes. - GIF = 3, -} - -/// Sampling filter for resize operations. -#[napi] -pub enum SamplingFilter { - /// Nearest-neighbor sampling (fast, low quality). - Nearest = 1, - /// Triangle filter (linear interpolation). - Triangle = 2, - /// Catmull-Rom filter with sharper edges. - CatmullRom = 3, - /// Gaussian filter for smoother results. - Gaussian = 4, - /// Lanczos3 filter for high-quality downscaling. - Lanczos3 = 5, -} - -impl From for FilterType { - fn from(filter: SamplingFilter) -> Self { - match filter { - SamplingFilter::Nearest => Self::Nearest, - SamplingFilter::Triangle => Self::Triangle, - SamplingFilter::CatmullRom => Self::CatmullRom, - SamplingFilter::Gaussian => Self::Gaussian, - SamplingFilter::Lanczos3 => Self::Lanczos3, - } - } -} - -/// Image container for native interop. -#[napi] -pub struct PhotonImage { - /// Shared decoded image data. - img: Arc, -} - -type ImageTask = task::Promise; - -#[napi] -impl PhotonImage { - /// Create a new `PhotonImage` from encoded image bytes (PNG, JPEG, WebP, - /// GIF). Returns the decoded image handle on success. - /// - /// # Errors - /// Returns an error if the image format cannot be detected or decoded. - #[napi] - pub fn parse(bytes: Uint8Array) -> ImageTask { - let bytes = bytes.as_ref().to_vec(); - task::blocking("image.decode", (), move |_| -> Result { - let img = decode_image_from_bytes(&bytes)?; - Ok(Self { img: Arc::new(img) }) - }) - } - - /// Get the image width in pixels. - #[napi(getter)] - pub fn get_width(&self) -> u32 { - self.img.width() - } - - /// Get the image height in pixels. - #[napi(getter)] - pub fn get_height(&self) -> u32 { - self.img.height() - } - - /// Encode the image to bytes in the specified format. - /// - /// # Errors - /// Returns an error if encoding fails or format is invalid. - #[napi] - pub fn encode(&self, format: ImageFormat, quality: u8) -> task::Promise> { - let img = Arc::clone(&self.img); - task::blocking("image.encode", (), move |_| encode_image(&img, format, quality)) - } - - /// Resize the image to the specified pixel dimensions using the filter. - /// Returns a new `PhotonImage` containing the resized image. - #[napi] - pub fn resize(&self, width: u32, height: u32, filter: SamplingFilter) -> ImageTask { - let img = Arc::clone(&self.img); - task::blocking("image.resize", (), move |_| { - Ok(Self { img: Arc::new(img.resize_exact(width, height, filter.into())) }) - }) - } -} - -/// Encode image bytes into a SIXEL escape sequence for terminal rendering. -/// -/// The input image is decoded and resized to the requested pixel dimensions -/// before encoding. -/// -/// # Errors -/// Returns an error if decoding, resizing, or SIXEL encoding fails. -#[napi] -pub fn encode_sixel( - bytes: Uint8Array, - target_width_px: u32, - target_height_px: u32, -) -> Result { - if target_width_px == 0 || target_height_px == 0 { - return Err(Error::from_reason("Target SIXEL dimensions must be greater than zero")); - } - - let source = decode_image_from_bytes(bytes.as_ref())?; - let resized = if source.width() == target_width_px && source.height() == target_height_px { - source - } else { - source.resize_exact(target_width_px, target_height_px, FilterType::Lanczos3) - }; - let rgba = resized.to_rgba8(); - let options = EncodeOptions::default(); - sixel_encode(rgba.as_raw(), target_width_px as usize, target_height_px as usize, &options) - .map_err(|err| Error::from_reason(format!("Failed to encode SIXEL: {err}"))) -} - -fn decode_image_from_bytes(bytes: &[u8]) -> Result { - let reader = ImageReader::new(Cursor::new(bytes)) - .with_guessed_format() - .map_err(|e| Error::from_reason(format!("Failed to detect image format: {e}")))?; - - reader - .decode() - .map_err(|e| Error::from_reason(format!("Failed to decode image: {e}"))) -} -fn encode_image(img: &DynamicImage, format: ImageFormat, quality: u8) -> Result> { - let (w, h) = (img.width(), img.height()); - - match format { - ImageFormat::PNG => { - let mut buffer = Vec::with_capacity((w * h * 4) as usize); - img.write_to(&mut Cursor::new(&mut buffer), StdImageFormat::Png) - .map_err(|e| Error::from_reason(format!("Failed to encode PNG: {e}")))?; - Ok(buffer) - }, - ImageFormat::JPEG => { - let mut buffer = Vec::with_capacity((w * h * 3) as usize); - let encoder = JpegEncoder::new_with_quality(&mut buffer, quality); - img.write_with_encoder(encoder) - .map_err(|e| Error::from_reason(format!("Failed to encode JPEG: {e}")))?; - Ok(buffer) - }, - ImageFormat::WEBP => { - // Lossy WebP via libwebp (the `image` crate's bundled WebP encoder is - // lossless-only). quality is 0..=100; libwebp accepts an f32 in the same - // range. - let encoder = webp::Encoder::from_image(img) - .map_err(|e| Error::from_reason(format!("Failed to prepare WebP encoder: {e}")))?; - let memory = encoder.encode(quality as f32); - Ok(memory.to_vec()) - }, - ImageFormat::GIF => { - let mut buffer = Vec::with_capacity((w * h) as usize); - img.write_to(&mut Cursor::new(&mut buffer), StdImageFormat::Gif) - .map_err(|e| Error::from_reason(format!("Failed to encode GIF: {e}")))?; - Ok(buffer) - }, - } -} diff --git a/crates/pi-natives/src/lib.rs b/crates/pi-natives/src/lib.rs index 2e392bb19..827696a51 100644 --- a/crates/pi-natives/src/lib.rs +++ b/crates/pi-natives/src/lib.rs @@ -3,7 +3,7 @@ //! # Overview //! High-performance primitives for clipboard access, grep, file discovery, //! ANSI-aware text measurement, syntax highlighting, HTML-to-Markdown -//! conversion, and image processing. +//! conversion, and terminal SIXEL encoding. //! //! # Example //! ```ignore @@ -15,7 +15,7 @@ //! //! # Architecture //! ```text -//! JS (packages/natives) -> N-API -> Rust modules (clipboard/fd/glob/grep/html/highlight/image/text) +//! JS (packages/natives) -> N-API -> Rust modules (clipboard/fd/glob/grep/html/highlight/sixel/text) //! ``` #![allow(clippy::trailing_empty_array, reason = "generated by napi macro")] @@ -35,8 +35,8 @@ pub mod glob_util; pub mod grep; pub mod highlight; pub mod html; -pub mod image; pub mod keys; +pub mod sixel; pub use pi_ast::language; pub mod power; diff --git a/crates/pi-natives/src/sixel.rs b/crates/pi-natives/src/sixel.rs new file mode 100644 index 000000000..e8fa1e8e1 --- /dev/null +++ b/crates/pi-natives/src/sixel.rs @@ -0,0 +1,54 @@ +//! SIXEL terminal-image encoding. +//! +//! Decodes an encoded image (PNG, JPEG, WebP, GIF), resizes to the target +//! pixel dimensions, and encodes the result as a SIXEL escape sequence. +//! +//! General-purpose image processing (decode/resize/encode for files and +//! buffers) lives in `Bun.Image` on the JS side; this module exists only +//! because SIXEL is a terminal-display protocol with no equivalent there. + +use std::io::Cursor; + +use icy_sixel::{EncodeOptions, sixel_encode}; +use image::{DynamicImage, ImageReader, imageops::FilterType}; +use napi::bindgen_prelude::*; +use napi_derive::napi; + +/// Encode image bytes into a SIXEL escape sequence for terminal rendering. +/// +/// The input image is decoded and resized to the requested pixel dimensions +/// before encoding. +/// +/// # Errors +/// Returns an error if decoding, resizing, or SIXEL encoding fails. +#[napi] +pub fn encode_sixel( + bytes: Uint8Array, + target_width_px: u32, + target_height_px: u32, +) -> Result { + if target_width_px == 0 || target_height_px == 0 { + return Err(Error::from_reason("Target SIXEL dimensions must be greater than zero")); + } + + let source = decode_image_from_bytes(bytes.as_ref())?; + let resized = if source.width() == target_width_px && source.height() == target_height_px { + source + } else { + source.resize_exact(target_width_px, target_height_px, FilterType::Lanczos3) + }; + let rgba = resized.to_rgba8(); + let options = EncodeOptions::default(); + sixel_encode(rgba.as_raw(), target_width_px as usize, target_height_px as usize, &options) + .map_err(|err| Error::from_reason(format!("Failed to encode SIXEL: {err}"))) +} + +fn decode_image_from_bytes(bytes: &[u8]) -> Result { + let reader = ImageReader::new(Cursor::new(bytes)) + .with_guessed_format() + .map_err(|e| Error::from_reason(format!("Failed to detect image format: {e}")))?; + + reader + .decode() + .map_err(|e| Error::from_reason(format!("Failed to decode image: {e}"))) +} diff --git a/package.json b/package.json index f4ace26b5..bd9eb98b9 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "omp-monorepo", "private": true, "type": "module", - "packageManager": "bun@1.3.13", + "packageManager": "bun@1.3.14", "workspaces": { "packages": [ "packages/*" @@ -35,7 +35,7 @@ "@tailwindcss/node": "^4.2.4", "@types/babel__generator": "^7.27.0", "@types/babel__traverse": "^7.28.0", - "@types/bun": "^1.3.13", + "@types/bun": "^1.3.14", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/turndown": "5.0.6", diff --git a/packages/agent/CHANGELOG.md b/packages/agent/CHANGELOG.md index 530ad0265..86915400e 100644 --- a/packages/agent/CHANGELOG.md +++ b/packages/agent/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## [Unreleased] +### Breaking Changes + +- Raised the minimum required Bun version from >=1.3.7 to >=1.3.14 ## [14.9.5] - 2026-05-12 diff --git a/packages/agent/package.json b/packages/agent/package.json index b4231d600..30836419a 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -44,7 +44,7 @@ "@types/bun": "catalog:" }, "engines": { - "bun": ">=1.3.7" + "bun": ">=1.3.14" }, "files": [ "src", diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 93580c0fa..a28b204dc 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -1,9 +1,13 @@ # Changelog ## [Unreleased] +### Breaking Changes + +- Increased the minimum Bun runtime version to `>=1.3.14` for the `@aws-?` package ### Added +- Added `installH2Fetch` to patch `globalThis.fetch` so HTTPS requests attempt HTTP/2 over ALPN with automatic HTTP/1.1 fallback when HTTP/2 is unsupported - Added priority service-tier traffic to the `premiumRequests` accounting on OpenAI and OpenAI Codex providers. Sending `serviceTier: "priority"` now increments `usage.premiumRequests` by 1 per request, matching the existing GitHub Copilot premium-request budget semantics so downstream consumers (e.g. the `omp stats` "Premium Reqs" card and `/usage`) reflect priority traffic alongside Copilot premium calls. ## [15.0.0] - 2026-05-13 @@ -2348,4 +2352,4 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_ ## [0.9.4] - 2025-11-26 -Initial release with multi-provider LLM support. +Initial release with multi-provider LLM support. \ No newline at end of file diff --git a/packages/ai/package.json b/packages/ai/package.json index 54cab4ea4..33344ebc2 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -61,7 +61,7 @@ "@types/bun": "catalog:" }, "engines": { - "bun": ">=1.3.7" + "bun": ">=1.3.14" }, "files": [ "src", diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 218b79ade..fdc5f2811 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -37,6 +37,7 @@ export * from "./usage/zai"; export * from "./utils/anthropic-auth"; export * from "./utils/discovery"; export * from "./utils/event-stream"; +export * from "./utils/h2-fetch"; export * from "./utils/overflow"; export * from "./utils/retry"; export * from "./utils/schema"; diff --git a/packages/ai/src/utils/h2-fetch.ts b/packages/ai/src/utils/h2-fetch.ts new file mode 100644 index 000000000..0e5ff5d36 --- /dev/null +++ b/packages/ai/src/utils/h2-fetch.ts @@ -0,0 +1,47 @@ +/** + * Patch `globalThis.fetch` to advertise HTTP/2 in TLS ALPN, with transparent + * HTTP/1.1 fallback when the server doesn't select `h2`. + * + * Bun's HTTP/2 client is gated on `BUN_FEATURE_FLAG_EXPERIMENTAL_HTTP2_CLIENT`, + * read by the native runtime before any JS executes; assigning to + * `process.env` from inside JS is a no-op. Per-request `protocol: "http2"` + * activates h2 over TLS ALPN and rejects with `error.code === "HTTP2Unsupported"` + * if the server picks anything else, so we catch and retry without the hint. + * + * Bun negotiates h2 via ALPN over TLS only (no h2c), so plain `http://` URLs + * skip the attempt entirely — avoids the throw/retry round-trip for localhost. + * + * Idempotent. + */ + +const installed: unique symbol = Symbol.for("oh-my-pi.h2fetch.installed"); + +interface PatchedFetch { + [installed]?: true; +} + +export function installH2Fetch(): void { + const original = globalThis.fetch as typeof fetch & PatchedFetch; + if (original[installed]) return; + + const wrapper = async function h2fetch(input: string | URL | Request, init?: RequestInit): Promise { + if (!isHttps(input)) return original(input, init); + try { + return await original(input, { ...init, protocol: "http2" }); + } catch (err) { + if ((err as { code?: unknown }).code !== "HTTP2Unsupported") throw err; + return original(input, init); + } + } as typeof fetch & PatchedFetch; + + // Preserve `fetch.preconnect` and any other statics SDK code might poke at. + Object.assign(wrapper, original); + wrapper[installed] = true; + globalThis.fetch = wrapper; +} + +function isHttps(input: string | URL | Request): boolean { + if (typeof input === "string") return input.startsWith("https:"); + if (input instanceof URL) return input.protocol === "https:"; + return input.url.startsWith("https:"); +} diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 3cbc4533e..21e3ba775 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,13 +1,19 @@ # Changelog ## [Unreleased] + ### Added - Added middle elision for streaming tool outputs (bash, ssh, python, js eval) and post-execution tool result spill. When `tools.artifactHeadBytes` is set (default 20 KB), large outputs now keep both the first N KB and the last N KB with an inline `[… N lines elided (M KB) …]` marker between them, instead of dropping everything before the trailing tail. Setting `tools.artifactHeadBytes = 0` reverts to the previous tail-only behavior. The full output is still mirrored to the session artifact (`artifact://`) regardless of elision mode. Exposes `truncateMiddle` and `formatMiddleElisionMarker` from `@oh-my-pi/pi-coding-agent/session/streaming-output`, extends `OutputSinkOptions` with `headBytes`, and adds `direction: "middle"` plus `headRange` / `tailRange` / `elidedLines` / `elidedBytes` to `TruncationMeta`. - Added per-line column cap shared across streaming tool outputs (`bash`, `ssh`, `python`, `js eval`) and the `read` tool. Lines wider than `tools.outputMaxColumns` bytes (default **768**) are ellipsis-truncated at write time and remaining bytes up to the next `\n` are dropped — bounded memory even on multi-MB single-line outputs (e.g. `cat /dev/urandom`). The cap lives on `OutputSink` as the new `maxColumns` option, persists state across chunk boundaries so split-mid-line writes still respect the budget, and exposes `columnDroppedBytes` / `columnTruncatedLines` on `OutputSummary`. Middle-elision byte math subtracts column drops so the "elided from middle" count stays honest. `read` reuses the same setting but trims its already-collected lines via `truncateLine`. Skipped when the read selector is `:raw`. The artifact file (`artifact://`) keeps the full uncapped stream. Set `tools.outputMaxColumns = 0` to disable. +- Added Bun HTTP/2 fetch opt-in. Dev scripts (`bun run dev`, `bun run stats`) now pass `bun --experimental-http2-fetch` so every `fetch()` advertises `h2` in the TLS ALPN list and falls back to HTTP/1.1 when the server doesn't select it. Multiplexing collapses parallel requests to the same origin onto one TLS connection. For the installed `omp` binary, export `BUN_FEATURE_FLAG_EXPERIMENTAL_HTTP2_CLIENT=1` in your shell to enable the same behavior (the flag has to be set before Bun starts; `process.env` from inside JS is too late). Requires Bun **1.3.14**. ### Changed +- Raised the image downscaling default JPEG quality from 75 to 80 in `resizeImage` output generation +- Changed image resize metadata notes from coordinate-scale hints to a simple `Image resized from to ` message and hide the note when the resized dimensions are unchanged +- Removed `utils/image-convert.ts` and its `convertToPng` helper; callers now inline `new Bun.Image(bytes).png().toBase64()` from [`Bun.Image`](https://bun.com/docs/runtime/image) (Bun 1.3.14+). +- Changed image decode/resize/encode in `utils/image-resize.ts` from the native `PhotonImage` binding to [`Bun.Image`](https://bun.com/docs/runtime/image). Same PNG/JPEG/WebP quality+dimension ladder, but pipelines run off-thread on Bun's statically-linked codecs with no native-addon round-trip. Bumped the minimum Bun runtime requirement to **1.3.14**. - Changed `search` pagination in multi-file scopes so `skip` now skips entire files and pages results in groups of up to 20 files, with output guiding the next `skip` value via `Showing files X-Y of N` - Changed multi-file search result selection to cap each file at 20 matches and round-robin across files, so one noisy file no longer suppresses visibility of hits in other files and truncation now reports per-file limits - Changed search truncation metadata/renderer output from match/result-based limits to file-based limits (`fileLimitReached`, `perFileLimitReached`) and updated truncation labels accordingly diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index c3dea2af2..9b5438563 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -74,7 +74,7 @@ "@types/bun": "catalog:" }, "engines": { - "bun": ">=1.3.7" + "bun": ">=1.3.14" }, "files": [ "src", diff --git a/packages/coding-agent/scripts/build-binary.ts b/packages/coding-agent/scripts/build-binary.ts index 0c56afa56..522c8c282 100644 --- a/packages/coding-agent/scripts/build-binary.ts +++ b/packages/coding-agent/scripts/build-binary.ts @@ -33,6 +33,11 @@ async function main(): Promise { "bun", "build", "--compile", + "--no-compile-autoload-bunfig", + "--no-compile-autoload-dotenv", + "--no-compile-autoload-tsconfig", + "--no-compile-autoload-package-json", + "--keep-names", "--define", 'process.env.PI_COMPILED="true"', "--external", diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index e0e7e0f3f..2732d3332 100755 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -1,6 +1,14 @@ #!/usr/bin/env bun +import { installH2Fetch } from "@oh-my-pi/pi-ai"; import { APP_NAME, MIN_BUN_VERSION, procmgr, VERSION } from "@oh-my-pi/pi-utils"; +// Activate HTTP/2 for all `fetch()` calls (provider streams, OAuth, model +// discovery, web tools). Bun's HTTP/2 client is gated on a startup flag we +// can't toggle from JS, so we patch globalThis.fetch to pass +// `protocol: "http2"` per request, with transparent HTTP/1.1 fallback on +// `HTTP2Unsupported`. See @oh-my-pi/pi-ai/utils/h2-fetch for details. +installH2Fetch(); + // Strip macOS malloc-stack-logging env vars before any subprocess is spawned. // Otherwise every child bun process (subagents, plugin installs, ptree spawns, // etc.) prints a `MallocStackLogging: can't turn off …` warning to stderr. diff --git a/packages/coding-agent/src/modes/components/assistant-message.ts b/packages/coding-agent/src/modes/components/assistant-message.ts index a5cd594cb..e0c495c12 100644 --- a/packages/coding-agent/src/modes/components/assistant-message.ts +++ b/packages/coding-agent/src/modes/components/assistant-message.ts @@ -4,7 +4,6 @@ import { formatNumber } from "@oh-my-pi/pi-utils"; import { settings } from "../../config/settings"; import { getMarkdownTheme, theme } from "../../modes/theme/theme"; import { resolveImageOptions } from "../../tools/render-utils"; -import { convertToPng } from "../../utils/image-convert"; /** * Component that renders a complete assistant message @@ -76,14 +75,15 @@ export class AssistantMessageComponent extends Container { const key = `${toolCallId}:${index}`; if (this.#convertedKittyImages.has(key) || this.#kittyConversionsInFlight.has(key)) continue; this.#kittyConversionsInFlight.add(key); - convertToPng(image.data, image.mimeType) - .then(converted => { + new Bun.Image(Buffer.from(image.data, "base64")) + .png() + .toBase64() + .then(data => { this.#kittyConversionsInFlight.delete(key); - if (!converted) return; this.#convertedKittyImages.set(key, { type: "image", - data: converted.data, - mimeType: converted.mimeType, + data, + mimeType: "image/png", }); if (this.#lastMessage) { this.updateContent(this.#lastMessage); diff --git a/packages/coding-agent/src/modes/components/tool-execution.ts b/packages/coding-agent/src/modes/components/tool-execution.ts index 515e7f2d8..5d39937b2 100644 --- a/packages/coding-agent/src/modes/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/components/tool-execution.ts @@ -32,7 +32,6 @@ import { import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils"; import { toolRenderers } from "../../tools/renderers"; import { renderStatusLine } from "../../tui"; -import { convertToPng } from "../../utils/image-convert"; import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel"; import { renderDiff } from "./diff"; @@ -295,13 +294,13 @@ export class ToolExecutionComponent extends Container { // Convert async - catch errors from processing const index = i; - convertToPng(img.data, img.mimeType) - .then(converted => { - if (converted) { - this.#convertedImages.set(index, converted); - this.#updateDisplay(); - this.#ui.requestRender(); - } + new Bun.Image(Buffer.from(img.data, "base64")) + .png() + .toBase64() + .then(data => { + this.#convertedImages.set(index, { data, mimeType: "image/png" }); + this.#updateDisplay(); + this.#ui.requestRender(); }) .catch(() => { // Ignore conversion failures - display will use original image format diff --git a/packages/coding-agent/src/utils/image-convert.ts b/packages/coding-agent/src/utils/image-convert.ts deleted file mode 100644 index 2c32d2fad..000000000 --- a/packages/coding-agent/src/utils/image-convert.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ImageFormat, PhotonImage } from "@oh-my-pi/pi-natives"; - -/** - * Convert image to PNG format for terminal display. - * Kitty graphics protocol requires PNG format (f=100). - */ -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 }; - } - - try { - const image = await PhotonImage.parse(new Uint8Array(Buffer.from(base64Data, "base64"))); - const pngBuffer = await image.encode(ImageFormat.PNG, 100); - return { - data: Buffer.from(pngBuffer).toBase64(), - mimeType: "image/png", - }; - } catch { - // Conversion failed - return null; - } -} diff --git a/packages/coding-agent/src/utils/image-loading.ts b/packages/coding-agent/src/utils/image-loading.ts index 7d9b2f35e..9f172188d 100644 --- a/packages/coding-agent/src/utils/image-loading.ts +++ b/packages/coding-agent/src/utils/image-loading.ts @@ -2,7 +2,6 @@ import * as fs from "node:fs/promises"; import type { ImageContent } from "@oh-my-pi/pi-ai"; import { formatBytes, readImageMetadata, SUPPORTED_IMAGE_MIME_TYPES } from "@oh-my-pi/pi-utils"; import { resolveReadPath } from "../tools/path-utils"; -import { convertToPng } from "./image-convert"; import { formatDimensionNote, resizeImage } from "./image-resize"; export const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024; @@ -42,8 +41,13 @@ export async function ensureSupportedImageInput(image: ImageContent): Promise { diff --git a/packages/coding-agent/src/utils/image-resize.ts b/packages/coding-agent/src/utils/image-resize.ts index 777dfd6ee..67ab2ab82 100644 --- a/packages/coding-agent/src/utils/image-resize.ts +++ b/packages/coding-agent/src/utils/image-resize.ts @@ -1,11 +1,10 @@ import type { ImageContent } from "@oh-my-pi/pi-ai"; -import { ImageFormat, PhotonImage, SamplingFilter } from "@oh-my-pi/pi-natives"; export interface ImageResizeOptions { - maxWidth?: number; // Default: 1568 - maxHeight?: number; // Default: 1568 - maxBytes?: number; // Default: 500KB - jpegQuality?: number; // Default: 75 + maxWidth?: number; + maxHeight?: number; + maxBytes?: number; + jpegQuality?: number; } export interface ResizedImage { @@ -24,12 +23,12 @@ export interface ResizedImage { const DEFAULT_MAX_BYTES = 500 * 1024; const DEFAULT_OPTIONS: Required = { - // 1568px — Anthropic downscales anything larger; OpenAI tiles at 768px; - // sending bigger pixels wastes bandwidth the model never sees. + // Anthropic's "internal recommended size" — Claude internally caps images at + // 1568px on the longest edge before vision processing. maxWidth: 1568, maxHeight: 1568, maxBytes: DEFAULT_MAX_BYTES, - jpegQuality: 75, + jpegQuality: 80, }; /** Pick the smallest of N encoded buffers. */ @@ -48,43 +47,34 @@ Buffer.prototype.toBase64 = function (this: Buffer) { /** * Resize and recompress an image to fit within the specified max dimensions and file size. * - * Defaults target Anthropic's internal 1568px downscale threshold and produce small - * lossy JPEG output suitable for tool-call payloads (~100–500KB typical). - * * Strategy: - * 1. Fast path — if input already fits dimensions AND is at <=25% of byte budget, - * return as-is. Avoids re-encoding tiny icons/diagrams. - * 2. Resize to maxWidth/maxHeight, encode both PNG and JPEG at default quality, - * pick whichever is smaller. PNG wins for line art / few-color UI; JPEG wins - * for photographic content. - * 3. If still too large, JPEG-only quality ladder (PNG quality is a no-op). - * 4. If still too large, progressively reduce dimensions and retry the JPEG ladder. - * 5. Last resort: ship the smallest variant produced. + * 1. Probe metadata. If already within all limits, return original. + * 2. Resize to fit max dimensions and encode at high quality across PNG/JPEG/WebP — return smallest. + * 3. If still too large, walk a lossy JPEG/WebP quality ladder. + * 4. If still too large, walk a dimension-scale ladder × quality ladder. + * 5. If still too large, return the smallest variant produced. * - * On any decode failure, returns the original bytes unchanged with wasResized=false. + * Backed by `Bun.Image`: a chainable native pipeline that runs decode/transform/encode + * off the JS thread when the terminal (`.bytes()`) is awaited. */ export async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise { const opts = { ...DEFAULT_OPTIONS, ...options }; const inputBuffer = Buffer.from(img.data, "base64"); try { - const image = await PhotonImage.parse(inputBuffer); + const { width: originalWidth, height: originalHeight, format } = await new Bun.Image(inputBuffer).metadata(); + const sourceMime = img.mimeType ?? `image/${format}`; - const originalWidth = image.width; - const originalHeight = image.height; - const format = img.mimeType?.split("/")[1] ?? "png"; - - // Check if already within all limits (dimensions AND size) - const originalSize = inputBuffer.length; - // Fast path: skip if already within dimensions AND well under budget. + // Fast path: already within dimensions AND well under budget. // Threshold is 1/4 of budget — if already that compact, don't re-encode. // Avoids wasted work on tiny icons/diagrams while ensuring larger PNGs // still get JPEG-compressed. + const originalSize = inputBuffer.length; const comfortableSize = opts.maxBytes / 4; if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= comfortableSize) { return { buffer: inputBuffer, - mimeType: img.mimeType ?? `image/${format}`, + mimeType: sourceMime, originalWidth, originalHeight, width: originalWidth, @@ -117,14 +107,11 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption height: number, quality: number, ): Promise<{ buffer: Uint8Array; mimeType: string }> { - const resized = await image.resize(width, height, SamplingFilter.Lanczos3); - const [pngBuffer, jpegBuffer, webpBuffer] = await Promise.all([ - resized.encode(ImageFormat.PNG, quality), - resized.encode(ImageFormat.JPEG, quality), - resized.encode(ImageFormat.WEBP, quality), + new Bun.Image(inputBuffer).resize(width, height).png().bytes(), + new Bun.Image(inputBuffer).resize(width, height).jpeg({ quality }).bytes(), + new Bun.Image(inputBuffer).resize(width, height).webp({ quality }).bytes(), ]); - return pickSmallest( { buffer: pngBuffer, mimeType: "image/png" }, { buffer: jpegBuffer, mimeType: "image/jpeg" }, @@ -140,10 +127,9 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption height: number, quality: number, ): Promise<{ buffer: Uint8Array; mimeType: string }> { - const resized = await image.resize(width, height, SamplingFilter.Lanczos3); const [jpegBuffer, webpBuffer] = await Promise.all([ - resized.encode(ImageFormat.JPEG, quality), - resized.encode(ImageFormat.WEBP, quality), + new Bun.Image(inputBuffer).resize(width, height).jpeg({ quality }).bytes(), + new Bun.Image(inputBuffer).resize(width, height).webp({ quality }).bytes(), ]); return pickSmallest( { buffer: jpegBuffer, mimeType: "image/jpeg" }, @@ -159,7 +145,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption let finalWidth = targetWidth; let finalHeight = targetHeight; - // First attempt: resize to target, try both PNG and JPEG, pick smaller + // First attempt: resize to target, try PNG/JPEG/WebP, pick smallest best = await encodeSmallest(targetWidth, targetHeight, opts.jpegQuality); if (best.buffer.length <= opts.maxBytes) { @@ -264,9 +250,11 @@ export function formatDimensionNote(result: ResizedImage): string | undefined { if (!result.wasResized) { return undefined; } - - const scale = result.originalWidth / result.width; - return `[Image: original ${result.originalWidth}x${result.originalHeight}, displayed at ${result.width}x${ - result.height - }. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`; + if (!result.originalWidth || !result.originalHeight || !result.width || !result.height) { + return undefined; + } + if (result.width === result.originalWidth && result.height === result.originalHeight) { + return undefined; + } + return `Image resized from ${result.originalWidth}x${result.originalHeight} to ${result.width}x${result.height}.`; } diff --git a/packages/coding-agent/test/image-input-normalization.test.ts b/packages/coding-agent/test/image-input-normalization.test.ts index 495a3df70..8e4a0ca2a 100644 --- a/packages/coding-agent/test/image-input-normalization.test.ts +++ b/packages/coding-agent/test/image-input-normalization.test.ts @@ -1,20 +1,43 @@ -import { afterEach, describe, expect, test, vi } from "bun:test"; -import * as imageConvert from "../src/utils/image-convert"; +import { describe, expect, test } from "bun:test"; import { ensureSupportedImageInput } from "../src/utils/image-loading"; +// 1x1 red PNG (69 bytes). Bun.Image sniffs format from bytes, so we can pass +// this with a non-supported MIME type and the conversion path runs over the +// real native pipeline. +const RED_1X1_PNG_BASE64 = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC"; + describe("ensureSupportedImageInput", () => { - afterEach(() => { - vi.restoreAllMocks(); + test("passes supported mime types through unchanged", async () => { + const input = { type: "image" as const, data: RED_1X1_PNG_BASE64, mimeType: "image/png" }; + const result = await ensureSupportedImageInput(input); + expect(result).toEqual(input); }); test("converts unsupported image input to png", async () => { - const convertToPngSpy = vi - .spyOn(imageConvert, "convertToPng") - .mockResolvedValue({ data: "pngdata", mimeType: "image/png" }); + const result = await ensureSupportedImageInput({ + type: "image", + data: RED_1X1_PNG_BASE64, + mimeType: "image/bmp", + }); + expect(result).not.toBeNull(); + expect(result?.mimeType).toBe("image/png"); + // PNG re-encode of a 1x1 image must yield a valid (non-empty) PNG signature + // (`89 50 4E 47`) when decoded from base64. + const bytes = Buffer.from(result!.data, "base64"); + expect(bytes.length).toBeGreaterThan(0); + expect(bytes[0]).toBe(0x89); + expect(bytes[1]).toBe(0x50); + expect(bytes[2]).toBe(0x4e); + expect(bytes[3]).toBe(0x47); + }); - const result = await ensureSupportedImageInput({ type: "image", data: "bmpdata", mimeType: "image/bmp" }); - - expect(convertToPngSpy).toHaveBeenCalledWith("bmpdata", "image/bmp"); - expect(result).toEqual({ type: "image", data: "pngdata", mimeType: "image/png" }); + test("returns null when input bytes are not a decodable image", async () => { + const result = await ensureSupportedImageInput({ + type: "image", + data: Buffer.from("not an image").toString("base64"), + mimeType: "image/bmp", + }); + expect(result).toBeNull(); }); }); diff --git a/packages/coding-agent/test/utils/image-resize.test.ts b/packages/coding-agent/test/utils/image-resize.test.ts index 4698bc097..708a4117b 100644 --- a/packages/coding-agent/test/utils/image-resize.test.ts +++ b/packages/coding-agent/test/utils/image-resize.test.ts @@ -1,17 +1,15 @@ import { describe, expect, it } from "bun:test"; -import { ImageFormat, PhotonImage, SamplingFilter } from "@oh-my-pi/pi-natives"; import { resizeImage } from "../../src/utils/image-resize"; -// 1x1 red PNG (69 bytes) — used as a Photon seed to synthesize larger fixtures +// 1x1 red PNG (69 bytes) — used as a Bun.Image seed to synthesize larger fixtures // without checking binary blobs into the repo. const RED_1X1_PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC"; async function makeRedPng(width: number, height: number): Promise { - const seed = await PhotonImage.parse(new Uint8Array(Buffer.from(RED_1X1_PNG_BASE64, "base64"))); - const upscaled = await seed.resize(width, height, SamplingFilter.Nearest); - const bytes = await upscaled.encode(ImageFormat.PNG, 100); - return Buffer.from(bytes).toBase64(); + const seed = Buffer.from(RED_1X1_PNG_BASE64, "base64"); + const upscaled = await new Bun.Image(seed).resize(width, height, { filter: "nearest" }).png().bytes(); + return Buffer.from(upscaled).toBase64(); } describe("resizeImage defaults", () => { diff --git a/packages/natives/CHANGELOG.md b/packages/natives/CHANGELOG.md index f835d3574..4509074ab 100644 --- a/packages/natives/CHANGELOG.md +++ b/packages/natives/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog ## [Unreleased] +### Breaking Changes + +- Raised the minimum required Bun runtime version to >=1.3.14 +- Removed `PhotonImage` class, `ImageFormat` enum, and `SamplingFilter` enum from native exports. General-purpose image decode/resize/encode now uses [`Bun.Image`](https://bun.com/docs/runtime/image), which ships in Bun 1.3.14+ with statically-linked libjpeg-turbo, libspng, and libwebp plus SIMD geometry kernels — same operations, zero native-addon footprint. `encodeSixel` stays (no Bun equivalent for the SIXEL terminal protocol). +- Removed `webp` Rust workspace dependency along with `PhotonImage`'s WebP encoder. ## [14.9.9] - 2026-05-12 ### Breaking Changes diff --git a/packages/natives/README.md b/packages/natives/README.md index f76dd1be6..0a88f6788 100644 --- a/packages/natives/README.md +++ b/packages/natives/README.md @@ -6,12 +6,17 @@ Native Rust functionality via N-API. - **Grep**: Regex-based search powered by ripgrep's engine with native file walking and matching - **Find**: Glob-based file/directory discovery with gitignore support (pure TypeScript via `globPaths`) -- **Image**: Image processing via photon-rs (resize, format conversion) exposed through N-API +- **SIXEL**: Terminal image encoding for SIXEL-capable terminals (decode, resize, encode in one pass) + +General-purpose image processing (decode/resize/encode for files and buffers) +lives in [`Bun.Image`](https://bun.com/docs/runtime/image) on the JS side; this +crate only ships the SIXEL encoder because no built-in equivalent exists for +that terminal protocol. ## Usage ```typescript -import { grep, find, PhotonImage, SamplingFilter, ImageFormat } from "@oh-my-pi/pi-natives"; +import { grep, find, encodeSixel } from "@oh-my-pi/pi-natives"; // Grep for a pattern const results = await grep({ @@ -28,10 +33,8 @@ const files = await find({ fileType: "file", }); -// Image processing -const image = await PhotonImage.parse(bytes); -const resized = await image.resize(800, 600, SamplingFilter.Lanczos3); -const pngBytes = await resized.encode(ImageFormat.PNG, 100); +// SIXEL encode for a terminal cell box (px) +const sequence = encodeSixel(pngBytes, widthPx, heightPx); ``` ## Building @@ -49,7 +52,7 @@ bun run check ``` crates/pi-natives/ # Rust source (workspace member) src/lib.rs # N-API exports - src/image.rs # Image processing (photon-rs) + src/sixel.rs # SIXEL terminal-image encoding Cargo.toml # Rust dependencies native/ # Native addon binaries pi_natives.--modern.node # x64 modern ISA (AVX2) diff --git a/packages/natives/native/index.d.ts b/packages/natives/native/index.d.ts index adb9c6088..fc5f13dca 100644 --- a/packages/natives/native/index.d.ts +++ b/packages/natives/native/index.d.ts @@ -39,34 +39,6 @@ export declare class MacOSPowerAssertion { stop(): void } -/** Image container for native interop. */ -export declare class PhotonImage { - /** - * Create a new `PhotonImage` from encoded image bytes (PNG, JPEG, WebP, - * GIF). Returns the decoded image handle on success. - * - * # Errors - * Returns an error if the image format cannot be detected or decoded. - */ - static parse(bytes: Uint8Array): ImageTask - /** Get the image width in pixels. */ - get width(): number - /** Get the image height in pixels. */ - get height(): number - /** - * Encode the image to bytes in the specified format. - * - * # Errors - * Returns an error if encoding fails or format is invalid. - */ - encode(format: ImageFormat, quality: number): Promise> - /** - * Resize the image to the specified pixel dimensions using the filter. - * Returns a new `PhotonImage` containing the resized image. - */ - resize(width: number, height: number, filter: SamplingFilter): ImageTask -} - /** Stable process reference. */ export declare class Process { /** Open a stable process reference from a PID. */ @@ -742,18 +714,6 @@ export interface HtmlToMarkdownOptions { skipImages?: boolean } -/** Output format for [`PhotonImage::encode`]. */ -export declare enum ImageFormat { - /** PNG encoded bytes. */ - PNG = 0, - /** JPEG encoded bytes. */ - JPEG = 1, - /** WebP encoded bytes. */ - WEBP = 2, - /** GIF encoded bytes. */ - GIF = 3 -} - /** * Invalidate the filesystem scan cache. * @@ -1149,20 +1109,6 @@ export interface PtyStartOptions { */ export declare function readImageFromClipboard(): Promise -/** Sampling filter for resize operations. */ -export declare enum SamplingFilter { - /** Nearest-neighbor sampling (fast, low quality). */ - Nearest = 1, - /** Triangle filter (linear interpolation). */ - Triangle = 2, - /** Catmull-Rom filter with sharper edges. */ - CatmullRom = 3, - /** Gaussian filter for smoother results. */ - Gaussian = 4, - /** Lanczos3 filter for high-quality downscaling. */ - Lanczos3 = 5 -} - /** * Strip ANSI escape sequences, remove control characters / lone surrogates, * and normalize line endings. diff --git a/packages/natives/native/index.js b/packages/natives/native/index.js index 0c9e5c732..f3e96999b 100644 --- a/packages/natives/native/index.js +++ b/packages/natives/native/index.js @@ -208,7 +208,6 @@ const nativeBindings = loadNative(); // classes export const MacAppearanceObserver = nativeBindings.MacAppearanceObserver; export const MacOSPowerAssertion = nativeBindings.MacOSPowerAssertion; -export const PhotonImage = nativeBindings.PhotonImage; export const Process = nativeBindings.Process; export const PtySession = nativeBindings.PtySession; export const Shell = nativeBindings.Shell; @@ -282,12 +281,6 @@ export const GrepOutputMode = { Count: "count", FilesWithMatches: "filesWithMatches", }; -export const ImageFormat = { - PNG: 0, - JPEG: 1, - WEBP: 2, - GIF: 3, -}; export const IsoBackendKind = { Apfs: 0, Btrfs: 1, @@ -316,11 +309,4 @@ export const ProcessStatus = { Running: "running", Exited: "exited", }; -export const SamplingFilter = { - Nearest: 1, - Triangle: 2, - CatmullRom: 3, - Gaussian: 4, - Lanczos3: 5, -}; // --- end generated native exports --- \ No newline at end of file diff --git a/packages/natives/package.json b/packages/natives/package.json index f14f5b2b8..164ba46d9 100644 --- a/packages/natives/package.json +++ b/packages/natives/package.json @@ -44,7 +44,7 @@ "@types/bun": "catalog:" }, "engines": { - "bun": ">=1.3.7" + "bun": ">=1.3.14" }, "napi": { "binaryName": "pi_natives", diff --git a/packages/stats/CHANGELOG.md b/packages/stats/CHANGELOG.md index ba1352f18..045cd98fe 100644 --- a/packages/stats/CHANGELOG.md +++ b/packages/stats/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## [Unreleased] +### Breaking Changes + +- Raised the minimum required Bun version to >=1.3.14 in package metadata ### Changed diff --git a/packages/stats/package.json b/packages/stats/package.json index 93a1a2743..1dc9e51fb 100644 --- a/packages/stats/package.json +++ b/packages/stats/package.json @@ -55,7 +55,7 @@ "postcss": "catalog:" }, "engines": { - "bun": ">=1.3.7" + "bun": ">=1.3.14" }, "files": [ "src", diff --git a/packages/swarm-extension/package.json b/packages/swarm-extension/package.json index 24f357d28..e57f01568 100644 --- a/packages/swarm-extension/package.json +++ b/packages/swarm-extension/package.json @@ -34,13 +34,13 @@ "@oh-my-pi/pi-utils": "workspace:*" }, "devDependencies": { - "@types/bun": "^1.3.13" + "@types/bun": "^1.3.14" }, "peerDependencies": { "@oh-my-pi/pi-coding-agent": "^13" }, "engines": { - "bun": ">=1.3.7" + "bun": ">=1.3.14" }, "omp": { "extensions": [ diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 67aa1fdaf..6ff94746f 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog ## [Unreleased] +### Breaking Changes + +- Increased the minimum required Bun version for the TUI package from >=1.3.7 to >=1.3.14 ## [14.9.8] - 2026-05-12 diff --git a/packages/tui/package.json b/packages/tui/package.json index d53af0871..770e8f4ce 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -47,7 +47,7 @@ "@xterm/headless": "catalog:" }, "engines": { - "bun": ">=1.3.7" + "bun": ">=1.3.14" }, "files": [ "src", diff --git a/packages/typescript-edit-benchmark/package.json b/packages/typescript-edit-benchmark/package.json index 82f7bbc7a..2b3fcac5b 100644 --- a/packages/typescript-edit-benchmark/package.json +++ b/packages/typescript-edit-benchmark/package.json @@ -45,6 +45,6 @@ "@types/bun": "catalog:" }, "engines": { - "bun": ">=1.3.7" + "bun": ">=1.3.14" } } diff --git a/packages/utils/package.json b/packages/utils/package.json index 109f54f4f..75bee7f4b 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -41,7 +41,7 @@ "@oh-my-pi/pi-natives": "catalog:" }, "engines": { - "bun": ">=1.3.7" + "bun": ">=1.3.14" }, "files": [ "src" diff --git a/scripts/ci-release-build-binaries.ts b/scripts/ci-release-build-binaries.ts index 77853af2e..177b7a4dc 100644 --- a/scripts/ci-release-build-binaries.ts +++ b/scripts/ci-release-build-binaries.ts @@ -106,7 +106,7 @@ async function buildBinary(target: BinaryTarget): Promise { console.log(`Building ${target.outfile}...`); await embedNative(target); if (isDryRun) { - console.log(`DRY RUN bun build --compile --no-compile-autoload-bunfig --no-compile-autoload-dotenv --define process.env.PI_COMPILED="true" --root . --external mupdf --target=${target.target} ${entrypoint} --outfile ${target.outfile}`); + console.log(`DRY RUN bun build --compile --no-compile-autoload-bunfig --no-compile-autoload-dotenv --no-compile-autoload-tsconfig --no-compile-autoload-package-json --keep-names --define process.env.PI_COMPILED="true" --root . --external mupdf --target=${target.target} ${entrypoint} --outfile ${target.outfile}`); return; } @@ -120,6 +120,9 @@ async function buildBinary(target: BinaryTarget): Promise { "--compile", "--no-compile-autoload-bunfig", "--no-compile-autoload-dotenv", + "--no-compile-autoload-tsconfig", + "--no-compile-autoload-package-json", + "--keep-names", "--define", 'process.env.PI_COMPILED="true"', "--root", diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 95a9f1408..70c2288cc 100755 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -20,7 +20,7 @@ $Repo = "can1357/oh-my-pi" $Package = "@oh-my-pi/pi-coding-agent" $InstallDir = if ($env:PI_INSTALL_DIR) { $env:PI_INSTALL_DIR } else { "$env:LOCALAPPDATA\omp" } $BinaryName = "omp-windows-x64.exe" -$MinimumBunVersion = "1.3.7" +$MinimumBunVersion = "1.3.14" function Test-BunInstalled { try { diff --git a/scripts/install.sh b/scripts/install.sh index e4db5af27..d65859483 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -13,7 +13,7 @@ set -e REPO="can1357/oh-my-pi" PACKAGE="@oh-my-pi/pi-coding-agent" INSTALL_DIR="${PI_INSTALL_DIR:-$HOME/.local/bin}" -MIN_BUN_VERSION="1.3.7" +MIN_BUN_VERSION="1.3.14" # Parse arguments MODE=""