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
354 lines
11 KiB
TypeScript
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);
|
|
},
|
|
});
|
|
}
|