@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user