@mariozechner/pi-coding-agent 0.45.3 → 0.45.5
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 +28 -0
- package/README.md +2 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +1 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +7 -9
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/model-registry.d.ts +4 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +6 -0
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +1 -0
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +7 -5
- package/dist/core/sdk.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +3 -4
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/light.json +9 -9
- package/dist/utils/image-convert.d.ts.map +1 -1
- package/dist/utils/image-convert.js +11 -4
- package/dist/utils/image-convert.js.map +1 -1
- package/dist/utils/image-resize.d.ts +1 -1
- package/dist/utils/image-resize.d.ts.map +1 -1
- package/dist/utils/image-resize.js +47 -25
- package/dist/utils/image-resize.js.map +1 -1
- package/dist/utils/vips.d.ts +11 -0
- package/dist/utils/vips.d.ts.map +1 -0
- package/dist/utils/vips.js +35 -0
- package/dist/utils/vips.js.map +1 -0
- package/docs/extensions.md +18 -17
- package/docs/sdk.md +21 -48
- package/examples/README.md +5 -2
- package/examples/extensions/README.md +19 -2
- package/examples/extensions/plan-mode/README.md +65 -0
- package/examples/extensions/plan-mode/index.ts +340 -0
- package/examples/extensions/plan-mode/utils.ts +168 -0
- package/examples/extensions/question.ts +211 -13
- package/examples/extensions/questionnaire.ts +427 -0
- package/examples/extensions/summarize.ts +195 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/README.md +3 -4
- package/package.json +5 -5
- package/examples/extensions/plan-mode.ts +0 -548
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/badlogic/pi-mono/main/packages/coding-agent/theme-schema.json",
|
|
3
3
|
"name": "light",
|
|
4
4
|
"vars": {
|
|
5
|
-
"teal": "#
|
|
6
|
-
"blue": "#
|
|
7
|
-
"green": "#
|
|
8
|
-
"red": "#
|
|
9
|
-
"yellow": "#
|
|
5
|
+
"teal": "#5a8080",
|
|
6
|
+
"blue": "#547da7",
|
|
7
|
+
"green": "#588458",
|
|
8
|
+
"red": "#aa5555",
|
|
9
|
+
"yellow": "#9a7326",
|
|
10
10
|
"mediumGray": "#6c6c6c",
|
|
11
|
-
"dimGray": "#
|
|
11
|
+
"dimGray": "#767676",
|
|
12
12
|
"lightGray": "#b0b0b0",
|
|
13
13
|
"selectedBg": "#d0d0e0",
|
|
14
14
|
"userMsgBg": "#e8e8e8",
|
|
@@ -68,9 +68,9 @@
|
|
|
68
68
|
"syntaxPunctuation": "#000000",
|
|
69
69
|
|
|
70
70
|
"thinkingOff": "lightGray",
|
|
71
|
-
"thinkingMinimal": "#
|
|
72
|
-
"thinkingLow": "
|
|
73
|
-
"thinkingMedium": "
|
|
71
|
+
"thinkingMinimal": "#767676",
|
|
72
|
+
"thinkingLow": "blue",
|
|
73
|
+
"thinkingMedium": "teal",
|
|
74
74
|
"thinkingHigh": "#875f87",
|
|
75
75
|
"thinkingXhigh": "#8b008b",
|
|
76
76
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-convert.d.ts","sourceRoot":"","sources":["../../src/utils/image-convert.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"image-convert.d.ts","sourceRoot":"","sources":["../../src/utils/image-convert.ts"],"names":[],"mappings":"AAEA;;;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,CAyBpD","sourcesContent":["import { getVips } from \"./vips.js\";\n\n/**\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\tconst vips = await getVips();\n\tif (!vips) {\n\t\t// wasm-vips not available\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst buffer = Buffer.from(base64Data, \"base64\");\n\t\tconst img = vips.Image.newFromBuffer(buffer);\n\t\tconst pngBuffer = img.writeToBuffer(\".png\");\n\t\timg.delete();\n\t\treturn {\n\t\t\tdata: Buffer.from(pngBuffer).toString(\"base64\"),\n\t\t\tmimeType: \"image/png\",\n\t\t};\n\t} catch {\n\t\t// Conversion failed\n\t\treturn null;\n\t}\n}\n"]}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getVips } from "./vips.js";
|
|
1
2
|
/**
|
|
2
3
|
* Convert image to PNG format for terminal display.
|
|
3
4
|
* Kitty graphics protocol requires PNG format (f=100).
|
|
@@ -7,17 +8,23 @@ export async function convertToPng(base64Data, mimeType) {
|
|
|
7
8
|
if (mimeType === "image/png") {
|
|
8
9
|
return { data: base64Data, mimeType };
|
|
9
10
|
}
|
|
11
|
+
const vips = await getVips();
|
|
12
|
+
if (!vips) {
|
|
13
|
+
// wasm-vips not available
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
10
16
|
try {
|
|
11
|
-
const sharp = (await import("sharp")).default;
|
|
12
17
|
const buffer = Buffer.from(base64Data, "base64");
|
|
13
|
-
const
|
|
18
|
+
const img = vips.Image.newFromBuffer(buffer);
|
|
19
|
+
const pngBuffer = img.writeToBuffer(".png");
|
|
20
|
+
img.delete();
|
|
14
21
|
return {
|
|
15
|
-
data: pngBuffer.toString("base64"),
|
|
22
|
+
data: Buffer.from(pngBuffer).toString("base64"),
|
|
16
23
|
mimeType: "image/png",
|
|
17
24
|
};
|
|
18
25
|
}
|
|
19
26
|
catch {
|
|
20
|
-
//
|
|
27
|
+
// Conversion failed
|
|
21
28
|
return null;
|
|
22
29
|
}
|
|
23
30
|
}
|
|
@@ -1 +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,
|
|
1
|
+
{"version":3,"file":"image-convert.js","sourceRoot":"","sources":["../../src/utils/image-convert.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC;;;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,MAAM,IAAI,GAAG,MAAM,OAAO,EAAE,CAAC;IAC7B,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,0BAA0B;QAC1B,OAAO,IAAI,CAAC;IACb,CAAC;IAED,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QACjD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QAC7C,MAAM,SAAS,GAAG,GAAG,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QAC5C,GAAG,CAAC,MAAM,EAAE,CAAC;QACb,OAAO;YACN,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAC/C,QAAQ,EAAE,WAAW;SACrB,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACR,oBAAoB;QACpB,OAAO,IAAI,CAAC;IACb,CAAC;AAAA,CACD","sourcesContent":["import { getVips } from \"./vips.js\";\n\n/**\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\tconst vips = await getVips();\n\tif (!vips) {\n\t\t// wasm-vips not available\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst buffer = Buffer.from(base64Data, \"base64\");\n\t\tconst img = vips.Image.newFromBuffer(buffer);\n\t\tconst pngBuffer = img.writeToBuffer(\".png\");\n\t\timg.delete();\n\t\treturn {\n\t\t\tdata: Buffer.from(pngBuffer).toString(\"base64\"),\n\t\t\tmimeType: \"image/png\",\n\t\t};\n\t} catch {\n\t\t// Conversion failed\n\t\treturn null;\n\t}\n}\n"]}
|
|
@@ -18,7 +18,7 @@ export interface ResizedImage {
|
|
|
18
18
|
* Resize an image to fit within the specified max dimensions and file size.
|
|
19
19
|
* Returns the original image if it already fits within the limits.
|
|
20
20
|
*
|
|
21
|
-
* Uses
|
|
21
|
+
* Uses wasm-vips for image processing. If wasm-vips is not available (e.g., in some
|
|
22
22
|
* environments), returns the original image unchanged.
|
|
23
23
|
*
|
|
24
24
|
* Strategy for staying under maxBytes:
|
|
@@ -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;
|
|
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;AAGxD,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,CA8KxG;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,GAAG,SAAS,CAO5E","sourcesContent":["import type { ImageContent } from \"@mariozechner/pi-ai\";\nimport { getVips } from \"./vips.js\";\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: Uint8Array; mimeType: string },\n\tb: { buffer: Uint8Array; mimeType: string },\n): { buffer: Uint8Array; 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 wasm-vips for image processing. If wasm-vips 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\tconst vipsOrNull = await getVips();\n\tif (!vipsOrNull) {\n\t\t// wasm-vips not available - return original image\n\t\t// We can't get dimensions without vips, 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\t// Capture non-null reference for use in nested functions\n\tconst vips = vipsOrNull;\n\n\t// Load image to get metadata\n\tlet sourceImg: InstanceType<typeof vips.Image>;\n\ttry {\n\t\tsourceImg = vips.Image.newFromBuffer(buffer);\n\t} catch {\n\t\t// Failed to load image\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 originalWidth = sourceImg.width;\n\tconst originalHeight = sourceImg.height;\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\tsourceImg.delete();\n\t\tconst format = img.mimeType?.split(\"/\")[1] ?? \"png\";\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\tfunction tryBothFormats(\n\t\twidth: number,\n\t\theight: number,\n\t\tjpegQuality: number,\n\t): { buffer: Uint8Array; mimeType: string } {\n\t\t// Load image fresh and resize using scale factor\n\t\t// (Using newFromBuffer + resize instead of thumbnailBuffer to avoid lazy re-read issues)\n\t\tconst img = vips.Image.newFromBuffer(buffer);\n\t\tconst scale = Math.min(width / img.width, height / img.height);\n\t\tconst resized = scale < 1 ? img.resize(scale) : img;\n\n\t\tconst pngBuffer = resized.writeToBuffer(\".png\");\n\t\tconst jpegBuffer = resized.writeToBuffer(\".jpg\", { Q: jpegQuality });\n\n\t\tif (resized !== img) {\n\t\t\tresized.delete();\n\t\t}\n\t\timg.delete();\n\n\t\treturn pickSmaller({ buffer: pngBuffer, mimeType: \"image/png\" }, { buffer: jpegBuffer, mimeType: \"image/jpeg\" });\n\t}\n\n\t// Clean up the source image\n\tsourceImg.delete();\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: Uint8Array; mimeType: string };\n\tlet finalWidth = targetWidth;\n\tlet finalHeight = targetHeight;\n\n\t// First attempt: resize to target dimensions, try both formats\n\tbest = tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);\n\n\tif (best.buffer.length <= opts.maxBytes) {\n\t\treturn {\n\t\t\tdata: Buffer.from(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 = tryBothFormats(targetWidth, targetHeight, quality);\n\n\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\treturn {\n\t\t\t\tdata: Buffer.from(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 = 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: Buffer.from(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: Buffer.from(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,3 +1,4 @@
|
|
|
1
|
+
import { getVips } from "./vips.js";
|
|
1
2
|
// 4.5MB - provides headroom below Anthropic's 5MB limit
|
|
2
3
|
const DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024;
|
|
3
4
|
const DEFAULT_OPTIONS = {
|
|
@@ -14,7 +15,7 @@ function pickSmaller(a, b) {
|
|
|
14
15
|
* Resize an image to fit within the specified max dimensions and file size.
|
|
15
16
|
* Returns the original image if it already fits within the limits.
|
|
16
17
|
*
|
|
17
|
-
* Uses
|
|
18
|
+
* Uses wasm-vips for image processing. If wasm-vips is not available (e.g., in some
|
|
18
19
|
* environments), returns the original image unchanged.
|
|
19
20
|
*
|
|
20
21
|
* Strategy for staying under maxBytes:
|
|
@@ -26,13 +27,29 @@ function pickSmaller(a, b) {
|
|
|
26
27
|
export async function resizeImage(img, options) {
|
|
27
28
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
28
29
|
const buffer = Buffer.from(img.data, "base64");
|
|
29
|
-
|
|
30
|
+
const vipsOrNull = await getVips();
|
|
31
|
+
if (!vipsOrNull) {
|
|
32
|
+
// wasm-vips not available - return original image
|
|
33
|
+
// We can't get dimensions without vips, so return 0s
|
|
34
|
+
return {
|
|
35
|
+
data: img.data,
|
|
36
|
+
mimeType: img.mimeType,
|
|
37
|
+
originalWidth: 0,
|
|
38
|
+
originalHeight: 0,
|
|
39
|
+
width: 0,
|
|
40
|
+
height: 0,
|
|
41
|
+
wasResized: false,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Capture non-null reference for use in nested functions
|
|
45
|
+
const vips = vipsOrNull;
|
|
46
|
+
// Load image to get metadata
|
|
47
|
+
let sourceImg;
|
|
30
48
|
try {
|
|
31
|
-
|
|
49
|
+
sourceImg = vips.Image.newFromBuffer(buffer);
|
|
32
50
|
}
|
|
33
51
|
catch {
|
|
34
|
-
//
|
|
35
|
-
// We can't get dimensions without sharp, so return 0s
|
|
52
|
+
// Failed to load image
|
|
36
53
|
return {
|
|
37
54
|
data: img.data,
|
|
38
55
|
mimeType: img.mimeType,
|
|
@@ -43,14 +60,13 @@ export async function resizeImage(img, options) {
|
|
|
43
60
|
wasResized: false,
|
|
44
61
|
};
|
|
45
62
|
}
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const originalWidth = metadata.width ?? 0;
|
|
49
|
-
const originalHeight = metadata.height ?? 0;
|
|
50
|
-
const format = metadata.format ?? img.mimeType?.split("/")[1] ?? "png";
|
|
63
|
+
const originalWidth = sourceImg.width;
|
|
64
|
+
const originalHeight = sourceImg.height;
|
|
51
65
|
// Check if already within all limits (dimensions AND size)
|
|
52
66
|
const originalSize = buffer.length;
|
|
53
67
|
if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {
|
|
68
|
+
sourceImg.delete();
|
|
69
|
+
const format = img.mimeType?.split("/")[1] ?? "png";
|
|
54
70
|
return {
|
|
55
71
|
data: img.data,
|
|
56
72
|
mimeType: img.mimeType ?? `image/${format}`,
|
|
@@ -73,16 +89,22 @@ export async function resizeImage(img, options) {
|
|
|
73
89
|
targetHeight = opts.maxHeight;
|
|
74
90
|
}
|
|
75
91
|
// Helper to resize and encode in both formats, returning the smaller one
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
function tryBothFormats(width, height, jpegQuality) {
|
|
93
|
+
// Load image fresh and resize using scale factor
|
|
94
|
+
// (Using newFromBuffer + resize instead of thumbnailBuffer to avoid lazy re-read issues)
|
|
95
|
+
const img = vips.Image.newFromBuffer(buffer);
|
|
96
|
+
const scale = Math.min(width / img.width, height / img.height);
|
|
97
|
+
const resized = scale < 1 ? img.resize(scale) : img;
|
|
98
|
+
const pngBuffer = resized.writeToBuffer(".png");
|
|
99
|
+
const jpegBuffer = resized.writeToBuffer(".jpg", { Q: jpegQuality });
|
|
100
|
+
if (resized !== img) {
|
|
101
|
+
resized.delete();
|
|
102
|
+
}
|
|
103
|
+
img.delete();
|
|
84
104
|
return pickSmaller({ buffer: pngBuffer, mimeType: "image/png" }, { buffer: jpegBuffer, mimeType: "image/jpeg" });
|
|
85
105
|
}
|
|
106
|
+
// Clean up the source image
|
|
107
|
+
sourceImg.delete();
|
|
86
108
|
// Try to produce an image under maxBytes
|
|
87
109
|
const qualitySteps = [85, 70, 55, 40];
|
|
88
110
|
const scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25];
|
|
@@ -90,10 +112,10 @@ export async function resizeImage(img, options) {
|
|
|
90
112
|
let finalWidth = targetWidth;
|
|
91
113
|
let finalHeight = targetHeight;
|
|
92
114
|
// First attempt: resize to target dimensions, try both formats
|
|
93
|
-
best =
|
|
115
|
+
best = tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);
|
|
94
116
|
if (best.buffer.length <= opts.maxBytes) {
|
|
95
117
|
return {
|
|
96
|
-
data: best.buffer.toString("base64"),
|
|
118
|
+
data: Buffer.from(best.buffer).toString("base64"),
|
|
97
119
|
mimeType: best.mimeType,
|
|
98
120
|
originalWidth,
|
|
99
121
|
originalHeight,
|
|
@@ -104,10 +126,10 @@ export async function resizeImage(img, options) {
|
|
|
104
126
|
}
|
|
105
127
|
// Still too large - try JPEG with decreasing quality (and compare to PNG each time)
|
|
106
128
|
for (const quality of qualitySteps) {
|
|
107
|
-
best =
|
|
129
|
+
best = tryBothFormats(targetWidth, targetHeight, quality);
|
|
108
130
|
if (best.buffer.length <= opts.maxBytes) {
|
|
109
131
|
return {
|
|
110
|
-
data: best.buffer.toString("base64"),
|
|
132
|
+
data: Buffer.from(best.buffer).toString("base64"),
|
|
111
133
|
mimeType: best.mimeType,
|
|
112
134
|
originalWidth,
|
|
113
135
|
originalHeight,
|
|
@@ -126,10 +148,10 @@ export async function resizeImage(img, options) {
|
|
|
126
148
|
break;
|
|
127
149
|
}
|
|
128
150
|
for (const quality of qualitySteps) {
|
|
129
|
-
best =
|
|
151
|
+
best = tryBothFormats(finalWidth, finalHeight, quality);
|
|
130
152
|
if (best.buffer.length <= opts.maxBytes) {
|
|
131
153
|
return {
|
|
132
|
-
data: best.buffer.toString("base64"),
|
|
154
|
+
data: Buffer.from(best.buffer).toString("base64"),
|
|
133
155
|
mimeType: best.mimeType,
|
|
134
156
|
originalWidth,
|
|
135
157
|
originalHeight,
|
|
@@ -143,7 +165,7 @@ export async function resizeImage(img, options) {
|
|
|
143
165
|
// Last resort: return smallest version we produced even if over limit
|
|
144
166
|
// (the API will reject it, but at least we tried everything)
|
|
145
167
|
return {
|
|
146
|
-
data: best.buffer.toString("base64"),
|
|
168
|
+
data: Buffer.from(best.buffer).toString("base64"),
|
|
147
169
|
mimeType: best.mimeType,
|
|
148
170
|
originalWidth,
|
|
149
171
|
originalHeight,
|
|
@@ -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\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
|
+
{"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\";\nimport { getVips } from \"./vips.js\";\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: Uint8Array; mimeType: string },\n\tb: { buffer: Uint8Array; mimeType: string },\n): { buffer: Uint8Array; 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 wasm-vips for image processing. If wasm-vips 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\tconst vipsOrNull = await getVips();\n\tif (!vipsOrNull) {\n\t\t// wasm-vips not available - return original image\n\t\t// We can't get dimensions without vips, 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\t// Capture non-null reference for use in nested functions\n\tconst vips = vipsOrNull;\n\n\t// Load image to get metadata\n\tlet sourceImg: InstanceType<typeof vips.Image>;\n\ttry {\n\t\tsourceImg = vips.Image.newFromBuffer(buffer);\n\t} catch {\n\t\t// Failed to load image\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 originalWidth = sourceImg.width;\n\tconst originalHeight = sourceImg.height;\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\tsourceImg.delete();\n\t\tconst format = img.mimeType?.split(\"/\")[1] ?? \"png\";\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\tfunction tryBothFormats(\n\t\twidth: number,\n\t\theight: number,\n\t\tjpegQuality: number,\n\t): { buffer: Uint8Array; mimeType: string } {\n\t\t// Load image fresh and resize using scale factor\n\t\t// (Using newFromBuffer + resize instead of thumbnailBuffer to avoid lazy re-read issues)\n\t\tconst img = vips.Image.newFromBuffer(buffer);\n\t\tconst scale = Math.min(width / img.width, height / img.height);\n\t\tconst resized = scale < 1 ? img.resize(scale) : img;\n\n\t\tconst pngBuffer = resized.writeToBuffer(\".png\");\n\t\tconst jpegBuffer = resized.writeToBuffer(\".jpg\", { Q: jpegQuality });\n\n\t\tif (resized !== img) {\n\t\t\tresized.delete();\n\t\t}\n\t\timg.delete();\n\n\t\treturn pickSmaller({ buffer: pngBuffer, mimeType: \"image/png\" }, { buffer: jpegBuffer, mimeType: \"image/jpeg\" });\n\t}\n\n\t// Clean up the source image\n\tsourceImg.delete();\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: Uint8Array; mimeType: string };\n\tlet finalWidth = targetWidth;\n\tlet finalHeight = targetHeight;\n\n\t// First attempt: resize to target dimensions, try both formats\n\tbest = tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);\n\n\tif (best.buffer.length <= opts.maxBytes) {\n\t\treturn {\n\t\t\tdata: Buffer.from(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 = tryBothFormats(targetWidth, targetHeight, quality);\n\n\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\treturn {\n\t\t\t\tdata: Buffer.from(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 = 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: Buffer.from(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: Buffer.from(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"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Singleton wrapper for wasm-vips initialization.
|
|
3
|
+
* wasm-vips requires async initialization, so we cache the instance.
|
|
4
|
+
*/
|
|
5
|
+
import type Vips from "wasm-vips";
|
|
6
|
+
/**
|
|
7
|
+
* Get the initialized wasm-vips instance.
|
|
8
|
+
* Returns null if wasm-vips is not available or fails to initialize.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getVips(): Promise<Awaited<ReturnType<typeof Vips>> | null>;
|
|
11
|
+
//# sourceMappingURL=vips.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vips.d.ts","sourceRoot":"","sources":["../../src/utils/vips.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAKlC;;;GAGG;AACH,wBAAsB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC,GAAG,IAAI,CAAC,CAyBhF","sourcesContent":["/**\n * Singleton wrapper for wasm-vips initialization.\n * wasm-vips requires async initialization, so we cache the instance.\n */\n\nimport type Vips from \"wasm-vips\";\n\nlet vipsInstance: Awaited<ReturnType<typeof Vips>> | null = null;\nlet vipsInitPromise: Promise<Awaited<ReturnType<typeof Vips>> | null> | null = null;\n\n/**\n * Get the initialized wasm-vips instance.\n * Returns null if wasm-vips is not available or fails to initialize.\n */\nexport async function getVips(): Promise<Awaited<ReturnType<typeof Vips>> | null> {\n\tif (vipsInstance) {\n\t\treturn vipsInstance;\n\t}\n\n\tif (vipsInitPromise) {\n\t\treturn vipsInitPromise;\n\t}\n\n\tvipsInitPromise = (async () => {\n\t\ttry {\n\t\t\tconst VipsInit = (await import(\"wasm-vips\")).default;\n\t\t\tvipsInstance = await VipsInit();\n\t\t\treturn vipsInstance;\n\t\t} catch {\n\t\t\t// wasm-vips not available\n\t\t\treturn null;\n\t\t}\n\t})();\n\n\tconst result = await vipsInitPromise;\n\tif (!result) {\n\t\tvipsInitPromise = null; // Allow retry on failure\n\t}\n\treturn result;\n}\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Singleton wrapper for wasm-vips initialization.
|
|
3
|
+
* wasm-vips requires async initialization, so we cache the instance.
|
|
4
|
+
*/
|
|
5
|
+
let vipsInstance = null;
|
|
6
|
+
let vipsInitPromise = null;
|
|
7
|
+
/**
|
|
8
|
+
* Get the initialized wasm-vips instance.
|
|
9
|
+
* Returns null if wasm-vips is not available or fails to initialize.
|
|
10
|
+
*/
|
|
11
|
+
export async function getVips() {
|
|
12
|
+
if (vipsInstance) {
|
|
13
|
+
return vipsInstance;
|
|
14
|
+
}
|
|
15
|
+
if (vipsInitPromise) {
|
|
16
|
+
return vipsInitPromise;
|
|
17
|
+
}
|
|
18
|
+
vipsInitPromise = (async () => {
|
|
19
|
+
try {
|
|
20
|
+
const VipsInit = (await import("wasm-vips")).default;
|
|
21
|
+
vipsInstance = await VipsInit();
|
|
22
|
+
return vipsInstance;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// wasm-vips not available
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
})();
|
|
29
|
+
const result = await vipsInitPromise;
|
|
30
|
+
if (!result) {
|
|
31
|
+
vipsInitPromise = null; // Allow retry on failure
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=vips.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vips.js","sourceRoot":"","sources":["../../src/utils/vips.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,IAAI,YAAY,GAA4C,IAAI,CAAC;AACjE,IAAI,eAAe,GAA4D,IAAI,CAAC;AAEpF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,GAAqD;IACjF,IAAI,YAAY,EAAE,CAAC;QAClB,OAAO,YAAY,CAAC;IACrB,CAAC;IAED,IAAI,eAAe,EAAE,CAAC;QACrB,OAAO,eAAe,CAAC;IACxB,CAAC;IAED,eAAe,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC;QAC9B,IAAI,CAAC;YACJ,MAAM,QAAQ,GAAG,CAAC,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC;YACrD,YAAY,GAAG,MAAM,QAAQ,EAAE,CAAC;YAChC,OAAO,YAAY,CAAC;QACrB,CAAC;QAAC,MAAM,CAAC;YACR,0BAA0B;YAC1B,OAAO,IAAI,CAAC;QACb,CAAC;IAAA,CACD,CAAC,EAAE,CAAC;IAEL,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC;IACrC,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,eAAe,GAAG,IAAI,CAAC,CAAC,yBAAyB;IAClD,CAAC;IACD,OAAO,MAAM,CAAC;AAAA,CACd","sourcesContent":["/**\n * Singleton wrapper for wasm-vips initialization.\n * wasm-vips requires async initialization, so we cache the instance.\n */\n\nimport type Vips from \"wasm-vips\";\n\nlet vipsInstance: Awaited<ReturnType<typeof Vips>> | null = null;\nlet vipsInitPromise: Promise<Awaited<ReturnType<typeof Vips>> | null> | null = null;\n\n/**\n * Get the initialized wasm-vips instance.\n * Returns null if wasm-vips is not available or fails to initialize.\n */\nexport async function getVips(): Promise<Awaited<ReturnType<typeof Vips>> | null> {\n\tif (vipsInstance) {\n\t\treturn vipsInstance;\n\t}\n\n\tif (vipsInitPromise) {\n\t\treturn vipsInitPromise;\n\t}\n\n\tvipsInitPromise = (async () => {\n\t\ttry {\n\t\t\tconst VipsInit = (await import(\"wasm-vips\")).default;\n\t\t\tvipsInstance = await VipsInit();\n\t\t\treturn vipsInstance;\n\t\t} catch {\n\t\t\t// wasm-vips not available\n\t\t\treturn null;\n\t\t}\n\t})();\n\n\tconst result = await vipsInitPromise;\n\tif (!result) {\n\t\tvipsInitPromise = null; // Allow retry on failure\n\t}\n\treturn result;\n}\n"]}
|
package/docs/extensions.md
CHANGED
|
@@ -18,6 +18,7 @@ Extensions are TypeScript modules that extend pi's behavior. They can subscribe
|
|
|
18
18
|
- Git checkpointing (stash at each turn, restore on branch)
|
|
19
19
|
- Path protection (block writes to `.env`, `node_modules/`)
|
|
20
20
|
- Custom compaction (summarize conversation your way)
|
|
21
|
+
- Conversation summaries (see `summarize.ts` example)
|
|
21
22
|
- Interactive tools (questions, wizards, custom dialogs)
|
|
22
23
|
- Stateful tools (todo lists, connection pools)
|
|
23
24
|
- External integrations (file watchers, webhooks, CI triggers)
|
|
@@ -438,7 +439,7 @@ pi.on("before_agent_start", async (event, ctx) => {
|
|
|
438
439
|
});
|
|
439
440
|
```
|
|
440
441
|
|
|
441
|
-
**Examples:** [claude-rules.ts](../examples/extensions/claude-rules.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [ssh.ts](../examples/extensions/ssh.ts)
|
|
442
|
+
**Examples:** [claude-rules.ts](../examples/extensions/claude-rules.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [ssh.ts](../examples/extensions/ssh.ts)
|
|
442
443
|
|
|
443
444
|
#### agent_start / agent_end
|
|
444
445
|
|
|
@@ -452,7 +453,7 @@ pi.on("agent_end", async (event, ctx) => {
|
|
|
452
453
|
});
|
|
453
454
|
```
|
|
454
455
|
|
|
455
|
-
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
456
|
+
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
|
|
456
457
|
|
|
457
458
|
#### turn_start / turn_end
|
|
458
459
|
|
|
@@ -468,7 +469,7 @@ pi.on("turn_end", async (event, ctx) => {
|
|
|
468
469
|
});
|
|
469
470
|
```
|
|
470
471
|
|
|
471
|
-
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [status-line.ts](../examples/extensions/status-line.ts)
|
|
472
|
+
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [status-line.ts](../examples/extensions/status-line.ts)
|
|
472
473
|
|
|
473
474
|
#### context
|
|
474
475
|
|
|
@@ -482,7 +483,7 @@ pi.on("context", async (event, ctx) => {
|
|
|
482
483
|
});
|
|
483
484
|
```
|
|
484
485
|
|
|
485
|
-
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
486
|
+
**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
|
|
486
487
|
|
|
487
488
|
### Model Events
|
|
488
489
|
|
|
@@ -527,7 +528,7 @@ pi.on("tool_call", async (event, ctx) => {
|
|
|
527
528
|
});
|
|
528
529
|
```
|
|
529
530
|
|
|
530
|
-
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [protected-paths.ts](../examples/extensions/protected-paths.ts)
|
|
531
|
+
**Examples:** [chalk-logger.ts](../examples/extensions/chalk-logger.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [protected-paths.ts](../examples/extensions/protected-paths.ts)
|
|
531
532
|
|
|
532
533
|
#### tool_result
|
|
533
534
|
|
|
@@ -549,7 +550,7 @@ pi.on("tool_result", async (event, ctx) => {
|
|
|
549
550
|
});
|
|
550
551
|
```
|
|
551
552
|
|
|
552
|
-
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
553
|
+
**Examples:** [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
|
|
553
554
|
|
|
554
555
|
### User Bash Events
|
|
555
556
|
|
|
@@ -723,7 +724,7 @@ pi.registerTool({
|
|
|
723
724
|
});
|
|
724
725
|
```
|
|
725
726
|
|
|
726
|
-
**Examples:** [hello.ts](../examples/extensions/hello.ts), [question.ts](../examples/extensions/question.ts), [todo.ts](../examples/extensions/todo.ts), [truncated-tool.ts](../examples/extensions/truncated-tool.ts)
|
|
727
|
+
**Examples:** [hello.ts](../examples/extensions/hello.ts), [question.ts](../examples/extensions/question.ts), [questionnaire.ts](../examples/extensions/questionnaire.ts), [todo.ts](../examples/extensions/todo.ts), [truncated-tool.ts](../examples/extensions/truncated-tool.ts)
|
|
727
728
|
|
|
728
729
|
### pi.sendMessage(message, options?)
|
|
729
730
|
|
|
@@ -748,7 +749,7 @@ pi.sendMessage({
|
|
|
748
749
|
- `"nextTurn"` - Queued for next user prompt. Does not interrupt or trigger anything.
|
|
749
750
|
- `triggerTurn: true` - If agent is idle, trigger an LLM response immediately. Only applies to `"steer"` and `"followUp"` modes (ignored for `"nextTurn"`).
|
|
750
751
|
|
|
751
|
-
**Examples:** [file-trigger.ts](../examples/extensions/file-trigger.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
752
|
+
**Examples:** [file-trigger.ts](../examples/extensions/file-trigger.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
|
|
752
753
|
|
|
753
754
|
### pi.sendUserMessage(content, options?)
|
|
754
755
|
|
|
@@ -795,7 +796,7 @@ pi.on("session_start", async (_event, ctx) => {
|
|
|
795
796
|
});
|
|
796
797
|
```
|
|
797
798
|
|
|
798
|
-
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [snake.ts](../examples/extensions/snake.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
799
|
+
**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [snake.ts](../examples/extensions/snake.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
799
800
|
|
|
800
801
|
### pi.setSessionName(name)
|
|
801
802
|
|
|
@@ -830,7 +831,7 @@ pi.registerCommand("stats", {
|
|
|
830
831
|
});
|
|
831
832
|
```
|
|
832
833
|
|
|
833
|
-
**Examples:** [custom-footer.ts](../examples/extensions/custom-footer.ts), [custom-header.ts](../examples/extensions/custom-header.ts), [handoff.ts](../examples/extensions/handoff.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [send-user-message.ts](../examples/extensions/send-user-message.ts), [snake.ts](../examples/extensions/snake.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
834
|
+
**Examples:** [custom-footer.ts](../examples/extensions/custom-footer.ts), [custom-header.ts](../examples/extensions/custom-header.ts), [handoff.ts](../examples/extensions/handoff.ts), [pirate.ts](../examples/extensions/pirate.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [send-user-message.ts](../examples/extensions/send-user-message.ts), [snake.ts](../examples/extensions/snake.ts), [summarize.ts](../examples/extensions/summarize.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
834
835
|
|
|
835
836
|
### pi.registerMessageRenderer(customType, renderer)
|
|
836
837
|
|
|
@@ -849,7 +850,7 @@ pi.registerShortcut("ctrl+shift+p", {
|
|
|
849
850
|
});
|
|
850
851
|
```
|
|
851
852
|
|
|
852
|
-
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
|
|
853
|
+
**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts)
|
|
853
854
|
|
|
854
855
|
### pi.registerFlag(name, options)
|
|
855
856
|
|
|
@@ -868,7 +869,7 @@ if (pi.getFlag("--plan")) {
|
|
|
868
869
|
}
|
|
869
870
|
```
|
|
870
871
|
|
|
871
|
-
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts)
|
|
872
|
+
**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts)
|
|
872
873
|
|
|
873
874
|
### pi.exec(command, args, options?)
|
|
874
875
|
|
|
@@ -892,7 +893,7 @@ const names = all.map(t => t.name); // Just names if needed
|
|
|
892
893
|
pi.setActiveTools(["read", "bash"]); // Switch to read-only
|
|
893
894
|
```
|
|
894
895
|
|
|
895
|
-
**Examples:** [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
896
|
+
**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)
|
|
896
897
|
|
|
897
898
|
### pi.setModel(model)
|
|
898
899
|
|
|
@@ -1243,7 +1244,7 @@ ctx.ui.notify("Done!", "info"); // "info" | "warning" | "error"
|
|
|
1243
1244
|
```
|
|
1244
1245
|
|
|
1245
1246
|
**Examples:**
|
|
1246
|
-
- `ctx.ui.select()`: [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [question.ts](../examples/extensions/question.ts)
|
|
1247
|
+
- `ctx.ui.select()`: [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts), [dirty-repo-guard.ts](../examples/extensions/dirty-repo-guard.ts), [git-checkpoint.ts](../examples/extensions/git-checkpoint.ts), [permission-gate.ts](../examples/extensions/permission-gate.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [question.ts](../examples/extensions/question.ts), [questionnaire.ts](../examples/extensions/questionnaire.ts)
|
|
1247
1248
|
- `ctx.ui.confirm()`: [confirm-destructive.ts](../examples/extensions/confirm-destructive.ts)
|
|
1248
1249
|
- `ctx.ui.editor()`: [handoff.ts](../examples/extensions/handoff.ts)
|
|
1249
1250
|
- `ctx.ui.setEditorText()`: [handoff.ts](../examples/extensions/handoff.ts), [qna.ts](../examples/extensions/qna.ts)
|
|
@@ -1345,8 +1346,8 @@ ctx.ui.theme.fg("accent", "styled text"); // Access current theme
|
|
|
1345
1346
|
```
|
|
1346
1347
|
|
|
1347
1348
|
**Examples:**
|
|
1348
|
-
- `ctx.ui.setStatus()`: [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [status-line.ts](../examples/extensions/status-line.ts)
|
|
1349
|
-
- `ctx.ui.setWidget()`: [plan-mode.ts](../examples/extensions/plan-mode.ts)
|
|
1349
|
+
- `ctx.ui.setStatus()`: [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [status-line.ts](../examples/extensions/status-line.ts)
|
|
1350
|
+
- `ctx.ui.setWidget()`: [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
|
|
1350
1351
|
- `ctx.ui.setFooter()`: [custom-footer.ts](../examples/extensions/custom-footer.ts)
|
|
1351
1352
|
- `ctx.ui.setHeader()`: [custom-header.ts](../examples/extensions/custom-header.ts)
|
|
1352
1353
|
- `ctx.ui.setEditorComponent()`: [modal-editor.ts](../examples/extensions/modal-editor.ts)
|
|
@@ -1397,7 +1398,7 @@ const result = await ctx.ui.custom<string | null>(
|
|
|
1397
1398
|
|
|
1398
1399
|
Overlay components should define a `width` property to control their size. The overlay is centered by default. See [overlay-test.ts](../examples/extensions/overlay-test.ts) for a complete example.
|
|
1399
1400
|
|
|
1400
|
-
**Examples:** [handoff.ts](../examples/extensions/handoff.ts), [plan-mode.ts](../examples/extensions/plan-mode.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [snake.ts](../examples/extensions/snake.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts), [overlay-test.ts](../examples/extensions/overlay-test.ts)
|
|
1401
|
+
**Examples:** [handoff.ts](../examples/extensions/handoff.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts), [qna.ts](../examples/extensions/qna.ts), [snake.ts](../examples/extensions/snake.ts), [summarize.ts](../examples/extensions/summarize.ts), [todo.ts](../examples/extensions/todo.ts), [tools.ts](../examples/extensions/tools.ts), [overlay-test.ts](../examples/extensions/overlay-test.ts)
|
|
1401
1402
|
|
|
1402
1403
|
### Custom Editor
|
|
1403
1404
|
|