summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorNoam Bressler <[email protected]>2026-02-24 15:54:10 +0200
committerGitHub <[email protected]>2026-02-24 23:54:10 +1000
commit2cee947671fa373098db308b173c859cada0b108 (patch)
treeee9ae27a5b530a88fa05de64fad17593c4c57893
parente27d3d5d4017b33b73d4278fac561513454b1cae (diff)
downloadopencode-2cee947671fa373098db308b173c859cada0b108.tar.gz
opencode-2cee947671fa373098db308b173c859cada0b108.zip
fix: ACP both live and load share synthetic pending status preceeding… (#14916)
-rw-r--r--packages/opencode/src/acp/agent.ts60
-rw-r--r--packages/opencode/test/acp/event-subscription.test.ts59
2 files changed, 84 insertions, 35 deletions
diff --git a/packages/opencode/src/acp/agent.ts b/packages/opencode/src/acp/agent.ts
index 5db98bc70..8b338f1b5 100644
--- a/packages/opencode/src/acp/agent.ts
+++ b/packages/opencode/src/acp/agent.ts
@@ -270,25 +270,7 @@ export namespace ACP {
const sessionId = session.id
if (part.type === "tool") {
- if (!this.toolStarts.has(part.callID)) {
- this.toolStarts.add(part.callID)
- await this.connection
- .sessionUpdate({
- sessionId,
- update: {
- sessionUpdate: "tool_call",
- toolCallId: part.callID,
- title: part.tool,
- kind: toToolKind(part.tool),
- status: "pending",
- locations: [],
- rawInput: {},
- },
- })
- .catch((error) => {
- log.error("failed to send tool pending to ACP", { error })
- })
- }
+ await this.toolStart(sessionId, part)
switch (part.state.status) {
case "pending":
@@ -829,25 +811,10 @@ export namespace ACP {
for (const part of message.parts) {
if (part.type === "tool") {
+ await this.toolStart(sessionId, part)
switch (part.state.status) {
case "pending":
this.bashSnapshots.delete(part.callID)
- await this.connection
- .sessionUpdate({
- sessionId,
- update: {
- sessionUpdate: "tool_call",
- toolCallId: part.callID,
- title: part.tool,
- kind: toToolKind(part.tool),
- status: "pending",
- locations: [],
- rawInput: {},
- },
- })
- .catch((err) => {
- log.error("failed to send tool pending to ACP", { error: err })
- })
break
case "running":
const output = this.bashOutput(part)
@@ -880,6 +847,7 @@ export namespace ACP {
})
break
case "completed":
+ this.toolStarts.delete(part.callID)
this.bashSnapshots.delete(part.callID)
const kind = toToolKind(part.tool)
const content: ToolCallContent[] = [
@@ -959,6 +927,7 @@ export namespace ACP {
})
break
case "error":
+ this.toolStarts.delete(part.callID)
this.bashSnapshots.delete(part.callID)
await this.connection
.sessionUpdate({
@@ -1116,6 +1085,27 @@ export namespace ACP {
return output
}
+ private async toolStart(sessionId: string, part: ToolPart) {
+ if (this.toolStarts.has(part.callID)) return
+ this.toolStarts.add(part.callID)
+ await this.connection
+ .sessionUpdate({
+ sessionId,
+ update: {
+ sessionUpdate: "tool_call",
+ toolCallId: part.callID,
+ title: part.tool,
+ kind: toToolKind(part.tool),
+ status: "pending",
+ locations: [],
+ rawInput: {},
+ },
+ })
+ .catch((error) => {
+ log.error("failed to send tool pending to ACP", { error })
+ })
+ }
+
private async loadAvailableModes(directory: string): Promise<ModeOption[]> {
const agents = await this.config.sdk.app
.agents(
diff --git a/packages/opencode/test/acp/event-subscription.test.ts b/packages/opencode/test/acp/event-subscription.test.ts
index d61240d71..1abf57828 100644
--- a/packages/opencode/test/acp/event-subscription.test.ts
+++ b/packages/opencode/test/acp/event-subscription.test.ts
@@ -572,6 +572,65 @@ describe("acp.agent event subscription", () => {
})
})
+ test("does not emit duplicate synthetic pending after replayed running tool", async () => {
+ await using tmp = await tmpdir()
+ await Instance.provide({
+ directory: tmp.path,
+ fn: async () => {
+ const { agent, controller, sessionUpdates, stop, sdk } = createFakeAgent()
+ const cwd = "/tmp/opencode-acp-test"
+ const sessionId = await agent.newSession({ cwd, mcpServers: [] } as any).then((x) => x.sessionId)
+ const input = { command: "echo hi", description: "run command" }
+
+ sdk.session.messages = async () => ({
+ data: [
+ {
+ info: {
+ role: "assistant",
+ sessionID: sessionId,
+ },
+ parts: [
+ {
+ type: "tool",
+ callID: "call_1",
+ tool: "bash",
+ state: {
+ status: "running",
+ input,
+ metadata: { output: "hi\n" },
+ time: { start: Date.now() },
+ },
+ },
+ ],
+ },
+ ],
+ })
+
+ await agent.loadSession({ sessionId, cwd, mcpServers: [] } as any)
+ controller.push(
+ toolEvent(sessionId, cwd, {
+ callID: "call_1",
+ tool: "bash",
+ status: "running",
+ input,
+ metadata: { output: "hi\nthere\n" },
+ }),
+ )
+ await new Promise((r) => setTimeout(r, 20))
+
+ const types = sessionUpdates
+ .filter((u) => u.sessionId === sessionId)
+ .map((u) => u.update)
+ .filter((u) => "toolCallId" in u && u.toolCallId === "call_1")
+ .map((u) => u.sessionUpdate)
+ .filter((u) => u === "tool_call" || u === "tool_call_update")
+
+ expect(types).toEqual(["tool_call", "tool_call_update", "tool_call_update"])
+ stop()
+ },
+ })
+ })
+
test("clears bash snapshot marker on pending state", async () => {
await using tmp = await tmpdir()
await Instance.provide({