Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 30 additions & 19 deletions packages/opencode/src/cli/cmd/run/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -286,9 +309,9 @@ function count(n: number, label: string): string {
}

function runGlob(p: ToolProps<typeof GlobTool>): 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 {
Expand All @@ -299,9 +322,9 @@ function runGlob(p: ToolProps<typeof GlobTool>): ToolInline {
}

function runGrep(p: ToolProps<typeof GrepTool>): 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 {
Expand Down Expand Up @@ -624,9 +647,7 @@ function snapQuestion(p: ToolProps<typeof QuestionTool>): ToolSnapshot {
function scrollBashStart(p: ToolProps<typeof BashTool>): 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}`
Expand Down Expand Up @@ -864,12 +885,7 @@ function scrollSkillStart(p: ToolProps<typeof SkillTool>): string {
function scrollGlobStart(p: ToolProps<typeof GlobTool>): 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<typeof GlobTool>): string {
Expand All @@ -879,12 +895,7 @@ function scrollGlobFinal(p: ToolProps<typeof GlobTool>): string {
function scrollGrepStart(p: ToolProps<typeof GrepTool>): 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 {
Expand Down
134 changes: 134 additions & 0 deletions packages/opencode/test/cli/run/tool.test.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>): ToolFrame {
return {
raw: "",
name,
input,
meta: {},
state: {
status: "running",
input,
metadata: {},
},
status: "running",
error: "",
}
}

function scroll(name: string, input: Record<string, unknown>) {
return toolScroll("start", frame(name, input))
}

function part(tool: string, input: Record<string, unknown>, metadata: Record<string, unknown> = {}): 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",
})
})
})
Loading