diff --git a/packages/db/src/migrations/0090_demonic_grey_gargoyle.sql b/packages/db/src/migrations/0090_demonic_grey_gargoyle.sql index 404047d5f93..8c17a9b3d26 100644 --- a/packages/db/src/migrations/0090_demonic_grey_gargoyle.sql +++ b/packages/db/src/migrations/0090_demonic_grey_gargoyle.sql @@ -23,4 +23,5 @@ ALTER TABLE "issue_plan_decompositions" ADD CONSTRAINT "issue_plan_decomposition ALTER TABLE "issue_plan_decompositions" ADD CONSTRAINT "issue_plan_decompositions_owner_agent_id_agents_id_fk" FOREIGN KEY ("owner_agent_id") REFERENCES "public"."agents"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint ALTER TABLE "issue_plan_decompositions" ADD CONSTRAINT "issue_plan_decompositions_owner_run_id_heartbeat_runs_id_fk" FOREIGN KEY ("owner_run_id") REFERENCES "public"."heartbeat_runs"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint CREATE INDEX "issue_plan_decompositions_company_source_status_idx" ON "issue_plan_decompositions" USING btree ("company_id","source_issue_id","status");--> statement-breakpoint +CREATE INDEX "issue_plan_decompositions_active_owner_idx" ON "issue_plan_decompositions" USING btree ("company_id","owner_agent_id") WHERE "issue_plan_decompositions"."status" = 'in_flight';--> statement-breakpoint CREATE UNIQUE INDEX "issue_plan_decompositions_source_revision_uq" ON "issue_plan_decompositions" USING btree ("company_id","source_issue_id","accepted_plan_revision_id"); diff --git a/packages/db/src/migrations/meta/0090_snapshot.json b/packages/db/src/migrations/meta/0090_snapshot.json index 25169aa3f66..65026785085 100644 --- a/packages/db/src/migrations/meta/0090_snapshot.json +++ b/packages/db/src/migrations/meta/0090_snapshot.json @@ -10302,6 +10302,28 @@ "method": "btree", "with": {} }, + "issue_plan_decompositions_active_owner_idx": { + "name": "issue_plan_decompositions_active_owner_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "where": "\"issue_plan_decompositions\".\"status\" = 'in_flight'", + "with": {} + }, "issue_plan_decompositions_source_revision_uq": { "name": "issue_plan_decompositions_source_revision_uq", "columns": [ @@ -18349,4 +18371,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/db/src/schema/issue_plan_decompositions.ts b/packages/db/src/schema/issue_plan_decompositions.ts index 47262eb0b3c..39ff5d97f71 100644 --- a/packages/db/src/schema/issue_plan_decompositions.ts +++ b/packages/db/src/schema/issue_plan_decompositions.ts @@ -36,6 +36,9 @@ export const issuePlanDecompositions = pgTable( table.sourceIssueId, table.status, ), + activeOwnerIdx: index("issue_plan_decompositions_active_owner_idx") + .on(table.companyId, table.ownerAgentId) + .where(sql`${table.status} = 'in_flight'`), sourceRevisionUq: uniqueIndex("issue_plan_decompositions_source_revision_uq").on( table.companyId, table.sourceIssueId, diff --git a/screenshots/pr-6831-plan-decomposition-completed.png b/screenshots/pr-6831-plan-decomposition-completed.png new file mode 100644 index 00000000000..c90422fe47a Binary files /dev/null and b/screenshots/pr-6831-plan-decomposition-completed.png differ diff --git a/screenshots/pr-6831-plan-decomposition-in-flight.png b/screenshots/pr-6831-plan-decomposition-in-flight.png new file mode 100644 index 00000000000..c90422fe47a Binary files /dev/null and b/screenshots/pr-6831-plan-decomposition-in-flight.png differ diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index 23f4f4f23b3..68fe6338ef0 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -3740,6 +3740,7 @@ describeEmbeddedPostgres("accepted plan decomposition", () => { expect(record?.childIssues.map((child) => child.id).sort()).toEqual( [...result.childIssueIds].sort(), ); + expect(record).not.toHaveProperty("requestedChildren"); expect(record?.childIssues.every((child) => typeof child.title === "string")).toBe(true); }); }); diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index b335ca2cac9..adf1c9c36bd 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -4414,9 +4414,24 @@ export function issueService(db: Db) { const childIssueMap = new Map(childIssueRows.map((row) => [row.id, row])); return rows.map((row) => { - const childIds = normalizeIssuePlanDecompositionChildIds(row.decomposition.childIssueIds); + const decomposition = row.decomposition; + const childIds = normalizeIssuePlanDecompositionChildIds(decomposition.childIssueIds); return { - ...row.decomposition, + id: decomposition.id, + companyId: decomposition.companyId, + sourceIssueId: decomposition.sourceIssueId, + acceptedPlanRevisionId: decomposition.acceptedPlanRevisionId, + acceptedInteractionId: decomposition.acceptedInteractionId, + status: decomposition.status, + requestFingerprint: decomposition.requestFingerprint, + requestedChildCount: decomposition.requestedChildCount, + childIssueIds: childIds, + ownerAgentId: decomposition.ownerAgentId, + ownerUserId: decomposition.ownerUserId, + ownerRunId: decomposition.ownerRunId, + completedAt: decomposition.completedAt, + createdAt: decomposition.createdAt, + updatedAt: decomposition.updatedAt, acceptedPlanRevisionNumber: row.revisionNumber ?? null, childIssues: childIds .map((childId) => childIssueMap.get(childId) ?? null) diff --git a/ui/storybook/stories/issue-plan-decompositions.stories.tsx b/ui/storybook/stories/issue-plan-decompositions.stories.tsx new file mode 100644 index 00000000000..e53f33c44f4 --- /dev/null +++ b/ui/storybook/stories/issue-plan-decompositions.stories.tsx @@ -0,0 +1,129 @@ +import { useState } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { useQueryClient } from "@tanstack/react-query"; +import type { AcceptedPlanDecompositionSummary } from "@paperclipai/shared"; +import { IssuePlanDecompositionsSection } from "@/components/IssuePlanDecompositionsSection"; +import { queryKeys } from "@/lib/queryKeys"; +import { storybookAgentMap } from "../fixtures/paperclipData"; + +const issueId = "issue-plan-decomposition-story"; +const issueIdentifier = "PAP-6831"; + +function buildDecomposition( + overrides: Partial, +): AcceptedPlanDecompositionSummary { + return { + id: "decomposition-story-1", + companyId: "company-storybook", + sourceIssueId: issueId, + acceptedPlanRevisionId: "revision-story-1", + acceptedInteractionId: "interaction-story-1", + status: "completed", + requestFingerprint: "fingerprint-story-1", + requestedChildCount: 2, + childIssueIds: ["issue-child-1", "issue-child-2"], + ownerAgentId: "agent-codex", + ownerUserId: null, + ownerRunId: "run-story-1", + completedAt: "2026-05-28T06:22:00.000Z", + createdAt: "2026-05-28T06:18:00.000Z", + updatedAt: "2026-05-28T06:22:00.000Z", + acceptedPlanRevisionNumber: 7, + childIssues: [ + { + id: "issue-child-1", + identifier: "PAP-6840", + title: "Harden accepted-plan wake routing", + status: "done", + priority: "medium", + assigneeAgentId: "agent-codex", + assigneeUserId: null, + }, + { + id: "issue-child-2", + identifier: "PAP-6841", + title: "Add decomposition regression coverage", + status: "in_progress", + priority: "medium", + assigneeAgentId: "agent-qa", + assigneeUserId: null, + }, + ], + ...overrides, + }; +} + +function HydratedSection({ + decompositions, +}: { + decompositions: AcceptedPlanDecompositionSummary[]; +}) { + const queryClient = useQueryClient(); + const [ready] = useState(() => { + queryClient.setQueryData(queryKeys.issues.acceptedPlanDecompositions(issueId), decompositions); + return true; + }); + + if (!ready) return null; + + return ( +
+
+
+ +
+
+
+ ); +} + +const meta = { + title: "Issue Detail/Plan Decompositions", + component: HydratedSection, + args: { + decompositions: [], + }, + parameters: { + layout: "fullscreen", + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const InFlight: Story = { + args: { + decompositions: [ + buildDecomposition({ + status: "in_flight", + completedAt: null, + updatedAt: "2026-05-28T06:20:00.000Z", + childIssueIds: ["issue-child-1"], + childIssues: [ + { + id: "issue-child-1", + identifier: "PAP-6840", + title: "Harden accepted-plan wake routing", + status: "done", + priority: "medium", + assigneeAgentId: "agent-codex", + assigneeUserId: null, + }, + ], + }), + ], + }, +}; + +export const Completed: Story = { + args: { + decompositions: [ + buildDecomposition({}), + ], + }, +};