From 2c33d28385020ed5e65c46e8e816ebe88d1f6541 Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Sat, 18 Jul 2026 23:20:53 +0200 Subject: [PATCH] mini: fix shell tool output display V2 shell content appends a model-only exit status after the real command output. Joining every text part showed that status in the CLI; take only the first text part for shell tools. --- packages/cli/src/mini/noninteractive.ts | 6 ++---- packages/cli/src/mini/stream-v2.subagent.ts | 7 ++----- packages/cli/src/mini/tool.ts | 6 ++++++ packages/cli/test/mini.test.ts | 20 +++++++++++++++++++- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/mini/noninteractive.ts b/packages/cli/src/mini/noninteractive.ts index 86e7cead2aee..c31569b70b6b 100644 --- a/packages/cli/src/mini/noninteractive.ts +++ b/packages/cli/src/mini/noninteractive.ts @@ -2,6 +2,7 @@ import type { EventSubscribeOutput, OpenCodeClient } from "@opencode-ai/client/p import { SessionMessage } from "@opencode-ai/schema/session-message" import { EOL } from "node:os" import { readFile } from "node:fs/promises" +import { toolOutputText } from "./tool" import { UI } from "./ui" import type { MiniToolPart } from "./types" @@ -274,10 +275,7 @@ export async function runNonInteractivePrompt(input: Input) { state: { status: "completed", input: current.input, - output: event.data.content - .filter((item) => item.type === "text") - .map((item) => item.text) - .join("\n"), + output: toolOutputText(current.tool, event.data.content), title: current.tool, metadata: { structured: event.data.structured, diff --git a/packages/cli/src/mini/stream-v2.subagent.ts b/packages/cli/src/mini/stream-v2.subagent.ts index fd716f4f75ca..eecc7b322ca6 100644 --- a/packages/cli/src/mini/stream-v2.subagent.ts +++ b/packages/cli/src/mini/stream-v2.subagent.ts @@ -23,6 +23,7 @@ import type { } from "@opencode-ai/client/promise" import { Locale } from "@opencode-ai/tui/util/locale" import type { FooterSubagentDetail, FooterSubagentState, FooterSubagentTab, MiniToolPart, StreamCommit } from "./types" +import { toolOutputText } from "./tool" const CHILD_MESSAGE_LIMIT = 80 const CHILD_FRAME_LIMIT = 80 @@ -32,10 +33,6 @@ const FALLBACK_LABEL = "Subagent" type V2Event = EventSubscribeOutput -export function outputText(content: ReadonlyArray<{ type: string; text?: string }>) { - return content.flatMap((item) => (item.type === "text" && item.text ? [item.text] : [])).join("\n") -} - export function miniTool(input: { sessionID: string messageID: string @@ -82,7 +79,7 @@ export function miniTool(input: { state: { status: "completed", input: tool.state.input, - output: outputText(tool.state.content), + output: toolOutputText(tool.name, tool.state.content), title: tool.name, metadata: { structured: tool.state.structured, diff --git a/packages/cli/src/mini/tool.ts b/packages/cli/src/mini/tool.ts index fc30c565ec99..0849fbf98343 100644 --- a/packages/cli/src/mini/tool.ts +++ b/packages/cli/src/mini/tool.ts @@ -177,6 +177,12 @@ function text(v: unknown): string { return typeof v === "string" ? v : "" } +export function toolOutputText(name: string, content: ReadonlyArray<{ type: string; text?: string }>) { + // V2 shell content appends model-only status after the user-visible command output. + if (name === "shell") return content.find((item) => item.type === "text")?.text ?? "" + return content.flatMap((item) => (item.type === "text" && item.text ? [item.text] : [])).join("\n") +} + function num(v: unknown): number | undefined { if (typeof v !== "number" || !Number.isFinite(v)) { return undefined diff --git a/packages/cli/test/mini.test.ts b/packages/cli/test/mini.test.ts index 5fb8b3be7f4e..4b4dc24aa827 100644 --- a/packages/cli/test/mini.test.ts +++ b/packages/cli/test/mini.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test" import { InstallationVersion } from "@opencode-ai/core/installation/version" import path from "node:path" import { mergeInteractiveInput, mergeNonInteractiveInput, parseRunModel, pickRunModel } from "../src/mini" -import { toolInlineInfo, toolView } from "../src/mini/tool" +import { toolInlineInfo, toolOutputText, toolView } from "../src/mini/tool" async function cli(args: string[]) { const child = Bun.spawn([process.execPath, "run", "src/index.ts", ...args], { @@ -36,6 +36,24 @@ describe("mini command", () => { expect(toolInlineInfo(part)).toMatchObject({ icon: "$", title: "pwd", mode: "block" }) }) + test("uses non-empty V2 shell output without the model-facing status", () => { + expect( + toolOutputText("shell", [ + { type: "text", text: "mini-output\n" }, + { type: "text", text: "Command exited with code 0." }, + ]), + ).toBe("mini-output\n") + }) + + test("keeps empty V2 shell output empty", () => { + expect( + toolOutputText("shell", [ + { type: "text", text: "" }, + { type: "text", text: "Command exited with code 0." }, + ]), + ).toBe("") + }) + test("uses piped stdin as the initial prompt", () => { expect(mergeInteractiveInput("from stdin", undefined)).toBe("from stdin") expect(mergeInteractiveInput("from stdin", "from flag")).toBe("from stdin\nfrom flag")