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`.
This commit is contained in:
Generated
-33
@@ -506,8 +506,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
|
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"jobserver",
|
|
||||||
"libc",
|
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1672,16 +1670,6 @@ version = "1.0.18"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
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]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.98"
|
version = "0.3.98"
|
||||||
@@ -1746,16 +1734,6 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -2492,7 +2470,6 @@ dependencies = [
|
|||||||
"toml",
|
"toml",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
"webp",
|
|
||||||
"winreg 0.56.0",
|
"winreg 0.56.0",
|
||||||
"xxhash-rust",
|
"xxhash-rust",
|
||||||
]
|
]
|
||||||
@@ -4428,16 +4405,6 @@ dependencies = [
|
|||||||
"string_cache_codegen",
|
"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]]
|
[[package]]
|
||||||
name = "weezl"
|
name = "weezl"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
|
|||||||
@@ -280,7 +280,6 @@ syntect = { version = "5.3", default-features = false, features = [
|
|||||||
"default-themes",
|
"default-themes",
|
||||||
"regex-fancy",
|
"regex-fancy",
|
||||||
] }
|
] }
|
||||||
webp = "0.3"
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# Markup Conversion
|
# Markup Conversion
|
||||||
|
|||||||
@@ -134,7 +134,7 @@
|
|||||||
"@oh-my-pi/pi-utils": "workspace:*",
|
"@oh-my-pi/pi-utils": "workspace:*",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.13",
|
"@types/bun": "^1.3.14",
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@oh-my-pi/pi-coding-agent": "^13",
|
"@oh-my-pi/pi-coding-agent": "^13",
|
||||||
@@ -226,7 +226,7 @@
|
|||||||
"@tailwindcss/node": "^4.2.4",
|
"@tailwindcss/node": "^4.2.4",
|
||||||
"@types/babel__generator": "^7.27.0",
|
"@types/babel__generator": "^7.27.0",
|
||||||
"@types/babel__traverse": "^7.28.0",
|
"@types/babel__traverse": "^7.28.0",
|
||||||
"@types/bun": "^1.3.13",
|
"@types/bun": "^1.3.14",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/turndown": "5.0.6",
|
"@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/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=="],
|
"@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=="],
|
"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=="],
|
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ telemetry = false
|
|||||||
|
|
||||||
[install]
|
[install]
|
||||||
minimumReleaseAge = 259200 # 3 days in seconds
|
minimumReleaseAge = 259200 # 3 days in seconds
|
||||||
|
minimumReleaseAgeExcludes = ["@types/bun"]
|
||||||
linker = "hoisted"
|
linker = "hoisted"
|
||||||
exact = true
|
exact = true
|
||||||
saveTextLockfile = true
|
saveTextLockfile = true
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ tokio-util.workspace = true
|
|||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
unicode-segmentation.workspace = true
|
unicode-segmentation.workspace = true
|
||||||
unicode-width.workspace = true
|
unicode-width.workspace = true
|
||||||
webp.workspace = true
|
|
||||||
xxhash-rust.workspace = true
|
xxhash-rust.workspace = true
|
||||||
|
|
||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
|||||||
@@ -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<SamplingFilter> 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<DynamicImage>,
|
|
||||||
}
|
|
||||||
|
|
||||||
type ImageTask = task::Promise<PhotonImage>;
|
|
||||||
|
|
||||||
#[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<Self> {
|
|
||||||
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<Vec<u8>> {
|
|
||||||
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<String> {
|
|
||||||
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<DynamicImage> {
|
|
||||||
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<Vec<u8>> {
|
|
||||||
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)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
//! # Overview
|
//! # Overview
|
||||||
//! High-performance primitives for clipboard access, grep, file discovery,
|
//! High-performance primitives for clipboard access, grep, file discovery,
|
||||||
//! ANSI-aware text measurement, syntax highlighting, HTML-to-Markdown
|
//! ANSI-aware text measurement, syntax highlighting, HTML-to-Markdown
|
||||||
//! conversion, and image processing.
|
//! conversion, and terminal SIXEL encoding.
|
||||||
//!
|
//!
|
||||||
//! # Example
|
//! # Example
|
||||||
//! ```ignore
|
//! ```ignore
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
//!
|
//!
|
||||||
//! # Architecture
|
//! # Architecture
|
||||||
//! ```text
|
//! ```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")]
|
#![allow(clippy::trailing_empty_array, reason = "generated by napi macro")]
|
||||||
@@ -35,8 +35,8 @@ pub mod glob_util;
|
|||||||
pub mod grep;
|
pub mod grep;
|
||||||
pub mod highlight;
|
pub mod highlight;
|
||||||
pub mod html;
|
pub mod html;
|
||||||
pub mod image;
|
|
||||||
pub mod keys;
|
pub mod keys;
|
||||||
|
pub mod sixel;
|
||||||
pub use pi_ast::language;
|
pub use pi_ast::language;
|
||||||
|
|
||||||
pub mod power;
|
pub mod power;
|
||||||
|
|||||||
@@ -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<String> {
|
||||||
|
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<DynamicImage> {
|
||||||
|
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}")))
|
||||||
|
}
|
||||||
+2
-2
@@ -2,7 +2,7 @@
|
|||||||
"name": "omp-monorepo",
|
"name": "omp-monorepo",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.3.13",
|
"packageManager": "bun@1.3.14",
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"@tailwindcss/node": "^4.2.4",
|
"@tailwindcss/node": "^4.2.4",
|
||||||
"@types/babel__generator": "^7.27.0",
|
"@types/babel__generator": "^7.27.0",
|
||||||
"@types/babel__traverse": "^7.28.0",
|
"@types/babel__traverse": "^7.28.0",
|
||||||
"@types/bun": "^1.3.13",
|
"@types/bun": "^1.3.14",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@types/turndown": "5.0.6",
|
"@types/turndown": "5.0.6",
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- Raised the minimum required Bun version from >=1.3.7 to >=1.3.14
|
||||||
|
|
||||||
## [14.9.5] - 2026-05-12
|
## [14.9.5] - 2026-05-12
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
"@types/bun": "catalog:"
|
"@types/bun": "catalog:"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.3.7"
|
"bun": ">=1.3.14"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src",
|
"src",
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- Increased the minimum Bun runtime version to `>=1.3.14` for the `@aws-?` package
|
||||||
|
|
||||||
### Added
|
### 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.
|
- 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
|
## [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
|
## [0.9.4] - 2025-11-26
|
||||||
|
|
||||||
Initial release with multi-provider LLM support.
|
Initial release with multi-provider LLM support.
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
"@types/bun": "catalog:"
|
"@types/bun": "catalog:"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.3.7"
|
"bun": ">=1.3.14"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src",
|
"src",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export * from "./usage/zai";
|
|||||||
export * from "./utils/anthropic-auth";
|
export * from "./utils/anthropic-auth";
|
||||||
export * from "./utils/discovery";
|
export * from "./utils/discovery";
|
||||||
export * from "./utils/event-stream";
|
export * from "./utils/event-stream";
|
||||||
|
export * from "./utils/h2-fetch";
|
||||||
export * from "./utils/overflow";
|
export * from "./utils/overflow";
|
||||||
export * from "./utils/retry";
|
export * from "./utils/retry";
|
||||||
export * from "./utils/schema";
|
export * from "./utils/schema";
|
||||||
|
|||||||
@@ -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<Response> {
|
||||||
|
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:");
|
||||||
|
}
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### 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://<id>`) 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 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://<id>`) 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://<id>`) keeps the full uncapped stream. Set `tools.outputMaxColumns = 0` to disable.
|
- 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://<id>`) 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
|
### 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 <original> to <displayed>` 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 `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 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
|
- Changed search truncation metadata/renderer output from match/result-based limits to file-based limits (`fileLimitReached`, `perFileLimitReached`) and updated truncation labels accordingly
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
"@types/bun": "catalog:"
|
"@types/bun": "catalog:"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.3.7"
|
"bun": ">=1.3.14"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src",
|
"src",
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ async function main(): Promise<void> {
|
|||||||
"bun",
|
"bun",
|
||||||
"build",
|
"build",
|
||||||
"--compile",
|
"--compile",
|
||||||
|
"--no-compile-autoload-bunfig",
|
||||||
|
"--no-compile-autoload-dotenv",
|
||||||
|
"--no-compile-autoload-tsconfig",
|
||||||
|
"--no-compile-autoload-package-json",
|
||||||
|
"--keep-names",
|
||||||
"--define",
|
"--define",
|
||||||
'process.env.PI_COMPILED="true"',
|
'process.env.PI_COMPILED="true"',
|
||||||
"--external",
|
"--external",
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
#!/usr/bin/env bun
|
#!/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";
|
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.
|
// Strip macOS malloc-stack-logging env vars before any subprocess is spawned.
|
||||||
// Otherwise every child bun process (subagents, plugin installs, ptree spawns,
|
// Otherwise every child bun process (subagents, plugin installs, ptree spawns,
|
||||||
// etc.) prints a `MallocStackLogging: can't turn off …` warning to stderr.
|
// etc.) prints a `MallocStackLogging: can't turn off …` warning to stderr.
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { formatNumber } from "@oh-my-pi/pi-utils";
|
|||||||
import { settings } from "../../config/settings";
|
import { settings } from "../../config/settings";
|
||||||
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
||||||
import { resolveImageOptions } from "../../tools/render-utils";
|
import { resolveImageOptions } from "../../tools/render-utils";
|
||||||
import { convertToPng } from "../../utils/image-convert";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component that renders a complete assistant message
|
* Component that renders a complete assistant message
|
||||||
@@ -76,14 +75,15 @@ export class AssistantMessageComponent extends Container {
|
|||||||
const key = `${toolCallId}:${index}`;
|
const key = `${toolCallId}:${index}`;
|
||||||
if (this.#convertedKittyImages.has(key) || this.#kittyConversionsInFlight.has(key)) continue;
|
if (this.#convertedKittyImages.has(key) || this.#kittyConversionsInFlight.has(key)) continue;
|
||||||
this.#kittyConversionsInFlight.add(key);
|
this.#kittyConversionsInFlight.add(key);
|
||||||
convertToPng(image.data, image.mimeType)
|
new Bun.Image(Buffer.from(image.data, "base64"))
|
||||||
.then(converted => {
|
.png()
|
||||||
|
.toBase64()
|
||||||
|
.then(data => {
|
||||||
this.#kittyConversionsInFlight.delete(key);
|
this.#kittyConversionsInFlight.delete(key);
|
||||||
if (!converted) return;
|
|
||||||
this.#convertedKittyImages.set(key, {
|
this.#convertedKittyImages.set(key, {
|
||||||
type: "image",
|
type: "image",
|
||||||
data: converted.data,
|
data,
|
||||||
mimeType: converted.mimeType,
|
mimeType: "image/png",
|
||||||
});
|
});
|
||||||
if (this.#lastMessage) {
|
if (this.#lastMessage) {
|
||||||
this.updateContent(this.#lastMessage);
|
this.updateContent(this.#lastMessage);
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ import {
|
|||||||
import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
|
import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
|
||||||
import { toolRenderers } from "../../tools/renderers";
|
import { toolRenderers } from "../../tools/renderers";
|
||||||
import { renderStatusLine } from "../../tui";
|
import { renderStatusLine } from "../../tui";
|
||||||
import { convertToPng } from "../../utils/image-convert";
|
|
||||||
import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
||||||
import { renderDiff } from "./diff";
|
import { renderDiff } from "./diff";
|
||||||
|
|
||||||
@@ -295,13 +294,13 @@ export class ToolExecutionComponent extends Container {
|
|||||||
|
|
||||||
// Convert async - catch errors from processing
|
// Convert async - catch errors from processing
|
||||||
const index = i;
|
const index = i;
|
||||||
convertToPng(img.data, img.mimeType)
|
new Bun.Image(Buffer.from(img.data, "base64"))
|
||||||
.then(converted => {
|
.png()
|
||||||
if (converted) {
|
.toBase64()
|
||||||
this.#convertedImages.set(index, converted);
|
.then(data => {
|
||||||
this.#updateDisplay();
|
this.#convertedImages.set(index, { data, mimeType: "image/png" });
|
||||||
this.#ui.requestRender();
|
this.#updateDisplay();
|
||||||
}
|
this.#ui.requestRender();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Ignore conversion failures - display will use original image format
|
// Ignore conversion failures - display will use original image format
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ import * as fs from "node:fs/promises";
|
|||||||
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
||||||
import { formatBytes, readImageMetadata, SUPPORTED_IMAGE_MIME_TYPES } from "@oh-my-pi/pi-utils";
|
import { formatBytes, readImageMetadata, SUPPORTED_IMAGE_MIME_TYPES } from "@oh-my-pi/pi-utils";
|
||||||
import { resolveReadPath } from "../tools/path-utils";
|
import { resolveReadPath } from "../tools/path-utils";
|
||||||
import { convertToPng } from "./image-convert";
|
|
||||||
import { formatDimensionNote, resizeImage } from "./image-resize";
|
import { formatDimensionNote, resizeImage } from "./image-resize";
|
||||||
|
|
||||||
export const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
|
export const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
|
||||||
@@ -42,8 +41,13 @@ export async function ensureSupportedImageInput(image: ImageContent): Promise<Im
|
|||||||
if (SUPPORTED_INPUT_IMAGE_MIME_TYPES.has(image.mimeType)) {
|
if (SUPPORTED_INPUT_IMAGE_MIME_TYPES.has(image.mimeType)) {
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
const converted = await convertToPng(image.data, image.mimeType);
|
try {
|
||||||
return converted ? { type: "image", data: converted.data, mimeType: converted.mimeType } : null;
|
const bytes = Buffer.from(image.data, "base64");
|
||||||
|
const data = await new Bun.Image(bytes).png().toBase64();
|
||||||
|
return { type: "image", data, mimeType: "image/png" };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadImageInput(options: LoadImageInputOptions): Promise<LoadedImageInput | null> {
|
export async function loadImageInput(options: LoadImageInputOptions): Promise<LoadedImageInput | null> {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
||||||
import { ImageFormat, PhotonImage, SamplingFilter } from "@oh-my-pi/pi-natives";
|
|
||||||
|
|
||||||
export interface ImageResizeOptions {
|
export interface ImageResizeOptions {
|
||||||
maxWidth?: number; // Default: 1568
|
maxWidth?: number;
|
||||||
maxHeight?: number; // Default: 1568
|
maxHeight?: number;
|
||||||
maxBytes?: number; // Default: 500KB
|
maxBytes?: number;
|
||||||
jpegQuality?: number; // Default: 75
|
jpegQuality?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResizedImage {
|
export interface ResizedImage {
|
||||||
@@ -24,12 +23,12 @@ export interface ResizedImage {
|
|||||||
const DEFAULT_MAX_BYTES = 500 * 1024;
|
const DEFAULT_MAX_BYTES = 500 * 1024;
|
||||||
|
|
||||||
const DEFAULT_OPTIONS: Required<ImageResizeOptions> = {
|
const DEFAULT_OPTIONS: Required<ImageResizeOptions> = {
|
||||||
// 1568px — Anthropic downscales anything larger; OpenAI tiles at 768px;
|
// Anthropic's "internal recommended size" — Claude internally caps images at
|
||||||
// sending bigger pixels wastes bandwidth the model never sees.
|
// 1568px on the longest edge before vision processing.
|
||||||
maxWidth: 1568,
|
maxWidth: 1568,
|
||||||
maxHeight: 1568,
|
maxHeight: 1568,
|
||||||
maxBytes: DEFAULT_MAX_BYTES,
|
maxBytes: DEFAULT_MAX_BYTES,
|
||||||
jpegQuality: 75,
|
jpegQuality: 80,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Pick the smallest of N encoded buffers. */
|
/** 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.
|
* 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:
|
* Strategy:
|
||||||
* 1. Fast path — if input already fits dimensions AND is at <=25% of byte budget,
|
* 1. Probe metadata. If already within all limits, return original.
|
||||||
* return as-is. Avoids re-encoding tiny icons/diagrams.
|
* 2. Resize to fit max dimensions and encode at high quality across PNG/JPEG/WebP — return smallest.
|
||||||
* 2. Resize to maxWidth/maxHeight, encode both PNG and JPEG at default quality,
|
* 3. If still too large, walk a lossy JPEG/WebP quality ladder.
|
||||||
* pick whichever is smaller. PNG wins for line art / few-color UI; JPEG wins
|
* 4. If still too large, walk a dimension-scale ladder × quality ladder.
|
||||||
* for photographic content.
|
* 5. If still too large, return the smallest variant produced.
|
||||||
* 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.
|
|
||||||
*
|
*
|
||||||
* 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<ResizedImage> {
|
export async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage> {
|
||||||
const opts = { ...DEFAULT_OPTIONS, ...options };
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||||
const inputBuffer = Buffer.from(img.data, "base64");
|
const inputBuffer = Buffer.from(img.data, "base64");
|
||||||
|
|
||||||
try {
|
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;
|
// Fast path: already within dimensions AND well under budget.
|
||||||
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.
|
|
||||||
// Threshold is 1/4 of budget — if already that compact, don't re-encode.
|
// 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
|
// Avoids wasted work on tiny icons/diagrams while ensuring larger PNGs
|
||||||
// still get JPEG-compressed.
|
// still get JPEG-compressed.
|
||||||
|
const originalSize = inputBuffer.length;
|
||||||
const comfortableSize = opts.maxBytes / 4;
|
const comfortableSize = opts.maxBytes / 4;
|
||||||
if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= comfortableSize) {
|
if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= comfortableSize) {
|
||||||
return {
|
return {
|
||||||
buffer: inputBuffer,
|
buffer: inputBuffer,
|
||||||
mimeType: img.mimeType ?? `image/${format}`,
|
mimeType: sourceMime,
|
||||||
originalWidth,
|
originalWidth,
|
||||||
originalHeight,
|
originalHeight,
|
||||||
width: originalWidth,
|
width: originalWidth,
|
||||||
@@ -117,14 +107,11 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|||||||
height: number,
|
height: number,
|
||||||
quality: number,
|
quality: number,
|
||||||
): Promise<{ buffer: Uint8Array; mimeType: string }> {
|
): Promise<{ buffer: Uint8Array; mimeType: string }> {
|
||||||
const resized = await image.resize(width, height, SamplingFilter.Lanczos3);
|
|
||||||
|
|
||||||
const [pngBuffer, jpegBuffer, webpBuffer] = await Promise.all([
|
const [pngBuffer, jpegBuffer, webpBuffer] = await Promise.all([
|
||||||
resized.encode(ImageFormat.PNG, quality),
|
new Bun.Image(inputBuffer).resize(width, height).png().bytes(),
|
||||||
resized.encode(ImageFormat.JPEG, quality),
|
new Bun.Image(inputBuffer).resize(width, height).jpeg({ quality }).bytes(),
|
||||||
resized.encode(ImageFormat.WEBP, quality),
|
new Bun.Image(inputBuffer).resize(width, height).webp({ quality }).bytes(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return pickSmallest(
|
return pickSmallest(
|
||||||
{ buffer: pngBuffer, mimeType: "image/png" },
|
{ buffer: pngBuffer, mimeType: "image/png" },
|
||||||
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
||||||
@@ -140,10 +127,9 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|||||||
height: number,
|
height: number,
|
||||||
quality: number,
|
quality: number,
|
||||||
): Promise<{ buffer: Uint8Array; mimeType: string }> {
|
): Promise<{ buffer: Uint8Array; mimeType: string }> {
|
||||||
const resized = await image.resize(width, height, SamplingFilter.Lanczos3);
|
|
||||||
const [jpegBuffer, webpBuffer] = await Promise.all([
|
const [jpegBuffer, webpBuffer] = await Promise.all([
|
||||||
resized.encode(ImageFormat.JPEG, quality),
|
new Bun.Image(inputBuffer).resize(width, height).jpeg({ quality }).bytes(),
|
||||||
resized.encode(ImageFormat.WEBP, quality),
|
new Bun.Image(inputBuffer).resize(width, height).webp({ quality }).bytes(),
|
||||||
]);
|
]);
|
||||||
return pickSmallest(
|
return pickSmallest(
|
||||||
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
||||||
@@ -159,7 +145,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|||||||
let finalWidth = targetWidth;
|
let finalWidth = targetWidth;
|
||||||
let finalHeight = targetHeight;
|
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);
|
best = await encodeSmallest(targetWidth, targetHeight, opts.jpegQuality);
|
||||||
|
|
||||||
if (best.buffer.length <= opts.maxBytes) {
|
if (best.buffer.length <= opts.maxBytes) {
|
||||||
@@ -264,9 +250,11 @@ export function formatDimensionNote(result: ResizedImage): string | undefined {
|
|||||||
if (!result.wasResized) {
|
if (!result.wasResized) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
if (!result.originalWidth || !result.originalHeight || !result.width || !result.height) {
|
||||||
const scale = result.originalWidth / result.width;
|
return undefined;
|
||||||
return `[Image: original ${result.originalWidth}x${result.originalHeight}, displayed at ${result.width}x${
|
}
|
||||||
result.height
|
if (result.width === result.originalWidth && result.height === result.originalHeight) {
|
||||||
}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`;
|
return undefined;
|
||||||
|
}
|
||||||
|
return `Image resized from ${result.originalWidth}x${result.originalHeight} to ${result.width}x${result.height}.`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,43 @@
|
|||||||
import { afterEach, describe, expect, test, vi } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import * as imageConvert from "../src/utils/image-convert";
|
|
||||||
import { ensureSupportedImageInput } from "../src/utils/image-loading";
|
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", () => {
|
describe("ensureSupportedImageInput", () => {
|
||||||
afterEach(() => {
|
test("passes supported mime types through unchanged", async () => {
|
||||||
vi.restoreAllMocks();
|
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 () => {
|
test("converts unsupported image input to png", async () => {
|
||||||
const convertToPngSpy = vi
|
const result = await ensureSupportedImageInput({
|
||||||
.spyOn(imageConvert, "convertToPng")
|
type: "image",
|
||||||
.mockResolvedValue({ data: "pngdata", mimeType: "image/png" });
|
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" });
|
test("returns null when input bytes are not a decodable image", async () => {
|
||||||
|
const result = await ensureSupportedImageInput({
|
||||||
expect(convertToPngSpy).toHaveBeenCalledWith("bmpdata", "image/bmp");
|
type: "image",
|
||||||
expect(result).toEqual({ type: "image", data: "pngdata", mimeType: "image/png" });
|
data: Buffer.from("not an image").toString("base64"),
|
||||||
|
mimeType: "image/bmp",
|
||||||
|
});
|
||||||
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
import { describe, expect, it } from "bun:test";
|
||||||
import { ImageFormat, PhotonImage, SamplingFilter } from "@oh-my-pi/pi-natives";
|
|
||||||
import { resizeImage } from "../../src/utils/image-resize";
|
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.
|
// without checking binary blobs into the repo.
|
||||||
const RED_1X1_PNG_BASE64 =
|
const RED_1X1_PNG_BASE64 =
|
||||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC";
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC";
|
||||||
|
|
||||||
async function makeRedPng(width: number, height: number): Promise<string> {
|
async function makeRedPng(width: number, height: number): Promise<string> {
|
||||||
const seed = await PhotonImage.parse(new Uint8Array(Buffer.from(RED_1X1_PNG_BASE64, "base64")));
|
const seed = Buffer.from(RED_1X1_PNG_BASE64, "base64");
|
||||||
const upscaled = await seed.resize(width, height, SamplingFilter.Nearest);
|
const upscaled = await new Bun.Image(seed).resize(width, height, { filter: "nearest" }).png().bytes();
|
||||||
const bytes = await upscaled.encode(ImageFormat.PNG, 100);
|
return Buffer.from(upscaled).toBase64();
|
||||||
return Buffer.from(bytes).toBase64();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("resizeImage defaults", () => {
|
describe("resizeImage defaults", () => {
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [14.9.9] - 2026-05-12
|
||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|||||||
@@ -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
|
- **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`)
|
- **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
|
## Usage
|
||||||
|
|
||||||
```typescript
|
```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
|
// Grep for a pattern
|
||||||
const results = await grep({
|
const results = await grep({
|
||||||
@@ -28,10 +33,8 @@ const files = await find({
|
|||||||
fileType: "file",
|
fileType: "file",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Image processing
|
// SIXEL encode for a terminal cell box (px)
|
||||||
const image = await PhotonImage.parse(bytes);
|
const sequence = encodeSixel(pngBytes, widthPx, heightPx);
|
||||||
const resized = await image.resize(800, 600, SamplingFilter.Lanczos3);
|
|
||||||
const pngBytes = await resized.encode(ImageFormat.PNG, 100);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
@@ -49,7 +52,7 @@ bun run check
|
|||||||
```
|
```
|
||||||
crates/pi-natives/ # Rust source (workspace member)
|
crates/pi-natives/ # Rust source (workspace member)
|
||||||
src/lib.rs # N-API exports
|
src/lib.rs # N-API exports
|
||||||
src/image.rs # Image processing (photon-rs)
|
src/sixel.rs # SIXEL terminal-image encoding
|
||||||
Cargo.toml # Rust dependencies
|
Cargo.toml # Rust dependencies
|
||||||
native/ # Native addon binaries
|
native/ # Native addon binaries
|
||||||
pi_natives.<platform>-<arch>-modern.node # x64 modern ISA (AVX2)
|
pi_natives.<platform>-<arch>-modern.node # x64 modern ISA (AVX2)
|
||||||
|
|||||||
Vendored
-54
@@ -39,34 +39,6 @@ export declare class MacOSPowerAssertion {
|
|||||||
stop(): void
|
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<Array<number>>
|
|
||||||
/**
|
|
||||||
* 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. */
|
/** Stable process reference. */
|
||||||
export declare class Process {
|
export declare class Process {
|
||||||
/** Open a stable process reference from a PID. */
|
/** Open a stable process reference from a PID. */
|
||||||
@@ -742,18 +714,6 @@ export interface HtmlToMarkdownOptions {
|
|||||||
skipImages?: boolean
|
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.
|
* Invalidate the filesystem scan cache.
|
||||||
*
|
*
|
||||||
@@ -1149,20 +1109,6 @@ export interface PtyStartOptions {
|
|||||||
*/
|
*/
|
||||||
export declare function readImageFromClipboard(): Promise<ClipboardImage | undefined | null>
|
export declare function readImageFromClipboard(): Promise<ClipboardImage | undefined | null>
|
||||||
|
|
||||||
/** 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,
|
* Strip ANSI escape sequences, remove control characters / lone surrogates,
|
||||||
* and normalize line endings.
|
* and normalize line endings.
|
||||||
|
|||||||
@@ -208,7 +208,6 @@ const nativeBindings = loadNative();
|
|||||||
// classes
|
// classes
|
||||||
export const MacAppearanceObserver = nativeBindings.MacAppearanceObserver;
|
export const MacAppearanceObserver = nativeBindings.MacAppearanceObserver;
|
||||||
export const MacOSPowerAssertion = nativeBindings.MacOSPowerAssertion;
|
export const MacOSPowerAssertion = nativeBindings.MacOSPowerAssertion;
|
||||||
export const PhotonImage = nativeBindings.PhotonImage;
|
|
||||||
export const Process = nativeBindings.Process;
|
export const Process = nativeBindings.Process;
|
||||||
export const PtySession = nativeBindings.PtySession;
|
export const PtySession = nativeBindings.PtySession;
|
||||||
export const Shell = nativeBindings.Shell;
|
export const Shell = nativeBindings.Shell;
|
||||||
@@ -282,12 +281,6 @@ export const GrepOutputMode = {
|
|||||||
Count: "count",
|
Count: "count",
|
||||||
FilesWithMatches: "filesWithMatches",
|
FilesWithMatches: "filesWithMatches",
|
||||||
};
|
};
|
||||||
export const ImageFormat = {
|
|
||||||
PNG: 0,
|
|
||||||
JPEG: 1,
|
|
||||||
WEBP: 2,
|
|
||||||
GIF: 3,
|
|
||||||
};
|
|
||||||
export const IsoBackendKind = {
|
export const IsoBackendKind = {
|
||||||
Apfs: 0,
|
Apfs: 0,
|
||||||
Btrfs: 1,
|
Btrfs: 1,
|
||||||
@@ -316,11 +309,4 @@ export const ProcessStatus = {
|
|||||||
Running: "running",
|
Running: "running",
|
||||||
Exited: "exited",
|
Exited: "exited",
|
||||||
};
|
};
|
||||||
export const SamplingFilter = {
|
|
||||||
Nearest: 1,
|
|
||||||
Triangle: 2,
|
|
||||||
CatmullRom: 3,
|
|
||||||
Gaussian: 4,
|
|
||||||
Lanczos3: 5,
|
|
||||||
};
|
|
||||||
// --- end generated native exports ---
|
// --- end generated native exports ---
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
"@types/bun": "catalog:"
|
"@types/bun": "catalog:"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.3.7"
|
"bun": ">=1.3.14"
|
||||||
},
|
},
|
||||||
"napi": {
|
"napi": {
|
||||||
"binaryName": "pi_natives",
|
"binaryName": "pi_natives",
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- Raised the minimum required Bun version to >=1.3.14 in package metadata
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
"postcss": "catalog:"
|
"postcss": "catalog:"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.3.7"
|
"bun": ">=1.3.14"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src",
|
"src",
|
||||||
|
|||||||
@@ -34,13 +34,13 @@
|
|||||||
"@oh-my-pi/pi-utils": "workspace:*"
|
"@oh-my-pi/pi-utils": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.3.13"
|
"@types/bun": "^1.3.14"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@oh-my-pi/pi-coding-agent": "^13"
|
"@oh-my-pi/pi-coding-agent": "^13"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.3.7"
|
"bun": ">=1.3.14"
|
||||||
},
|
},
|
||||||
"omp": {
|
"omp": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [14.9.8] - 2026-05-12
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@
|
|||||||
"@xterm/headless": "catalog:"
|
"@xterm/headless": "catalog:"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.3.7"
|
"bun": ">=1.3.14"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src",
|
"src",
|
||||||
|
|||||||
@@ -45,6 +45,6 @@
|
|||||||
"@types/bun": "catalog:"
|
"@types/bun": "catalog:"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.3.7"
|
"bun": ">=1.3.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
"@oh-my-pi/pi-natives": "catalog:"
|
"@oh-my-pi/pi-natives": "catalog:"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"bun": ">=1.3.7"
|
"bun": ">=1.3.14"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"src"
|
"src"
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ async function buildBinary(target: BinaryTarget): Promise<void> {
|
|||||||
console.log(`Building ${target.outfile}...`);
|
console.log(`Building ${target.outfile}...`);
|
||||||
await embedNative(target);
|
await embedNative(target);
|
||||||
if (isDryRun) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +120,9 @@ async function buildBinary(target: BinaryTarget): Promise<void> {
|
|||||||
"--compile",
|
"--compile",
|
||||||
"--no-compile-autoload-bunfig",
|
"--no-compile-autoload-bunfig",
|
||||||
"--no-compile-autoload-dotenv",
|
"--no-compile-autoload-dotenv",
|
||||||
|
"--no-compile-autoload-tsconfig",
|
||||||
|
"--no-compile-autoload-package-json",
|
||||||
|
"--keep-names",
|
||||||
"--define",
|
"--define",
|
||||||
'process.env.PI_COMPILED="true"',
|
'process.env.PI_COMPILED="true"',
|
||||||
"--root",
|
"--root",
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@ $Repo = "can1357/oh-my-pi"
|
|||||||
$Package = "@oh-my-pi/pi-coding-agent"
|
$Package = "@oh-my-pi/pi-coding-agent"
|
||||||
$InstallDir = if ($env:PI_INSTALL_DIR) { $env:PI_INSTALL_DIR } else { "$env:LOCALAPPDATA\omp" }
|
$InstallDir = if ($env:PI_INSTALL_DIR) { $env:PI_INSTALL_DIR } else { "$env:LOCALAPPDATA\omp" }
|
||||||
$BinaryName = "omp-windows-x64.exe"
|
$BinaryName = "omp-windows-x64.exe"
|
||||||
$MinimumBunVersion = "1.3.7"
|
$MinimumBunVersion = "1.3.14"
|
||||||
|
|
||||||
function Test-BunInstalled {
|
function Test-BunInstalled {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ set -e
|
|||||||
REPO="can1357/oh-my-pi"
|
REPO="can1357/oh-my-pi"
|
||||||
PACKAGE="@oh-my-pi/pi-coding-agent"
|
PACKAGE="@oh-my-pi/pi-coding-agent"
|
||||||
INSTALL_DIR="${PI_INSTALL_DIR:-$HOME/.local/bin}"
|
INSTALL_DIR="${PI_INSTALL_DIR:-$HOME/.local/bin}"
|
||||||
MIN_BUN_VERSION="1.3.7"
|
MIN_BUN_VERSION="1.3.14"
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
MODE=""
|
MODE=""
|
||||||
|
|||||||
Reference in New Issue
Block a user