diff --git a/packages/plugins/sdk/src/protocol.ts b/packages/plugins/sdk/src/protocol.ts index ad11522d933..c19db622319 100644 --- a/packages/plugins/sdk/src/protocol.ts +++ b/packages/plugins/sdk/src/protocol.ts @@ -1496,6 +1496,8 @@ export interface WorkerToHostMethods { */ actorUserId?: string; reason?: string | null; + /** Explicit checkbox selection for request_checkbox_confirmation accepts. */ + selectedOptionIds?: string[]; }, result: { interaction: IssueThreadInteraction; applied: boolean }, ]; diff --git a/packages/plugins/sdk/src/testing.ts b/packages/plugins/sdk/src/testing.ts index 8894bda8901..e02c335b903 100644 --- a/packages/plugins/sdk/src/testing.ts +++ b/packages/plugins/sdk/src/testing.ts @@ -1810,6 +1810,16 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness { const resolved: IssueThreadInteraction = { ...current, status: input.action === "accept" ? "accepted" : "rejected", + ...(input.action === "accept" + && current.kind === "request_checkbox_confirmation" + && input.selectedOptionIds !== undefined + ? { + result: { + outcome: "accepted", + selectedOptionIds: input.selectedOptionIds, + }, + } + : {}), updatedAt: new Date(), } as IssueThreadInteraction; const index = list.indexOf(current); diff --git a/packages/plugins/sdk/src/types.ts b/packages/plugins/sdk/src/types.ts index 55e34cbddb3..c2ab3931aac 100644 --- a/packages/plugins/sdk/src/types.ts +++ b/packages/plugins/sdk/src/types.ts @@ -1530,7 +1530,13 @@ export interface PluginIssuesClient { respondInteraction( issueId: string, interactionId: string, - input: { action: "accept" | "reject"; actorUserId?: string; reason?: string | null }, + input: { + action: "accept" | "reject"; + actorUserId?: string; + reason?: string | null; + /** Explicit checkbox selection; omit to accept the interaction defaults. */ + selectedOptionIds?: string[]; + }, companyId: string, ): Promise<{ interaction: IssueThreadInteraction; applied: boolean }>; /** List attachment metadata for an issue. Requires `issue.attachments.read`. */ diff --git a/packages/plugins/sdk/src/worker-rpc-host.ts b/packages/plugins/sdk/src/worker-rpc-host.ts index 72dcf9238ad..e5c0e4fc8b6 100644 --- a/packages/plugins/sdk/src/worker-rpc-host.ts +++ b/packages/plugins/sdk/src/worker-rpc-host.ts @@ -993,7 +993,12 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost async respondInteraction( issueId: string, interactionId: string, - input: { action: "accept" | "reject"; actorUserId?: string; reason?: string | null }, + input: { + action: "accept" | "reject"; + actorUserId?: string; + reason?: string | null; + selectedOptionIds?: string[]; + }, companyId: string, ) { return callHost("issues.respondInteraction", { @@ -1003,6 +1008,7 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost action: input.action, actorUserId: input.actorUserId, reason: input.reason, + selectedOptionIds: input.selectedOptionIds, }); }, diff --git a/packages/plugins/sdk/tests/host-client-factory.test.ts b/packages/plugins/sdk/tests/host-client-factory.test.ts index 2c87650af5b..59c8ef12ed9 100644 --- a/packages/plugins/sdk/tests/host-client-factory.test.ts +++ b/packages/plugins/sdk/tests/host-client-factory.test.ts @@ -337,10 +337,22 @@ describe("createHostClientHandlers capability gating for LOOA-641 methods", () = }); await expect( handlers["issues.respondInteraction"]({ - issueId: "issue-a", interactionId: "int-a", companyId: "company-a", action: "accept", actorUserId: "user-a", + issueId: "issue-a", + interactionId: "int-a", + companyId: "company-a", + action: "accept", + actorUserId: "user-a", + selectedOptionIds: ["spike-b"], }, context), ).resolves.toEqual({ interaction: { id: "i" }, applied: true }); - expect(respondInteraction).toHaveBeenCalledOnce(); + expect(respondInteraction).toHaveBeenCalledWith({ + issueId: "issue-a", + interactionId: "int-a", + companyId: "company-a", + action: "accept", + actorUserId: "user-a", + selectedOptionIds: ["spike-b"], + }); }); it("denies approvals.decide without approvals.respond", async () => { diff --git a/server/src/__tests__/plugin-orchestration-apis.test.ts b/server/src/__tests__/plugin-orchestration-apis.test.ts index 1f3f2e06581..968c01ccfe2 100644 --- a/server/src/__tests__/plugin-orchestration-apis.test.ts +++ b/server/src/__tests__/plugin-orchestration-apis.test.ts @@ -991,6 +991,51 @@ describeEmbeddedPostgres("plugin orchestration APIs", () => { expect(row?.status).toBe("accepted"); }); + it("respondInteraction forwards an explicit checkbox selection", async () => { + const { companyId } = await seedCompanyAndAgent(); + const operatorUserId = randomUUID(); + await db.insert(companyMemberships).values({ + companyId, principalType: "user", principalId: operatorUserId, status: "active", membershipRole: "operator", + }); + const issueId = randomUUID(); + await db.insert(issues).values({ + id: issueId, companyId, title: "Choose campaigns", status: "in_review", priority: "medium", + }); + const interactionId = await seedInteraction(companyId, issueId, { + kind: "request_checkbox_confirmation", + payload: { + version: 1, + prompt: "Which spikes should become campaigns?", + options: [ + { id: "spike-a", label: "Spike A" }, + { id: "spike-b", label: "Spike B" }, + ], + defaultSelectedOptionIds: [], + minSelected: 0, + maxSelected: 2, + } as never, + }); + const services = buildHostServices(db, "plugin-record-id", "paperclip.gateway", createEventBusStub()); + + const result = await services.issues.respondInteraction({ + issueId, + interactionId, + companyId, + action: "accept", + actorUserId: operatorUserId, + selectedOptionIds: ["spike-b"], + }); + + expect(result.applied).toBe(true); + expect(result.interaction).toMatchObject({ + status: "accepted", + result: { + outcome: "accepted", + selectedOptionIds: ["spike-b"], + }, + }); + }); + it("respondInteraction converges (applied:false) when the interaction is already resolved", async () => { const { companyId, agentId } = await seedCompanyAndAgent(); const humanUserId = randomUUID(); diff --git a/server/src/services/plugin-host-services.ts b/server/src/services/plugin-host-services.ts index 0c2d10e2e26..b1753cfd87f 100644 --- a/server/src/services/plugin-host-services.ts +++ b/server/src/services/plugin-host-services.ts @@ -2289,7 +2289,9 @@ export function buildHostServices( const result = await interactions.acceptInteraction( { id: issue.id, companyId, projectId: issue.projectId ?? null, goalId: issue.goalId ?? null }, params.interactionId, - {}, + params.selectedOptionIds === undefined + ? {} + : { selectedOptionIds: params.selectedOptionIds }, actor, ); resolved = result.interaction as typeof current;