Просмотр исходного кода

fix: ACP both live and load share synthetic pending status preceeding… (#14916)

Noam Bressler 6 месяцев назад
Родитель
Сommit
2cee947671

+ 25 - 35
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(

+ 59 - 0
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({