FN-6910: hide subtask quick-add behind experiment

Hide the AI subtask-breakdown quick-add action unless the experimental flag is explicitly enabled.

- Gate Subtask Breakdown callbacks from App on the default-off subtaskBreakdown experiment.
- Remove Subtask quick-add buttons from inline create and quick entry surfaces when no callback is wired.
- Add settings, quick-add, documentation, and changeset coverage for the new experiment toggle.

Files changed:
 .changeset/fn-6910-subtask-breakdown-flag.md       |  5 ++++
 docs/dashboard-guide.md                            |  2 +-
 packages/dashboard/app/App.tsx                     |  8 ++++---
 .../dashboard/app/components/InlineCreateCard.tsx  | 27 ++++++++++++----------
 .../dashboard/app/components/QuickEntryBox.tsx     | 27 ++++++++++++----------
 .../dashboard/app/components/SettingsModal.tsx     |  2 ++
 .../components/__tests__/InlineCreateCard.test.tsx | 13 +++++++++++
 .../components/__tests__/QuickEntryBox.test.tsx    | 12 ++++++++++
 .../components/__tests__/SettingsModal.test.tsx    | 13 +++++++++++
 9 files changed, 81 insertions(+), 28 deletions(-)

Fusion-Task-Id: FN-6910

Fusion-Task-Lineage: a7738b1f-7b41-438d-bd94-79ef56928262
This commit is contained in:
gsxdsm
2026-06-21 23:02:16 -07:00
parent c78b44ce4c
commit f4d2fa2ece
9 changed files with 81 additions and 28 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Hide the dashboard AI subtask-breakdown quick-add button behind the default-off `subtaskBreakdown` experimental feature flag.

View File

@@ -259,7 +259,7 @@ Rules:
- `Merge target / base branch` stays optional for all modes and uses the same branch-dropdown + `Custom…` fallback behavior as Planning Mode.
- In **More options → Model Configuration**, **Auto-merge** is a per-task override with three states: **Default** (follow project setting), **Enabled**, or **Disabled**.
The dialog also exposes the board quick-add AI handoffs: **Plan** opens Planning Mode with the current description, and **Subtask** opens Subtask Breakdown with the current description. Both buttons remain disabled until the description has content, matching the quick-add row behavior.
The dialog also exposes the board quick-add AI handoffs: **Plan** opens Planning Mode with the current description, and **Subtask** opens Subtask Breakdown with the current description when **Settings → Experimental Features → Subtask Breakdown** is enabled. The Subtask handoff is hidden by default; visible handoff buttons remain disabled until the description has content, matching the quick-add row behavior.
## Chat View

View File

