@mariozechner/pi-coding-agent 0.45.7 → 0.47.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 +59 -0
- package/README.md +24 -3
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +11 -3
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +81 -14
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +6 -5
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/export-html/ansi-to-html.d.ts +22 -0
- package/dist/core/export-html/ansi-to-html.d.ts.map +1 -0
- package/dist/core/export-html/ansi-to-html.js +249 -0
- package/dist/core/export-html/ansi-to-html.js.map +1 -0
- package/dist/core/export-html/index.d.ts +17 -0
- package/dist/core/export-html/index.d.ts.map +1 -1
- package/dist/core/export-html/index.js +52 -23
- package/dist/core/export-html/index.js.map +1 -1
- package/dist/core/export-html/template.css +0 -33
- package/dist/core/export-html/template.js +171 -18
- package/dist/core/export-html/tool-renderer.d.ts +35 -0
- package/dist/core/export-html/tool-renderer.d.ts.map +1 -0
- package/dist/core/export-html/tool-renderer.js +57 -0
- package/dist/core/export-html/tool-renderer.js.map +1 -0
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +5 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +41 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +24 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/keybindings.d.ts +1 -5
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +4 -12
- package/dist/core/keybindings.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/prompt-templates.d.ts.map +1 -1
- package/dist/core/prompt-templates.js +4 -27
- package/dist/core/prompt-templates.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +6 -37
- package/dist/core/skills.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +19 -14
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +30 -0
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +82 -10
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +16 -13
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/path-utils.d.ts +1 -0
- package/dist/core/tools/path-utils.d.ts.map +1 -1
- package/dist/core/tools/path-utils.js +7 -0
- package/dist/core/tools/path-utils.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +13 -2
- package/dist/core/tools/read.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +70 -9
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +4 -3
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.js +2 -1
- package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +4 -1
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js +4 -1
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +2 -2
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +5 -5
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts +3 -1
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +15 -9
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-input.js +2 -1
- package/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +6 -1
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -0
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -0
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts +41 -0
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -0
- package/dist/modes/interactive/components/keybinding-hints.js +61 -0
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -0
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +4 -3
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/session-selector-search.d.ts +21 -0
- package/dist/modes/interactive/components/session-selector-search.d.ts.map +1 -0
- package/dist/modes/interactive/components/session-selector-search.js +146 -0
- package/dist/modes/interactive/components/session-selector-search.js.map +1 -0
- package/dist/modes/interactive/components/session-selector.d.ts +7 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +35 -8
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +14 -8
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts +7 -0
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +143 -4
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +4 -5
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +63 -126
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +1 -1
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +1 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/utils/frontmatter.d.ts +8 -0
- package/dist/utils/frontmatter.d.ts.map +1 -0
- package/dist/utils/frontmatter.js +26 -0
- package/dist/utils/frontmatter.js.map +1 -0
- package/dist/utils/image-convert.d.ts.map +1 -1
- package/dist/utils/image-convert.js +12 -14
- package/dist/utils/image-convert.js.map +1 -1
- package/dist/utils/image-resize.d.ts +2 -2
- package/dist/utils/image-resize.d.ts.map +1 -1
- package/dist/utils/image-resize.js +102 -122
- package/dist/utils/image-resize.js.map +1 -1
- package/docs/extensions.md +51 -0
- package/docs/rpc.md +15 -15
- package/docs/tui.md +26 -0
- package/examples/extensions/input-transform.ts +43 -0
- package/examples/extensions/modal-editor.ts +1 -1
- package/examples/extensions/overlay-test.ts +8 -3
- package/examples/extensions/plan-mode/README.md +1 -1
- package/examples/extensions/plan-mode/index.ts +2 -2
- package/examples/extensions/question.ts +1 -1
- package/examples/extensions/questionnaire.ts +1 -1
- package/examples/extensions/rainbow-editor.ts +1 -8
- package/examples/extensions/subagent/agents.ts +3 -32
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +6 -5
- package/dist/utils/vips.d.ts +0 -11
- package/dist/utils/vips.d.ts.map +0 -1
- package/dist/utils/vips.js +0 -35
- package/dist/utils/vips.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import photon from "@silvia-odwyer/photon-node";
|
|
2
2
|
// 4.5MB - provides headroom below Anthropic's 5MB limit
|
|
3
3
|
const DEFAULT_MAX_BYTES = 4.5 * 1024 * 1024;
|
|
4
4
|
const DEFAULT_OPTIONS = {
|
|
@@ -15,8 +15,8 @@ function pickSmaller(a, b) {
|
|
|
15
15
|
* Resize an image to fit within the specified max dimensions and file size.
|
|
16
16
|
* Returns the original image if it already fits within the limits.
|
|
17
17
|
*
|
|
18
|
-
* Uses
|
|
19
|
-
*
|
|
18
|
+
* Uses Photon (Rust/WASM) for image processing. If Photon is not available,
|
|
19
|
+
* returns the original image unchanged.
|
|
20
20
|
*
|
|
21
21
|
* Strategy for staying under maxBytes:
|
|
22
22
|
* 1. First resize to maxWidth/maxHeight
|
|
@@ -26,107 +26,57 @@ function pickSmaller(a, b) {
|
|
|
26
26
|
*/
|
|
27
27
|
export async function resizeImage(img, options) {
|
|
28
28
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
29
|
-
const
|
|
30
|
-
|
|
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;
|
|
29
|
+
const inputBuffer = Buffer.from(img.data, "base64");
|
|
30
|
+
let image;
|
|
48
31
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// Failed to load image
|
|
53
|
-
return {
|
|
54
|
-
data: img.data,
|
|
55
|
-
mimeType: img.mimeType,
|
|
56
|
-
originalWidth: 0,
|
|
57
|
-
originalHeight: 0,
|
|
58
|
-
width: 0,
|
|
59
|
-
height: 0,
|
|
60
|
-
wasResized: false,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
const originalWidth = sourceImg.width;
|
|
64
|
-
const originalHeight = sourceImg.height;
|
|
65
|
-
// Check if already within all limits (dimensions AND size)
|
|
66
|
-
const originalSize = buffer.length;
|
|
67
|
-
if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {
|
|
68
|
-
sourceImg.delete();
|
|
32
|
+
image = photon.PhotonImage.new_from_byteslice(new Uint8Array(inputBuffer));
|
|
33
|
+
const originalWidth = image.get_width();
|
|
34
|
+
const originalHeight = image.get_height();
|
|
69
35
|
const format = img.mimeType?.split("/")[1] ?? "png";
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
let targetHeight = originalHeight;
|
|
83
|
-
if (targetWidth > opts.maxWidth) {
|
|
84
|
-
targetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);
|
|
85
|
-
targetWidth = opts.maxWidth;
|
|
86
|
-
}
|
|
87
|
-
if (targetHeight > opts.maxHeight) {
|
|
88
|
-
targetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);
|
|
89
|
-
targetHeight = opts.maxHeight;
|
|
90
|
-
}
|
|
91
|
-
// Helper to resize and encode in both formats, returning the smaller one
|
|
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();
|
|
36
|
+
// Check if already within all limits (dimensions AND size)
|
|
37
|
+
const originalSize = inputBuffer.length;
|
|
38
|
+
if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {
|
|
39
|
+
return {
|
|
40
|
+
data: img.data,
|
|
41
|
+
mimeType: img.mimeType ?? `image/${format}`,
|
|
42
|
+
originalWidth,
|
|
43
|
+
originalHeight,
|
|
44
|
+
width: originalWidth,
|
|
45
|
+
height: originalHeight,
|
|
46
|
+
wasResized: false,
|
|
47
|
+
};
|
|
102
48
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
best
|
|
49
|
+
// Calculate initial dimensions respecting max limits
|
|
50
|
+
let targetWidth = originalWidth;
|
|
51
|
+
let targetHeight = originalHeight;
|
|
52
|
+
if (targetWidth > opts.maxWidth) {
|
|
53
|
+
targetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);
|
|
54
|
+
targetWidth = opts.maxWidth;
|
|
55
|
+
}
|
|
56
|
+
if (targetHeight > opts.maxHeight) {
|
|
57
|
+
targetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);
|
|
58
|
+
targetHeight = opts.maxHeight;
|
|
59
|
+
}
|
|
60
|
+
// Helper to resize and encode in both formats, returning the smaller one
|
|
61
|
+
function tryBothFormats(width, height, jpegQuality) {
|
|
62
|
+
const resized = photon.resize(image, width, height, photon.SamplingFilter.Lanczos3);
|
|
63
|
+
try {
|
|
64
|
+
const pngBuffer = resized.get_bytes();
|
|
65
|
+
const jpegBuffer = resized.get_bytes_jpeg(jpegQuality);
|
|
66
|
+
return pickSmaller({ buffer: pngBuffer, mimeType: "image/png" }, { buffer: jpegBuffer, mimeType: "image/jpeg" });
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
resized.free();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Try to produce an image under maxBytes
|
|
73
|
+
const qualitySteps = [85, 70, 55, 40];
|
|
74
|
+
const scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25];
|
|
75
|
+
let best;
|
|
76
|
+
let finalWidth = targetWidth;
|
|
77
|
+
let finalHeight = targetHeight;
|
|
78
|
+
// First attempt: resize to target dimensions, try both formats
|
|
79
|
+
best = tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);
|
|
130
80
|
if (best.buffer.length <= opts.maxBytes) {
|
|
131
81
|
return {
|
|
132
82
|
data: Buffer.from(best.buffer).toString("base64"),
|
|
@@ -138,17 +88,9 @@ export async function resizeImage(img, options) {
|
|
|
138
88
|
wasResized: true,
|
|
139
89
|
};
|
|
140
90
|
}
|
|
141
|
-
|
|
142
|
-
// Still too large - reduce dimensions progressively
|
|
143
|
-
for (const scale of scaleSteps) {
|
|
144
|
-
finalWidth = Math.round(targetWidth * scale);
|
|
145
|
-
finalHeight = Math.round(targetHeight * scale);
|
|
146
|
-
// Skip if dimensions are too small
|
|
147
|
-
if (finalWidth < 100 || finalHeight < 100) {
|
|
148
|
-
break;
|
|
149
|
-
}
|
|
91
|
+
// Still too large - try JPEG with decreasing quality
|
|
150
92
|
for (const quality of qualitySteps) {
|
|
151
|
-
best = tryBothFormats(
|
|
93
|
+
best = tryBothFormats(targetWidth, targetHeight, quality);
|
|
152
94
|
if (best.buffer.length <= opts.maxBytes) {
|
|
153
95
|
return {
|
|
154
96
|
data: Buffer.from(best.buffer).toString("base64"),
|
|
@@ -161,18 +103,56 @@ export async function resizeImage(img, options) {
|
|
|
161
103
|
};
|
|
162
104
|
}
|
|
163
105
|
}
|
|
106
|
+
// Still too large - reduce dimensions progressively
|
|
107
|
+
for (const scale of scaleSteps) {
|
|
108
|
+
finalWidth = Math.round(targetWidth * scale);
|
|
109
|
+
finalHeight = Math.round(targetHeight * scale);
|
|
110
|
+
if (finalWidth < 100 || finalHeight < 100) {
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
for (const quality of qualitySteps) {
|
|
114
|
+
best = tryBothFormats(finalWidth, finalHeight, quality);
|
|
115
|
+
if (best.buffer.length <= opts.maxBytes) {
|
|
116
|
+
return {
|
|
117
|
+
data: Buffer.from(best.buffer).toString("base64"),
|
|
118
|
+
mimeType: best.mimeType,
|
|
119
|
+
originalWidth,
|
|
120
|
+
originalHeight,
|
|
121
|
+
width: finalWidth,
|
|
122
|
+
height: finalHeight,
|
|
123
|
+
wasResized: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Last resort: return smallest version we produced
|
|
129
|
+
return {
|
|
130
|
+
data: Buffer.from(best.buffer).toString("base64"),
|
|
131
|
+
mimeType: best.mimeType,
|
|
132
|
+
originalWidth,
|
|
133
|
+
originalHeight,
|
|
134
|
+
width: finalWidth,
|
|
135
|
+
height: finalHeight,
|
|
136
|
+
wasResized: true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Failed to load image
|
|
141
|
+
return {
|
|
142
|
+
data: img.data,
|
|
143
|
+
mimeType: img.mimeType,
|
|
144
|
+
originalWidth: 0,
|
|
145
|
+
originalHeight: 0,
|
|
146
|
+
width: 0,
|
|
147
|
+
height: 0,
|
|
148
|
+
wasResized: false,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
if (image) {
|
|
153
|
+
image.free();
|
|
154
|
+
}
|
|
164
155
|
}
|
|
165
|
-
// Last resort: return smallest version we produced even if over limit
|
|
166
|
-
// (the API will reject it, but at least we tried everything)
|
|
167
|
-
return {
|
|
168
|
-
data: Buffer.from(best.buffer).toString("base64"),
|
|
169
|
-
mimeType: best.mimeType,
|
|
170
|
-
originalWidth,
|
|
171
|
-
originalHeight,
|
|
172
|
-
width: finalWidth,
|
|
173
|
-
height: finalHeight,
|
|
174
|
-
wasResized: true,
|
|
175
|
-
};
|
|
176
156
|
}
|
|
177
157
|
/**
|
|
178
158
|
* Format a dimension note for resized images.
|
|
@@ -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\";\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
|
+
{"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 photon from \"@silvia-odwyer/photon-node\";\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 Photon (Rust/WASM) for image processing. If Photon is not available,\n * 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 inputBuffer = Buffer.from(img.data, \"base64\");\n\n\tlet image: ReturnType<typeof photon.PhotonImage.new_from_byteslice> | undefined;\n\ttry {\n\t\timage = photon.PhotonImage.new_from_byteslice(new Uint8Array(inputBuffer));\n\n\t\tconst originalWidth = image.get_width();\n\t\tconst originalHeight = image.get_height();\n\t\tconst format = img.mimeType?.split(\"/\")[1] ?? \"png\";\n\n\t\t// Check if already within all limits (dimensions AND size)\n\t\tconst originalSize = inputBuffer.length;\n\t\tif (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= opts.maxBytes) {\n\t\t\treturn {\n\t\t\t\tdata: img.data,\n\t\t\t\tmimeType: img.mimeType ?? `image/${format}`,\n\t\t\t\toriginalWidth,\n\t\t\t\toriginalHeight,\n\t\t\t\twidth: originalWidth,\n\t\t\t\theight: originalHeight,\n\t\t\t\twasResized: false,\n\t\t\t};\n\t\t}\n\n\t\t// Calculate initial dimensions respecting max limits\n\t\tlet targetWidth = originalWidth;\n\t\tlet targetHeight = originalHeight;\n\n\t\tif (targetWidth > opts.maxWidth) {\n\t\t\ttargetHeight = Math.round((targetHeight * opts.maxWidth) / targetWidth);\n\t\t\ttargetWidth = opts.maxWidth;\n\t\t}\n\t\tif (targetHeight > opts.maxHeight) {\n\t\t\ttargetWidth = Math.round((targetWidth * opts.maxHeight) / targetHeight);\n\t\t\ttargetHeight = opts.maxHeight;\n\t\t}\n\n\t\t// Helper to resize and encode in both formats, returning the smaller one\n\t\tfunction tryBothFormats(\n\t\t\twidth: number,\n\t\t\theight: number,\n\t\t\tjpegQuality: number,\n\t\t): { buffer: Uint8Array; mimeType: string } {\n\t\t\tconst resized = photon.resize(image!, width, height, photon.SamplingFilter.Lanczos3);\n\n\t\t\ttry {\n\t\t\t\tconst pngBuffer = resized.get_bytes();\n\t\t\t\tconst jpegBuffer = resized.get_bytes_jpeg(jpegQuality);\n\n\t\t\t\treturn pickSmaller(\n\t\t\t\t\t{ buffer: pngBuffer, mimeType: \"image/png\" },\n\t\t\t\t\t{ buffer: jpegBuffer, mimeType: \"image/jpeg\" },\n\t\t\t\t);\n\t\t\t} finally {\n\t\t\t\tresized.free();\n\t\t\t}\n\t\t}\n\n\t\t// Try to produce an image under maxBytes\n\t\tconst qualitySteps = [85, 70, 55, 40];\n\t\tconst scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25];\n\n\t\tlet best: { buffer: Uint8Array; mimeType: string };\n\t\tlet finalWidth = targetWidth;\n\t\tlet finalHeight = targetHeight;\n\n\t\t// First attempt: resize to target dimensions, try both formats\n\t\tbest = tryBothFormats(targetWidth, targetHeight, opts.jpegQuality);\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\n\t\t// Still too large - try JPEG with decreasing quality\n\t\tfor (const quality of qualitySteps) {\n\t\t\tbest = tryBothFormats(targetWidth, targetHeight, 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\n\t\t// Still too large - reduce dimensions progressively\n\t\tfor (const scale of scaleSteps) {\n\t\t\tfinalWidth = Math.round(targetWidth * scale);\n\t\t\tfinalHeight = Math.round(targetHeight * scale);\n\n\t\t\tif (finalWidth < 100 || finalHeight < 100) {\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\tfor (const quality of qualitySteps) {\n\t\t\t\tbest = tryBothFormats(finalWidth, finalHeight, quality);\n\n\t\t\t\tif (best.buffer.length <= opts.maxBytes) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tdata: Buffer.from(best.buffer).toString(\"base64\"),\n\t\t\t\t\t\tmimeType: best.mimeType,\n\t\t\t\t\t\toriginalWidth,\n\t\t\t\t\t\toriginalHeight,\n\t\t\t\t\t\twidth: finalWidth,\n\t\t\t\t\t\theight: finalHeight,\n\t\t\t\t\t\twasResized: true,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Last resort: return smallest version we produced\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} 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} finally {\n\t\tif (image) {\n\t\t\timage.free();\n\t\t}\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/extensions.md
CHANGED
|
@@ -256,6 +256,9 @@ pi starts
|
|
|
256
256
|
▼
|
|
257
257
|
user sends prompt ─────────────────────────────────────────┐
|
|
258
258
|
│ │
|
|
259
|
+
├─► (extension commands checked first, bypass if found) │
|
|
260
|
+
├─► input (can intercept, transform, or handle) │
|
|
261
|
+
├─► (skill/template expansion if not handled) │
|
|
259
262
|
├─► before_agent_start (can inject message, modify system prompt)
|
|
260
263
|
├─► agent_start │
|
|
261
264
|
│ │
|
|
@@ -574,6 +577,54 @@ pi.on("user_bash", (event, ctx) => {
|
|
|
574
577
|
|
|
575
578
|
**Examples:** [ssh.ts](../examples/extensions/ssh.ts), [interactive-shell.ts](../examples/extensions/interactive-shell.ts)
|
|
576
579
|
|
|
580
|
+
### Input Events
|
|
581
|
+
|
|
582
|
+
#### input
|
|
583
|
+
|
|
584
|
+
Fired when user input is received, after extension commands are checked but before skill and template expansion. The event sees the raw input text, so `/skill:foo` and `/template` are not yet expanded.
|
|
585
|
+
|
|
586
|
+
**Processing order:**
|
|
587
|
+
1. Extension commands (`/cmd`) checked first - if found, handler runs and input event is skipped
|
|
588
|
+
2. `input` event fires - can intercept, transform, or handle
|
|
589
|
+
3. If not handled: skill commands (`/skill:name`) expanded to skill content
|
|
590
|
+
4. If not handled: prompt templates (`/template`) expanded to template content
|
|
591
|
+
5. Agent processing begins (`before_agent_start`, etc.)
|
|
592
|
+
|
|
593
|
+
```typescript
|
|
594
|
+
pi.on("input", async (event, ctx) => {
|
|
595
|
+
// event.text - raw input (before skill/template expansion)
|
|
596
|
+
// event.images - attached images, if any
|
|
597
|
+
// event.source - "interactive" (typed), "rpc" (API), or "extension" (via sendUserMessage)
|
|
598
|
+
|
|
599
|
+
// Transform: rewrite input before expansion
|
|
600
|
+
if (event.text.startsWith("?quick "))
|
|
601
|
+
return { action: "transform", text: `Respond briefly: ${event.text.slice(7)}` };
|
|
602
|
+
|
|
603
|
+
// Handle: respond without LLM (extension shows its own feedback)
|
|
604
|
+
if (event.text === "ping") {
|
|
605
|
+
ctx.ui.notify("pong", "info");
|
|
606
|
+
return { action: "handled" };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Route by source: skip processing for extension-injected messages
|
|
610
|
+
if (event.source === "extension") return { action: "continue" };
|
|
611
|
+
|
|
612
|
+
// Intercept skill commands before expansion
|
|
613
|
+
if (event.text.startsWith("/skill:")) {
|
|
614
|
+
// Could transform, block, or let pass through
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return { action: "continue" }; // Default: pass through to expansion
|
|
618
|
+
});
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
**Results:**
|
|
622
|
+
- `continue` - pass through unchanged (default if handler returns nothing)
|
|
623
|
+
- `transform` - modify text/images, then continue to expansion
|
|
624
|
+
- `handled` - skip agent entirely (first handler to return this wins)
|
|
625
|
+
|
|
626
|
+
Transforms chain across handlers. See [input-transform.ts](../examples/extensions/input-transform.ts).
|
|
627
|
+
|
|
577
628
|
## ExtensionContext
|
|
578
629
|
|
|
579
630
|
Every handler receives `ctx: ExtensionContext`:
|
package/docs/rpc.md
CHANGED
|
@@ -52,9 +52,9 @@ With images:
|
|
|
52
52
|
|
|
53
53
|
If the agent is streaming and no `streamingBehavior` is specified, the command returns an error.
|
|
54
54
|
|
|
55
|
-
**Extension commands**: If the message is
|
|
55
|
+
**Extension commands**: If the message is an extension command (e.g., `/mycommand`), it executes immediately even during streaming. Extension commands manage their own LLM interaction via `pi.sendMessage()`.
|
|
56
56
|
|
|
57
|
-
**
|
|
57
|
+
**Input expansion**: Skill commands (`/skill:name`) and prompt templates (`/template`) are expanded before sending/queueing.
|
|
58
58
|
|
|
59
59
|
Response:
|
|
60
60
|
```json
|
|
@@ -65,7 +65,7 @@ The `images` field is optional. Each image uses `ImageContent` format with base6
|
|
|
65
65
|
|
|
66
66
|
#### steer
|
|
67
67
|
|
|
68
|
-
Queue a steering message to interrupt the agent mid-run. Delivered after current tool execution, remaining tools are skipped.
|
|
68
|
+
Queue a steering message to interrupt the agent mid-run. Delivered after current tool execution, remaining tools are skipped. Skill commands and prompt templates are expanded. Extension commands are not allowed (use `prompt` instead).
|
|
69
69
|
|
|
70
70
|
```json
|
|
71
71
|
{"type": "steer", "message": "Stop and do this instead"}
|
|
@@ -80,7 +80,7 @@ See [set_steering_mode](#set_steering_mode) for controlling how steering message
|
|
|
80
80
|
|
|
81
81
|
#### follow_up
|
|
82
82
|
|
|
83
|
-
Queue a follow-up message to be processed after the agent finishes. Delivered only when agent has no more tool calls or steering messages.
|
|
83
|
+
Queue a follow-up message to be processed after the agent finishes. Delivered only when agent has no more tool calls or steering messages. Skill commands and prompt templates are expanded. Extension commands are not allowed (use `prompt` instead).
|
|
84
84
|
|
|
85
85
|
```json
|
|
86
86
|
{"type": "follow_up", "message": "After you're done, also do this"}
|
|
@@ -108,7 +108,7 @@ Response:
|
|
|
108
108
|
|
|
109
109
|
#### new_session
|
|
110
110
|
|
|
111
|
-
Start a fresh session. Can be cancelled by a `session_before_switch`
|
|
111
|
+
Start a fresh session. Can be cancelled by a `session_before_switch` extension event handler.
|
|
112
112
|
|
|
113
113
|
```json
|
|
114
114
|
{"type": "new_session"}
|
|
@@ -124,7 +124,7 @@ Response:
|
|
|
124
124
|
{"type": "response", "command": "new_session", "success": true, "data": {"cancelled": false}}
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
If
|
|
127
|
+
If an extension cancelled:
|
|
128
128
|
```json
|
|
129
129
|
{"type": "response", "command": "new_session", "success": true, "data": {"cancelled": true}}
|
|
130
130
|
```
|
|
@@ -525,7 +525,7 @@ Response:
|
|
|
525
525
|
|
|
526
526
|
#### switch_session
|
|
527
527
|
|
|
528
|
-
Load a different session file. Can be cancelled by a `
|
|
528
|
+
Load a different session file. Can be cancelled by a `session_before_switch` extension event handler.
|
|
529
529
|
|
|
530
530
|
```json
|
|
531
531
|
{"type": "switch_session", "sessionPath": "/path/to/session.jsonl"}
|
|
@@ -536,14 +536,14 @@ Response:
|
|
|
536
536
|
{"type": "response", "command": "switch_session", "success": true, "data": {"cancelled": false}}
|
|
537
537
|
```
|
|
538
538
|
|
|
539
|
-
If
|
|
539
|
+
If an extension cancelled the switch:
|
|
540
540
|
```json
|
|
541
541
|
{"type": "response", "command": "switch_session", "success": true, "data": {"cancelled": true}}
|
|
542
542
|
```
|
|
543
543
|
|
|
544
544
|
#### fork
|
|
545
545
|
|
|
546
|
-
Create a new fork from a previous user message. Can be cancelled by a `
|
|
546
|
+
Create a new fork from a previous user message. Can be cancelled by a `session_before_fork` extension event handler. Returns the text of the message being forked from.
|
|
547
547
|
|
|
548
548
|
```json
|
|
549
549
|
{"type": "fork", "entryId": "abc123"}
|
|
@@ -559,7 +559,7 @@ Response:
|
|
|
559
559
|
}
|
|
560
560
|
```
|
|
561
561
|
|
|
562
|
-
If
|
|
562
|
+
If an extension cancelled the fork:
|
|
563
563
|
```json
|
|
564
564
|
{
|
|
565
565
|
"type": "response",
|
|
@@ -634,7 +634,7 @@ Events are streamed to stdout as JSON lines during agent operation. Events do NO
|
|
|
634
634
|
| `auto_compaction_end` | Auto-compaction completes |
|
|
635
635
|
| `auto_retry_start` | Auto-retry begins (after transient error) |
|
|
636
636
|
| `auto_retry_end` | Auto-retry completes (success or final failure) |
|
|
637
|
-
| `
|
|
637
|
+
| `extension_error` | Extension threw an error |
|
|
638
638
|
|
|
639
639
|
### agent_start
|
|
640
640
|
|
|
@@ -827,14 +827,14 @@ On final failure (max retries exceeded):
|
|
|
827
827
|
}
|
|
828
828
|
```
|
|
829
829
|
|
|
830
|
-
###
|
|
830
|
+
### extension_error
|
|
831
831
|
|
|
832
|
-
Emitted when
|
|
832
|
+
Emitted when an extension throws an error.
|
|
833
833
|
|
|
834
834
|
```json
|
|
835
835
|
{
|
|
836
|
-
"type": "
|
|
837
|
-
"
|
|
836
|
+
"type": "extension_error",
|
|
837
|
+
"extensionPath": "/path/to/extension.ts",
|
|
838
838
|
"event": "tool_call",
|
|
839
839
|
"error": "Error message..."
|
|
840
840
|
}
|
package/docs/tui.md
CHANGED
|
@@ -26,6 +26,32 @@ interface Component {
|
|
|
26
26
|
|
|
27
27
|
The TUI appends a full SGR reset and OSC 8 reset at the end of each rendered line. Styles do not carry across lines. If you emit multi-line text with styling, reapply styles per line or use `wrapTextWithAnsi()` so styles are preserved for each wrapped line.
|
|
28
28
|
|
|
29
|
+
## Focusable Interface (IME Support)
|
|
30
|
+
|
|
31
|
+
Components that display a text cursor and need IME (Input Method Editor) support should implement the `Focusable` interface:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { CURSOR_MARKER, type Component, type Focusable } from "@mariozechner/pi-tui";
|
|
35
|
+
|
|
36
|
+
class MyInput implements Component, Focusable {
|
|
37
|
+
focused: boolean = false; // Set by TUI when focus changes
|
|
38
|
+
|
|
39
|
+
render(width: number): string[] {
|
|
40
|
+
const marker = this.focused ? CURSOR_MARKER : "";
|
|
41
|
+
// Emit marker right before the fake cursor
|
|
42
|
+
return [`> ${beforeCursor}${marker}\x1b[7m${atCursor}\x1b[27m${afterCursor}`];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
When a `Focusable` component has focus, TUI:
|
|
48
|
+
1. Sets `focused = true` on the component
|
|
49
|
+
2. Scans rendered output for `CURSOR_MARKER` (a zero-width APC escape sequence)
|
|
50
|
+
3. Positions the hardware terminal cursor at that location
|
|
51
|
+
4. Shows the hardware cursor
|
|
52
|
+
|
|
53
|
+
This enables IME candidate windows to appear at the correct position for CJK input methods. The `Editor` and `Input` built-in components already implement this interface.
|
|
54
|
+
|
|
29
55
|
## Using Components
|
|
30
56
|
|
|
31
57
|
**In hooks** via `ctx.ui.custom()`:
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Transform Example - demonstrates the `input` event for intercepting user input.
|
|
3
|
+
*
|
|
4
|
+
* Start pi with this extension:
|
|
5
|
+
* pi -e ./examples/extensions/input-transform.ts
|
|
6
|
+
*
|
|
7
|
+
* Then type these inside pi:
|
|
8
|
+
* ?quick What is TypeScript? → "Respond briefly: What is TypeScript?"
|
|
9
|
+
* ping → "pong" (instant, no LLM)
|
|
10
|
+
* time → current time (instant, no LLM)
|
|
11
|
+
*/
|
|
12
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
13
|
+
|
|
14
|
+
export default function (pi: ExtensionAPI) {
|
|
15
|
+
pi.on("input", async (event, ctx) => {
|
|
16
|
+
// Source-based logic: skip processing for extension-injected messages
|
|
17
|
+
if (event.source === "extension") {
|
|
18
|
+
return { action: "continue" };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Transform: ?quick prefix for brief responses
|
|
22
|
+
if (event.text.startsWith("?quick ")) {
|
|
23
|
+
const query = event.text.slice(7).trim();
|
|
24
|
+
if (!query) {
|
|
25
|
+
ctx.ui.notify("Usage: ?quick <question>", "warning");
|
|
26
|
+
return { action: "handled" };
|
|
27
|
+
}
|
|
28
|
+
return { action: "transform", text: `Respond briefly in 1-2 sentences: ${query}` };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Handle: instant responses without LLM (extension shows its own feedback)
|
|
32
|
+
if (event.text.toLowerCase() === "ping") {
|
|
33
|
+
ctx.ui.notify("pong", "info");
|
|
34
|
+
return { action: "handled" };
|
|
35
|
+
}
|
|
36
|
+
if (event.text.toLowerCase() === "time") {
|
|
37
|
+
ctx.ui.notify(new Date().toLocaleString(), "info");
|
|
38
|
+
return { action: "handled" };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { action: "continue" };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -80,6 +80,6 @@ class ModalEditor extends CustomEditor {
|
|
|
80
80
|
|
|
81
81
|
export default function (pi: ExtensionAPI) {
|
|
82
82
|
pi.on("session_start", (_event, ctx) => {
|
|
83
|
-
ctx.ui.setEditorComponent((
|
|
83
|
+
ctx.ui.setEditorComponent((tui, theme, kb) => new ModalEditor(tui, theme, kb));
|
|
84
84
|
});
|
|
85
85
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@mariozechner/pi-coding-agent";
|
|
12
|
-
import { matchesKey, visibleWidth } from "@mariozechner/pi-tui";
|
|
12
|
+
import { CURSOR_MARKER, type Focusable, matchesKey, visibleWidth } from "@mariozechner/pi-tui";
|
|
13
13
|
|
|
14
14
|
export default function (pi: ExtensionAPI) {
|
|
15
15
|
pi.registerCommand("overlay-test", {
|
|
@@ -28,9 +28,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
class OverlayTestComponent {
|
|
31
|
+
class OverlayTestComponent implements Focusable {
|
|
32
32
|
readonly width = 70;
|
|
33
33
|
|
|
34
|
+
/** Focusable interface - set by TUI when focus changes */
|
|
35
|
+
focused = false;
|
|
36
|
+
|
|
34
37
|
private selected = 0;
|
|
35
38
|
private items = [
|
|
36
39
|
{ label: "Search", hasInput: true, text: "", cursor: 0 },
|
|
@@ -123,7 +126,9 @@ class OverlayTestComponent {
|
|
|
123
126
|
const before = inputDisplay.slice(0, item.cursor);
|
|
124
127
|
const cursorChar = item.cursor < inputDisplay.length ? inputDisplay[item.cursor] : " ";
|
|
125
128
|
const after = inputDisplay.slice(item.cursor + 1);
|
|
126
|
-
|
|
129
|
+
// Emit hardware cursor marker for IME support when focused
|
|
130
|
+
const marker = this.focused ? CURSOR_MARKER : "";
|
|
131
|
+
inputDisplay = `${before}${marker}\x1b[7m${cursorChar}\x1b[27m${after}`;
|
|
127
132
|
}
|
|
128
133
|
content = `${prefix + label} ${inputDisplay}`;
|
|
129
134
|
} else {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* When enabled, only read-only tools are available.
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
8
|
-
* - /plan command or
|
|
8
|
+
* - /plan command or Ctrl+Alt+P to toggle
|
|
9
9
|
* - Bash restricted to allowlisted read-only commands
|
|
10
10
|
* - Extracts numbered plan steps from "Plan:" sections
|
|
11
11
|
* - [DONE:n] markers to complete steps during execution
|
|
@@ -113,7 +113,7 @@ export default function planModeExtension(pi: ExtensionAPI): void {
|
|
|
113
113
|
},
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
pi.registerShortcut(Key.
|
|
116
|
+
pi.registerShortcut(Key.ctrlAlt("p"), {
|
|
117
117
|
description: "Toggle plan mode",
|
|
118
118
|
handler: async (ctx) => togglePlanMode(ctx),
|
|
119
119
|
});
|
|
@@ -90,7 +90,7 @@ export default function question(pi: ExtensionAPI) {
|
|
|
90
90
|
noMatch: (t) => theme.fg("warning", t),
|
|
91
91
|
},
|
|
92
92
|
};
|
|
93
|
-
const editor = new Editor(editorTheme);
|
|
93
|
+
const editor = new Editor(tui, editorTheme);
|
|
94
94
|
|
|
95
95
|
editor.onSubmit = (value) => {
|
|
96
96
|
const trimmed = value.trim();
|