From f9836fe706409ca20b140ca979ee2a082eb0b0f4 Mon Sep 17 00:00:00 2001 From: ry2009 <134240944+ry2009@users.noreply.github.com> Date: Fri, 2 Jan 2026 16:37:03 -0500 Subject: [PATCH 1/3] tool: collapse bash \r progress output --- packages/opencode/src/tool/bash.ts | 55 +++++++++++++++++++----- packages/opencode/test/tool/bash.test.ts | 40 +++++++++++++++++ 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 965e8d5450f..61c66ab5d59 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -161,6 +161,10 @@ export const BashTool = Tool.define("bash", async () => { }) let output = "" + let outputLimitReached = false + let outputBuffer = "" + let lineBuffer = "" + let overwritingLine = false // Initialize metadata with empty output ctx.metadata({ @@ -170,16 +174,48 @@ export const BashTool = Tool.define("bash", async () => { }, }) + const appendText = (text: string) => { + for (let i = 0; i < text.length; i++) { + const char = text[i] + if (char === "\r") { + overwritingLine = true + continue + } + + if (char === "\n") { + outputBuffer += lineBuffer + "\n" + lineBuffer = "" + overwritingLine = false + continue + } + + if (overwritingLine) { + lineBuffer = "" + overwritingLine = false + } + + lineBuffer += char + } + + output = outputBuffer + lineBuffer + } + const append = (chunk: Buffer) => { - if (output.length <= MAX_OUTPUT_LENGTH) { - output += chunk.toString() - ctx.metadata({ - metadata: { - output, - description: params.description, - }, - }) + if (outputLimitReached) return + + appendText(chunk.toString()) + + if (output.length > MAX_OUTPUT_LENGTH) { + output = output.slice(0, MAX_OUTPUT_LENGTH) + outputLimitReached = true } + + ctx.metadata({ + metadata: { + output, + description: params.description, + }, + }) } proc.stdout?.on("data", append) @@ -229,8 +265,7 @@ export const BashTool = Tool.define("bash", async () => { let resultMetadata: String[] = [""] - if (output.length > MAX_OUTPUT_LENGTH) { - output = output.slice(0, MAX_OUTPUT_LENGTH) + if (outputLimitReached) { resultMetadata.push(`bash tool truncated output as it exceeded ${MAX_OUTPUT_LENGTH} char limit`) } diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index 2eb17a9fc94..8e65fd2a24b 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -35,6 +35,46 @@ describe("tool.bash", () => { }, }) }) + + test("collapses carriage-return progress updates", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const bash = await BashTool.init() + const result = await bash.execute( + { + command: "printf 'Downloading 0%%\\rDownloading 50%%\\rDownloading 100%%\\nDone\\n'", + description: "Print progress output", + }, + ctx, + ) + expect(result.metadata.exit).toBe(0) + expect(result.metadata.output).toContain("Downloading 100%") + expect(result.metadata.output).toContain("Done") + expect(result.metadata.output).not.toContain("Downloading 0%") + expect(result.metadata.output).not.toContain("Downloading 50%") + expect(result.metadata.output).not.toContain("\r") + }, + }) + }) + + test("does not drop trailing carriage-return output", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const bash = await BashTool.init() + const result = await bash.execute( + { + command: "printf 'hello\\r'", + description: "Print hello carriage return", + }, + ctx, + ) + expect(result.metadata.exit).toBe(0) + expect(result.metadata.output).toBe("hello") + }, + }) + }) }) describe("tool.bash permissions", () => { From 128859fbdd70a18126469640181cb0c0cea301a8 Mon Sep 17 00:00:00 2001 From: ry2009 <134240944+ry2009@users.noreply.github.com> Date: Fri, 2 Jan 2026 16:40:51 -0500 Subject: [PATCH 2/3] tool: coalesce bash metadata on \r --- packages/opencode/src/tool/bash.ts | 54 +++++++++++++++++------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 61c66ab5d59..0f2836a5cd2 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -160,16 +160,19 @@ export const BashTool = Tool.define("bash", async () => { detached: process.platform !== "win32", }) - let output = "" - let outputLimitReached = false - let outputBuffer = "" - let lineBuffer = "" - let overwritingLine = false + const state = { + output: "", + outputLimitReached: false, + outputBuffer: "", + lineBuffer: "", + overwritingLine: false, + lastSent: "", + } // Initialize metadata with empty output ctx.metadata({ metadata: { - output: "", + output: state.output, description: params.description, }, }) @@ -178,41 +181,44 @@ export const BashTool = Tool.define("bash", async () => { for (let i = 0; i < text.length; i++) { const char = text[i] if (char === "\r") { - overwritingLine = true + state.overwritingLine = true continue } if (char === "\n") { - outputBuffer += lineBuffer + "\n" - lineBuffer = "" - overwritingLine = false + state.outputBuffer += state.lineBuffer + "\n" + state.lineBuffer = "" + state.overwritingLine = false continue } - if (overwritingLine) { - lineBuffer = "" - overwritingLine = false + if (state.overwritingLine) { + state.lineBuffer = "" + state.overwritingLine = false } - lineBuffer += char + state.lineBuffer += char } - output = outputBuffer + lineBuffer + state.output = state.outputBuffer + state.lineBuffer } const append = (chunk: Buffer) => { - if (outputLimitReached) return + if (state.outputLimitReached) return appendText(chunk.toString()) - if (output.length > MAX_OUTPUT_LENGTH) { - output = output.slice(0, MAX_OUTPUT_LENGTH) - outputLimitReached = true + if (state.output.length > MAX_OUTPUT_LENGTH) { + state.output = state.output.slice(0, MAX_OUTPUT_LENGTH) + state.outputLimitReached = true } + if (state.output === state.lastSent) return + state.lastSent = state.output + ctx.metadata({ metadata: { - output, + output: state.output, description: params.description, }, }) @@ -265,7 +271,7 @@ export const BashTool = Tool.define("bash", async () => { let resultMetadata: String[] = [""] - if (outputLimitReached) { + if (state.outputLimitReached) { resultMetadata.push(`bash tool truncated output as it exceeded ${MAX_OUTPUT_LENGTH} char limit`) } @@ -279,17 +285,17 @@ export const BashTool = Tool.define("bash", async () => { if (resultMetadata.length > 1) { resultMetadata.push("") - output += "\n\n" + resultMetadata.join("\n") + state.output += "\n\n" + resultMetadata.join("\n") } return { title: params.description, metadata: { - output, + output: state.output, exit: proc.exitCode, description: params.description, }, - output, + output: state.output, } }, } From bcabeb44326a46ff31fb3049c4c48e14bb90dc9f Mon Sep 17 00:00:00 2001 From: ry2009 <134240944+ry2009@users.noreply.github.com> Date: Fri, 2 Jan 2026 16:42:04 -0500 Subject: [PATCH 3/3] tool: avoid index loop for bash output --- packages/opencode/src/tool/bash.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 0f2836a5cd2..d171005fa2a 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -178,8 +178,7 @@ export const BashTool = Tool.define("bash", async () => { }) const appendText = (text: string) => { - for (let i = 0; i < text.length; i++) { - const char = text[i] + for (const char of text) { if (char === "\r") { state.overwritingLine = true continue