Commit 0627d4c90979
Changed files (5)
dots
pi
agent
dots/pi/agent/extensions/custom-header.ts
@@ -57,7 +57,7 @@ export default function (pi: ExtensionAPI) {
// Set custom header on load
pi.on("session_start", async (_event, ctx) => {
- if (ctx.hasUI) {
+ if (ctx.mode === "tui") {
ctx.ui.setHeader((_tui, theme) => {
return {
render(_width: number): string[] {
dots/pi/agent/extensions/kitty-image.ts
@@ -3,7 +3,8 @@
*
* Displays images directly in the Kitty terminal using the Kitty Graphics Protocol
* (raw escape sequences via process.stdout). No kitten or external tools required
- * for PNG files. Other formats are auto-converted to PNG via python3 or ImageMagick.
+ * for PNG files. Other formats are auto-converted to PNG via Pi's native
+ * (Photon/WASM) converter, falling back to python3 or ImageMagick if unavailable.
*
* Provides:
* - show_image tool: LLM can display any image in the terminal
@@ -11,6 +12,7 @@
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
+import { convertToPng as convertToPngNative } from "@mariozechner/pi-coding-agent";
import { Text } from "@mariozechner/pi-tui";
import { Type } from "@sinclair/typebox";
import { readFileSync, existsSync, statSync } from "node:fs";
@@ -62,11 +64,36 @@ function sendPngToKitty(pngBytes: Buffer): void {
process.stdout.write("\n");
}
+// Map file extensions to MIME types for the native converter.
+const MIME_BY_EXT: Record<string, string> = {
+ ".png": "image/png",
+ ".jpg": "image/jpeg",
+ ".jpeg": "image/jpeg",
+ ".gif": "image/gif",
+ ".webp": "image/webp",
+ ".bmp": "image/bmp",
+ ".tiff": "image/tiff",
+ ".tif": "image/tiff",
+};
+
/**
- * Attempt to convert any image format to PNG bytes.
+ * Convert image bytes to PNG using Pi's native (Photon/WASM) converter.
+ * Returns null if the native converter is unavailable or fails.
+ */
+async function convertToPngNativeBytes(imagePath: string): Promise<Buffer | null> {
+ const ext = extname(imagePath).toLowerCase();
+ const mimeType = MIME_BY_EXT[ext] ?? "application/octet-stream";
+ const base64 = readFileSync(imagePath).toString("base64");
+ const result = await convertToPngNative(base64, mimeType);
+ if (!result) return null;
+ return Buffer.from(result.data, "base64");
+}
+
+/**
+ * Fallback: convert any image format to PNG bytes via external tools.
* Tries python3 (Pillow) first, then ImageMagick `convert`.
*/
-function convertToPng(imagePath: string): Buffer {
+function convertToPngExternal(imagePath: string): Buffer {
// Try python3 with Pillow
try {
const script = `
@@ -101,15 +128,16 @@ sys.stdout.buffer.write(buf.getvalue())
/**
* Load image at path and return PNG bytes, converting if necessary.
*/
-function loadAsPng(imagePath: string): { png: Buffer; originalFormat: string } {
+async function loadAsPng(imagePath: string): Promise<{ png: Buffer; originalFormat: string }> {
const ext = extname(imagePath).toLowerCase();
if (ext === ".png") {
return { png: readFileSync(imagePath), originalFormat: "png" };
}
- // For other formats, convert to PNG
- const png = convertToPng(imagePath);
+ // Prefer Pi's native converter (no external dependency), fall back to tools.
+ const native = await convertToPngNativeBytes(imagePath);
+ const png = native ?? convertToPngExternal(imagePath);
return { png, originalFormat: ext.slice(1) || "unknown" };
}
@@ -124,7 +152,7 @@ export default function (pi: ExtensionAPI) {
name: "show_image",
label: "Show Image",
description: `Display an image directly in the Kitty terminal using the Kitty Graphics Protocol.
-Supports PNG natively. JPEG, GIF, WEBP, BMP, and other formats are auto-converted via Pillow or ImageMagick.
+Supports PNG natively. JPEG, GIF, WEBP, BMP, and other formats are auto-converted.
Use this when:
- User asks to "display", "show", or "view" an image
@@ -155,7 +183,7 @@ Use this when:
}
try {
- const { png, originalFormat } = loadAsPng(imagePath);
+ const { png, originalFormat } = await loadAsPng(imagePath);
const sizeMB = (stat.size / 1024 / 1024).toFixed(2);
// Write the image to the terminal via Kitty Graphics Protocol
@@ -237,7 +265,7 @@ Use this when:
}
try {
- const { png, originalFormat } = loadAsPng(imagePath);
+ const { png, originalFormat } = await loadAsPng(imagePath);
const converted = originalFormat !== "png" ? ` (from ${originalFormat})` : "";
process.stdout.write("\n");
dots/pi/agent/extensions/message-queue.ts
@@ -314,7 +314,12 @@ export default function (pi: ExtensionAPI) {
return { action: "continue" as const };
}
- if (ctx.isIdle()) {
+ // streamingBehavior is set ("steer"|"followUp") only when the agent is
+ // actively streaming; undefined means idle. Prefer it over isIdle() as it
+ // reflects how *this* input would be delivered, avoiding races with the
+ // idle-state read. Fall back to isIdle() if the host predates it.
+ const isStreaming = event.streamingBehavior !== undefined || !ctx.isIdle();
+ if (!isStreaming) {
return { action: "continue" as const };
}
dots/pi/agent/extensions/terminal-status.ts
@@ -307,9 +307,13 @@ function ringBell(): void {
export default function (pi: ExtensionAPI) {
// Set initial title on session start
pi.on("session_start", async (_event, ctx) => {
- // Detect non-interactive modes (RPC, JSON, print) to skip terminal output
- // process.argv check in init covers most cases; ctx.hasUI is a backup
- if (ctx.hasUI === false) isInteractive = false;
+ // Only paint the terminal in true TUI mode. ctx.hasUI is also true in RPC
+ // mode, so prefer ctx.mode to avoid emitting escape sequences into
+ // RPC/JSON/print output streams.
+ if (ctx.mode !== "tui") {
+ isInteractive = false;
+ return;
+ }
currentBranch = getGitBranch();
// Get initial model if available
if (ctx.model) {