Files
alexion 6081e3e325 chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree.

git-subtree-dir: third_party/pi

git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
2026-10-15 17:58:48 -04:00

354 lines
11 KiB
TypeScript

/**
* Minimal Mode Example - Demonstrates a "minimal" tool display mode
*
* This extension overrides built-in tools to provide custom rendering:
* - Collapsed mode: Only shows the tool call (command/path), no output
* - Expanded mode: Shows full output like the built-in renderers
*
* This demonstrates how a "minimal mode" could work, where ctrl+o cycles through:
* - Standard: Shows truncated output (current default)
* - Expanded: Shows full output (current expanded)
* - Minimal: Shows only tool call, no output (this extension's collapsed mode)
*
* Usage:
* pi -e ./minimal-mode.ts
*
* Then use ctrl+o to toggle between minimal (collapsed) and full (expanded) views.
*/
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
import {
createBashToolDefinition,
createEditToolDefinition,
createFindToolDefinition,
createGrepToolDefinition,
createLsToolDefinition,
createReadToolDefinition,
createWriteToolDefinition,
formatReadLineRange,
} from "@earendil-works/pi-coding-agent";
import { Text } from "@earendil-works/pi-tui";
import { homedir } from "os";
/**
* Shorten a path by replacing home directory with ~
*/
function shortenPath(path: string): string {
const home = homedir();
if (path.startsWith(home)) {
return `~${path.slice(home.length)}`;
}
return path;
}
function createBuiltInTools(cwd: string) {
return {
read: createReadToolDefinition(cwd),
bash: createBashToolDefinition(cwd),
edit: createEditToolDefinition(cwd),
write: createWriteToolDefinition(cwd),
find: createFindToolDefinition(cwd),
grep: createGrepToolDefinition(cwd),
ls: createLsToolDefinition(cwd),
};
}
export default function (pi: ExtensionAPI) {
const tools = createBuiltInTools(process.cwd());
// =========================================================================
// Read Tool
// =========================================================================
pi.registerTool({
...tools.read,
renderCall(args, theme, _context) {
const path = shortenPath(args.path || "");
let pathDisplay = path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
const lineRange = formatReadLineRange(args.offset, args.limit);
if (lineRange) {
pathDisplay += theme.fg("warning", lineRange);
}
return new Text(`${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}`, 0, 0);
},
renderResult(result, { expanded }, theme, _context) {
// Minimal mode: show nothing in collapsed state
if (!expanded) {
return new Text("", 0, 0);
}
// Expanded mode: show full output
const textContent = result.content.find((c) => c.type === "text");
if (!textContent || textContent.type !== "text") {
return new Text("", 0, 0);
}
const lines = textContent.text.split("\n");
const output = lines.map((line) => theme.fg("toolOutput", line)).join("\n");
return new Text(`\n${output}`, 0, 0);
},
});
// =========================================================================
// Bash Tool
// =========================================================================
pi.registerTool({
...tools.bash,
renderCall(args, theme, _context) {
const command = args.command || "...";
const timeout = args.timeout as number | undefined;
const timeoutSuffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : "";
return new Text(theme.fg("toolTitle", theme.bold(`$ ${command}`)) + timeoutSuffix, 0, 0);
},
renderResult(result, { expanded }, theme, _context) {
// Minimal mode: show nothing in collapsed state
if (!expanded) {
return new Text("", 0, 0);
}
// Expanded mode: show full output
const textContent = result.content.find((c) => c.type === "text");
if (!textContent || textContent.type !== "text") {
return new Text("", 0, 0);
}
const output = textContent.text
.trim()
.split("\n")
.map((line) => theme.fg("toolOutput", line))
.join("\n");
if (!output) {
return new Text("", 0, 0);
}
return new Text(`\n${output}`, 0, 0);
},
});
// =========================================================================
// Write Tool
// =========================================================================
pi.registerTool({
...tools.write,
renderCall(args, theme, _context) {
const path = shortenPath(args.path || "");
const pathDisplay = path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
const lineCount = args.content ? args.content.split("\n").length : 0;
const lineInfo = lineCount > 0 ? theme.fg("muted", ` (${lineCount} lines)`) : "";
return new Text(`${theme.fg("toolTitle", theme.bold("write"))} ${pathDisplay}${lineInfo}`, 0, 0);
},
renderResult(result, { expanded }, theme, _context) {
// Minimal mode: show nothing (file was written)
if (!expanded) {
return new Text("", 0, 0);
}
// Expanded mode: show error if any
if (result.content.some((c) => c.type === "text" && c.text)) {
const textContent = result.content.find((c) => c.type === "text");
if (textContent?.type === "text" && textContent.text) {
return new Text(`\n${theme.fg("error", textContent.text)}`, 0, 0);
}
}
return new Text("", 0, 0);
},
});
// =========================================================================
// Edit Tool
// =========================================================================
pi.registerTool({
...tools.edit,
renderShell: "default",
renderCall(args, theme, _context) {
const path = shortenPath(args.path || "");
const pathDisplay = path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
return new Text(`${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`, 0, 0);
},
renderResult(result, { expanded }, theme, _context) {
// Minimal mode: show nothing in collapsed state
if (!expanded) {
return new Text("", 0, 0);
}
// Expanded mode: show diff or error
const textContent = result.content.find((c) => c.type === "text");
if (!textContent || textContent.type !== "text") {
return new Text("", 0, 0);
}
// For errors, show the error message
const text = textContent.text;
if (text.includes("Error") || text.includes("error")) {
return new Text(`\n${theme.fg("error", text)}`, 0, 0);
}
// Otherwise show the text (would be nice to show actual diff here)
return new Text(`\n${theme.fg("toolOutput", text)}`, 0, 0);
},
});
// =========================================================================
// Find Tool
// =========================================================================
pi.registerTool({
...tools.find,
renderCall(args, theme, _context) {
const pattern = args.pattern || "";
const path = shortenPath(args.path || ".");
const limit = args.limit;
let text = `${theme.fg("toolTitle", theme.bold("find"))} ${theme.fg("accent", pattern)}`;
text += theme.fg("toolOutput", ` in ${path}`);
if (limit !== undefined) {
text += theme.fg("toolOutput", ` (limit ${limit})`);
}
return new Text(text, 0, 0);
},
renderResult(result, { expanded }, theme, _context) {
if (!expanded) {
// Minimal: just show count
const textContent = result.content.find((c) => c.type === "text");
if (textContent?.type === "text") {
const count = textContent.text.trim().split("\n").filter(Boolean).length;
if (count > 0) {
return new Text(theme.fg("muted", ` → ${count} files`), 0, 0);
}
}
return new Text("", 0, 0);
}
// Expanded: show full results
const textContent = result.content.find((c) => c.type === "text");
if (!textContent || textContent.type !== "text") {
return new Text("", 0, 0);
}
const output = textContent.text
.trim()
.split("\n")
.map((line) => theme.fg("toolOutput", line))
.join("\n");
return new Text(`\n${output}`, 0, 0);
},
});
// =========================================================================
// Grep Tool
// =========================================================================
pi.registerTool({
...tools.grep,
renderCall(args, theme, _context) {
const pattern = args.pattern || "";
const path = shortenPath(args.path || ".");
const glob = args.glob;
const limit = args.limit;
let text = `${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", `/${pattern}/`)}`;
text += theme.fg("toolOutput", ` in ${path}`);
if (glob) {
text += theme.fg("toolOutput", ` (${glob})`);
}
if (limit !== undefined) {
text += theme.fg("toolOutput", ` limit ${limit}`);
}
return new Text(text, 0, 0);
},
renderResult(result, { expanded }, theme, _context) {
if (!expanded) {
// Minimal: just show match count
const textContent = result.content.find((c) => c.type === "text");
if (textContent?.type === "text") {
const count = textContent.text.trim().split("\n").filter(Boolean).length;
if (count > 0) {
return new Text(theme.fg("muted", ` → ${count} matches`), 0, 0);
}
}
return new Text("", 0, 0);
}
// Expanded: show full results
const textContent = result.content.find((c) => c.type === "text");
if (!textContent || textContent.type !== "text") {
return new Text("", 0, 0);
}
const output = textContent.text
.trim()
.split("\n")
.map((line) => theme.fg("toolOutput", line))
.join("\n");
return new Text(`\n${output}`, 0, 0);
},
});
// =========================================================================
// Ls Tool
// =========================================================================
pi.registerTool({
...tools.ls,
renderCall(args, theme, _context) {
const path = shortenPath(args.path || ".");
const limit = args.limit;
let text = `${theme.fg("toolTitle", theme.bold("ls"))} ${theme.fg("accent", path)}`;
if (limit !== undefined) {
text += theme.fg("toolOutput", ` (limit ${limit})`);
}
return new Text(text, 0, 0);
},
renderResult(result, { expanded }, theme, _context) {
if (!expanded) {
// Minimal: just show entry count
const textContent = result.content.find((c) => c.type === "text");
if (textContent?.type === "text") {
const count = textContent.text.trim().split("\n").filter(Boolean).length;
if (count > 0) {
return new Text(theme.fg("muted", ` → ${count} entries`), 0, 0);
}
}
return new Text("", 0, 0);
}
// Expanded: show full listing
const textContent = result.content.find((c) => c.type === "text");
if (!textContent || textContent.type !== "text") {
return new Text("", 0, 0);
}
const output = textContent.text
.trim()
.split("\n")
.map((line) => theme.fg("toolOutput", line))
.join("\n");
return new Text(`\n${output}`, 0, 0);
},
});
}