@earendil-works/pi-coding-agent
Version:
Coding agent CLI with read, bash, edit, write tools and session management
126 lines • 6.12 kB
JavaScript
/**
* Presentation for the read tool.
*
* Renderers live apart from the implementation so a process that only displays tool output does not
* load the execution path or its typebox parameter schema. `read.ts` spreads these into its
* definition, so the tool's public shape is unchanged.
*/
import { basename, dirname, isAbsolute, relative, resolve as resolvePath, sep } from "node:path";
import { Text } from "@earendil-works/pi-tui";
import { getReadmePath } from "../../../config.js";
import { keyHint, keyText } from "../../../modes/interactive/components/keybinding-hints.js";
import { getLanguageFromPath, highlightCode } from "../../../modes/interactive/theme/theme.js";
import { formatPathRelativeToCwdOrAbsolute } from "../../../utils/paths.js";
import { resolveToCwd } from "../path-utils.js";
import { formatReadLineRange, getTextOutput, renderToolPath, replaceTabs, str } from "../render-utils.js";
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "../truncate.js";
const COMPACT_RESOURCE_FILE_NAMES = new Set(["AGENTS.override.md", "AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]);
function styledReadLineRange(args, theme) {
const range = formatReadLineRange(args?.offset, args?.limit);
return range ? theme.fg("warning", range) : "";
}
function formatReadCall(args, theme, cwd) {
const pathDisplay = renderToolPath(str(args?.file_path ?? args?.path), theme, cwd);
return `${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}${styledReadLineRange(args, theme)}`;
}
function trimTrailingEmptyLines(lines) {
let end = lines.length;
while (end > 0 && lines[end - 1] === "") {
end--;
}
return lines.slice(0, end);
}
function toPosixPath(filePath) {
return filePath.split(sep).join("/");
}
function getPiDocsClassification(absolutePath) {
const packageRoot = dirname(getReadmePath());
const relativePath = relative(resolvePath(packageRoot), resolvePath(absolutePath));
if (relativePath === "" ||
relativePath === ".." ||
relativePath.startsWith(`..${sep}`) ||
isAbsolute(relativePath)) {
return undefined;
}
const label = toPosixPath(relativePath);
if (label === "README.md" || label.startsWith("docs/") || label.startsWith("examples/")) {
return { kind: "docs", label };
}
return undefined;
}
function getCompactReadClassification(args, cwd) {
const rawPath = str(args?.file_path ?? args?.path);
if (!rawPath)
return undefined;
const absolutePath = resolveToCwd(rawPath, cwd);
const fileName = basename(absolutePath);
if (fileName === "SKILL.md") {
return { kind: "skill", label: basename(dirname(absolutePath)) || fileName };
}
const docsClassification = getPiDocsClassification(absolutePath);
if (docsClassification)
return docsClassification;
if (COMPACT_RESOURCE_FILE_NAMES.has(fileName)) {
return { kind: "resource", label: formatPathRelativeToCwdOrAbsolute(absolutePath, cwd) };
}
return undefined;
}
function formatCompactReadCall(classification, args, theme) {
const expandHint = theme.fg("dim", ` (${keyText("app.tools.expand")} to expand)`);
if (classification.kind === "skill") {
return (theme.fg("customMessageLabel", `\x1b[1m[skill]\x1b[22m `) +
theme.fg("customMessageText", classification.label) +
styledReadLineRange(args, theme) +
expandHint);
}
return (theme.fg("toolTitle", theme.bold(`read ${classification.kind}`)) +
" " +
theme.fg("accent", classification.label) +
styledReadLineRange(args, theme) +
expandHint);
}
function formatReadResult(args, result, options, theme, showImages, _cwd, isError) {
if (!options.expanded && !isError) {
return "";
}
const rawPath = str(args?.file_path ?? args?.path);
const output = getTextOutput(result, showImages);
const lang = !isError && rawPath ? getLanguageFromPath(rawPath) : undefined;
const renderedLines = lang ? highlightCode(replaceTabs(output), lang) : output.split("\n");
const lines = trimTrailingEmptyLines(renderedLines);
const maxLines = options.expanded ? lines.length : 10;
const displayLines = lines.slice(0, maxLines);
const remaining = lines.length - maxLines;
let text = `\n${displayLines.map((line) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line)))).join("\n")}`;
if (remaining > 0) {
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
}
const truncation = result.details?.truncation;
if (truncation?.truncated) {
if (truncation.firstLineExceedsLimit) {
text += `\n${theme.fg("warning", `[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`)}`;
}
else if (truncation.truncatedBy === "lines") {
text += `\n${theme.fg("warning", `[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`)}`;
}
else {
text += `\n${theme.fg("warning", `[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`)}`;
}
}
return text;
}
export const readRenderers = {
renderCall(rawArgs, theme, context) {
const args = rawArgs;
const text = context.lastComponent ?? new Text("", 0, 0);
const classification = !context.expanded ? getCompactReadClassification(args, context.cwd) : undefined;
text.setText(classification ? formatCompactReadCall(classification, args, theme) : formatReadCall(args, theme, context.cwd));
return text;
},
renderResult(result, options, theme, context) {
const text = context.lastComponent ?? new Text("", 0, 0);
text.setText(formatReadResult(context.args, result, options, theme, context.showImages, context.cwd, context.isError));
return text;
},
};
//# sourceMappingURL=read.js.map