diff --git a/packages/opencode/src/cli/cmd/run/tool.ts b/packages/opencode/src/cli/cmd/run/tool.ts index 3dab7aa8df10..0ad7b5170ed3 100644 --- a/packages/opencode/src/cli/cmd/run/tool.ts +++ b/packages/opencode/src/cli/cmd/run/tool.ts @@ -272,6 +272,29 @@ export function toolPath(input?: string, opts: { home?: boolean } = {}): string return abs.replaceAll("\\", "/") } +function toolLocation(input?: string, opts: { home?: boolean } = {}): string { + const raw = input?.trim() + if (!raw) { + return "" + } + + const formatted = toolPath(raw, opts) + if (!formatted || formatted === ".") { + return "" + } + + return formatted +} + +function appendLocation(title: string, input?: string, opts: { home?: boolean } = {}): string { + const location = toolLocation(input, opts) + if (!location || title.includes(location)) { + return title + } + + return `${title} in ${location}` +} + function fallbackInline(ctx: ToolFrame): ToolInline { const title = text(ctx.state.title) || (Object.keys(ctx.input).length > 0 ? JSON.stringify(ctx.input) : "Unknown") @@ -286,9 +309,9 @@ function count(n: number, label: string): string { } function runGlob(p: ToolProps): ToolInline { - const root = p.input.path ?? "" const title = `Glob "${p.input.pattern ?? ""}"` - const suffix = root ? `in ${toolPath(root)}` : "" + const location = toolLocation(p.input.path) + const suffix = location ? `in ${location}` : "" const matches = p.metadata.count const description = matches === undefined ? suffix : `${suffix}${suffix ? " · " : ""}${count(matches, "match")}` return { @@ -299,9 +322,9 @@ function runGlob(p: ToolProps): ToolInline { } function runGrep(p: ToolProps): ToolInline { - const root = p.input.path ?? "" const title = `Grep "${p.input.pattern ?? ""}"` - const suffix = root ? `in ${toolPath(root)}` : "" + const location = toolLocation(p.input.path) + const suffix = location ? `in ${location}` : "" const matches = p.metadata.matches const description = matches === undefined ? suffix : `${suffix}${suffix ? " · " : ""}${count(matches, "match")}` return { @@ -624,9 +647,7 @@ function snapQuestion(p: ToolProps): ToolSnapshot { function scrollBashStart(p: ToolProps): string { const cmd = p.input.command ?? "" const desc = p.input.description || "Shell" - const wd = p.input.workdir ?? "" - const dir = wd && wd !== "." ? toolPath(wd) : "" - const title = dir && !desc.includes(dir) ? `${desc} in ${dir}` : desc + const title = appendLocation(desc, p.input.workdir) if (!cmd) { return `# ${title}` @@ -864,12 +885,7 @@ function scrollSkillStart(p: ToolProps): string { function scrollGlobStart(p: ToolProps): string { const pattern = p.input.pattern ?? "" const head = pattern ? `✱ Glob "${pattern}"` : "✱ Glob" - const dir = p.input.path ?? "" - if (!dir) { - return head - } - - return `${head} in ${toolPath(dir)}` + return appendLocation(head, p.input.path) } function scrollGlobFinal(p: ToolProps): string { @@ -879,12 +895,7 @@ function scrollGlobFinal(p: ToolProps): string { function scrollGrepStart(p: ToolProps): string { const pattern = p.input.pattern ?? "" const head = pattern ? `✱ Grep "${pattern}"` : "✱ Grep" - const dir = p.input.path ?? "" - if (!dir) { - return head - } - - return `${head} in ${toolPath(dir)}` + return appendLocation(head, p.input.path) } function scrollListStart(p: ToolProps): string { diff --git a/packages/opencode/test/cli/run/tool.test.ts b/packages/opencode/test/cli/run/tool.test.ts new file mode 100644 index 000000000000..ebe3efff639a --- /dev/null +++ b/packages/opencode/test/cli/run/tool.test.ts @@ -0,0 +1,134 @@ +import path from "path" +import { describe, expect, test } from "bun:test" +import type { ToolPart } from "@opencode-ai/sdk/v2" +import { toolInlineInfo, toolScroll, type ToolFrame } from "@/cli/cmd/run/tool" + +function frame(name: string, input: Record): ToolFrame { + return { + raw: "", + name, + input, + meta: {}, + state: { + status: "running", + input, + metadata: {}, + }, + status: "running", + error: "", + } +} + +function scroll(name: string, input: Record) { + return toolScroll("start", frame(name, input)) +} + +function part(tool: string, input: Record, metadata: Record = {}): ToolPart { + return { + id: `${tool}-1`, + sessionID: "session-1", + messageID: `msg-${tool}`, + type: "tool", + callID: `call-${tool}`, + tool, + state: { + status: "completed", + input, + metadata, + }, + } as ToolPart +} + +describe("run tool display", () => { + test("omits root workdir from bash scroll title", () => { + expect( + scroll("bash", { + command: "git diff --check", + workdir: process.cwd(), + description: "Final whitespace check before commit", + }), + ).toBe("# Final whitespace check before commit\n$ git diff --check") + }) + + test("keeps meaningful bash workdir in scroll title", () => { + expect( + scroll("bash", { + command: "bun test", + workdir: path.join(process.cwd(), "apps/api"), + description: "Run API tests", + }), + ).toBe("# Run API tests in apps/api\n$ bun test") + + expect( + scroll("bash", { + command: "bun test", + workdir: path.join(process.cwd(), "apps/api"), + description: "Run API tests in apps/api", + }), + ).toBe("# Run API tests in apps/api\n$ bun test") + }) + + test("omits root path from glob and grep scroll titles", () => { + expect( + scroll("glob", { + pattern: "**/*.ts", + path: process.cwd(), + }), + ).toBe('✱ Glob "**/*.ts"') + + expect( + scroll("grep", { + pattern: "tool", + path: process.cwd(), + }), + ).toBe('✱ Grep "tool"') + }) + + test("applies the same root path rule to glob and grep inline descriptions", () => { + expect( + toolInlineInfo( + part("glob", { + pattern: "**/*.ts", + path: process.cwd(), + }), + ), + ).toEqual({ + icon: "✱", + title: 'Glob "**/*.ts"', + }) + + expect( + toolInlineInfo( + part("grep", { + pattern: "tool", + path: process.cwd(), + }), + ), + ).toEqual({ + icon: "✱", + title: 'Grep "tool"', + }) + + expect( + toolInlineInfo( + part("glob", { + pattern: "**/*.ts", + path: path.join(process.cwd(), "apps/api"), + }), + ), + ).toMatchObject({ + description: "in apps/api", + }) + + expect( + toolInlineInfo( + part("grep", { + pattern: "tool", + path: path.join(process.cwd(), "apps/api"), + }), + ), + ).toMatchObject({ + description: "in apps/api", + }) + }) +})