Skip to content

Commit 2cee947

Browse files
authored
fix: ACP both live and load share synthetic pending status preceeding… (anomalyco#14916)
1 parent e27d3d5 commit 2cee947

2 files changed

Lines changed: 84 additions & 35 deletions

File tree

packages/opencode/src/acp/agent.ts

Lines changed: 25 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -270,25 +270,7 @@ export namespace ACP {
270270
const sessionId = session.id
271271

272272
if (part.type === "tool") {
273-
if (!this.toolStarts.has(part.callID)) {
274-
this.toolStarts.add(part.callID)
275-
await this.connection
276-
.sessionUpdate({
277-
sessionId,
278-
update: {
279-
sessionUpdate: "tool_call",
280-
toolCallId: part.callID,
281-
title: part.tool,
282-
kind: toToolKind(part.tool),
283-
status: "pending",
284-
locations: [],
285-
rawInput: {},
286-
},
287-
})
288-
.catch((error) => {
289-
log.error("failed to send tool pending to ACP", { error })
290-
})
291-
}
273+
await this.toolStart(sessionId, part)
292274

293275
switch (part.state.status) {
294276
case "pending":
@@ -829,25 +811,10 @@ export namespace ACP {
829811

830812
for (const part of message.parts) {
831813
if (part.type === "tool") {
814+
await this.toolStart(sessionId, part)
832815
switch (part.state.status) {
833816
case "pending":
834817
this.bashSnapshots.delete(part.callID)
835-
await this.connection
836-
.sessionUpdate({
837-
sessionId,
838-
update: {
839-
sessionUpdate: "tool_call",
840-
toolCallId: part.callID,
841-
title: part.tool,
842-
kind: toToolKind(part.tool),
843-
status: "pending",
844-
locations: [],
845-
rawInput: {},
846-
},
847-
})
848-
.catch((err) => {
849-
log.error("failed to send tool pending to ACP", { error: err })
850-
})
851818
break
852819
case "running":
853820
const output = this.bashOutput(part)
@@ -880,6 +847,7 @@ export namespace ACP {
880847
})
881848
break
882849
case "completed":
850+
this.toolStarts.delete(part.callID)
883851
this.bashSnapshots.delete(part.callID)
884852
const kind = toToolKind(part.tool)
885853
const content: ToolCallContent[] = [
@@ -959,6 +927,7 @@ export namespace ACP {
959927
})
960928
break
961929
case "error":
930+
this.toolStarts.delete(part.callID)
962931
this.bashSnapshots.delete(part.callID)
963932
await this.connection
964933
.sessionUpdate({
@@ -1116,6 +1085,27 @@ export namespace ACP {
11161085
return output
11171086
}
11181087

1088+
private async toolStart(sessionId: string, part: ToolPart) {
1089+
if (this.toolStarts.has(part.callID)) return
1090+
this.toolStarts.add(part.callID)
1091+
await this.connection
1092+
.sessionUpdate({
1093+
sessionId,
1094+
update: {
1095+
sessionUpdate: "tool_call",
1096+
toolCallId: part.callID,
1097+
title: part.tool,
1098+
kind: toToolKind(part.tool),
1099+
status: "pending",
1100+
locations: [],
1101+
rawInput: {},
1102+
},
1103+
})
1104+
.catch((error) => {
1105+
log.error("failed to send tool pending to ACP", { error })
1106+
})
1107+
}
1108+
11191109
private async loadAvailableModes(directory: string): Promise<ModeOption[]> {
11201110
const agents = await this.config.sdk.app
11211111
.agents(

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,65 @@ describe("acp.agent event subscription", () => {
572572
})
573573
})
574574

575+
test("does not emit duplicate synthetic pending after replayed running tool", async () => {
576+
await using tmp = await tmpdir()
577+
await Instance.provide({
578+
directory: tmp.path,
579+
fn: async () => {
580+
const { agent, controller, sessionUpdates, stop, sdk } = createFakeAgent()
581+
const cwd = "/tmp/opencode-acp-test"
582+
const sessionId = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId)
583+
const input = { command: "echo hi", description: "run command" }
584+
585+
sdk.session.messages = async () => ({
586+
data: [
587+
{
588+
info: {
589+
role: "assistant",
590+
sessionID: sessionId,
591+
},
592+
parts: [
593+
{
594+
type: "tool",
595+
callID: "call_1",
596+
tool: "bash",
597+
state: {
598+
status: "running",
599+
input,
600+
metadata: { output: "hi\n" },
601+
time: { start: Date.now() },
602+
},
603+
},
604+
],
605+
},
606+
],
607+
})
608+
609+
await agent.loadSession({ sessionId, cwd, mcpServers: [] } as any)
610+
controller.push(
611+
toolEvent(sessionId, cwd, {
612+
callID: "call_1",
613+
tool: "bash",
614+
status: "running",
615+
input,
616+
metadata: { output: "hi\nthere\n" },
617+
}),
618+
)
619+
await new Promise((r) => setTimeout(r, 20))
620+
621+
const types = sessionUpdates
622+
.filter((u) => u.sessionId === sessionId)
623+
.map((u) => u.update)
624+
.filter((u) => "toolCallId" in u && u.toolCallId === "call_1")
625+
.map((u) => u.sessionUpdate)
626+
.filter((u) => u === "tool_call" || u === "tool_call_update")
627+
628+
expect(types).toEqual(["tool_call", "tool_call_update", "tool_call_update"])
629+
stop()
630+
},
631+
})
632+
})
633+
575634
test("clears bash snapshot marker on pending state", async () => {
576635
await using tmp = await tmpdir()
577636
await Instance.provide({

0 commit comments

Comments
 (0)