refactor(shell): centralize shell tool identity

Move shell tool ID checks behind shared helpers so runtime code and tests stop duplicating bash, pwsh, and powershell branches. This keeps shell-specific behavior aligned across consumers and makes follow-on shell changes less error-prone.
This commit is contained in:
LukeParkerDev 2026-04-03 14:56:40 +10:00
commit 2eb9ae4d34
19 changed files with 99 additions and 92 deletions

View file

@ -1,20 +1,12 @@
import type { ToolPart } from "@opencode-ai/sdk/v2/client"
import type { Page } from "@playwright/test"
import { test, expect } from "../fixtures"
import { assistantText } from "../actions"
import { promptSelector } from "../selectors"
import { createSdk } from "../utils"
import { createSdk, isShell } from "../utils"
const text = (value: string | null) => (value ?? "").replace(/\u200B/g, "").trim()
type Sdk = ReturnType<typeof createSdk>
const isBash = (part: unknown): part is ToolPart => {
if (!part || typeof part !== "object") return false
if (!("type" in part) || part.type !== "tool") return false
if (!("tool" in part) || part.tool !== "bash") return false
return "state" in part
}
async function wait(page: Page, value: string) {
await expect.poll(async () => text(await page.locator(promptSelector).textContent())).toBe(value)
}
@ -31,7 +23,7 @@ async function shell(sdk: Sdk, sessionID: string, cmd: string, token: string) {
const part = messages
.filter((item) => item.info.role === "assistant")
.flatMap((item) => item.parts)
.filter(isBash)
.filter(isShell)
.find((item) => item.state.input?.command === cmd && item.state.status === "completed")
if (!part || part.state.status !== "completed") return

View file

@ -1,13 +1,6 @@
import type { ToolPart } from "@opencode-ai/sdk/v2/client"
import { test, expect } from "../fixtures"
import { withSession } from "../actions"
const isBash = (part: unknown): part is ToolPart => {
if (!part || typeof part !== "object") return false
if (!("type" in part) || part.type !== "tool") return false
if (!("tool" in part) || part.tool !== "bash") return false
return "state" in part
}
import { isShell } from "../utils"
async function setAutoAccept(page: Parameters<typeof test>[0]["page"], enabled: boolean) {
const button = page.locator('[data-action="prompt-permissions"]').first()
@ -42,7 +35,7 @@ test("shell mode runs a command in the project directory", async ({ page, projec
if (!msg) return
const part = msg.parts
.filter(isBash)
.filter(isShell)
.find((item) => item.state.input?.command === cmd && item.state.status === "completed")
if (!part || part.state.status !== "completed") return

View file

@ -1,4 +1,4 @@
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"
import { createOpencodeClient, type ToolPart } from "@opencode-ai/sdk/v2/client"
import { base64Encode, checksum } from "@opencode-ai/util/encode"
export const serverHost = process.env.PLAYWRIGHT_SERVER_HOST ?? "127.0.0.1"
@ -18,6 +18,7 @@ const serverLabels = (() => {
export const serverNames = [...new Set(serverLabels)]
export const serverUrls = serverNames.map((name) => `http://${name}`)
const shell = new Set(["bash", "pwsh", "powershell"])
const escape = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
@ -30,6 +31,13 @@ export function createSdk(directory?: string, baseUrl = serverUrl) {
return createOpencodeClient({ baseUrl, directory, throwOnError: true })
}
export function isShell(part: unknown): part is ToolPart {
if (!part || typeof part !== "object") return false
if (!("type" in part) || part.type !== "tool") return false
if (!("tool" in part) || typeof part.tool !== "string" || !shell.has(part.tool)) return false
return "state" in part
}
export async function resolveDirectory(directory: string, baseUrl = serverUrl) {
return createSdk(directory, baseUrl)
.path.get()