diff --git a/packages/core/test/tool-shell.test.ts b/packages/core/test/tool-shell.test.ts index 7bd3e0e339..83c94a5c7b 100644 --- a/packages/core/test/tool-shell.test.ts +++ b/packages/core/test/tool-shell.test.ts @@ -148,6 +148,12 @@ const call = (input: typeof ShellTool.Input.Type, id = "call-shell") => ({ const isWindows = process.platform === "win32" const cwdCommand = isWindows ? "(Get-Location).Path; Start-Sleep -Milliseconds 100" : "pwd" const helloCommand = isWindows ? "[Console]::Out.Write('hello'); Start-Sleep -Milliseconds 100" : "printf hello" +const stderrCommand = isWindows + ? "[Console]::Error.Write('stderr only'); Start-Sleep -Milliseconds 100" + : "printf 'stderr only' >&2" +const mixedOutputCommand = isWindows + ? "[Console]::Out.Write('stdout'); Start-Sleep -Milliseconds 50; [Console]::Error.Write('stderr'); Start-Sleep -Milliseconds 100" + : "printf stdout; sleep 0.05; printf stderr >&2" const idleCommand = isWindows ? "Start-Sleep -Seconds 60" : "sleep 60" const bodyExitCommand = isWindows ? "[Console]::Out.Write('body'); Start-Sleep -Milliseconds 100; exit 7" @@ -230,6 +236,29 @@ describe("ShellTool", () => { ), ) + it.live("captures stderr-only and mixed stdout/stderr output", () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + return withSession(tmp.path, (registry) => + Effect.gen(function* () { + const stderr = yield* settleTool(registry, call({ command: stderrCommand }, "call-stderr")) + expect(stderr.output?.structured).toMatchObject({ exit: 0, truncated: false }) + expect(stderr.output?.content[0]).toEqual({ type: "text", text: "stderr only" }) + + const mixed = yield* settleTool(registry, call({ command: mixedOutputCommand }, "call-mixed")) + expect(mixed.output?.structured).toMatchObject({ exit: 0, truncated: false }) + const output = mixed.output?.content[0]?.type === "text" ? mixed.output.content[0].text : "" + expect(output).toContain("stdout") + expect(output).toContain("stderr") + }), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]().then(() => undefined)), + ), + ) + it.live("rejects a workdir that stops being a directory during approval", () => Effect.acquireUseRelease( Effect.promise(() => tmpdir()), diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index 8cc13d76c6..f1f3e18283 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -1687,6 +1687,7 @@ function ToolPart(props: { part: SessionMessageAssistantTool }) { if (ctx.showDetails()) return false if (runningShell()) return false if (props.part.state.status !== "completed") return false + if (display() === "shell") return false return true })