fix(coding-agent): correct bash truncation line count

closes #4818
This commit is contained in:
Mario Zechner
2026-05-21 16:54:56 +02:00
parent 6a38d7ff6e
commit f953067814
4 changed files with 45 additions and 5 deletions

View File

@@ -9,6 +9,7 @@
### Fixed
- Fixed final bash tool cards to avoid rendering duplicate full-output truncation paths ([#4819](https://github.com/earendil-works/pi/issues/4819)).
- Fixed bash tool truncation line counts to ignore the trailing newline as an extra output line ([#4818](https://github.com/earendil-works/pi/issues/4818)).
## [0.75.4] - 2026-05-20

View File

@@ -45,8 +45,10 @@ export class OutputAccumulator {
private tailStartsAtLineBoundary = true;
private totalRawBytes = 0;
private totalDecodedBytes = 0;
private totalLines = 1;
private completedLines = 0;
private totalLines = 0;
private currentLineBytes = 0;
private hasOpenLine = false;
private finished = false;
private tempFilePath: string | undefined;
@@ -164,10 +166,14 @@ export class OutputAccumulator {
}
if (newlines === 0) {
this.currentLineBytes += bytes;
this.hasOpenLine = true;
} else {
this.totalLines += newlines;
this.currentLineBytes = byteLength(text.slice(lastNewline + 1));
this.completedLines += newlines;
const tail = text.slice(lastNewline + 1);
this.currentLineBytes = byteLength(tail);
this.hasOpenLine = tail.length > 0;
}
this.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0);
}
private trimTail(): void {

View File

@@ -44,6 +44,17 @@ export interface TruncationOptions {
maxBytes?: number;
}
function splitLinesForCounting(content: string): string[] {
if (content.length === 0) {
return [];
}
const lines = content.split("\n");
if (content.endsWith("\n")) {
lines.pop();
}
return lines;
}
/**
* Format bytes as human-readable size.
*/
@@ -69,7 +80,7 @@ export function truncateHead(content: string, options: TruncationOptions = {}):
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
const totalBytes = Buffer.byteLength(content, "utf-8");
const lines = content.split("\n");
const lines = splitLinesForCounting(content);
const totalLines = lines.length;
// Check if no truncation needed
@@ -159,7 +170,7 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
const totalBytes = Buffer.byteLength(content, "utf-8");
const lines = content.split("\n");
const lines = splitLinesForCounting(content);
const totalLines = lines.length;
// Check if no truncation needed

View File

@@ -580,6 +580,28 @@ describe("Coding Agent Tools", () => {
expect(getTextOutput(result)).toContain("line 4999");
});
it("should not count a trailing newline as an extra truncated bash output line", async () => {
const operations: BashOperations = {
exec: async (_command, _cwd, { onData }) => {
for (let i = 1; i <= 4000; i++) {
onData(Buffer.from(`line-${String(i).padStart(4, "0")}\n`, "utf-8"));
}
return { exitCode: 0 };
},
};
const bash = createBashTool(testDir, { operations });
const result = await bash.execute("test-call-trailing-newline-line-count", { command: "many-lines" });
const output = getTextOutput(result);
expect(result.details?.truncation?.totalLines).toBe(4000);
expect(result.details?.truncation?.outputLines).toBe(2000);
expect(output).toContain("line-2001");
expect(output).toContain("line-4000");
expect(output).toMatch(/\[Showing lines 2001-4000 of 4000\. Full output: /);
expect(output).not.toContain("4001");
});
it("should decode UTF-8 characters split across output chunks", async () => {
const euro = Buffer.from("€\n", "utf-8");
const operations: BashOperations = {