mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 01:41:42 +02:00
Add checkbox confirmation helpers
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
3e2dde1c4e
commit
356adb443c
8 files changed
+175
-2
No files matched your search
@@ -163,6 +163,7 @@ describe("issue subresource commands", () => {
|
||||
]);
|
||||
await run(["issue", "interaction:accept", ISSUE_ID, INTERACTION_ID]);
|
||||
await run(["issue", "interaction:accept", ISSUE_ID, INTERACTION_ID, "--selected-client-keys", "yes"]);
|
||||
await run(["issue", "interaction:accept", ISSUE_ID, INTERACTION_ID, "--selected-option-ids", "file-a,file-b"]);
|
||||
await run(["issue", "interaction:reject", ISSUE_ID, INTERACTION_ID, "--reason", "no"]);
|
||||
await run(["issue", "interaction:cancel", ISSUE_ID, INTERACTION_ID, "--reason", "stale"]);
|
||||
await run([
|
||||
@@ -196,6 +197,7 @@ describe("issue subresource commands", () => {
|
||||
["POST", `http://localhost:3100/api/issues/${ISSUE_ID}/interactions`],
|
||||
["POST", `http://localhost:3100/api/issues/${ISSUE_ID}/interactions/${INTERACTION_ID}/accept`],
|
||||
["POST", `http://localhost:3100/api/issues/${ISSUE_ID}/interactions/${INTERACTION_ID}/accept`],
|
||||
["POST", `http://localhost:3100/api/issues/${ISSUE_ID}/interactions/${INTERACTION_ID}/accept`],
|
||||
["POST", `http://localhost:3100/api/issues/${ISSUE_ID}/interactions/${INTERACTION_ID}/reject`],
|
||||
["POST", `http://localhost:3100/api/issues/${ISSUE_ID}/interactions/${INTERACTION_ID}/cancel`],
|
||||
["POST", `http://localhost:3100/api/issues/${ISSUE_ID}/interactions/${INTERACTION_ID}/respond`],
|
||||
@@ -215,6 +217,9 @@ describe("issue subresource commands", () => {
|
||||
["GET", `http://localhost:3100/api/issues/${ISSUE_ID}/feedback-votes`],
|
||||
["POST", `http://localhost:3100/api/issues/${ISSUE_ID}/feedback-votes`],
|
||||
]);
|
||||
expect(JSON.parse(String(fetchMock.mock.calls[4]?.[1]?.body))).toEqual({
|
||||
selectedOptionIds: ["file-a", "file-b"],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -149,6 +149,7 @@ interface IssueRecoveryResolveOptions extends BaseClientOptions {
|
||||
|
||||
interface InteractionAcceptOptions extends BaseClientOptions {
|
||||
selectedClientKeys?: string;
|
||||
selectedOptionIds?: string;
|
||||
}
|
||||
|
||||
interface InteractionReasonOptions extends BaseClientOptions {
|
||||
@@ -745,11 +746,13 @@ export function registerIssueCommands(program: Command): void {
|
||||
.argument("<issueId>", "Issue ID")
|
||||
.argument("<interactionId>", "Interaction ID")
|
||||
.option("--selected-client-keys <csv>", "Client keys to accept")
|
||||
.option("--selected-option-ids <csv>", "Checkbox option IDs to accept")
|
||||
.action(async (issueId: string, interactionId: string, opts: InteractionAcceptOptions) => {
|
||||
try {
|
||||
const ctx = resolveCommandContext(opts);
|
||||
const payload = acceptIssueThreadInteractionSchema.parse({
|
||||
selectedClientKeys: opts.selectedClientKeys === undefined ? undefined : parseCsv(opts.selectedClientKeys),
|
||||
selectedOptionIds: opts.selectedOptionIds === undefined ? undefined : parseCsv(opts.selectedOptionIds),
|
||||
});
|
||||
const interaction = await ctx.api.post(apiPath`/api/issues/${issueId}/interactions/${interactionId}/accept`, payload);
|
||||
printOutput(interaction, { json: ctx.json });
|
||||
@@ -761,7 +764,7 @@ export function registerIssueCommands(program: Command): void {
|
||||
|
||||
for (const [name, action, schema, description] of [
|
||||
["interaction:reject", "reject", rejectIssueThreadInteractionSchema, "Reject an issue thread interaction"],
|
||||
["interaction:cancel", "cancel", cancelIssueThreadInteractionSchema, "Cancel an ask_user_questions issue thread interaction"],
|
||||
["interaction:cancel", "cancel", cancelIssueThreadInteractionSchema, "Cancel an issue thread interaction"],
|
||||
] as const) {
|
||||
addCommonClientOptions(
|
||||
issue
|
||||
|
||||
@@ -292,6 +292,62 @@ describe("paperclip MCP tools", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("creates request_checkbox_confirmation interactions with checkbox payloads", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
mockJsonResponse({ id: "interaction-1", kind: "request_checkbox_confirmation" }),
|
||||
);
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const tool = getTool("paperclipRequestCheckboxConfirmation");
|
||||
await tool.execute({
|
||||
issueId: "PAP-1135",
|
||||
idempotencyKey: "confirmation:PAP-1135:files",
|
||||
title: "Choose files",
|
||||
payload: {
|
||||
version: 1,
|
||||
prompt: "Which files should be included?",
|
||||
detailsMarkdown: "Pick the files to attach.",
|
||||
options: [
|
||||
{ id: "file-a", label: "File A", description: "Primary draft" },
|
||||
{ id: "file-b", label: "File B" },
|
||||
],
|
||||
defaultSelectedOptionIds: ["file-a"],
|
||||
minSelected: 1,
|
||||
maxSelected: 2,
|
||||
acceptLabel: "Use selected files",
|
||||
rejectLabel: "Do not attach files",
|
||||
rejectRequiresReason: true,
|
||||
allowDeclineReason: false,
|
||||
},
|
||||
});
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
|
||||
expect(String(url)).toBe("http://localhost:3100/api/issues/PAP-1135/interactions");
|
||||
expect(init.method).toBe("POST");
|
||||
expect(JSON.parse(String(init.body))).toEqual({
|
||||
kind: "request_checkbox_confirmation",
|
||||
continuationPolicy: "wake_assignee",
|
||||
idempotencyKey: "confirmation:PAP-1135:files",
|
||||
title: "Choose files",
|
||||
payload: {
|
||||
version: 1,
|
||||
prompt: "Which files should be included?",
|
||||
detailsMarkdown: "Pick the files to attach.",
|
||||
options: [
|
||||
{ id: "file-a", label: "File A", description: "Primary draft" },
|
||||
{ id: "file-b", label: "File B" },
|
||||
],
|
||||
defaultSelectedOptionIds: ["file-a"],
|
||||
minSelected: 1,
|
||||
maxSelected: 2,
|
||||
acceptLabel: "Use selected files",
|
||||
rejectLabel: "Do not attach files",
|
||||
rejectRequiresReason: true,
|
||||
allowDeclineReason: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("creates approvals with the expected company-scoped payload", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
mockJsonResponse({ id: "approval-1" }),
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
createApprovalSchema,
|
||||
createIssueInputSchema,
|
||||
issueThreadInteractionContinuationPolicySchema,
|
||||
requestCheckboxConfirmationPayloadSchema,
|
||||
requestConfirmationPayloadSchema,
|
||||
suggestTasksPayloadSchema,
|
||||
updateIssueSchema,
|
||||
@@ -144,6 +145,17 @@ const createRequestConfirmationToolSchema = z.object({
|
||||
payload: requestConfirmationPayloadSchema,
|
||||
});
|
||||
|
||||
const createRequestCheckboxConfirmationToolSchema = z.object({
|
||||
issueId: issueIdSchema,
|
||||
idempotencyKey: z.string().trim().max(255).nullable().optional(),
|
||||
sourceCommentId: z.string().uuid().nullable().optional(),
|
||||
sourceRunId: z.string().uuid().nullable().optional(),
|
||||
title: z.string().trim().max(240).nullable().optional(),
|
||||
summary: z.string().trim().max(1000).nullable().optional(),
|
||||
continuationPolicy: issueThreadInteractionContinuationPolicySchema.optional().default("wake_assignee"),
|
||||
payload: requestCheckboxConfirmationPayloadSchema,
|
||||
});
|
||||
|
||||
const approvalDecisionSchema = z.object({
|
||||
approvalId: approvalIdSchema,
|
||||
action: z.enum(["approve", "reject", "requestRevision", "resubmit"]),
|
||||
@@ -516,6 +528,18 @@ export function createToolDefinitions(client: PaperclipApiClient): ToolDefinitio
|
||||
},
|
||||
}),
|
||||
),
|
||||
makeTool(
|
||||
"paperclipRequestCheckboxConfirmation",
|
||||
"Create a request_checkbox_confirmation interaction on an issue",
|
||||
createRequestCheckboxConfirmationToolSchema,
|
||||
async ({ issueId, ...body }) =>
|
||||
client.requestJson("POST", `/issues/${encodeURIComponent(issueId)}/interactions`, {
|
||||
body: {
|
||||
kind: "request_checkbox_confirmation",
|
||||
...body,
|
||||
},
|
||||
}),
|
||||
),
|
||||
makeTool(
|
||||
"paperclipUpsertIssueDocument",
|
||||
"Create or update an issue document",
|
||||
|
||||
@@ -1680,6 +1680,9 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness {
|
||||
async requestConfirmation(issueId, interaction, companyId, options) {
|
||||
return this.createInteraction(issueId, { ...interaction, kind: "request_confirmation" }, companyId, options) as Promise<any>;
|
||||
},
|
||||
async requestCheckboxConfirmation(issueId, interaction, companyId, options) {
|
||||
return this.createInteraction(issueId, { ...interaction, kind: "request_checkbox_confirmation" }, companyId, options) as Promise<any>;
|
||||
},
|
||||
documents: {
|
||||
async list(issueId, companyId) {
|
||||
requireCapability(manifest, capabilitySet, "issue.documents.read");
|
||||
|
||||
@@ -27,6 +27,7 @@ import type {
|
||||
SuggestTasksInteraction,
|
||||
AskUserQuestionsInteraction,
|
||||
RequestConfirmationInteraction,
|
||||
RequestCheckboxConfirmationInteraction,
|
||||
CreateIssueThreadInteraction,
|
||||
PluginIssueOriginKind,
|
||||
IssueSurfaceVisibility,
|
||||
@@ -122,6 +123,7 @@ export type {
|
||||
SuggestTasksInteraction,
|
||||
AskUserQuestionsInteraction,
|
||||
RequestConfirmationInteraction,
|
||||
RequestCheckboxConfirmationInteraction,
|
||||
CreateIssueThreadInteraction,
|
||||
PluginIssueOriginKind,
|
||||
IssueSurfaceVisibility,
|
||||
@@ -1321,7 +1323,7 @@ export interface PluginIssueSummariesClient {
|
||||
* - `issues.orchestration.read` for orchestration summaries
|
||||
* - `issue.comments.read` for `listComments`
|
||||
* - `issue.comments.create` for `createComment`
|
||||
* - `issue.interactions.create` for `createInteraction`, `suggestTasks`, `askUserQuestions`, and `requestConfirmation`
|
||||
* - `issue.interactions.create` for `createInteraction`, `suggestTasks`, `askUserQuestions`, `requestConfirmation`, and `requestCheckboxConfirmation`
|
||||
* - `issue.documents.read` for `documents.list` and `documents.get`
|
||||
* - `issue.documents.write` for `documents.upsert` and `documents.delete`
|
||||
*/
|
||||
@@ -1454,6 +1456,12 @@ export interface PluginIssuesClient {
|
||||
companyId: string,
|
||||
options?: { authorAgentId?: string },
|
||||
): Promise<RequestConfirmationInteraction>;
|
||||
requestCheckboxConfirmation(
|
||||
issueId: string,
|
||||
interaction: Omit<Extract<CreateIssueThreadInteraction, { kind: "request_checkbox_confirmation" }>, "kind">,
|
||||
companyId: string,
|
||||
options?: { authorAgentId?: string },
|
||||
): Promise<RequestCheckboxConfirmationInteraction>;
|
||||
/** Read and write issue documents. Requires `issue.documents.read` / `issue.documents.write`. */
|
||||
documents: PluginIssueDocumentsClient;
|
||||
/** Read and write blocker relationships. */
|
||||
|
||||
@@ -43,6 +43,7 @@ import { fileURLToPath } from "node:url";
|
||||
import type {
|
||||
AskUserQuestionsInteraction,
|
||||
PaperclipPluginManifestV1,
|
||||
RequestCheckboxConfirmationInteraction,
|
||||
RequestConfirmationInteraction,
|
||||
SuggestTasksInteraction,
|
||||
} from "@paperclipai/shared";
|
||||
@@ -913,6 +914,23 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost
|
||||
}) as Promise<RequestConfirmationInteraction>;
|
||||
},
|
||||
|
||||
async requestCheckboxConfirmation(
|
||||
issueId: string,
|
||||
interaction,
|
||||
companyId: string,
|
||||
options?: { authorAgentId?: string },
|
||||
): Promise<RequestCheckboxConfirmationInteraction> {
|
||||
return callHost("issues.createInteraction", {
|
||||
issueId,
|
||||
companyId,
|
||||
interaction: {
|
||||
...interaction,
|
||||
kind: "request_checkbox_confirmation",
|
||||
},
|
||||
authorAgentId: options?.authorAgentId,
|
||||
}) as Promise<RequestCheckboxConfirmationInteraction>;
|
||||
},
|
||||
|
||||
documents: {
|
||||
async list(issueId: string, companyId: string) {
|
||||
return callHost("issues.documents.list", { issueId, companyId });
|
||||
|
||||
@@ -72,3 +72,59 @@ describe("createTestHarness action context", () => {
|
||||
await expect(harness.performAction("legacy", { ok: true })).resolves.toEqual({ ok: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe("createTestHarness issue interactions", () => {
|
||||
it("creates request_checkbox_confirmation interactions through the typed host helper", async () => {
|
||||
const harness = createTestHarness({
|
||||
manifest,
|
||||
capabilities: ["issues.create", "issue.interactions.create"],
|
||||
});
|
||||
const issue = await harness.ctx.issues.create({
|
||||
companyId: "company-1",
|
||||
title: "Pick files",
|
||||
});
|
||||
|
||||
const interaction = await harness.ctx.issues.requestCheckboxConfirmation(
|
||||
issue.id,
|
||||
{
|
||||
idempotencyKey: "checkbox:files",
|
||||
title: "Choose files",
|
||||
payload: {
|
||||
version: 1,
|
||||
prompt: "Which files should be included?",
|
||||
options: [
|
||||
{ id: "file-a", label: "File A" },
|
||||
{ id: "file-b", label: "File B", description: "Secondary draft" },
|
||||
],
|
||||
defaultSelectedOptionIds: ["file-a"],
|
||||
minSelected: 1,
|
||||
maxSelected: 2,
|
||||
},
|
||||
},
|
||||
"company-1",
|
||||
{ authorAgentId: "agent-1" },
|
||||
);
|
||||
|
||||
expect(interaction).toMatchObject({
|
||||
issueId: issue.id,
|
||||
companyId: "company-1",
|
||||
kind: "request_checkbox_confirmation",
|
||||
status: "pending",
|
||||
continuationPolicy: "wake_assignee",
|
||||
idempotencyKey: "checkbox:files",
|
||||
title: "Choose files",
|
||||
createdByAgentId: "agent-1",
|
||||
payload: {
|
||||
version: 1,
|
||||
prompt: "Which files should be included?",
|
||||
options: [
|
||||
{ id: "file-a", label: "File A" },
|
||||
{ id: "file-b", label: "File B", description: "Secondary draft" },
|
||||
],
|
||||
defaultSelectedOptionIds: ["file-a"],
|
||||
minSelected: 1,
|
||||
maxSelected: 2,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user