feat(read): compact resource read rendering
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import { basename, dirname, isAbsolute, relative, resolve as resolvePath, sep } from "node:path";
|
||||
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
||||
import type { Api, ImageContent, Model, TextContent } from "@mariozechner/pi-ai";
|
||||
import { Text } from "@mariozechner/pi-tui";
|
||||
import { constants } from "fs";
|
||||
import { access as fsAccess, readFile as fsReadFile } from "fs/promises";
|
||||
import { type Static, Type } from "typebox";
|
||||
import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.js";
|
||||
import { getReadmePath } from "../../config.js";
|
||||
import { keyHint, keyText } from "../../modes/interactive/components/keybinding-hints.js";
|
||||
import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/interactive/theme/theme.js";
|
||||
import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";
|
||||
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
|
||||
import type { ToolDefinition, ToolRenderResultOptions } from "../extensions/types.js";
|
||||
@@ -26,6 +28,27 @@ export interface ReadToolDetails {
|
||||
truncation?: TruncationResult;
|
||||
}
|
||||
|
||||
interface CompactReadClassification {
|
||||
kind: "docs" | "resource" | "skill";
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface ReadRenderState {
|
||||
hideCall?: boolean;
|
||||
}
|
||||
|
||||
class ReadCallText extends Text {
|
||||
constructor(private state: ReadRenderState) {
|
||||
super("", 0, 0);
|
||||
}
|
||||
|
||||
override render(width: number): string[] {
|
||||
return this.state.hideCall ? [] : super.render(width);
|
||||
}
|
||||
}
|
||||
|
||||
const COMPACT_RESOURCE_FILE_NAMES = new Set(["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]);
|
||||
|
||||
/**
|
||||
* Pluggable operations for the read tool.
|
||||
* Override these to delegate file reading to remote systems (for example SSH).
|
||||
@@ -54,7 +77,7 @@ export interface ReadToolOptions {
|
||||
|
||||
function formatReadCall(
|
||||
args: { path?: string; file_path?: string; offset?: number; limit?: number } | undefined,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: Theme,
|
||||
): string {
|
||||
const rawPath = str(args?.file_path ?? args?.path);
|
||||
const path = rawPath !== null ? shortenPath(rawPath) : null;
|
||||
@@ -85,15 +108,87 @@ function getNonVisionImageNote(model: Model<Api> | undefined): string | undefine
|
||||
return "[Current model does not support images. The image will be omitted from this request.]";
|
||||
}
|
||||
|
||||
function toPosixPath(filePath: string): string {
|
||||
return filePath.split(sep).join("/");
|
||||
}
|
||||
|
||||
function getPiDocsClassification(absolutePath: string): CompactReadClassification | undefined {
|
||||
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: { path?: string; file_path?: string } | undefined,
|
||||
cwd: string,
|
||||
): CompactReadClassification | undefined {
|
||||
const rawPath = str(args?.file_path ?? args?.path);
|
||||
if (!rawPath) return undefined;
|
||||
|
||||
const absolutePath = resolveReadPath(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: fileName };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function formatCompactReadResult(classification: CompactReadClassification, theme: Theme): string {
|
||||
if (classification.kind === "skill") {
|
||||
return (
|
||||
theme.fg("customMessageLabel", `\x1b[1m[skill]\x1b[22m `) +
|
||||
theme.fg("customMessageText", classification.label) +
|
||||
theme.fg("dim", ` (${keyText("app.tools.expand")} to expand)`)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
theme.fg("toolTitle", theme.bold(`read ${classification.kind}`)) +
|
||||
" " +
|
||||
theme.fg("accent", classification.label) +
|
||||
theme.fg("dim", ` (${keyText("app.tools.expand")} to expand)`)
|
||||
);
|
||||
}
|
||||
|
||||
function formatReadResult(
|
||||
args: { path?: string; file_path?: string; offset?: number; limit?: number } | undefined,
|
||||
result: { content: (TextContent | ImageContent)[]; details?: ReadToolDetails },
|
||||
options: ToolRenderResultOptions,
|
||||
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
|
||||
theme: Theme,
|
||||
showImages: boolean,
|
||||
cwd: string,
|
||||
isError: boolean,
|
||||
): string {
|
||||
if (!options.expanded && !isError) {
|
||||
const classification = getCompactReadClassification(args, cwd);
|
||||
if (classification) {
|
||||
return formatCompactReadResult(classification, theme);
|
||||
}
|
||||
}
|
||||
|
||||
const rawPath = str(args?.file_path ?? args?.path);
|
||||
const output = getTextOutput(result as any, showImages);
|
||||
const output = getTextOutput(result, showImages);
|
||||
const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
|
||||
const renderedLines = lang ? highlightCode(replaceTabs(output), lang) : output.split("\n");
|
||||
const lines = trimTrailingEmptyLines(renderedLines);
|
||||
@@ -121,7 +216,7 @@ function formatReadResult(
|
||||
export function createReadToolDefinition(
|
||||
cwd: string,
|
||||
options?: ReadToolOptions,
|
||||
): ToolDefinition<typeof readSchema, ReadToolDetails | undefined> {
|
||||
): ToolDefinition<typeof readSchema, ReadToolDetails | undefined, ReadRenderState> {
|
||||
const autoResizeImages = options?.autoResizeImages ?? true;
|
||||
const ops = options?.operations ?? defaultReadOperations;
|
||||
return {
|
||||
@@ -256,13 +351,22 @@ export function createReadToolDefinition(
|
||||
);
|
||||
},
|
||||
renderCall(args, theme, context) {
|
||||
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
||||
const text =
|
||||
context.lastComponent instanceof ReadCallText ? context.lastComponent : new ReadCallText(context.state);
|
||||
context.state.hideCall = false;
|
||||
text.setText(formatReadCall(args, theme));
|
||||
return text;
|
||||
},
|
||||
renderResult(result, options, theme, context) {
|
||||
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
||||
text.setText(formatReadResult(context.args, result as any, options, theme, context.showImages));
|
||||
const hideCall =
|
||||
!options.expanded &&
|
||||
!context.isError &&
|
||||
getCompactReadClassification(context.args, context.cwd) !== undefined;
|
||||
text.setText(
|
||||
formatReadResult(context.args, result, options, theme, context.showImages, context.cwd, context.isError),
|
||||
);
|
||||
context.state.hideCall = hideCall;
|
||||
return text;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user