@mariozechner/pi-coding-agent 0.32.2 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +86 -3
- package/dist/core/export-html/template.css +34 -4
- package/dist/core/export-html/template.js +17 -4
- package/dist/core/keybindings.d.ts +59 -0
- package/dist/core/keybindings.d.ts.map +1 -0
- package/dist/core/keybindings.js +149 -0
- package/dist/core/keybindings.js.map +1 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +11 -12
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +48 -72
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/hook-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/hook-editor.js +5 -4
- package/dist/modes/interactive/components/hook-editor.js.map +1 -1
- package/dist/modes/interactive/components/hook-input.d.ts.map +1 -1
- package/dist/modes/interactive/components/hook-input.js +4 -3
- package/dist/modes/interactive/components/hook-input.js.map +1 -1
- package/dist/modes/interactive/components/hook-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/hook-selector.js +6 -5
- package/dist/modes/interactive/components/hook-selector.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +6 -5
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +6 -5
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +6 -9
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +6 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +48 -2
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +14 -15
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.js +6 -11
- package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +21 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +175 -45
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/image-convert.d.ts +9 -0
- package/dist/utils/image-convert.d.ts.map +1 -0
- package/dist/utils/image-convert.js +24 -0
- package/dist/utils/image-convert.js.map +1 -0
- package/dist/utils/image-resize.d.ts +8 -1
- package/dist/utils/image-resize.d.ts.map +1 -1
- package/dist/utils/image-resize.js +104 -49
- package/dist/utils/image-resize.js.map +1 -1
- package/docs/tui.md +18 -15
- package/examples/custom-tools/subagent/README.md +2 -2
- package/examples/hooks/snake.ts +7 -7
- package/examples/hooks/todo/index.ts +2 -2
- package/package.json +5 -4
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert image to PNG format for terminal display.
|
|
3
|
+
* Kitty graphics protocol requires PNG format (f=100).
|
|
4
|
+
*/
|
|
5
|
+
export declare function convertToPng(base64Data: string, mimeType: string): Promise<{
|
|
6
|
+
data: string;
|
|
7
|
+
mimeType: string;
|
|
8
|
+
} | null>;
|
|
9
|
+
//# sourceMappingURL=image-convert.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-convert.d.ts","sourceRoot":"","sources":["../../src/utils/image-convert.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAsB,YAAY,CACjC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GACd,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAkBpD","sourcesContent":["/**\n * Convert image to PNG format for terminal display.\n * Kitty graphics protocol requires PNG format (f=100).\n */\nexport async function convertToPng(\n\tbase64Data: string,\n\tmimeType: string,\n): Promise<{ data: string; mimeType: string } | null> {\n\t// Already PNG, no conversion needed\n\tif (mimeType === \"image/png\") {\n\t\treturn { data: base64Data, mimeType };\n\t}\n\n\ttry {\n\t\tconst sharp = (await import(\"sharp\")).default;\n\t\tconst buffer = Buffer.from(base64Data, \"base64\");\n\t\tconst pngBuffer = await sharp(buffer).png().toBuffer();\n\t\treturn {\n\t\t\tdata: pngBuffer.toString(\"base64\"),\n\t\t\tmimeType: \"image/png\",\n\t\t};\n\t} catch {\n\t\t// Sharp not available or conversion failed\n\t\treturn null;\n\t}\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert image to PNG format for terminal display.
|
|
3
|
+
* Kitty graphics protocol requires PNG format (f=100).
|
|
4
|
+
*/
|
|
5
|
+
export async function convertToPng(base64Data, mimeType) {
|
|
6
|
+
// Already PNG, no conversion needed
|
|
7
|
+
if (mimeType === "image/png") {
|
|
8
|
+
return { data: base64Data, mimeType };
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const sharp = (await import("sharp")).default;
|
|
12
|
+
const buffer = Buffer.from(base64Data, "base64");
|
|
13
|
+
const pngBuffer = await sharp(buffer).png().toBuffer();
|
|
14
|
+
return {
|
|
15
|
+
data: pngBuffer.toString("base64"),
|
|
16
|
+
mimeType: "image/png",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Sharp not available or conversion failed
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=image-convert.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-convert.js","sourceRoot":"","sources":["../../src/utils/image-convert.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,UAAkB,EAClB,QAAgB,EACqC;IACrD,oCAAoC;IACpC,IAAI,QAAQ,KAAK,WAAW,EAAE,CAAC;QAC9B,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,CAAC;IACvC,CAAC;IAED,IAAI,CAAC;QACJ,MAAM,KAAK,GAAG,CAAC,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;QAC9C,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QACjD,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,CAAC;QACvD,OAAO;YACN,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAClC,QAAQ,EAAE,WAAW;SACrB,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACR,2CAA2C;QAC3C,OAAO,IAAI,CAAC;IACb,CAAC;AAAA,CACD","sourcesContent":["/**\n * Convert image to PNG format for terminal display.\n * Kitty graphics protocol requires PNG format (f=100).\n */\nexport async function convertToPng(\n\tbase64Data: string,\n\tmimeType: string,\n): Promise<{ data: string; mimeType: string } | null> {\n\t// Already PNG, no conversion needed\n\tif (mimeType === \"image/png\") {\n\t\treturn { data: base64Data, mimeType };\n\t}\n\n\ttry {\n\t\tconst sharp = (await import(\"sharp\")).default;\n\t\tconst buffer = Buffer.from(base64Data, \"base64\");\n\t\tconst pngBuffer = await sharp(buffer).png().toBuffer();\n\t\treturn {\n\t\t\tdata: pngBuffer.toString(\"base64\"),\n\t\t\tmimeType: \"image/png\",\n\t\t};\n\t} catch {\n\t\t// Sharp not available or conversion failed\n\t\treturn null;\n\t}\n}\n"]}
|
|
@@ -2,6 +2,7 @@ import type { ImageContent } from "@mariozechner/pi-ai";
|
|
|
2
2
|
export interface ImageResizeOptions {
|
|
3
3
|
maxWidth?: number;
|
|
4
4
|
maxHeight?: number;
|
|
5
|
+
maxBytes?: number;
|
|
5
6
|
jpegQuality?: number;
|
|
6
7
|
}
|
|
7
8
|
export interface ResizedImage {
|
|
@@ -14,11 +15,17 @@ export interface ResizedImage {
|
|
|
14
15
|
wasResized: boolean;
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
|
-
* Resize an image to fit within the specified max dimensions.
|
|
18
|
+
* Resize an image to fit within the specified max dimensions and file size.
|
|
18
19
|
* Returns the original image if it already fits within the limits.
|
|
19
20
|
*
|
|
20
21
|
* Uses sharp for image processing. If sharp is not available (e.g., in some
|
|
21
22
|
* environments), returns the original image unchanged.
|
|
23
|
+
*
|
|
24
|
+
* Strategy for staying under maxBytes:
|
|
25
|
+
* 1. First resize to maxWidth/maxHeight
|
|
26
|
+
* 2. Try both PNG and JPEG formats, pick the smaller one
|
|
27
|
+
* 3. If still too large, try JPEG with decreasing quality
|
|
28
|
+
* 4. If still too large, progressively reduce dimensions
|
|
22
29
|
*/
|
|
23
30
|
export declare function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage>;
|
|
24
31
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-resize.d.ts","sourceRoot":"","sources":["../../src/utils/image-resize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;CACpB;
|
|
1
|
+
{"version":3,"file":"image-resize.d.ts","sourceRoot":"","sources":["../../src/utils/image-resize.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,MAAM,WAAW,kBAAkB;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;CACpB;AAoBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,WAAW,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAuJxG;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,GAAG,SAAS,CAO5E","sourcesContent":["import type { ImageContent } from \"@mariozechner/pi-ai\";\n\nexport interface ImageResizeOptions {\n\tmaxWidth?: number; // Default: 2000\n\tmaxHeight?: number; // Default: 2000\n\tmaxBytes?: number; // Default: 4.5MB (below Anthropic's 5MB limit)\n\tjpegQuality?: number; // Default: 80\n}\n\nexport interface ResizedImage {\n\tdata: string; // base64\n\tmimeType: string;\n\toriginalWidth: number;\n\toriginalHeight: number;\n\twidth: number;\n\theight: number;\n\twasResized: boolean;\n}\n\n// 4.5MB - provides headroom below Anthropic's 5MB limit\nconst DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024;\n\nconst DEFAULT_OPTIONS: Required<ImageResizeOptions> = {\n\tmaxWidth: 2000,\n\tmaxHeight: 2000,\n\tmaxBytes: DEFAULT_MAX_BYTES,\n\tjpegQuality: 80,\n};\n\n/** Helper to pick the smaller of two buffers */\nfunction pickSmaller(\n\ta: { buffer: Buffer; mimeType: string },\n\tb: { buffer: Buffer; mimeType: string },\n): { buffer: Buffer; mimeType: string } {\n\treturn a.buffer.length <= b.buffer.length ? a : b;\n}\n\n/**\n * Resize an image to fit within the specified max dimensions and file size.\n * Returns the original image if it already fits within the limits.\n *\n * Uses sharp for image processing. If sharp is not available (e.g., in some\n * environments), returns the original image unchanged.\n *\n * Strategy for staying under maxBytes:\n * 1. First resize to maxWidth/maxHeight\n * 2. Try both PNG and JPEG formats, pick the smaller one\n * 3. If still too large, try JPEG with decreasing quality\n * 4. If still too large, progressively reduce dimensions\n */\nexport async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage> {\n\tconst opts = { ...DEFAULT_OPTIONS, ...options };\n\tconst buffer = Buffer.from(img.data, \"base64\");\n\n\tlet sharp: typeof import(\"sharp\") | undefined;\n\ttry {\n\t\tsharp = (await import(\"sharp\")).default;\n\t} catch {\n\t\t// Sharp not available - return original image\n\t\t// We can't get dimensions without sharp, so return 0s\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType,\n\t\t\toriginalWidth: 0,\n\t\t\toriginalHeight: 0,\n\t\t\twidth: 0,\n\t\t\theight: 0,\n\t\t\twasResized: false,\n\t\t};\n\t}\n\n\tconst sharpImg = sharp(buffer);\n\tconst metadata = await sharpImg.metadata();\n\n\tconst originalWidth = metadata.width ?? 0;\n\tconst originalHeight = metadata.height ?? 0;\n\tconst format = metadata.format ?? img.mimeType?.split(\"/\")[1] ?? \"png\";\n\n\t// Check if already within all limits (dimensions AND size)\n\tconst originalSize = buffer.length;\n\tif (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType ?? `image/${format}`,\n\t\t\toriginalWidth,\n\t\t\toriginalHeight,\n\t\t\twidth: originalWidth,\n\t\t\theight: originalHeight,\n\t\t\twasResized: false,\n\t\t};\n\t}\n\n\t// Calculate initial dimensions respecting max limits\n\tlet targetWidth = originalWidth;\n\tlet targetHeight = originalHeight;\n\n\tif (targetWidth > opts.maxWidth) {\n\t\ttargetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);\n\t\ttargetWidth = opts.maxWidth;\n\t}\n\tif (targetHeight > opts.maxHeight) {\n\t\ttargetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);\n\t\ttargetHeight = opts.maxHeight;\n\t}\n\n\t// Helper to resize and encode in both formats, returning the smaller one\n\tasync function tryBothFormats(\n\t\twidth: number,\n\t\theight: number,\n\t\tjpegQuality: number,\n\t): Promise<{ buffer: Buffer; mimeType: string }> {\n\t\tconst resized = await sharp!(buffer)\n\t\t\t.resize(width, height, { fit: \"inside\", withoutEnlargement: true })\n\t\t\t.toBuffer();\n\n\t\tconst [pngBuffer, jpegBuffer] = await Promise.all([\n\t\t\tsharp!(resized).png({ compressionLevel: 9 }).toBuffer(),\n\t\t\tsharp!(resized).jpeg({ quality: jpegQuality }).toBuffer(),\n\t\t]);\n\n\t\treturn pickSmaller({ buffer: pngBuffer, mimeType: \"image/png\" }, { buffer: jpegBuffer, mimeType: \"image/jpeg\" });\n\t}\n\n\t// Try to produce an image under maxBytes\n\tconst qualitySteps = [85, 70, 55, 40];\n\tconst scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25];\n\n\tlet best: { buffer: Buffer; mimeType: string };\n\tlet finalWidth = targetWidth;\n\tlet finalHeight = targetHeight;\n\n\t// First attempt: resize to target dimensions, try both formats\n\tbest = await tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);\n\n\tif (best.buffer.length <= opts.maxBytes) {\n\t\treturn {\n\t\t\tdata: best.buffer.toString(\"base64\"),\n\t\t\tmimeType: best.mimeType,\n\t\t\toriginalWidth,\n\t\t\toriginalHeight,\n\t\t\twidth: finalWidth,\n\t\t\theight: finalHeight,\n\t\t\twasResized: true,\n\t\t};\n\t}\n\n\t// Still too large - try JPEG with decreasing quality (and compare to PNG each time)\n\tfor (const quality of qualitySteps) {\n\t\tbest = await tryBothFormats(targetWidth, targetHeight, quality);\n\n\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\treturn {\n\t\t\t\tdata: best.buffer.toString(\"base64\"),\n\t\t\t\tmimeType: best.mimeType,\n\t\t\t\toriginalWidth,\n\t\t\t\toriginalHeight,\n\t\t\t\twidth: finalWidth,\n\t\t\t\theight: finalHeight,\n\t\t\t\twasResized: true,\n\t\t\t};\n\t\t}\n\t}\n\n\t// Still too large - reduce dimensions progressively\n\tfor (const scale of scaleSteps) {\n\t\tfinalWidth = Math.round(targetWidth * scale);\n\t\tfinalHeight = Math.round(targetHeight * scale);\n\n\t\t// Skip if dimensions are too small\n\t\tif (finalWidth < 100 || finalHeight < 100) {\n\t\t\tbreak;\n\t\t}\n\n\t\tfor (const quality of qualitySteps) {\n\t\t\tbest = await tryBothFormats(finalWidth, finalHeight, quality);\n\n\t\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\t\treturn {\n\t\t\t\t\tdata: best.buffer.toString(\"base64\"),\n\t\t\t\t\tmimeType: best.mimeType,\n\t\t\t\t\toriginalWidth,\n\t\t\t\t\toriginalHeight,\n\t\t\t\t\twidth: finalWidth,\n\t\t\t\t\theight: finalHeight,\n\t\t\t\t\twasResized: true,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n\n\t// Last resort: return smallest version we produced even if over limit\n\t// (the API will reject it, but at least we tried everything)\n\treturn {\n\t\tdata: best.buffer.toString(\"base64\"),\n\t\tmimeType: best.mimeType,\n\t\toriginalWidth,\n\t\toriginalHeight,\n\t\twidth: finalWidth,\n\t\theight: finalHeight,\n\t\twasResized: true,\n\t};\n}\n\n/**\n * Format a dimension note for resized images.\n * This helps the model understand the coordinate mapping.\n */\nexport function formatDimensionNote(result: ResizedImage): string | undefined {\n\tif (!result.wasResized) {\n\t\treturn undefined;\n\t}\n\n\tconst scale = result.originalWidth / result.width;\n\treturn `[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.]`;\n}\n"]}
|
|
@@ -1,14 +1,27 @@
|
|
|
1
|
+
// 4.5MB - provides headroom below Anthropic's 5MB limit
|
|
2
|
+
const DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024;
|
|
1
3
|
const DEFAULT_OPTIONS = {
|
|
2
4
|
maxWidth: 2000,
|
|
3
5
|
maxHeight: 2000,
|
|
6
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
4
7
|
jpegQuality: 80,
|
|
5
8
|
};
|
|
9
|
+
/** Helper to pick the smaller of two buffers */
|
|
10
|
+
function pickSmaller(a, b) {
|
|
11
|
+
return a.buffer.length <= b.buffer.length ? a : b;
|
|
12
|
+
}
|
|
6
13
|
/**
|
|
7
|
-
* Resize an image to fit within the specified max dimensions.
|
|
14
|
+
* Resize an image to fit within the specified max dimensions and file size.
|
|
8
15
|
* Returns the original image if it already fits within the limits.
|
|
9
16
|
*
|
|
10
17
|
* Uses sharp for image processing. If sharp is not available (e.g., in some
|
|
11
18
|
* environments), returns the original image unchanged.
|
|
19
|
+
*
|
|
20
|
+
* Strategy for staying under maxBytes:
|
|
21
|
+
* 1. First resize to maxWidth/maxHeight
|
|
22
|
+
* 2. Try both PNG and JPEG formats, pick the smaller one
|
|
23
|
+
* 3. If still too large, try JPEG with decreasing quality
|
|
24
|
+
* 4. If still too large, progressively reduce dimensions
|
|
12
25
|
*/
|
|
13
26
|
export async function resizeImage(img, options) {
|
|
14
27
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
@@ -32,68 +45,110 @@ export async function resizeImage(img, options) {
|
|
|
32
45
|
}
|
|
33
46
|
const sharpImg = sharp(buffer);
|
|
34
47
|
const metadata = await sharpImg.metadata();
|
|
35
|
-
const
|
|
36
|
-
const
|
|
48
|
+
const originalWidth = metadata.width ?? 0;
|
|
49
|
+
const originalHeight = metadata.height ?? 0;
|
|
37
50
|
const format = metadata.format ?? img.mimeType?.split("/")[1] ?? "png";
|
|
38
|
-
// Check if already within limits
|
|
39
|
-
|
|
51
|
+
// Check if already within all limits (dimensions AND size)
|
|
52
|
+
const originalSize = buffer.length;
|
|
53
|
+
if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {
|
|
40
54
|
return {
|
|
41
55
|
data: img.data,
|
|
42
56
|
mimeType: img.mimeType ?? `image/${format}`,
|
|
43
|
-
originalWidth
|
|
44
|
-
originalHeight
|
|
45
|
-
width,
|
|
46
|
-
height,
|
|
57
|
+
originalWidth,
|
|
58
|
+
originalHeight,
|
|
59
|
+
width: originalWidth,
|
|
60
|
+
height: originalHeight,
|
|
47
61
|
wasResized: false,
|
|
48
62
|
};
|
|
49
63
|
}
|
|
50
|
-
// Calculate
|
|
51
|
-
let
|
|
52
|
-
let
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
if (newHeight > opts.maxHeight) {
|
|
58
|
-
newWidth = Math.round((newWidth * opts.maxHeight) / newHeight);
|
|
59
|
-
newHeight = opts.maxHeight;
|
|
64
|
+
// Calculate initial dimensions respecting max limits
|
|
65
|
+
let targetWidth = originalWidth;
|
|
66
|
+
let targetHeight = originalHeight;
|
|
67
|
+
if (targetWidth > opts.maxWidth) {
|
|
68
|
+
targetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);
|
|
69
|
+
targetWidth = opts.maxWidth;
|
|
60
70
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
.toBuffer();
|
|
65
|
-
// Determine output format - preserve original if possible, otherwise use JPEG
|
|
66
|
-
let outputMimeType;
|
|
67
|
-
let outputBuffer;
|
|
68
|
-
if (format === "jpeg" || format === "jpg") {
|
|
69
|
-
outputBuffer = await sharp(resized).jpeg({ quality: opts.jpegQuality }).toBuffer();
|
|
70
|
-
outputMimeType = "image/jpeg";
|
|
71
|
+
if (targetHeight > opts.maxHeight) {
|
|
72
|
+
targetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);
|
|
73
|
+
targetHeight = opts.maxHeight;
|
|
71
74
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
// Helper to resize and encode in both formats, returning the smaller one
|
|
76
|
+
async function tryBothFormats(width, height, jpegQuality) {
|
|
77
|
+
const resized = await sharp(buffer)
|
|
78
|
+
.resize(width, height, { fit: "inside", withoutEnlargement: true })
|
|
79
|
+
.toBuffer();
|
|
80
|
+
const [pngBuffer, jpegBuffer] = await Promise.all([
|
|
81
|
+
sharp(resized).png({ compressionLevel: 9 }).toBuffer(),
|
|
82
|
+
sharp(resized).jpeg({ quality: jpegQuality }).toBuffer(),
|
|
83
|
+
]);
|
|
84
|
+
return pickSmaller({ buffer: pngBuffer, mimeType: "image/png" }, { buffer: jpegBuffer, mimeType: "image/jpeg" });
|
|
75
85
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
// Try to produce an image under maxBytes
|
|
87
|
+
const qualitySteps = [85, 70, 55, 40];
|
|
88
|
+
const scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25];
|
|
89
|
+
let best;
|
|
90
|
+
let finalWidth = targetWidth;
|
|
91
|
+
let finalHeight = targetHeight;
|
|
92
|
+
// First attempt: resize to target dimensions, try both formats
|
|
93
|
+
best = await tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);
|
|
94
|
+
if (best.buffer.length <= opts.maxBytes) {
|
|
95
|
+
return {
|
|
96
|
+
data: best.buffer.toString("base64"),
|
|
97
|
+
mimeType: best.mimeType,
|
|
98
|
+
originalWidth,
|
|
99
|
+
originalHeight,
|
|
100
|
+
width: finalWidth,
|
|
101
|
+
height: finalHeight,
|
|
102
|
+
wasResized: true,
|
|
103
|
+
};
|
|
80
104
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
105
|
+
// Still too large - try JPEG with decreasing quality (and compare to PNG each time)
|
|
106
|
+
for (const quality of qualitySteps) {
|
|
107
|
+
best = await tryBothFormats(targetWidth, targetHeight, quality);
|
|
108
|
+
if (best.buffer.length <= opts.maxBytes) {
|
|
109
|
+
return {
|
|
110
|
+
data: best.buffer.toString("base64"),
|
|
111
|
+
mimeType: best.mimeType,
|
|
112
|
+
originalWidth,
|
|
113
|
+
originalHeight,
|
|
114
|
+
width: finalWidth,
|
|
115
|
+
height: finalHeight,
|
|
116
|
+
wasResized: true,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
84
119
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
120
|
+
// Still too large - reduce dimensions progressively
|
|
121
|
+
for (const scale of scaleSteps) {
|
|
122
|
+
finalWidth = Math.round(targetWidth * scale);
|
|
123
|
+
finalHeight = Math.round(targetHeight * scale);
|
|
124
|
+
// Skip if dimensions are too small
|
|
125
|
+
if (finalWidth < 100 || finalHeight < 100) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
for (const quality of qualitySteps) {
|
|
129
|
+
best = await tryBothFormats(finalWidth, finalHeight, quality);
|
|
130
|
+
if (best.buffer.length <= opts.maxBytes) {
|
|
131
|
+
return {
|
|
132
|
+
data: best.buffer.toString("base64"),
|
|
133
|
+
mimeType: best.mimeType,
|
|
134
|
+
originalWidth,
|
|
135
|
+
originalHeight,
|
|
136
|
+
width: finalWidth,
|
|
137
|
+
height: finalHeight,
|
|
138
|
+
wasResized: true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
89
142
|
}
|
|
143
|
+
// Last resort: return smallest version we produced even if over limit
|
|
144
|
+
// (the API will reject it, but at least we tried everything)
|
|
90
145
|
return {
|
|
91
|
-
data:
|
|
92
|
-
mimeType:
|
|
93
|
-
originalWidth
|
|
94
|
-
originalHeight
|
|
95
|
-
width:
|
|
96
|
-
height:
|
|
146
|
+
data: best.buffer.toString("base64"),
|
|
147
|
+
mimeType: best.mimeType,
|
|
148
|
+
originalWidth,
|
|
149
|
+
originalHeight,
|
|
150
|
+
width: finalWidth,
|
|
151
|
+
height: finalHeight,
|
|
97
152
|
wasResized: true,
|
|
98
153
|
};
|
|
99
154
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-resize.js","sourceRoot":"","sources":["../../src/utils/image-resize.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ImageContent } from \"@mariozechner/pi-ai\";\n\nexport interface ImageResizeOptions {\n\tmaxWidth?: number; // Default: 2000\n\tmaxHeight?: number; // Default: 2000\n\tjpegQuality?: number; // Default: 80\n}\n\nexport interface ResizedImage {\n\tdata: string; // base64\n\tmimeType: string;\n\toriginalWidth: number;\n\toriginalHeight: number;\n\twidth: number;\n\theight: number;\n\twasResized: boolean;\n}\n\nconst DEFAULT_OPTIONS: Required<ImageResizeOptions> = {\n\tmaxWidth: 2000,\n\tmaxHeight: 2000,\n\tjpegQuality: 80,\n};\n\n/**\n * Resize an image to fit within the specified max dimensions.\n * Returns the original image if it already fits within the limits.\n *\n * Uses sharp for image processing. If sharp is not available (e.g., in some\n * environments), returns the original image unchanged.\n */\nexport async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage> {\n\tconst opts = { ...DEFAULT_OPTIONS, ...options };\n\tconst buffer = Buffer.from(img.data, \"base64\");\n\n\tlet sharp: typeof import(\"sharp\") | undefined;\n\ttry {\n\t\tsharp = (await import(\"sharp\")).default;\n\t} catch {\n\t\t// Sharp not available - return original image\n\t\t// We can't get dimensions without sharp, so return 0s\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType,\n\t\t\toriginalWidth: 0,\n\t\t\toriginalHeight: 0,\n\t\t\twidth: 0,\n\t\t\theight: 0,\n\t\t\twasResized: false,\n\t\t};\n\t}\n\n\tconst sharpImg = sharp(buffer);\n\tconst metadata = await sharpImg.metadata();\n\n\tconst width = metadata.width ?? 0;\n\tconst height = metadata.height ?? 0;\n\tconst format = metadata.format ?? img.mimeType?.split(\"/\")[1] ?? \"png\";\n\n\t// Check if already within limits\n\tif (width <= opts.maxWidth && height <= opts.maxHeight) {\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType ?? `image/${format}`,\n\t\t\toriginalWidth: width,\n\t\t\toriginalHeight: height,\n\t\t\twidth,\n\t\t\theight,\n\t\t\twasResized: false,\n\t\t};\n\t}\n\n\t// Calculate new dimensions maintaining aspect ratio\n\tlet newWidth = width;\n\tlet newHeight = height;\n\n\tif (newWidth > opts.maxWidth) {\n\t\tnewHeight = Math.round((newHeight * opts.maxWidth) / newWidth);\n\t\tnewWidth = opts.maxWidth;\n\t}\n\tif (newHeight > opts.maxHeight) {\n\t\tnewWidth = Math.round((newWidth * opts.maxHeight) / newHeight);\n\t\tnewHeight = opts.maxHeight;\n\t}\n\n\t// Resize the image\n\tconst resized = await sharp(buffer)\n\t\t.resize(newWidth, newHeight, { fit: \"inside\", withoutEnlargement: true })\n\t\t.toBuffer();\n\n\t// Determine output format - preserve original if possible, otherwise use JPEG\n\tlet outputMimeType: string;\n\tlet outputBuffer: Buffer;\n\n\tif (format === \"jpeg\" || format === \"jpg\") {\n\t\toutputBuffer = await sharp(resized).jpeg({ quality: opts.jpegQuality }).toBuffer();\n\t\toutputMimeType = \"image/jpeg\";\n\t} else if (format === \"png\") {\n\t\toutputBuffer = resized;\n\t\toutputMimeType = \"image/png\";\n\t} else if (format === \"gif\") {\n\t\t// GIF resize might not preserve animation; convert to PNG for quality\n\t\toutputBuffer = resized;\n\t\toutputMimeType = \"image/png\";\n\t} else if (format === \"webp\") {\n\t\toutputBuffer = resized;\n\t\toutputMimeType = \"image/webp\";\n\t} else {\n\t\t// Default to JPEG for unknown formats\n\t\toutputBuffer = await sharp(resized).jpeg({ quality: opts.jpegQuality }).toBuffer();\n\t\toutputMimeType = \"image/jpeg\";\n\t}\n\n\treturn {\n\t\tdata: outputBuffer.toString(\"base64\"),\n\t\tmimeType: outputMimeType,\n\t\toriginalWidth: width,\n\t\toriginalHeight: height,\n\t\twidth: newWidth,\n\t\theight: newHeight,\n\t\twasResized: true,\n\t};\n}\n\n/**\n * Format a dimension note for resized images.\n * This helps the model understand the coordinate mapping.\n */\nexport function formatDimensionNote(result: ResizedImage): string | undefined {\n\tif (!result.wasResized) {\n\t\treturn undefined;\n\t}\n\n\tconst scale = result.originalWidth / result.width;\n\treturn `[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.]`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"image-resize.js","sourceRoot":"","sources":["../../src/utils/image-resize.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ImageContent } from \"@mariozechner/pi-ai\";\n\nexport interface ImageResizeOptions {\n\tmaxWidth?: number; // Default: 2000\n\tmaxHeight?: number; // Default: 2000\n\tmaxBytes?: number; // Default: 4.5MB (below Anthropic's 5MB limit)\n\tjpegQuality?: number; // Default: 80\n}\n\nexport interface ResizedImage {\n\tdata: string; // base64\n\tmimeType: string;\n\toriginalWidth: number;\n\toriginalHeight: number;\n\twidth: number;\n\theight: number;\n\twasResized: boolean;\n}\n\n// 4.5MB - provides headroom below Anthropic's 5MB limit\nconst DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024;\n\nconst DEFAULT_OPTIONS: Required<ImageResizeOptions> = {\n\tmaxWidth: 2000,\n\tmaxHeight: 2000,\n\tmaxBytes: DEFAULT_MAX_BYTES,\n\tjpegQuality: 80,\n};\n\n/** Helper to pick the smaller of two buffers */\nfunction pickSmaller(\n\ta: { buffer: Buffer; mimeType: string },\n\tb: { buffer: Buffer; mimeType: string },\n): { buffer: Buffer; mimeType: string } {\n\treturn a.buffer.length <= b.buffer.length ? a : b;\n}\n\n/**\n * Resize an image to fit within the specified max dimensions and file size.\n * Returns the original image if it already fits within the limits.\n *\n * Uses sharp for image processing. If sharp is not available (e.g., in some\n * environments), returns the original image unchanged.\n *\n * Strategy for staying under maxBytes:\n * 1. First resize to maxWidth/maxHeight\n * 2. Try both PNG and JPEG formats, pick the smaller one\n * 3. If still too large, try JPEG with decreasing quality\n * 4. If still too large, progressively reduce dimensions\n */\nexport async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage> {\n\tconst opts = { ...DEFAULT_OPTIONS, ...options };\n\tconst buffer = Buffer.from(img.data, \"base64\");\n\n\tlet sharp: typeof import(\"sharp\") | undefined;\n\ttry {\n\t\tsharp = (await import(\"sharp\")).default;\n\t} catch {\n\t\t// Sharp not available - return original image\n\t\t// We can't get dimensions without sharp, so return 0s\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType,\n\t\t\toriginalWidth: 0,\n\t\t\toriginalHeight: 0,\n\t\t\twidth: 0,\n\t\t\theight: 0,\n\t\t\twasResized: false,\n\t\t};\n\t}\n\n\tconst sharpImg = sharp(buffer);\n\tconst metadata = await sharpImg.metadata();\n\n\tconst originalWidth = metadata.width ?? 0;\n\tconst originalHeight = metadata.height ?? 0;\n\tconst format = metadata.format ?? img.mimeType?.split(\"/\")[1] ?? \"png\";\n\n\t// Check if already within all limits (dimensions AND size)\n\tconst originalSize = buffer.length;\n\tif (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {\n\t\treturn {\n\t\t\tdata: img.data,\n\t\t\tmimeType: img.mimeType ?? `image/${format}`,\n\t\t\toriginalWidth,\n\t\t\toriginalHeight,\n\t\t\twidth: originalWidth,\n\t\t\theight: originalHeight,\n\t\t\twasResized: false,\n\t\t};\n\t}\n\n\t// Calculate initial dimensions respecting max limits\n\tlet targetWidth = originalWidth;\n\tlet targetHeight = originalHeight;\n\n\tif (targetWidth > opts.maxWidth) {\n\t\ttargetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);\n\t\ttargetWidth = opts.maxWidth;\n\t}\n\tif (targetHeight > opts.maxHeight) {\n\t\ttargetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);\n\t\ttargetHeight = opts.maxHeight;\n\t}\n\n\t// Helper to resize and encode in both formats, returning the smaller one\n\tasync function tryBothFormats(\n\t\twidth: number,\n\t\theight: number,\n\t\tjpegQuality: number,\n\t): Promise<{ buffer: Buffer; mimeType: string }> {\n\t\tconst resized = await sharp!(buffer)\n\t\t\t.resize(width, height, { fit: \"inside\", withoutEnlargement: true })\n\t\t\t.toBuffer();\n\n\t\tconst [pngBuffer, jpegBuffer] = await Promise.all([\n\t\t\tsharp!(resized).png({ compressionLevel: 9 }).toBuffer(),\n\t\t\tsharp!(resized).jpeg({ quality: jpegQuality }).toBuffer(),\n\t\t]);\n\n\t\treturn pickSmaller({ buffer: pngBuffer, mimeType: \"image/png\" }, { buffer: jpegBuffer, mimeType: \"image/jpeg\" });\n\t}\n\n\t// Try to produce an image under maxBytes\n\tconst qualitySteps = [85, 70, 55, 40];\n\tconst scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25];\n\n\tlet best: { buffer: Buffer; mimeType: string };\n\tlet finalWidth = targetWidth;\n\tlet finalHeight = targetHeight;\n\n\t// First attempt: resize to target dimensions, try both formats\n\tbest = await tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);\n\n\tif (best.buffer.length <= opts.maxBytes) {\n\t\treturn {\n\t\t\tdata: best.buffer.toString(\"base64\"),\n\t\t\tmimeType: best.mimeType,\n\t\t\toriginalWidth,\n\t\t\toriginalHeight,\n\t\t\twidth: finalWidth,\n\t\t\theight: finalHeight,\n\t\t\twasResized: true,\n\t\t};\n\t}\n\n\t// Still too large - try JPEG with decreasing quality (and compare to PNG each time)\n\tfor (const quality of qualitySteps) {\n\t\tbest = await tryBothFormats(targetWidth, targetHeight, quality);\n\n\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\treturn {\n\t\t\t\tdata: best.buffer.toString(\"base64\"),\n\t\t\t\tmimeType: best.mimeType,\n\t\t\t\toriginalWidth,\n\t\t\t\toriginalHeight,\n\t\t\t\twidth: finalWidth,\n\t\t\t\theight: finalHeight,\n\t\t\t\twasResized: true,\n\t\t\t};\n\t\t}\n\t}\n\n\t// Still too large - reduce dimensions progressively\n\tfor (const scale of scaleSteps) {\n\t\tfinalWidth = Math.round(targetWidth * scale);\n\t\tfinalHeight = Math.round(targetHeight * scale);\n\n\t\t// Skip if dimensions are too small\n\t\tif (finalWidth < 100 || finalHeight < 100) {\n\t\t\tbreak;\n\t\t}\n\n\t\tfor (const quality of qualitySteps) {\n\t\t\tbest = await tryBothFormats(finalWidth, finalHeight, quality);\n\n\t\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\t\treturn {\n\t\t\t\t\tdata: best.buffer.toString(\"base64\"),\n\t\t\t\t\tmimeType: best.mimeType,\n\t\t\t\t\toriginalWidth,\n\t\t\t\t\toriginalHeight,\n\t\t\t\t\twidth: finalWidth,\n\t\t\t\t\theight: finalHeight,\n\t\t\t\t\twasResized: true,\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t}\n\n\t// Last resort: return smallest version we produced even if over limit\n\t// (the API will reject it, but at least we tried everything)\n\treturn {\n\t\tdata: best.buffer.toString(\"base64\"),\n\t\tmimeType: best.mimeType,\n\t\toriginalWidth,\n\t\toriginalHeight,\n\t\twidth: finalWidth,\n\t\theight: finalHeight,\n\t\twasResized: true,\n\t};\n}\n\n/**\n * Format a dimension note for resized images.\n * This helps the model understand the coordinate mapping.\n */\nexport function formatDimensionNote(result: ResizedImage): string | undefined {\n\tif (!result.wasResized) {\n\t\treturn undefined;\n\t}\n\n\tconst scale = result.originalWidth / result.width;\n\treturn `[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.]`;\n}\n"]}
|
package/docs/tui.md
CHANGED
|
@@ -130,27 +130,30 @@ const image = new Image(
|
|
|
130
130
|
|
|
131
131
|
## Keyboard Input
|
|
132
132
|
|
|
133
|
-
Use key detection
|
|
133
|
+
Use `matchesKey()` for key detection:
|
|
134
134
|
|
|
135
135
|
```typescript
|
|
136
|
-
import {
|
|
137
|
-
isEnter, isEscape, isTab,
|
|
138
|
-
isArrowUp, isArrowDown, isArrowLeft, isArrowRight,
|
|
139
|
-
isCtrlC, isCtrlO, isBackspace, isDelete,
|
|
140
|
-
// ... and more
|
|
141
|
-
} from "@mariozechner/pi-tui";
|
|
136
|
+
import { matchesKey, Key } from "@mariozechner/pi-tui";
|
|
142
137
|
|
|
143
138
|
handleInput(data: string) {
|
|
144
|
-
if (
|
|
139
|
+
if (matchesKey(data, Key.up)) {
|
|
145
140
|
this.selectedIndex--;
|
|
146
|
-
} else if (
|
|
141
|
+
} else if (matchesKey(data, Key.enter)) {
|
|
147
142
|
this.onSelect?.(this.selectedIndex);
|
|
148
|
-
} else if (
|
|
143
|
+
} else if (matchesKey(data, Key.escape)) {
|
|
149
144
|
this.onCancel?.();
|
|
145
|
+
} else if (matchesKey(data, Key.ctrl("c"))) {
|
|
146
|
+
// Ctrl+C
|
|
150
147
|
}
|
|
151
148
|
}
|
|
152
149
|
```
|
|
153
150
|
|
|
151
|
+
**Key identifiers** (use `Key.*` for autocomplete, or string literals):
|
|
152
|
+
- Basic keys: `Key.enter`, `Key.escape`, `Key.tab`, `Key.space`, `Key.backspace`, `Key.delete`, `Key.home`, `Key.end`
|
|
153
|
+
- Arrow keys: `Key.up`, `Key.down`, `Key.left`, `Key.right`
|
|
154
|
+
- With modifiers: `Key.ctrl("c")`, `Key.shift("tab")`, `Key.alt("left")`, `Key.ctrlShift("p")`
|
|
155
|
+
- String format also works: `"enter"`, `"ctrl+c"`, `"shift+tab"`, `"ctrl+shift+p"`
|
|
156
|
+
|
|
154
157
|
## Line Width
|
|
155
158
|
|
|
156
159
|
**Critical:** Each line from `render()` must not exceed the `width` parameter.
|
|
@@ -175,7 +178,7 @@ Example: Interactive selector
|
|
|
175
178
|
|
|
176
179
|
```typescript
|
|
177
180
|
import {
|
|
178
|
-
|
|
181
|
+
matchesKey, Key,
|
|
179
182
|
truncateToWidth, visibleWidth
|
|
180
183
|
} from "@mariozechner/pi-tui";
|
|
181
184
|
|
|
@@ -193,15 +196,15 @@ class MySelector {
|
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
handleInput(data: string): void {
|
|
196
|
-
if (
|
|
199
|
+
if (matchesKey(data, Key.up) && this.selected > 0) {
|
|
197
200
|
this.selected--;
|
|
198
201
|
this.invalidate();
|
|
199
|
-
} else if (
|
|
202
|
+
} else if (matchesKey(data, Key.down) && this.selected < this.items.length - 1) {
|
|
200
203
|
this.selected++;
|
|
201
204
|
this.invalidate();
|
|
202
|
-
} else if (
|
|
205
|
+
} else if (matchesKey(data, Key.enter)) {
|
|
203
206
|
this.onSelect?.(this.items[this.selected]);
|
|
204
|
-
} else if (
|
|
207
|
+
} else if (matchesKey(data, Key.escape)) {
|
|
205
208
|
this.onCancel?.();
|
|
206
209
|
}
|
|
207
210
|
}
|
|
@@ -16,7 +16,7 @@ Delegate tasks to specialized subagents with isolated context windows.
|
|
|
16
16
|
```
|
|
17
17
|
subagent/
|
|
18
18
|
├── README.md # This file
|
|
19
|
-
├──
|
|
19
|
+
├── index.ts # The custom tool (entry point)
|
|
20
20
|
├── agents.ts # Agent discovery logic
|
|
21
21
|
├── agents/ # Sample agent definitions
|
|
22
22
|
│ ├── scout.md # Fast recon, returns compressed context
|
|
@@ -36,7 +36,7 @@ From the repository root, symlink the files:
|
|
|
36
36
|
```bash
|
|
37
37
|
# Symlink the tool (must be in a subdirectory with index.ts)
|
|
38
38
|
mkdir -p ~/.pi/agent/tools/subagent
|
|
39
|
-
ln -sf "$(pwd)/packages/coding-agent/examples/custom-tools/subagent/
|
|
39
|
+
ln -sf "$(pwd)/packages/coding-agent/examples/custom-tools/subagent/index.ts" ~/.pi/agent/tools/subagent/index.ts
|
|
40
40
|
ln -sf "$(pwd)/packages/coding-agent/examples/custom-tools/subagent/agents.ts" ~/.pi/agent/tools/subagent/agents.ts
|
|
41
41
|
|
|
42
42
|
# Symlink agents
|
package/examples/hooks/snake.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { HookAPI } from "@mariozechner/pi-coding-agent";
|
|
6
|
-
import {
|
|
6
|
+
import { matchesKey, visibleWidth } from "@mariozechner/pi-tui";
|
|
7
7
|
|
|
8
8
|
const GAME_WIDTH = 40;
|
|
9
9
|
const GAME_HEIGHT = 15;
|
|
@@ -150,7 +150,7 @@ class SnakeComponent {
|
|
|
150
150
|
handleInput(data: string): void {
|
|
151
151
|
// If paused (resuming), wait for any key
|
|
152
152
|
if (this.paused) {
|
|
153
|
-
if (
|
|
153
|
+
if (matchesKey(data, "escape") || data === "q" || data === "Q") {
|
|
154
154
|
// Quit without clearing save
|
|
155
155
|
this.dispose();
|
|
156
156
|
this.onClose();
|
|
@@ -163,7 +163,7 @@ class SnakeComponent {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
// ESC to pause and save
|
|
166
|
-
if (
|
|
166
|
+
if (matchesKey(data, "escape")) {
|
|
167
167
|
this.dispose();
|
|
168
168
|
this.onSave(this.state);
|
|
169
169
|
this.onClose();
|
|
@@ -179,13 +179,13 @@ class SnakeComponent {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
// Arrow keys or WASD
|
|
182
|
-
if (
|
|
182
|
+
if (matchesKey(data, "up") || data === "w" || data === "W") {
|
|
183
183
|
if (this.state.direction !== "down") this.state.nextDirection = "up";
|
|
184
|
-
} else if (
|
|
184
|
+
} else if (matchesKey(data, "down") || data === "s" || data === "S") {
|
|
185
185
|
if (this.state.direction !== "up") this.state.nextDirection = "down";
|
|
186
|
-
} else if (
|
|
186
|
+
} else if (matchesKey(data, "right") || data === "d" || data === "D") {
|
|
187
187
|
if (this.state.direction !== "left") this.state.nextDirection = "right";
|
|
188
|
-
} else if (
|
|
188
|
+
} else if (matchesKey(data, "left") || data === "a" || data === "A") {
|
|
189
189
|
if (this.state.direction !== "right") this.state.nextDirection = "left";
|
|
190
190
|
}
|
|
191
191
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { HookAPI, Theme } from "@mariozechner/pi-coding-agent";
|
|
9
|
-
import {
|
|
9
|
+
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
10
10
|
|
|
11
11
|
interface Todo {
|
|
12
12
|
id: number;
|
|
@@ -35,7 +35,7 @@ class TodoListComponent {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
handleInput(data: string): void {
|
|
38
|
-
if (
|
|
38
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
39
39
|
this.onClose();
|
|
40
40
|
}
|
|
41
41
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mariozechner/pi-coding-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"piConfig": {
|
|
@@ -38,9 +38,10 @@
|
|
|
38
38
|
"prepublishOnly": "npm run clean && npm run build"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@
|
|
42
|
-
"@mariozechner/pi-
|
|
43
|
-
"@mariozechner/pi-
|
|
41
|
+
"@crosscopy/clipboard": "^0.2.8",
|
|
42
|
+
"@mariozechner/pi-agent-core": "^0.33.0",
|
|
43
|
+
"@mariozechner/pi-ai": "^0.33.0",
|
|
44
|
+
"@mariozechner/pi-tui": "^0.33.0",
|
|
44
45
|
"chalk": "^5.5.0",
|
|
45
46
|
"cli-highlight": "^2.1.11",
|
|
46
47
|
"diff": "^8.0.2",
|