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
44 changes: 28 additions & 16 deletions packages/opencode/src/acp/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ export class Agent implements ACPAgent {
return

case "running":
const input = this.toolInput(part)
const output = this.bashOutput(part)
const content: ToolCallContent[] = []
if (output) {
Expand All @@ -300,9 +301,9 @@ export class Agent implements ACPAgent {
toolCallId: part.callID,
status: "in_progress",
kind: toToolKind(part.tool),
title: part.tool,
locations: toLocations(part.tool, part.state.input),
rawInput: part.state.input,
title: this.toolTitle(part),
locations: toLocations(part.tool, input),
rawInput: input,
},
})
.catch((error) => {
Expand All @@ -328,9 +329,9 @@ export class Agent implements ACPAgent {
toolCallId: part.callID,
status: "in_progress",
kind: toToolKind(part.tool),
title: part.tool,
locations: toLocations(part.tool, part.state.input),
rawInput: part.state.input,
title: this.toolTitle(part),
locations: toLocations(part.tool, input),
rawInput: input,
...(content.length > 0 && { content }),
},
})
Expand Down Expand Up @@ -431,8 +432,8 @@ export class Agent implements ACPAgent {
toolCallId: part.callID,
status: "failed",
kind: toToolKind(part.tool),
title: part.tool,
rawInput: part.state.input,
title: this.toolTitle(part),
rawInput: this.toolInput(part),
content: [
{
type: "content",
Expand Down Expand Up @@ -839,6 +840,7 @@ export class Agent implements ACPAgent {
this.bashSnapshots.delete(part.callID)
break
case "running":
const input = this.toolInput(part)
const output = this.bashOutput(part)
const runningContent: ToolCallContent[] = []
if (output) {
Expand All @@ -858,9 +860,9 @@ export class Agent implements ACPAgent {
toolCallId: part.callID,
status: "in_progress",
kind: toToolKind(part.tool),
title: part.tool,
locations: toLocations(part.tool, part.state.input),
rawInput: part.state.input,
title: this.toolTitle(part),
locations: toLocations(part.tool, input),
rawInput: input,
...(runningContent.length > 0 && { content: runningContent }),
},
})
Expand Down Expand Up @@ -959,8 +961,8 @@ export class Agent implements ACPAgent {
toolCallId: part.callID,
status: "failed",
kind: toToolKind(part.tool),
title: part.tool,
rawInput: part.state.input,
title: this.toolTitle(part),
rawInput: this.toolInput(part),
content: [
{
type: "content",
Expand Down Expand Up @@ -1112,20 +1114,30 @@ export class Agent implements ACPAgent {
return output
}

private toolInput(part: ToolPart) {
return part.state.input ?? {}
}

private toolTitle(part: ToolPart) {
if ("title" in part.state && typeof part.state.title === "string") return part.state.title
return part.tool
}

private async toolStart(sessionId: string, part: ToolPart) {
if (this.toolStarts.has(part.callID)) return
this.toolStarts.add(part.callID)
const input = this.toolInput(part)
await this.connection
.sessionUpdate({
sessionId,
update: {
sessionUpdate: "tool_call",
toolCallId: part.callID,
title: part.tool,
title: this.toolTitle(part),
kind: toToolKind(part.tool),
status: "pending",
locations: [],
rawInput: {},
locations: toLocations(part.tool, input),
rawInput: input,
},
})
.catch((error) => {
Expand Down
67 changes: 67 additions & 0 deletions packages/opencode/test/acp/event-subscription.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ function isToolCallUpdate(
return update.sessionUpdate === "tool_call_update"
}

function isToolCall(
update: SessionUpdateParams["update"],
): update is Extract<SessionUpdateParams["update"], { sessionUpdate: "tool_call" }> {
return update.sessionUpdate === "tool_call"
}

function toolEvent(
sessionId: string,
cwd: string,
Expand Down Expand Up @@ -614,6 +620,67 @@ describe("acp.agent event subscription", () => {
})
})

test("prefers semantic title and input for pending and running tool events", async () => {
await using tmp = await tmpdir()
await Instance.provide({
directory: tmp.path,
fn: async () => {
const { agent, controller, sessionUpdates, stop } = createFakeAgent()
const cwd = "/tmp/opencode-acp-test"
const sessionId = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId)
const input = { filePath: "/tmp/semantic.txt" }

controller.push({
directory: cwd,
payload: {
type: "message.part.updated",
properties: {
sessionID: sessionId,
time: Date.now(),
part: {
id: "part_call_semantic",
sessionID: sessionId,
messageID: "msg_call_semantic",
type: "tool",
callID: "call_semantic",
tool: "read",
state: {
status: "running",
title: "Fetch cluster metadata",
input,
time: { start: Date.now() },
},
},
},
},
})
await new Promise((r) => setTimeout(r, 20))

const semanticUpdates = sessionUpdates
.filter((u) => u.sessionId === sessionId)
.map((u) => u.update)
.filter((u) => "toolCallId" in u && u.toolCallId === "call_semantic")

expect(semanticUpdates).toHaveLength(2)

const pending = semanticUpdates.find(isToolCall)
expect(pending).toBeDefined()
expect(pending?.title).toBe("Fetch cluster metadata")
expect(pending?.rawInput).toEqual(input)
expect(pending?.locations).toEqual([{ path: "/tmp/semantic.txt" }])

const running = semanticUpdates.find(isToolCallUpdate)
expect(running).toBeDefined()
expect(running?.status).toBe("in_progress")
expect(running?.title).toBe("Fetch cluster metadata")
expect(running?.rawInput).toEqual(input)
expect(running?.locations).toEqual([{ path: "/tmp/semantic.txt" }])

stop()
},
})
})

test("does not emit duplicate synthetic pending after replayed running tool", async () => {
await using tmp = await tmpdir()
await Instance.provide({
Expand Down
Loading