diff --git a/packages/shared/src/types/instance.ts b/packages/shared/src/types/instance.ts index 3be7c6fd6ce..2dc7aac7994 100644 --- a/packages/shared/src/types/instance.ts +++ b/packages/shared/src/types/instance.ts @@ -29,6 +29,7 @@ export interface InstanceGeneralSettings { export interface InstanceExperimentalSettings { enableEnvironments: boolean; enableIsolatedWorkspaces: boolean; + enableIssuePlanDecompositions: boolean; enableCloudSync: boolean; autoRestartDevServerWhenIdle: boolean; enableIssueGraphLivenessAutoRecovery: boolean; diff --git a/packages/shared/src/validators/instance.ts b/packages/shared/src/validators/instance.ts index 52da4d5f637..beb1c8db07d 100644 --- a/packages/shared/src/validators/instance.ts +++ b/packages/shared/src/validators/instance.ts @@ -38,6 +38,7 @@ export const patchInstanceGeneralSettingsSchema = instanceGeneralSettingsSchema. export const instanceExperimentalSettingsSchema = z.object({ enableEnvironments: z.boolean().default(false), enableIsolatedWorkspaces: z.boolean().default(false), + enableIssuePlanDecompositions: z.boolean().default(false), enableCloudSync: z.boolean().default(false), autoRestartDevServerWhenIdle: z.boolean().default(false), enableIssueGraphLivenessAutoRecovery: z.boolean().default(false), diff --git a/server/src/__tests__/instance-settings-routes.test.ts b/server/src/__tests__/instance-settings-routes.test.ts index 5d324cd4b02..0b575d26303 100644 --- a/server/src/__tests__/instance-settings-routes.test.ts +++ b/server/src/__tests__/instance-settings-routes.test.ts @@ -64,6 +64,7 @@ describe("instance settings routes", () => { mockInstanceSettingsService.getExperimental.mockResolvedValue({ enableEnvironments: false, enableIsolatedWorkspaces: false, + enableIssuePlanDecompositions: false, enableCloudSync: false, autoRestartDevServerWhenIdle: false, enableIssueGraphLivenessAutoRecovery: true, @@ -82,6 +83,7 @@ describe("instance settings routes", () => { experimental: { enableEnvironments: true, enableIsolatedWorkspaces: true, + enableIssuePlanDecompositions: true, enableCloudSync: true, autoRestartDevServerWhenIdle: false, enableIssueGraphLivenessAutoRecovery: true, @@ -125,6 +127,7 @@ describe("instance settings routes", () => { expect(getRes.body).toEqual({ enableEnvironments: false, enableIsolatedWorkspaces: false, + enableIssuePlanDecompositions: false, enableCloudSync: false, autoRestartDevServerWhenIdle: false, enableIssueGraphLivenessAutoRecovery: true, diff --git a/server/src/__tests__/instance-settings-service.test.ts b/server/src/__tests__/instance-settings-service.test.ts index 6669414d697..fbb6ea5adb9 100644 --- a/server/src/__tests__/instance-settings-service.test.ts +++ b/server/src/__tests__/instance-settings-service.test.ts @@ -6,6 +6,7 @@ describe("instance settings service", () => { expect(normalizeExperimentalSettings({ enableEnvironments: true, enableIsolatedWorkspaces: true, + enableIssuePlanDecompositions: true, enableCloudSync: true, autoRestartDevServerWhenIdle: true, enableIssueGraphLivenessAutoRecovery: true, @@ -14,6 +15,7 @@ describe("instance settings service", () => { })).toEqual({ enableEnvironments: true, enableIsolatedWorkspaces: true, + enableIssuePlanDecompositions: true, enableCloudSync: true, autoRestartDevServerWhenIdle: true, enableIssueGraphLivenessAutoRecovery: true, diff --git a/server/src/services/instance-settings.ts b/server/src/services/instance-settings.ts index 8b02f57c39d..f6bdb98a9a4 100644 --- a/server/src/services/instance-settings.ts +++ b/server/src/services/instance-settings.ts @@ -43,6 +43,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta return { enableEnvironments: parsed.data.enableEnvironments ?? false, enableIsolatedWorkspaces: parsed.data.enableIsolatedWorkspaces ?? false, + enableIssuePlanDecompositions: parsed.data.enableIssuePlanDecompositions ?? false, enableCloudSync: parsed.data.enableCloudSync ?? false, autoRestartDevServerWhenIdle: parsed.data.autoRestartDevServerWhenIdle ?? false, enableIssueGraphLivenessAutoRecovery: parsed.data.enableIssueGraphLivenessAutoRecovery ?? false, @@ -54,6 +55,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta return { enableEnvironments: false, enableIsolatedWorkspaces: false, + enableIssuePlanDecompositions: false, enableCloudSync: false, autoRestartDevServerWhenIdle: false, enableIssueGraphLivenessAutoRecovery: false, diff --git a/ui/src/pages/InstanceExperimentalSettings.tsx b/ui/src/pages/InstanceExperimentalSettings.tsx index 011d4c070ab..f9bb34af6ce 100644 --- a/ui/src/pages/InstanceExperimentalSettings.tsx +++ b/ui/src/pages/InstanceExperimentalSettings.tsx @@ -205,6 +205,8 @@ export function InstanceExperimentalSettings() { const enableEnvironments = experimentalQuery.data?.enableEnvironments === true; const enableIsolatedWorkspaces = experimentalQuery.data?.enableIsolatedWorkspaces === true; + const enableIssuePlanDecompositions = + experimentalQuery.data?.enableIssuePlanDecompositions === true; const enableCloudSync = experimentalQuery.data?.enableCloudSync === true; const autoRestartDevServerWhenIdle = experimentalQuery.data?.autoRestartDevServerWhenIdle === true; const enableIssueGraphLivenessAutoRecovery = @@ -299,6 +301,28 @@ export function InstanceExperimentalSettings() { +
+
+
+

Issue Plan Decomposition Panel

+

+ Show accepted-plan decomposition history on issue detail pages. Intended for debugging and validating + subtask creation behavior while the presentation is still being refined. +

+
+ + toggleMutation.mutate({ + enableIssuePlanDecompositions: !enableIssuePlanDecompositions, + }) + } + disabled={toggleMutation.isPending} + aria-label="Toggle issue plan decomposition panel experimental setting" + /> +
+
+
diff --git a/ui/src/pages/IssueDetail.test.tsx b/ui/src/pages/IssueDetail.test.tsx index 98f611d1458..fb8823669cc 100644 --- a/ui/src/pages/IssueDetail.test.tsx +++ b/ui/src/pages/IssueDetail.test.tsx @@ -10,6 +10,7 @@ import { canBoardResolveRecoveryAction, IssueDetail } from "./IssueDetail"; const mockIssuesApi = vi.hoisted(() => ({ get: vi.fn(), list: vi.fn(), + listAcceptedPlanDecompositions: vi.fn(), listComments: vi.fn(), listAttachments: vi.fn(), listFeedbackVotes: vi.fn(), @@ -59,6 +60,7 @@ const mockProjectsApi = vi.hoisted(() => ({ const mockInstanceSettingsApi = vi.hoisted(() => ({ getGeneral: vi.fn(), + getExperimental: vi.fn(), })); const mockNavigate = vi.hoisted(() => vi.fn()); @@ -823,6 +825,10 @@ describe("IssueDetail", () => { keyboardShortcuts: false, feedbackDataSharingPreference: "prompt", }); + mockInstanceSettingsApi.getExperimental.mockResolvedValue({ + enableIssuePlanDecompositions: false, + }); + mockIssuesApi.listAcceptedPlanDecompositions.mockResolvedValue([]); mockIssuesListRender.mockClear(); mockIssueChatThreadRender.mockClear(); }); @@ -858,6 +864,78 @@ describe("IssueDetail", () => { expect(consoleErrorSpy).not.toHaveBeenCalled(); }); + it("hides the plan decomposition panel by default", async () => { + mockIssuesApi.get.mockResolvedValue(createIssue()); + + await act(async () => { + root.render( + + + , + ); + }); + + await flushReact(); + await flushReact(); + + expect(container.textContent).not.toContain("Plan decomposition"); + expect(mockIssuesApi.listAcceptedPlanDecompositions).not.toHaveBeenCalled(); + }); + + it("shows the plan decomposition panel when the experimental flag is enabled", async () => { + mockIssuesApi.get.mockResolvedValue(createIssue()); + mockInstanceSettingsApi.getExperimental.mockResolvedValue({ + enableIssuePlanDecompositions: true, + }); + mockIssuesApi.listAcceptedPlanDecompositions.mockResolvedValue([ + { + id: "decomp-1", + companyId: "company-1", + sourceIssueId: "issue-1", + acceptedPlanRevisionId: "plan-rev-1", + acceptedPlanRevisionNumber: 2, + acceptedInteractionId: null, + status: "completed", + requestFingerprint: "fingerprint-1", + requestedChildCount: 1, + childIssueIds: ["issue-2"], + childIssues: [ + { + id: "issue-2", + identifier: "PAP-2", + title: "First child issue", + status: "todo", + priority: "medium", + assigneeAgentId: null, + assigneeUserId: null, + }, + ], + ownerAgentId: null, + ownerUserId: null, + ownerRunId: null, + completedAt: "2026-05-28T06:00:00.000Z", + createdAt: "2026-05-28T05:50:00.000Z", + updatedAt: "2026-05-28T06:00:00.000Z", + }, + ]); + + await act(async () => { + root.render( + + + , + ); + }); + + await flushReact(); + await flushReact(); + + expect(container.textContent).toContain("Plan decomposition"); + expect(container.textContent).toContain("Plan revision 2"); + expect(container.textContent).toContain("First child issue"); + expect(mockIssuesApi.listAcceptedPlanDecompositions).toHaveBeenCalledWith("issue-1"); + }); + it("renders sibling previous and next navigation at the chat footer", async () => { const issue = createIssue({ id: "issue-2", diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index fa038edcdf6..6f4b86b723e 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -1441,8 +1441,16 @@ export function IssueDetail() { enabled: !!issueId, retry: false, }); + const { data: instanceExperimentalSettings } = useQuery({ + queryKey: queryKeys.instance.experimentalSettings, + queryFn: () => instanceSettingsApi.getExperimental(), + enabled: !!issueId, + retry: false, + }); const keyboardShortcutsEnabled = instanceGeneralSettings?.keyboardShortcuts === true; const feedbackDataSharingPreference = instanceGeneralSettings?.feedbackDataSharingPreference ?? "prompt"; + const showPlanDecompositionsSection = + instanceExperimentalSettings?.enableIssuePlanDecompositions === true; const { orderedProjects } = useProjectOrder({ projects: projects ?? [], companyId: selectedCompanyId, @@ -3714,11 +3722,13 @@ export function IssueDetail() {
)} - + {showPlanDecompositionsSection ? ( + + ) : null}