From 59ecd2a4d8b488ffca1c702084f1999fd65e33d2 Mon Sep 17 00:00:00 2001 From: can1357 Date: Mon, 13 Jul 2026 18:53:19 +0200 Subject: [PATCH] refactor(coding-agent/modes): added ACP type guards for elicitation narrowing - Added `isAcceptedElicitation` in the ACP agent to narrow accepted elicitations before accessing response content. - Added `isFormElicitation` in ACP tests and used it to narrow form-mode requests before assertions. --- packages/coding-agent/src/modes/acp/acp-agent.ts | 9 ++++++++- packages/coding-agent/test/acp-agent.test.ts | 15 +++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/coding-agent/src/modes/acp/acp-agent.ts b/packages/coding-agent/src/modes/acp/acp-agent.ts index 5b31ec935..3bca86ddc 100644 --- a/packages/coding-agent/src/modes/acp/acp-agent.ts +++ b/packages/coding-agent/src/modes/acp/acp-agent.ts @@ -351,12 +351,19 @@ async function elicitFromAcpClient( finish(undefined); }); const response = await promise; - if (response?.action !== "accept" || !response.content) { + if (!isAcceptedElicitation(response) || !response.content) { return undefined; } return response.content.value; } +/** Narrows a `CreateElicitationResponse` to the accepted-with-content branch; the SDK's `action: string` catch-all arm otherwise defeats literal narrowing on `action !== "accept"`. */ +function isAcceptedElicitation( + response: CreateElicitationResponse | undefined, +): response is Extract { + return response?.action === "accept"; +} + /** * Build an {@link ExtensionUIContext} that translates skill/extension UI * requests into ACP elicitations against `connection` for the session diff --git a/packages/coding-agent/test/acp-agent.test.ts b/packages/coding-agent/test/acp-agent.test.ts index f8892c557..886fedb6c 100644 --- a/packages/coding-agent/test/acp-agent.test.ts +++ b/packages/coding-agent/test/acp-agent.test.ts @@ -2244,6 +2244,13 @@ describe("ACP agent", () => { return { connection, calls }; } + /** Narrows `CreateElicitationRequest` to the `mode: "form"` branch; the SDK's `mode: string` catch-all arm otherwise defeats literal narrowing on `mode !== "form"`. */ + function isFormElicitation( + request: CreateElicitationRequest, + ): request is Extract { + return request.mode === "form"; + } + it("translates select to a single-property string-enum elicitation", async () => { const { connection, calls } = createElicitConnection(async () => ({ action: "accept", @@ -2258,7 +2265,7 @@ describe("ACP agent", () => { const request = calls[0]!; expect(request.mode).toBe("form"); expect(request.message).toBe("Pick one"); - if (request.mode !== "form" || !("sessionId" in request)) { + if (!isFormElicitation(request) || !("sessionId" in request)) { throw new Error("expected session-scoped form elicitation"); } expect(request.sessionId).toBe("session-select"); @@ -2281,7 +2288,7 @@ describe("ACP agent", () => { expect(result).toBe(true); expect(calls).toHaveLength(1); const request = calls[0]!; - if (request.mode !== "form") { + if (!isFormElicitation(request)) { throw new Error("expected form-mode elicitation"); } expect(request.message).toBe("Proceed?\n\nThis will overwrite the file."); @@ -2301,7 +2308,7 @@ describe("ACP agent", () => { expect(result).toBe("claude"); expect(calls).toHaveLength(1); const request = calls[0]!; - if (request.mode !== "form") { + if (!isFormElicitation(request)) { throw new Error("expected form-mode elicitation"); } expect(request.message).toBe("Your name?"); @@ -2450,7 +2457,7 @@ describe("ACP agent", () => { expect(calls).toHaveLength(1); const request = calls[0]!; - if (request.mode !== "form") throw new Error("expected form-mode elicitation"); + if (!isFormElicitation(request)) throw new Error("expected form-mode elicitation"); expect(request.requestedSchema.properties?.value).toEqual({ type: "string" }); });