mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 07:04:45 +00:00
docs(coding-agent): add built-in tool renderer extension example
Shows how to override rendering of read, bash, edit, write tools with compact output while delegating execution to the originals.
This commit is contained in:
parent
7364696ae6
commit
21141e0040
1 changed files with 246 additions and 0 deletions
|
|
@ -0,0 +1,246 @@
|
|||
/**
|
||||
* Built-in Tool Renderer Example - Custom rendering for built-in tools
|
||||
*
|
||||
* Demonstrates how to override the rendering of built-in tools (read, bash,
|
||||
* edit, write) without changing their behavior. Each tool is re-registered
|
||||
* with the same name, delegating execution to the original implementation
|
||||
* while providing compact custom renderCall/renderResult functions.
|
||||
*
|
||||
* This is useful for users who prefer more concise tool output, or who want
|
||||
* to highlight specific information (e.g., showing only the diff stats for
|
||||
* edit, or just the exit code for bash).
|
||||
*
|
||||
* How it works:
|
||||
* - registerTool() with the same name as a built-in replaces it entirely
|
||||
* - We create instances of the original tools via createReadTool(), etc.
|
||||
* and delegate execute() to them
|
||||
* - renderCall() controls what's shown when the tool is invoked
|
||||
* - renderResult() controls what's shown after execution completes
|
||||
* - The `expanded` flag in renderResult indicates whether the user has
|
||||
* toggled the tool output open (via ctrl+e or clicking)
|
||||
*
|
||||
* Usage:
|
||||
* pi -e ./built-in-tool-renderer.ts
|
||||
*/
|
||||
|
||||
import type { BashToolDetails, EditToolDetails, ExtensionAPI, ReadToolDetails } from "@mariozechner/pi-coding-agent";
|
||||
import { createBashTool, createEditTool, createReadTool, createWriteTool } from "@mariozechner/pi-coding-agent";
|
||||
import { Text } from "@mariozechner/pi-tui";
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
const cwd = process.cwd();
|
||||
|
||||
// --- Read tool: show path and line count ---
|
||||
const originalRead = createReadTool(cwd);
|
||||
pi.registerTool({
|
||||
name: "read",
|
||||
label: "read",
|
||||
description: originalRead.description,
|
||||
parameters: originalRead.parameters,
|
||||
|
||||
async execute(toolCallId, params, signal, onUpdate) {
|
||||
return originalRead.execute(toolCallId, params, signal, onUpdate);
|
||||
},
|
||||
|
||||
renderCall(args, theme) {
|
||||
let text = theme.fg("toolTitle", theme.bold("read "));
|
||||
text += theme.fg("accent", args.path);
|
||||
if (args.offset || args.limit) {
|
||||
const parts: string[] = [];
|
||||
if (args.offset) parts.push(`offset=${args.offset}`);
|
||||
if (args.limit) parts.push(`limit=${args.limit}`);
|
||||
text += theme.fg("dim", ` (${parts.join(", ")})`);
|
||||
}
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
|
||||
renderResult(result, { expanded, isPartial }, theme) {
|
||||
if (isPartial) return new Text(theme.fg("warning", "Reading..."), 0, 0);
|
||||
|
||||
const details = result.details as ReadToolDetails | undefined;
|
||||
const content = result.content[0];
|
||||
|
||||
if (content?.type === "image") {
|
||||
return new Text(theme.fg("success", "Image loaded"), 0, 0);
|
||||
}
|
||||
|
||||
if (content?.type !== "text") {
|
||||
return new Text(theme.fg("error", "No content"), 0, 0);
|
||||
}
|
||||
|
||||
const lineCount = content.text.split("\n").length;
|
||||
let text = theme.fg("success", `${lineCount} lines`);
|
||||
|
||||
if (details?.truncation?.truncated) {
|
||||
text += theme.fg("warning", ` (truncated from ${details.truncation.totalLines})`);
|
||||
}
|
||||
|
||||
if (expanded) {
|
||||
const lines = content.text.split("\n").slice(0, 15);
|
||||
for (const line of lines) {
|
||||
text += `\n${theme.fg("dim", line)}`;
|
||||
}
|
||||
if (lineCount > 15) {
|
||||
text += `\n${theme.fg("muted", `... ${lineCount - 15} more lines`)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
});
|
||||
|
||||
// --- Bash tool: show command and exit code ---
|
||||
const originalBash = createBashTool(cwd);
|
||||
pi.registerTool({
|
||||
name: "bash",
|
||||
label: "bash",
|
||||
description: originalBash.description,
|
||||
parameters: originalBash.parameters,
|
||||
|
||||
async execute(toolCallId, params, signal, onUpdate) {
|
||||
return originalBash.execute(toolCallId, params, signal, onUpdate);
|
||||
},
|
||||
|
||||
renderCall(args, theme) {
|
||||
let text = theme.fg("toolTitle", theme.bold("$ "));
|
||||
const cmd = args.command.length > 80 ? `${args.command.slice(0, 77)}...` : args.command;
|
||||
text += theme.fg("accent", cmd);
|
||||
if (args.timeout) {
|
||||
text += theme.fg("dim", ` (timeout: ${args.timeout}s)`);
|
||||
}
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
|
||||
renderResult(result, { expanded, isPartial }, theme) {
|
||||
if (isPartial) return new Text(theme.fg("warning", "Running..."), 0, 0);
|
||||
|
||||
const details = result.details as BashToolDetails | undefined;
|
||||
const content = result.content[0];
|
||||
const output = content?.type === "text" ? content.text : "";
|
||||
|
||||
const exitMatch = output.match(/exit code: (\d+)/);
|
||||
const exitCode = exitMatch ? parseInt(exitMatch[1], 10) : null;
|
||||
const lineCount = output.split("\n").filter((l) => l.trim()).length;
|
||||
|
||||
let text = "";
|
||||
if (exitCode === 0 || exitCode === null) {
|
||||
text += theme.fg("success", "done");
|
||||
} else {
|
||||
text += theme.fg("error", `exit ${exitCode}`);
|
||||
}
|
||||
text += theme.fg("dim", ` (${lineCount} lines)`);
|
||||
|
||||
if (details?.truncation?.truncated) {
|
||||
text += theme.fg("warning", " [truncated]");
|
||||
}
|
||||
|
||||
if (expanded) {
|
||||
const lines = output.split("\n").slice(0, 20);
|
||||
for (const line of lines) {
|
||||
text += `\n${theme.fg("dim", line)}`;
|
||||
}
|
||||
if (output.split("\n").length > 20) {
|
||||
text += `\n${theme.fg("muted", "... more output")}`;
|
||||
}
|
||||
}
|
||||
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
});
|
||||
|
||||
// --- Edit tool: show path and diff stats ---
|
||||
const originalEdit = createEditTool(cwd);
|
||||
pi.registerTool({
|
||||
name: "edit",
|
||||
label: "edit",
|
||||
description: originalEdit.description,
|
||||
parameters: originalEdit.parameters,
|
||||
|
||||
async execute(toolCallId, params, signal, onUpdate) {
|
||||
return originalEdit.execute(toolCallId, params, signal, onUpdate);
|
||||
},
|
||||
|
||||
renderCall(args, theme) {
|
||||
let text = theme.fg("toolTitle", theme.bold("edit "));
|
||||
text += theme.fg("accent", args.path);
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
|
||||
renderResult(result, { expanded, isPartial }, theme) {
|
||||
if (isPartial) return new Text(theme.fg("warning", "Editing..."), 0, 0);
|
||||
|
||||
const details = result.details as EditToolDetails | undefined;
|
||||
const content = result.content[0];
|
||||
|
||||
if (content?.type === "text" && content.text.startsWith("Error")) {
|
||||
return new Text(theme.fg("error", content.text.split("\n")[0]), 0, 0);
|
||||
}
|
||||
|
||||
if (!details?.diff) {
|
||||
return new Text(theme.fg("success", "Applied"), 0, 0);
|
||||
}
|
||||
|
||||
// Count additions and removals from the diff
|
||||
const diffLines = details.diff.split("\n");
|
||||
let additions = 0;
|
||||
let removals = 0;
|
||||
for (const line of diffLines) {
|
||||
if (line.startsWith("+") && !line.startsWith("+++")) additions++;
|
||||
if (line.startsWith("-") && !line.startsWith("---")) removals++;
|
||||
}
|
||||
|
||||
let text = theme.fg("success", `+${additions}`);
|
||||
text += theme.fg("dim", " / ");
|
||||
text += theme.fg("error", `-${removals}`);
|
||||
|
||||
if (expanded) {
|
||||
for (const line of diffLines.slice(0, 30)) {
|
||||
if (line.startsWith("+") && !line.startsWith("+++")) {
|
||||
text += `\n${theme.fg("success", line)}`;
|
||||
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
||||
text += `\n${theme.fg("error", line)}`;
|
||||
} else {
|
||||
text += `\n${theme.fg("dim", line)}`;
|
||||
}
|
||||
}
|
||||
if (diffLines.length > 30) {
|
||||
text += `\n${theme.fg("muted", `... ${diffLines.length - 30} more diff lines`)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
});
|
||||
|
||||
// --- Write tool: show path and size ---
|
||||
const originalWrite = createWriteTool(cwd);
|
||||
pi.registerTool({
|
||||
name: "write",
|
||||
label: "write",
|
||||
description: originalWrite.description,
|
||||
parameters: originalWrite.parameters,
|
||||
|
||||
async execute(toolCallId, params, signal, onUpdate) {
|
||||
return originalWrite.execute(toolCallId, params, signal, onUpdate);
|
||||
},
|
||||
|
||||
renderCall(args, theme) {
|
||||
let text = theme.fg("toolTitle", theme.bold("write "));
|
||||
text += theme.fg("accent", args.path);
|
||||
const lineCount = args.content.split("\n").length;
|
||||
text += theme.fg("dim", ` (${lineCount} lines)`);
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
|
||||
renderResult(result, { isPartial }, theme) {
|
||||
if (isPartial) return new Text(theme.fg("warning", "Writing..."), 0, 0);
|
||||
|
||||
const content = result.content[0];
|
||||
if (content?.type === "text" && content.text.startsWith("Error")) {
|
||||
return new Text(theme.fg("error", content.text.split("\n")[0]), 0, 0);
|
||||
}
|
||||
|
||||
return new Text(theme.fg("success", "Written"), 0, 0);
|
||||
},
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue