Skip to content

Commit be0ae80

Browse files
committed
fix(opencode): prefer semantic ACP tool title and input before completion
1 parent a419f1c commit be0ae80

2 files changed

Lines changed: 95 additions & 16 deletions

File tree

packages/opencode/src/acp/agent.ts

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ export class Agent implements ACPAgent {
286286
return
287287

288288
case "running":
289+
const input = this.toolInput(part)
289290
const output = this.bashOutput(part)
290291
const content: ToolCallContent[] = []
291292
if (output) {
@@ -300,9 +301,9 @@ export class Agent implements ACPAgent {
300301
toolCallId: part.callID,
301302
status: "in_progress",
302303
kind: toToolKind(part.tool),
303-
title: part.tool,
304-
locations: toLocations(part.tool, part.state.input),
305-
rawInput: part.state.input,
304+
title: this.toolTitle(part),
305+
locations: toLocations(part.tool, input),
306+
rawInput: input,
306307
},
307308
})
308309
.catch((error) => {
@@ -328,9 +329,9 @@ export class Agent implements ACPAgent {
328329
toolCallId: part.callID,
329330
status: "in_progress",
330331
kind: toToolKind(part.tool),
331-
title: part.tool,
332-
locations: toLocations(part.tool, part.state.input),
333-
rawInput: part.state.input,
332+
title: this.toolTitle(part),
333+
locations: toLocations(part.tool, input),
334+
rawInput: input,
334335
...(content.length > 0 && { content }),
335336
},
336337
})
@@ -431,8 +432,8 @@ export class Agent implements ACPAgent {
431432
toolCallId: part.callID,
432433
status: "failed",
433434
kind: toToolKind(part.tool),
434-
title: part.tool,
435-
rawInput: part.state.input,
435+
title: this.toolTitle(part),
436+
rawInput: this.toolInput(part),
436437
content: [
437438
{
438439
type: "content",
@@ -839,6 +840,7 @@ export class Agent implements ACPAgent {
839840
this.bashSnapshots.delete(part.callID)
840841
break
841842
case "running":
843+
const input = this.toolInput(part)
842844
const output = this.bashOutput(part)
843845
const runningContent: ToolCallContent[] = []
844846
if (output) {
@@ -858,9 +860,9 @@ export class Agent implements ACPAgent {
858860
toolCallId: part.callID,
859861
status: "in_progress",
860862
kind: toToolKind(part.tool),
861-
title: part.tool,
862-
locations: toLocations(part.tool, part.state.input),
863-
rawInput: part.state.input,
863+
title: this.toolTitle(part),
864+
locations: toLocations(part.tool, input),
865+
rawInput: input,
864866
...(runningContent.length > 0 && { content: runningContent }),
865867
},
866868
})
@@ -959,8 +961,8 @@ export class Agent implements ACPAgent {
959961
toolCallId: part.callID,
960962
status: "failed",
961963
kind: toToolKind(part.tool),
962-
title: part.tool,
963-
rawInput: part.state.input,
964+
title: this.toolTitle(part),
965+
rawInput: this.toolInput(part),
964966
content: [
965967
{
966968
type: "content",
@@ -1112,20 +1114,30 @@ export class Agent implements ACPAgent {
11121114
return output
11131115
}
11141116

1117+
private toolInput(part: ToolPart) {
1118+
return part.state.input ?? {}
1119+
}
1120+
1121+
private toolTitle(part: ToolPart) {
1122+
if ("title" in part.state && typeof part.state.title === "string") return part.state.title
1123+
return part.tool
1124+
}
1125+
11151126
private async toolStart(sessionId: string, part: ToolPart) {
11161127
if (this.toolStarts.has(part.callID)) return
11171128
this.toolStarts.add(part.callID)
1129+
const input = this.toolInput(part)
11181130
await this.connection
11191131
.sessionUpdate({
11201132
sessionId,
11211133
update: {
11221134
sessionUpdate: "tool_call",
11231135
toolCallId: part.callID,
1124-
title: part.tool,
1136+
title: this.toolTitle(part),
11251137
kind: toToolKind(part.tool),
11261138
status: "pending",
1127-
locations: [],
1128-
rawInput: {},
1139+
locations: toLocations(part.tool, input),
1140+
rawInput: input,
11291141
},
11301142
})
11311143
.catch((error) => {

packages/opencode/test/acp/event-subscription.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ function isToolCallUpdate(
3535
return update.sessionUpdate === "tool_call_update"
3636
}
3737

38+
function isToolCall(
39+
update: SessionUpdateParams["update"],
40+
): update is Extract<SessionUpdateParams["update"], { sessionUpdate: "tool_call" }> {
41+
return update.sessionUpdate === "tool_call"
42+
}
43+
3844
function toolEvent(
3945
sessionId: string,
4046
cwd: string,
@@ -614,6 +620,67 @@ describe("acp.agent event subscription", () => {
614620
})
615621
})
616622

623+
test("prefers semantic title and input for pending and running tool events", async () => {
624+
await using tmp = await tmpdir()
625+
await Instance.provide({
626+
directory: tmp.path,
627+
fn: async () => {
628+
const { agent, controller, sessionUpdates, stop } = createFakeAgent()
629+
const cwd = "/tmp/opencode-acp-test"
630+
const sessionId = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId)
631+
const input = { filePath: "/tmp/semantic.txt" }
632+
633+
controller.push({
634+
directory: cwd,
635+
payload: {
636+
type: "message.part.updated",
637+
properties: {
638+
sessionID: sessionId,
639+
time: Date.now(),
640+
part: {
641+
id: "part_call_semantic",
642+
sessionID: sessionId,
643+
messageID: "msg_call_semantic",
644+
type: "tool",
645+
callID: "call_semantic",
646+
tool: "read",
647+
state: {
648+
status: "running",
649+
title: "Fetch cluster metadata",
650+
input,
651+
time: { start: Date.now() },
652+
},
653+
},
654+
},
655+
},
656+
})
657+
await new Promise((r) => setTimeout(r, 20))
658+
659+
const semanticUpdates = sessionUpdates
660+
.filter((u) => u.sessionId === sessionId)
661+
.map((u) => u.update)
662+
.filter((u) => "toolCallId" in u && u.toolCallId === "call_semantic")
663+
664+
expect(semanticUpdates).toHaveLength(2)
665+
666+
const pending = semanticUpdates.find(isToolCall)
667+
expect(pending).toBeDefined()
668+
expect(pending?.title).toBe("Fetch cluster metadata")
669+
expect(pending?.rawInput).toEqual(input)
670+
expect(pending?.locations).toEqual([{ path: "/tmp/semantic.txt" }])
671+
672+
const running = semanticUpdates.find(isToolCallUpdate)
673+
expect(running).toBeDefined()
674+
expect(running?.status).toBe("in_progress")
675+
expect(running?.title).toBe("Fetch cluster metadata")
676+
expect(running?.rawInput).toEqual(input)
677+
expect(running?.locations).toEqual([{ path: "/tmp/semantic.txt" }])
678+
679+
stop()
680+
},
681+
})
682+
})
683+
617684
test("does not emit duplicate synthetic pending after replayed running tool", async () => {
618685
await using tmp = await tmpdir()
619686
await Instance.provide({

0 commit comments

Comments
 (0)