@@ -1029,6 +1029,8 @@ function AppInner() {
const nodesEnabled = experimentalFeatures.nodesView === true;
const researchEnabled = experimentalFeatures.researchView === true;
const evalsEnabled = experimentalFeatures.evalsView === true;
/* FNXC:QuickAddSubtaskFlag 2026-06-21-00:00: Missing or false `subtaskBreakdown` settings must hide the AI Subtask quick-add handoff across List, Board, and New Task Modal surfaces; only an explicit true wires the callback. */
const subtaskBreakdownEnabled = experimentalFeatures.subtaskBreakdown === true;
/*
FNXC:Navigation 2026-06-19-00:00:
Experimental left sidebar navigation replaces the Header view shortcuts with a persistent sidebar on non-mobile project screens, while mobile continues to use the bottom navigation bar as the only primary navigation surface.
@@ -1861,7 +1863,7 @@ function AppInner() {
onQuickCreate={handleBoardQuickCreate}
onNewTask={openNewTaskWithNav}
onPlanningMode={openPlanningWithInitialPlanWithNav}
onSubtaskBreakdown={openSubtaskBreakdownWithNav}
onSubtaskBreakdown={subtaskBreakdownEnabled ? openSubtaskBreakdownWithNav : undefined}
autoMerge={autoMerge}
onToggleAutoMerge={toggleAutoMerge}
globalPaused={globalPaused}
@@ -1915,7 +1917,7 @@ function AppInner() {
onNewTask={openNewTaskWithNav}
onQuickCreate={handleBoardQuickCreate}
onPlanningMode={openPlanningWithInitialPlanWithNav}
onSubtaskBreakdown={openSubtaskBreakdownWithNav}
onSubtaskBreakdown={subtaskBreakdownEnabled ? openSubtaskBreakdownWithNav : undefined}
availableModels={availableModels}
favoriteProviders={favoriteProviders}
favoriteModels={favoriteModels}
@@ -2273,7 +2275,7 @@ function AppInner() {
handleGitHubImport,
}}
onPlanningMode={openPlanningWithInitialPlanWithNav}
onSubtaskBreakdown={openSubtaskBreakdownWithNav}
onSubtaskBreakdown={subtaskBreakdownEnabled ? openSubtaskBreakdownWithNav : undefined}
taskOperations={{ moveTask, deleteTask, mergeTask, archiveTask, retryTask, resetTask, duplicateTask }}
deepLink={{ handleDetailClose }}
settings={{ prAuthAvailable, autoMerge, themeMode, colorTheme, dashboardFontScalePct, shadcnCustomColors, resolvedThemeMode, setThemeMode, setColorTheme, setDashboardFontScalePct, setShadcnCustomColors }}

View File

@@ -904,18 +904,21 @@ export function InlineCreateCard({
<Lightbulb size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
{t("inline.plan", "Plan")}
</button>
<button
type="button"
className="btn btn-sm"
onClick={handleSubtaskClick}
onMouseDown={(e) => e.preventDefault()}
disabled={!description.trim()}
data-testid="subtask-button"
title={t("inline.breakDownSubtasks", "Break down into AI-generated subtasks")}
>
<ListTree size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
{t("inline.subtask", "Subtask")}
</button>
{/* FNXC:QuickAddSubtaskFlag 2026-06-21-00:00: Render no Subtask button or orphaned inline-create click target unless the default-off `subtaskBreakdown` experiment wires this callback. */}
{onSubtaskBreakdown && (
<button
type="button"
className="btn btn-sm"
onClick={handleSubtaskClick}
onMouseDown={(e) => e.preventDefault()}
disabled={!description.trim()}
data-testid="subtask-button"
title={t("inline.breakDownSubtasks", "Break down into AI-generated subtasks")}
>
<ListTree size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
{t("inline.subtask", "Subtask")}
</button>
)}
<div className="dep-trigger-wrap">
<button
type="button"

View File

@@ -1656,18 +1656,21 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
<Lightbulb size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
{t("tasks.plan", "Plan")}
</button>
<button
type="button"
className="btn btn-sm"
onClick={handleSubtaskClick}
onMouseDown={(e) => e.preventDefault()}
disabled={!description.trim()}
data-testid="subtask-button"
title={t("tasks.subtaskButtonTitle", "Break down into AI-generated subtasks")}
>
<ListTree size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
{t("tasks.subtask", "Subtask")}
</button>
{/* FNXC:QuickAddSubtaskFlag 2026-06-21-00:00: Render no Subtask button or click target unless App wires the default-off `subtaskBreakdown` experiment callback. */}
{onSubtaskBreakdown && (
<button
type="button"
className="btn btn-sm"
onClick={handleSubtaskClick}
onMouseDown={(e) => e.preventDefault()}
disabled={!description.trim()}
data-testid="subtask-button"
title={t("tasks.subtaskButtonTitle", "Break down into AI-generated subtasks")}
>
<ListTree size={12} style={{ verticalAlign: "middle", marginRight: 4 }} />
{t("tasks.subtask", "Subtask")}
</button>
)}
<div className="refine-trigger-wrap" ref={refineMenuRef}>
<button
type="button"

View File

