Gate plan decomposition panel behind experimental flag

This commit is contained in:
Devin Foley committed 2026-05-27 23:21:04 -07:00
1 parent 41b08c4255
commit 32dbcc1da4
8 files changed
+126 -5

No files matched your search

+1
View File
@@ -29,6 +29,7 @@ export interface InstanceGeneralSettings {
export interface InstanceExperimentalSettings {
enableEnvironments: boolean;
enableIsolatedWorkspaces: boolean;
enableIssuePlanDecompositions: boolean;
enableCloudSync: boolean;
autoRestartDevServerWhenIdle: boolean;
enableIssueGraphLivenessAutoRecovery: boolean;
@@ -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),
@@ -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,
@@ -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,
+2
View File
@@ -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,
@@ -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() {
</div>
</section>
<section className="rounded-xl border border-border bg-card p-5">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1.5">
<h2 className="text-sm font-semibold">Issue Plan Decomposition Panel</h2>
<p className="max-w-2xl text-sm text-muted-foreground">
Show accepted-plan decomposition history on issue detail pages. Intended for debugging and validating
subtask creation behavior while the presentation is still being refined.
</p>
</div>
<ToggleSwitch
checked={enableIssuePlanDecompositions}
onCheckedChange={() =>
toggleMutation.mutate({
enableIssuePlanDecompositions: !enableIssuePlanDecompositions,
})
}
disabled={toggleMutation.isPending}
aria-label="Toggle issue plan decomposition panel experimental setting"
/>
</div>
</section>
<section className="rounded-xl border border-border bg-card p-5">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1.5">
+78
View File
@@ -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(
<QueryClientProvider client={queryClient}>
<IssueDetail />
</QueryClientProvider>,
);
});
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(
<QueryClientProvider client={queryClient}>
<IssueDetail />
</QueryClientProvider>,
);
});
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",
+15 -5
View File
@@ -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() {
</div>
)}
<IssuePlanDecompositionsSection
issueId={issue.id}
issueIdentifier={issue.identifier}
agentMap={agentMap}
/>
{showPlanDecompositionsSection ? (
<IssuePlanDecompositionsSection
issueId={issue.id}
issueIdentifier={issue.identifier}
agentMap={agentMap}
/>
) : null}
<IssueDocumentsSection
issue={issue}