fix: render pwsh and powershell tools correctly in UI

This fixes regressions from splitting the shell tools where powershell commands were missing their native exit codes and their correct UI rendering.
This commit is contained in:
LukeParkerDev 2026-04-03 14:01:13 +10:00
commit 6ad6358eb1
4 changed files with 51 additions and 7 deletions

View file

@ -0,0 +1,31 @@
{
"name": ".opencode",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@opencode-ai/plugin": "*"
}
},
"node_modules/@opencode-ai/plugin": {
"version": "1.2.24",
"license": "MIT",
"dependencies": {
"@opencode-ai/sdk": "1.2.24",
"zod": "4.1.8"
}
},
"node_modules/@opencode-ai/sdk": {
"version": "1.2.24",
"license": "MIT"
},
"node_modules/zod": {
"version": "4.1.8",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View file

@ -11,6 +11,11 @@ export function preview(text: string) {
} }
export namespace ShellRunner { export namespace ShellRunner {
function wrap(name: string, command: string) {
if (name !== "powershell" && name !== "pwsh") return command
return `${command}; if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE }; if ($?) { exit 0 }; exit 1`
}
export async function shellEnv(ctx: Tool.Context, cwd: string) { export async function shellEnv(ctx: Tool.Context, cwd: string) {
const extra = await Plugin.trigger("shell.env", { cwd, sessionID: ctx.sessionID, callID: ctx.callID }, { env: {} }) const extra = await Plugin.trigger("shell.env", { cwd, sessionID: ctx.sessionID, callID: ctx.callID }, { env: {} })
return { return {
@ -21,7 +26,7 @@ export namespace ShellRunner {
export function launch(shell: string, name: string, command: string, cwd: string, env: NodeJS.ProcessEnv) { export function launch(shell: string, name: string, command: string, cwd: string, env: NodeJS.ProcessEnv) {
if (process.platform === "win32" && (name === "powershell" || name === "pwsh")) { if (process.platform === "win32" && (name === "powershell" || name === "pwsh")) {
return spawn(shell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", command], { return spawn(shell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", wrap(name, command)], {
cwd, cwd,
env, env,
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
@ -54,6 +59,7 @@ export namespace ShellRunner {
) { ) {
const proc = launch(input.shell, input.name, input.command, input.cwd, input.env) const proc = launch(input.shell, input.name, input.command, input.cwd, input.env)
let output = "" let output = ""
let code: number | null = null
ctx.metadata({ ctx.metadata({
metadata: { metadata: {
@ -103,12 +109,14 @@ export namespace ShellRunner {
ctx.abort.removeEventListener("abort", abort) ctx.abort.removeEventListener("abort", abort)
} }
proc.once("exit", () => { proc.once("exit", (next) => {
exited = true exited = true
code = next
}) })
proc.once("close", () => { proc.once("close", (next) => {
exited = true exited = true
code = next
cleanup() cleanup()
resolve() resolve()
}) })
@ -131,7 +139,7 @@ export namespace ShellRunner {
title: input.description, title: input.description,
metadata: { metadata: {
output: preview(output), output: preview(output),
exit: proc.exitCode, exit: code,
description: input.description, description: input.description,
}, },
output, output,

View file

@ -269,6 +269,8 @@ export type ToolInfo = {
subtitle?: string subtitle?: string
} }
const SHELL = new Set(["bash", "pwsh", "powershell"])
function agentTitle(i18n: UiI18n, type?: string) { function agentTitle(i18n: UiI18n, type?: string) {
if (!type) return i18n.t("ui.tool.agent.default") if (!type) return i18n.t("ui.tool.agent.default")
return i18n.t("ui.tool.agent", { type }) return i18n.t("ui.tool.agent", { type })
@ -331,6 +333,8 @@ export function getToolInfo(tool: string, input: any = {}): ToolInfo {
} }
} }
case "bash": case "bash":
case "pwsh":
case "powershell":
return { return {
icon: "console", icon: "console",
title: i18n.t("ui.tool.shell"), title: i18n.t("ui.tool.shell"),
@ -518,7 +522,7 @@ function renderable(part: PartType, showReasoningSummaries = true) {
} }
function toolDefaultOpen(tool: string, shell = false, edit = false) { function toolDefaultOpen(tool: string, shell = false, edit = false) {
if (tool === "bash") return shell if (SHELL.has(tool)) return shell
if (tool === "edit" || tool === "write" || tool === "apply_patch") return edit if (tool === "edit" || tool === "write" || tool === "apply_patch") return edit
} }

View file

@ -33,6 +33,7 @@ import type { Diagnostic } from "vscode-languageserver-types"
import styles from "./part.module.css" import styles from "./part.module.css"
const MIN_DURATION = 2000 const MIN_DURATION = 2000
const SHELL = new Set(["bash", "pwsh", "powershell"])
export interface PartProps { export interface PartProps {
index: number index: number
@ -90,7 +91,7 @@ export function Part(props: PartProps) {
<Match when={props.part.type === "tool" && props.part.tool === "todowrite"}> <Match when={props.part.type === "tool" && props.part.tool === "todowrite"}>
<IconQueueList width={18} height={18} /> <IconQueueList width={18} height={18} />
</Match> </Match>
<Match when={props.part.type === "tool" && props.part.tool === "bash"}> <Match when={props.part.type === "tool" && SHELL.has(props.part.tool)}>
<IconCommandLine width={18} height={18} /> <IconCommandLine width={18} height={18} />
</Match> </Match>
<Match when={props.part.type === "tool" && props.part.tool === "edit"}> <Match when={props.part.type === "tool" && props.part.tool === "edit"}>
@ -240,7 +241,7 @@ export function Part(props: PartProps) {
state={props.part.state} state={props.part.state}
/> />
</Match> </Match>
<Match when={props.part.tool === "bash"}> <Match when={SHELL.has(props.part.tool)}>
<BashTool <BashTool
id={props.part.id} id={props.part.id}
tool={props.part.tool} tool={props.part.tool}