Add tool output truncation with line/byte limits

- Add truncate.ts utility with truncateHead/truncateTail functions
- Both respect 2000 line and 30KB limits (whichever hits first)
- read: head truncation, returns truncation info in details
- bash: tail truncation, writes full output to temp file if large
- grep: head truncation + 100 match limit
- find: head truncation + 1000 result limit
- ls: head truncation + 500 entry limit
- tool-execution.ts displays truncation notices in warning color
- All tools return clean output + structured truncation in details
This commit is contained in:
Mario Zechner 2025-12-07 00:03:16 +01:00
parent 95eadb9ed7
commit de77cd1419
7 changed files with 611 additions and 219 deletions

View file

@ -115,7 +115,6 @@ export class ToolExecutionComponent extends Container {
text = theme.fg("toolTitle", theme.bold(`$ ${command || theme.fg("toolOutput", "...")}`));
if (this.result) {
// Show output without code fences - more minimal
const output = this.getTextOutput().trim();
if (output) {
const lines = output.split("\n");
@ -128,17 +127,28 @@ export class ToolExecutionComponent extends Container {
text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
}
}
// Show truncation notice at the bottom in warning color if present in details
const truncation = this.result.details?.truncation;
const fullOutputPath = this.result.details?.fullOutputPath;
if (truncation?.truncated) {
if (fullOutputPath) {
text += "\n" + theme.fg("warning", `[Full output: ${fullOutputPath}]`);
}
text += "\n" + theme.fg("warning", truncation.notice);
}
}
} else if (this.toolName === "read") {
const path = shortenPath(this.args?.file_path || this.args?.path || "");
const offset = this.args?.offset;
const limit = this.args?.limit;
// Build path display with offset/limit suffix
// Build path display with offset/limit suffix (in warning color if offset/limit used)
let pathDisplay = path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
if (offset !== undefined) {
const endLine = limit !== undefined ? offset + limit : "";
pathDisplay += theme.fg("toolOutput", `:${offset}${endLine ? `-${endLine}` : ""}`);
if (offset !== undefined || limit !== undefined) {
const startLine = offset ?? 1;
const endLine = limit !== undefined ? startLine + limit - 1 : "";
pathDisplay += theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
}
text = theme.fg("toolTitle", theme.bold("read")) + " " + pathDisplay;
@ -146,6 +156,7 @@ export class ToolExecutionComponent extends Container {
if (this.result) {
const output = this.getTextOutput();
const lines = output.split("\n");
const maxLines = this.expanded ? lines.length : 10;
const displayLines = lines.slice(0, maxLines);
const remaining = lines.length - maxLines;
@ -154,6 +165,12 @@ export class ToolExecutionComponent extends Container {
if (remaining > 0) {
text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
}
// Show truncation notice at the bottom in warning color if present in details
const truncation = this.result.details?.truncation;
if (truncation?.truncated) {
text += "\n" + theme.fg("warning", truncation.notice);
}
}
} else if (this.toolName === "write") {
const path = shortenPath(this.args?.file_path || this.args?.path || "");
@ -231,6 +248,16 @@ export class ToolExecutionComponent extends Container {
text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
}
}
// Show truncation notice from details
const entryLimit = this.result.details?.entryLimitReached;
const truncation = this.result.details?.truncation;
if (entryLimit) {
text += "\n" + theme.fg("warning", `[Truncated: ${entryLimit} entries limit reached]`);
}
if (truncation?.truncated) {
text += "\n" + theme.fg("warning", truncation.notice);
}
}
} else if (this.toolName === "find") {
const pattern = this.args?.pattern || "";
@ -259,6 +286,16 @@ export class ToolExecutionComponent extends Container {
text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
}
}
// Show truncation notice from details
const resultLimit = this.result.details?.resultLimitReached;
const truncation = this.result.details?.truncation;
if (resultLimit) {
text += "\n" + theme.fg("warning", `[Truncated: ${resultLimit} results limit reached]`);
}
if (truncation?.truncated) {
text += "\n" + theme.fg("warning", truncation.notice);
}
}
} else if (this.toolName === "grep") {
const pattern = this.args?.pattern || "";
@ -291,6 +328,16 @@ export class ToolExecutionComponent extends Container {
text += theme.fg("toolOutput", `\n... (${remaining} more lines)`);
}
}
// Show truncation notice from details
const matchLimit = this.result.details?.matchLimitReached;
const truncation = this.result.details?.truncation;
if (matchLimit) {
text += "\n" + theme.fg("warning", `[Truncated: ${matchLimit} matches limit reached]`);
}
if (truncation?.truncated) {
text += "\n" + theme.fg("warning", truncation.notice);
}
}
} else {
// Generic tool