feat(plugins): forward checkbox selections

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Michael NguyenandPaperclip committed 2026-07-24 01:00:36 -07:00
1 parent 14f20be92b
commit 896295b163
7 files changed
+88 -5

No files matched your search

+2
View File
@@ -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 },
];
+10
View File
@@ -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);
+7 -1
View File
@@ -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`. */
+7 -1
View File
@@ -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,
});
},
@@ -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 () => {
@@ -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();
+3 -1
View File
@@ -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;