@@ -276,6 +276,8 @@ const KNOWN_EXPERIMENTAL_FEATURES: Record<string, string> = {
researchView: "Research View",
evalsView: "Evals View",
goalsView: "Goals View",
/* FNXC:QuickAddSubtaskFlag 2026-06-21-00:00: The AI subtask-breakdown quick-add affordance is exposed only through this default-off experimental flag so missing settings keep every quick-add Subtask button hidden. */
subtaskBreakdown: "Subtask Breakdown",
leftSidebarNav: "Left Sidebar Navigation",
rightDock: "Right Dock Panel",
sandbox: "Sandbox (command isolation)",

View File

@@ -197,6 +197,7 @@ function renderCard(
addToast: vi.fn(),
availableModels: MOCK_MODELS,
projectId: TEST_PROJECT_ID,
onSubtaskBreakdown: vi.fn(),
...overrides,
};
const result = render(<InlineCreateCard {...props} />);
@@ -859,6 +860,18 @@ describe("InlineCreateCard Plan and Subtask buttons", () => {
expect((textarea as HTMLTextAreaElement).value).toBe("");
});
it("hides the Subtask quick-add action without leaving an inline controls shell when the callback is omitted", () => {
renderCard([], { onSubtaskBreakdown: undefined });
expandCard();
const controlsRow = document.querySelector(".inline-create-controls") as HTMLElement;
expect(controlsRow).toBeTruthy();
expect(screen.queryByTestId("subtask-button")).not.toBeInTheDocument();
expect(screen.queryByTitle("Break down into AI-generated subtasks")).not.toBeInTheDocument();
expect(controlsRow.contains(screen.getByTestId("plan-button"))).toBe(true);
expect(controlsRow.querySelector(".dep-trigger")).toBeTruthy();
});
it("shows toast when Plan clicked with empty description (via direct handler call)", () => {
const addToast = vi.fn();
const onPlanningMode = vi.fn();

View File

@@ -242,6 +242,7 @@ function renderQuickEntryBox(props = {}, { startExpanded = false } = {}) {
tasks: mockTasks,
availableModels: MOCK_MODELS,
projectId: TEST_PROJECT_ID,
onSubtaskBreakdown: vi.fn(),
};
const result = render(<QuickEntryBox {...defaultProps} {...props} />);
return { ...result, props: { ...defaultProps, ...props } };
@@ -3349,6 +3350,17 @@ describe("QuickEntryBox", () => {
expect(actionsContainer.contains(screen.getByTestId("refine-button"))).toBe(true);
});
it("hides the Subtask quick-add action without leaving an action-row shell when the callback is omitted", () => {
renderQuickEntryBox({ onSubtaskBreakdown: undefined });
expandQuickEntry();
const actionsContainer = screen.getByTestId("quick-entry-actions");
expect(screen.queryByTestId("subtask-button")).not.toBeInTheDocument();
expect(screen.queryByTitle("Break down into AI-generated subtasks")).not.toBeInTheDocument();
expect(actionsContainer.contains(screen.getByTestId("plan-button"))).toBe(true);
expect(actionsContainer.contains(screen.getByTestId("refine-button"))).toBe(true);
});
it("does not render actions when not expanded", () => {
renderQuickEntryBox({});
toggleQuickEntry();

View File

@@ -3777,6 +3777,7 @@ describe("SettingsModal", () => {
for (const featureLabel of [
"Research View",
"Evals View",
"Subtask Breakdown",
"Chat Rooms",
"Sandbox (command isolation)",
"Planning-style Agent Onboarding",
@@ -3788,6 +3789,18 @@ describe("SettingsModal", () => {
expect(screen.getAllByLabelText("Dev Server")).toHaveLength(1);
});
it("shows the Subtask Breakdown toggle as off when the setting is missing", async () => {
mockFetchSettings.mockResolvedValue({
...defaultSettings,
experimentalFeatures: {},
});
renderModal();
await openExperimentalFeaturesSection();
expect(screen.getByLabelText("Subtask Breakdown")).not.toBeChecked();
});
it("does not render duplicate Dev Server rows when legacy and canonical keys are both present", async () => {
mockFetchSettings.mockResolvedValue({
...defaultSettings,