@@ -9,6 +9,7 @@
|
|||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Fixed final bash tool cards to avoid rendering duplicate full-output truncation paths ([#4819](https://github.com/earendil-works/pi/issues/4819)).
|
- 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
|
## [0.75.4] - 2026-05-20
|
||||||
|
|
||||||
|
|||||||
@@ -45,8 +45,10 @@ export class OutputAccumulator {
|
|||||||
private tailStartsAtLineBoundary = true;
|
private tailStartsAtLineBoundary = true;
|
||||||
private totalRawBytes = 0;
|
private totalRawBytes = 0;
|
||||||
private totalDecodedBytes = 0;
|
private totalDecodedBytes = 0;
|
||||||
private totalLines = 1;
|
private completedLines = 0;
|
||||||
|
private totalLines = 0;
|
||||||
private currentLineBytes = 0;
|
private currentLineBytes = 0;
|
||||||
|
private hasOpenLine = false;
|
||||||
private finished = false;
|
private finished = false;
|
||||||
|
|
||||||
private tempFilePath: string | undefined;
|
private tempFilePath: string | undefined;
|
||||||
@@ -164,10 +166,14 @@ export class OutputAccumulator {
|
|||||||
}
|
}
|
||||||
if (newlines === 0) {
|
if (newlines === 0) {
|
||||||
this.currentLineBytes += bytes;
|
this.currentLineBytes += bytes;
|
||||||
|
this.hasOpenLine = true;
|
||||||
} else {
|
} else {
|
||||||
this.totalLines += newlines;
|
this.completedLines += newlines;
|
||||||
this.currentLineBytes = byteLength(text.slice(lastNewline + 1));
|
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 {
|
private trimTail(): void {
|
||||||
|
|||||||
@@ -44,6 +44,17 @@ export interface TruncationOptions {
|
|||||||
maxBytes?: number;
|
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.
|
* 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 maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
||||||
|
|
||||||
const totalBytes = Buffer.byteLength(content, "utf-8");
|
const totalBytes = Buffer.byteLength(content, "utf-8");
|
||||||
const lines = content.split("\n");
|
const lines = splitLinesForCounting(content);
|
||||||
const totalLines = lines.length;
|
const totalLines = lines.length;
|
||||||
|
|
||||||
// Check if no truncation needed
|
// Check if no truncation needed
|
||||||
@@ -159,7 +170,7 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
|
|||||||
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
||||||
|
|
||||||
const totalBytes = Buffer.byteLength(content, "utf-8");
|
const totalBytes = Buffer.byteLength(content, "utf-8");
|
||||||
const lines = content.split("\n");
|
const lines = splitLinesForCounting(content);
|
||||||
const totalLines = lines.length;
|
const totalLines = lines.length;
|
||||||
|
|
||||||
// Check if no truncation needed
|
// Check if no truncation needed
|
||||||
|
|||||||
@@ -580,6 +580,28 @@ describe("Coding Agent Tools", () => {
|
|||||||
expect(getTextOutput(result)).toContain("line 4999");
|
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 () => {
|
it("should decode UTF-8 characters split across output chunks", async () => {
|
||||||
const euro = Buffer.from("€\n", "utf-8");
|
const euro = Buffer.from("€\n", "utf-8");
|
||||||
const operations: BashOperations = {
|
const operations: BashOperations = {
|
||||||
|
|||||||
Reference in New Issue
Block a user