From 8d02ddaeaf6399b06e3e2e4c28fab53eee86c48d Mon Sep 17 00:00:00 2001 From: Admin Date: Sat, 1 Aug 2026 09:11:30 +0800 Subject: [PATCH 1/2] feat(tool): concise error output for failed shell commands --- packages/opencode/src/tool/shell.ts | 12 +++++-- packages/opencode/test/tool/shell.test.ts | 43 +++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/tool/shell.ts b/packages/opencode/src/tool/shell.ts index 1e4423e01774..32aea57b7b1d 100644 --- a/packages/opencode/src/tool/shell.ts +++ b/packages/opencode/src/tool/shell.ts @@ -254,6 +254,9 @@ function tail(text: string, maxLines: number, maxBytes: number) { } } +const FAIL_TAIL_LINES = 50 +const FAIL_TAIL_BYTES = 8 * 1024 + const parse = Effect.fn("ShellTool.parse")(function* (command: string, ps: boolean) { const tree = yield* Effect.promise(() => parser().then((p) => (ps ? p.ps : p.bash).parse(command))) if (!tree) throw new Error("Failed to parse command") @@ -566,7 +569,10 @@ export const ShellTool = Tool.define( } if (aborted) meta.push("User aborted the command") const raw = list.map((item) => item.text).join("") - const end = tail(raw, limits.maxLines, limits.maxBytes) + const failed = typeof code === "number" && code !== 0 + const end = failed + ? tail(raw, FAIL_TAIL_LINES, FAIL_TAIL_BYTES) + : tail(raw, limits.maxLines, limits.maxBytes) if (end.cut) cut = true if (!file && end.cut) { file = yield* trunc.write(raw) @@ -576,7 +582,9 @@ export const ShellTool = Tool.define( if (!output) output = "(no output)" if (cut && file) { - output = `...output truncated...\n\nFull output saved to: ${file}\n\n` + output + const status = failed ? `Command failed with exit code ${code}.\n\n` : "" + output = + `...output truncated...\n\n${status}Full output saved to: ${file}\n\n` + output } if (meta.length > 0) { diff --git a/packages/opencode/test/tool/shell.test.ts b/packages/opencode/test/tool/shell.test.ts index a970f85d468f..bcf5134ec311 100644 --- a/packages/opencode/test/tool/shell.test.ts +++ b/packages/opencode/test/tool/shell.test.ts @@ -122,6 +122,10 @@ const fill = (mode: "lines" | "bytes", n: number) => { if (PS.has(sh())) return `& ${text}` return text } +const failFill = (mode: "lines" | "bytes", n: number) => { + const text = fill(mode, n) + return sh() === "cmd" ? `${text} & exit 1` : `${text}; exit 1` +} const glob = (p: string) => process.platform === "win32" ? Filesystem.normalizePathPattern(p) : p.replaceAll("\\", "/") @@ -1196,4 +1200,43 @@ describe("tool.shell truncation", () => { }), ), ) + it.live("truncates failed command output to a short tail and saves full output to file", () => + runIn( + projectRoot, + Effect.gen(function* () { + const lineCount = Truncate.MAX_LINES + 500 + const result = yield* run({ + command: failFill("lines", lineCount), + }) + expect(result.metadata.exit).not.toBe(0) + expect(result.metadata.truncated).toBe(true) + expect(result.output).toMatch(/\.\.\.output truncated\.\.\./) + expect(result.output).toMatch(/Command failed with exit code 1/) + expect(result.output).toMatch(/Full output saved to:\s+\S+/) + expect(result.output.split("\n").length).toBeLessThan(Truncate.MAX_LINES) + + const filepath = (result.metadata as { outputPath?: string }).outputPath + expect(filepath).toBeTruthy() + const saved = yield* (yield* FSUtil.Service).readFileString(filepath!) + const lines = saved.trim().split(/\r?\n/) + expect(lines.length).toBe(lineCount) + }), + ), + 30_000, + ) + + it.live("does not truncate failed command output when it is small", () => + runIn( + projectRoot, + Effect.gen(function* () { + const result = yield* run({ + command: `exit 3`, + }) + expect(result.metadata.exit).toBe(3) + expect(result.metadata.truncated).toBe(false) + expect(result.output).toBe("(no output)") + }), + ), + 30_000, + ) }) From 2dbfb9713294fcd90a997c5a4a7213854238e418 Mon Sep 17 00:00:00 2001 From: Admin Date: Sat, 1 Aug 2026 09:35:15 +0800 Subject: [PATCH 2/2] fix(tool): label failed shell command with exit code even when not truncated --- packages/opencode/src/tool/shell.ts | 4 +++- packages/opencode/test/tool/shell.test.ts | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/tool/shell.ts b/packages/opencode/src/tool/shell.ts index 32aea57b7b1d..3cbafee36373 100644 --- a/packages/opencode/src/tool/shell.ts +++ b/packages/opencode/src/tool/shell.ts @@ -581,10 +581,12 @@ export const ShellTool = Tool.define( let output = end.text if (!output) output = "(no output)" + const status = failed ? `Command failed with exit code ${code}.\n\n` : "" if (cut && file) { - const status = failed ? `Command failed with exit code ${code}.\n\n` : "" output = `...output truncated...\n\n${status}Full output saved to: ${file}\n\n` + output + } else if (status) { + output = status + output } if (meta.length > 0) { diff --git a/packages/opencode/test/tool/shell.test.ts b/packages/opencode/test/tool/shell.test.ts index bcf5134ec311..11610d7dc658 100644 --- a/packages/opencode/test/tool/shell.test.ts +++ b/packages/opencode/test/tool/shell.test.ts @@ -1234,7 +1234,8 @@ describe("tool.shell truncation", () => { }) expect(result.metadata.exit).toBe(3) expect(result.metadata.truncated).toBe(false) - expect(result.output).toBe("(no output)") + expect(result.output).toContain("Command failed with exit code 3") + expect(result.output).toContain("(no output)") }), ), 30_000,