From ec0fa960d706d4072ea3995a2b4860b81c8755ac Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 1 Jul 2026 11:01:50 -0700 Subject: [PATCH] FN-7382: add triage plan auto-approve toggle Add a board-column shortcut for toggling project plan auto-approval. - Surface an Auto-approve plan switch in Triage and workflow intake/hold column menus. - Wire the dashboard setting hook to persist planApprovalMode between workflow and auto-approve-all with optimistic rollback. - Cover legacy and workflow board propagation plus column menu behavior with tests. - Document the shortcut and add a patch changeset for the published CLI package. Files changed: .changeset/fn-7382-triage-auto-approve-toggle.md | 7 ++ docs/dashboard-guide.md | 2 + docs/settings-reference.md | 2 +- packages/dashboard/app/App.tsx | 4 + packages/dashboard/app/components/Board.tsx | 7 +- packages/dashboard/app/components/Column.tsx | 35 ++++++- .../app/components/__tests__/Board.test.tsx | 55 +++++++++- .../app/components/__tests__/Column.test.tsx | 75 ++++++++++++++ .../app/components/dashboard/MainContent.tsx | 6 ++ .../dashboard/app/components/dashboard/types.ts | 2 + .../app/hooks/__tests__/useAppSettings.test.ts | 114 +++++++++++++++++++++ packages/dashboard/app/hooks/useAppSettings.ts | 39 +++++++ 12 files changed, 343 insertions(+), 5 deletions(-) Fusion-Task-Id: FN-7382 Fusion-Task-Lineage: 1db96b50-3a8f-4074-b957-c30d1745aa23 Co-authored-by: Fusion (runfusion.ai) --- .../fn-7382-triage-auto-approve-toggle.md | 7 ++ docs/dashboard-guide.md | 2 + docs/settings-reference.md | 2 +- packages/dashboard/app/App.tsx | 4 + packages/dashboard/app/components/Board.tsx | 7 +- packages/dashboard/app/components/Column.tsx | 35 +++++- .../app/components/__tests__/Board.test.tsx | 55 ++++++++- .../app/components/__tests__/Column.test.tsx | 75 ++++++++++++ .../app/components/dashboard/MainContent.tsx | 6 + .../app/components/dashboard/types.ts | 2 + .../hooks/__tests__/useAppSettings.test.ts | 114 ++++++++++++++++++ .../dashboard/app/hooks/useAppSettings.ts | 39 ++++++ 12 files changed, 343 insertions(+), 5 deletions(-) create mode 100644 .changeset/fn-7382-triage-auto-approve-toggle.md diff --git a/.changeset/fn-7382-triage-auto-approve-toggle.md b/.changeset/fn-7382-triage-auto-approve-toggle.md new file mode 100644 index 0000000000..98f46dd860 --- /dev/null +++ b/.changeset/fn-7382-triage-auto-approve-toggle.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Add a Triage column shortcut for plan auto-approval. +category: feature +dev: Adds a Board column switch that mirrors project planApprovalMode=auto-approve-all. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index e07cf5cfa1..cc91490a95 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -149,6 +149,8 @@ FNXC:TaskCardMobileSelection 2026-07-01-00:00: Mobile Board long-press is a task + +- The Triage column actions dropdown includes **Auto-approve plan**. Turning it on sets the project plan approval mode to auto-approve all planned tasks; turning it off returns to the workflow/default plan approval behavior. In workflow-mode Boards, the same switch appears on intake/hold planning columns and on the equivalent **All workflows** aggregate intake column. Use Settings → Merge for the full three-state project control, including **Require approval for all tasks**. - Column ordering semantics: `todo` mirrors scheduler pickup order (priority descending, then oldest `createdAt`, then task ID); `triage`, `in-progress`, `in-review`, and `archived` remain priority-first with task-ID tie-breaks; `done` defaults to most recent completion first (`columnMovedAt`, then `updatedAt`, then `createdAt` fallback) and can be switched from the Done/complete column actions dropdown to descending task ID. In workflow mode, non-archived columns marked with the `complete` flag use the same Done menu items even when their column ID or label is customized. - Done-column sorting has two descending modes: **Completion date (newest first)** keeps the default completion-time order, while **Task ID (newest first)** places the highest numeric task IDs first. The sort actions are only shown in Done/complete column action menus, including custom workflow completion lanes; Archive All Done lives in the same menu when available. - On mobile, both default and workflow-mode boards fill the project viewport while the column strip remains the internal horizontal scroller with contained edge overscroll. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index 621d04967b..f0e2df603c 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -397,7 +397,7 @@ Security-sensitive file-browser escape hatches are project-only. `allowAbsoluteF | `overlapIgnorePaths` | `string[]` | `[]` | Optional project-relative file or directory paths to exclude from overlap blocking (for example `docs` or `generated/openapi.json`). Entries are trimmed, deduplicated, and must not be absolute or contain `..` traversal. | | `allowAbsoluteFileBrowserPaths` | `boolean` | `false` | Project-scoped Settings → General toggle for the workspace file browser. When enabled, slash-prefixed paths such as `/tmp` can be listed/read/written/downloaded through workspace file-browser routes while keeping existing file-size, binary, type, null-byte, traversal, and permission checks. Windows drive-letter paths remain blocked, and task-local file routes, memory APIs, worktree-copy validation, plugin bundle paths, and other validators are unchanged. | | `autoMerge` | `boolean` | `true` | Auto-finalize tasks from `in-review`. Tasks can override this per-task (including at create time in New Task modal via **Auto-merge** = Default/Enabled/Disabled); explicit overrides are tagged with `autoMergeProvenance: "user"`, while tasks left at **Default** keep following the live global setting and do not snapshot it when entering review. Legacy pre-FN-6245 in-review rows that were stamped `autoMerge: true` are marked `autoMergeProvenance: "legacy-stamp"` on startup and can be inspected/cleared with Settings → Merge → **Legacy auto-merge stamp cleanup**, `fn pr automerge-cleanup [--apply] [--json]`, or `reconcileLegacyAutoMergeStamps({ apply: true })` after operator review. For grouped branch flows, per-task `autoMerge` governs member→group-integration landing while group `autoMerge` governs group→default-branch promotion eligibility. | -| `planApprovalMode` | `"workflow" \| "auto-approve-all" \| "require-all"` | `"workflow"` | Project-scoped override for the manual planning approval gate. `"workflow"` preserves the workflow-resolved `requirePlanApproval`; `"auto-approve-all"` moves every successfully specified task to `todo` without manual plan approval even when the selected workflow or stored workflow setting has `requirePlanApproval: true`; `"require-all"` parks every specified task at `status: "awaiting-approval"` regardless of workflow settings. This does not disable Workflow Plan Review, release authorization, or other non-plan safety gates. | +| `planApprovalMode` | `"workflow" \| "auto-approve-all" \| "require-all"` | `"workflow"` | Project-scoped override for the manual planning approval gate. `"workflow"` preserves the workflow-resolved `requirePlanApproval`; `"auto-approve-all"` moves every successfully specified task to `todo` without manual plan approval even when the selected workflow or stored workflow setting has `requirePlanApproval: true`; `"require-all"` parks every specified task at `status: "awaiting-approval"` regardless of workflow settings. Settings → Merge remains the full three-state editor; the Board Triage/intake **Auto-approve plan** switch is a binary shortcut for `"auto-approve-all"` vs `"workflow"`. This does not disable Workflow Plan Review, release authorization, or other non-plan safety gates. | | `maxAutoMergeRetries` | `number` | `3` | Project-scoped positive-integer cap for auto-merge conflict-resolution retries before Fusion parks or bounces a task for human/recovery handling. Unset, non-finite, zero, or negative values fall back to `3` to preserve historical behavior. | | `mergeRequestContractShadowEnabled` | `boolean` | `false` | Phase-1 FN-5741 write-only shadow flag (project/global setting). When enabled, executor/self-healing/merger persist merge-request records and `completion_handoff_accepted` markers for observation only; legacy mergeQueue + lifecycle remains authoritative. | | `mergeStrategy` | `"direct" \| "pull-request"` | `"direct"` | Completion mode (local direct merge vs PR-first). | diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx index aebef2c01e..eff31fa2fc 100644 --- a/packages/dashboard/app/App.tsx +++ b/packages/dashboard/app/App.tsx @@ -551,6 +551,7 @@ function AppInner() { maxConcurrent, autoMerge, mergeStrategy, + planAutoApproveEnabled, showWorktreeGrouping, globalPaused, isTestMode, @@ -575,6 +576,7 @@ function AppInner() { goalsEnabled, setQuickChatButtonModeImmediate, toggleAutoMerge, + togglePlanAutoApprove, refresh: refreshAppSettings, } = useAppSettings(currentProject?.id); @@ -1219,6 +1221,7 @@ function AppInner() { prAuthAvailable, autoMerge, mergeStrategy, + planAutoApproveEnabled, settingsLoaded, taskDetailChatFirst, skillsEnabled, @@ -1270,6 +1273,7 @@ function AppInner() { subtaskBreakdownEnabled, openSubtaskBreakdownWithNav, toggleAutoMerge, + togglePlanAutoApprove, globalPaused, updateTask, retryTask, diff --git a/packages/dashboard/app/components/Board.tsx b/packages/dashboard/app/components/Board.tsx index b23fe1bd2b..9c5dcdaba6 100644 --- a/packages/dashboard/app/components/Board.tsx +++ b/packages/dashboard/app/components/Board.tsx @@ -45,6 +45,8 @@ interface BoardProps { /** Project merge strategy passed to Board-owned card context menus. */ mergeStrategy?: string; onToggleAutoMerge: () => void; + planAutoApproveEnabled: boolean; + onTogglePlanAutoApprove: () => void; globalPaused?: boolean; onUpdateTask?: ( id: string, @@ -156,7 +158,7 @@ function BoardWorkflowSkeleton({ empty = false }: { empty?: boolean }) { ); } -export function Board({ tasks, projectId, maxConcurrent, showWorktreeGrouping, onMoveTask, onPauseTask, onUnpauseTask, onResetTask, onDuplicateTask, onMergeTask, onOpenDetail, onOpenRefine, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, mergeStrategy = "direct", onToggleAutoMerge, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onDeleteTask, onArchiveAllDone, onLoadArchivedTasks, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, taskStuckTimeoutMs, onOpenMission, staleHighFanoutBlockerAgeThresholdMs, lastFetchTimeMs, prAuthAvailable, onOpenWorkflowEditor, onCreateWorkflow, workflowColumnsEnabled, settingsLoaded, workflowControlsInHeader = false }: BoardProps) { +export function Board({ tasks, projectId, maxConcurrent, showWorktreeGrouping, onMoveTask, onPauseTask, onUnpauseTask, onResetTask, onDuplicateTask, onMergeTask, onOpenDetail, onOpenRefine, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, mergeStrategy = "direct", onToggleAutoMerge, planAutoApproveEnabled, onTogglePlanAutoApprove, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onDeleteTask, onArchiveAllDone, onLoadArchivedTasks, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, taskStuckTimeoutMs, onOpenMission, staleHighFanoutBlockerAgeThresholdMs, lastFetchTimeMs, prAuthAvailable, onOpenWorkflowEditor, onCreateWorkflow, workflowColumnsEnabled, settingsLoaded, workflowControlsInHeader = false }: BoardProps) { const [archivedCollapsed, setArchivedCollapsed] = useState(true); /* FNXC:DoneColumnSorting 2026-06-29-16:57: @@ -859,6 +861,7 @@ export function Board({ tasks, projectId, maxConcurrent, showWorktreeGrouping, o prAuthAvailable={prAuthAvailable} autoMerge={autoMerge} mergeStrategy={mergeStrategy} + {...(((columnDef.flags.intake || columnDef.flags.hold) && !columnDef.flags.archived && !columnDef.flags.complete && !columnDef.flags.countsTowardWip && !columnDef.flags.mergeBlocker && !columnDef.flags.humanReview) ? { planAutoApproveEnabled, onTogglePlanAutoApprove } : {})} {...(isCreateColumn && aggregateQuickCreateTarget ? { workflowId: aggregateQuickCreateTarget.workflowId, workflowOptions, defaultWorkflowId: boardWorkflows?.defaultWorkflowId ?? null, onQuickCreate: handleAggregateWorkflowQuickCreate, onNewTask, onPlanningMode, onSubtaskBreakdown } : {})} {...(columnDef.flags.mergeBlocker || columnDef.flags.humanReview ? { onToggleAutoMerge: handleToggleAutoMerge } : {})} {...(columnDef.id === "done" ? { onArchiveAllDone } : {})} @@ -938,6 +941,7 @@ export function Board({ tasks, projectId, maxConcurrent, showWorktreeGrouping, o prAuthAvailable={prAuthAvailable} autoMerge={autoMerge} mergeStrategy={mergeStrategy} + {...(((columnDef.flags.intake || columnDef.flags.hold) && !columnDef.flags.archived && !columnDef.flags.complete && !columnDef.flags.countsTowardWip && !columnDef.flags.mergeBlocker && !columnDef.flags.humanReview) ? { planAutoApproveEnabled, onTogglePlanAutoApprove } : {})} {...(isCreateColumn ? { workflowOptions, defaultWorkflowId: selectedWorkflow.id, onQuickCreate: handleWorkflowQuickCreate, onNewTask, onPlanningMode, onSubtaskBreakdown } : {})} {...(columnDef.flags.mergeBlocker || columnDef.flags.humanReview ? { onToggleAutoMerge: handleToggleAutoMerge } : {})} {...(columnDef.id === "done" ? { onArchiveAllDone } : {})} @@ -1045,6 +1049,7 @@ export function Board({ tasks, projectId, maxConcurrent, showWorktreeGrouping, o prAuthAvailable={prAuthAvailable} autoMerge={autoMerge} mergeStrategy={mergeStrategy} + {...(col === "triage" ? { planAutoApproveEnabled, onTogglePlanAutoApprove } : {})} {...(col === "triage" ? { onQuickCreate, onNewTask, onPlanningMode, onSubtaskBreakdown } : {})} {...(col === "in-review" ? { onToggleAutoMerge: handleToggleAutoMerge } : {})} {...(col === "done" ? { onArchiveAllDone, doneSortMode, onDoneSortModeChange: setDoneSortMode } : {})} diff --git a/packages/dashboard/app/components/Column.tsx b/packages/dashboard/app/components/Column.tsx index 23a4463163..9973a13de8 100644 --- a/packages/dashboard/app/components/Column.tsx +++ b/packages/dashboard/app/components/Column.tsx @@ -110,6 +110,8 @@ interface ColumnProps { /** Project merge strategy for Task Detail-equivalent card context actions. */ mergeStrategy?: string; onToggleAutoMerge?: () => void; + planAutoApproveEnabled?: boolean; + onTogglePlanAutoApprove?: () => void; globalPaused?: boolean; onUpdateTask?: ( id: string, @@ -195,7 +197,7 @@ interface ColumnProps { getDraggingTaskId?: () => string | null; } -function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktreeGrouping, onMoveTask, onPauseTask, onUnpauseTask, onResetTask, onDuplicateTask, onMergeTask, onOpenDetail, onOpenRefine, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, mergeStrategy = "direct", onToggleAutoMerge, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onDeleteTask, onArchiveAllDone, doneSortMode, onDoneSortModeChange, collapsed, onToggleCollapse, allTasks, availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, isSearchActive, taskStuckTimeoutMs, onOpenMission, lastFetchTimeMs, taskCardFieldDefs, taskWorkflowBadges, blockerFanoutMap, prAuthAvailable, workflowMode, workflowId, workflowOptions, defaultWorkflowId, columnDisplayName, columnFlags, workflowContextMenuColumns, taskContextMenuColumnsByTaskId, onPromote, canDropTask, getDraggingTaskId }: ColumnProps) { +function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktreeGrouping, onMoveTask, onPauseTask, onUnpauseTask, onResetTask, onDuplicateTask, onMergeTask, onOpenDetail, onOpenRefine, onOpenGroupModal, addToast, onQuickCreate, onNewTask, autoMerge, mergeStrategy = "direct", onToggleAutoMerge, planAutoApproveEnabled, onTogglePlanAutoApprove, globalPaused, onUpdateTask, onRetryTask, onArchiveTask, onUnarchiveTask, onDeleteTask, onArchiveAllDone, doneSortMode, onDoneSortModeChange, collapsed, onToggleCollapse, allTasks, availableModels, onPlanningMode, onSubtaskBreakdown, onOpenDetailWithTab, favoriteProviders, favoriteModels, onToggleFavorite, onToggleModelFavorite, isSearchActive, taskStuckTimeoutMs, onOpenMission, lastFetchTimeMs, taskCardFieldDefs, taskWorkflowBadges, blockerFanoutMap, prAuthAvailable, workflowMode, workflowId, workflowOptions, defaultWorkflowId, columnDisplayName, columnFlags, workflowContextMenuColumns, taskContextMenuColumnsByTaskId, onPromote, canDropTask, getDraggingTaskId }: ColumnProps) { const { t } = useTranslation("app"); // Anchor the board.rejection.* catalog keys for the i18next extractor (it // scopes `t` to the useTranslation binding, so the shared translateRejection @@ -571,8 +573,13 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktree const isDoneSortColumn = workflowMode ? columnFlags?.complete === true && columnFlags?.archived !== true : column === "done"; const showDoneSortControl = isDoneSortColumn && doneSortMode !== undefined && !!onDoneSortModeChange; const showDoneArchiveAction = isDoneSortColumn && !!onArchiveAllDone; + /* + FNXC:PlanApproval 2026-07-01-08:44: + Triage and workflow intake/hold column actions need a Board shortcut that mirrors the project auto-approve plan override without replacing Settings modal's full workflow/auto-approve/require-all editor. + */ + const hasPlanAutoApproveAction = !!onTogglePlanAutoApprove; const hasDoneMenuActions = showDoneSortControl || showDoneArchiveAction; - const hasColumnMenu = hasColumnBulkActions || hasDoneMenuActions; + const hasColumnMenu = hasColumnBulkActions || hasDoneMenuActions || hasPlanAutoApproveAction; const doneSortControlLabel = t("column.doneSortControlLabel", "Sort Done tasks"); const doneSortOptions: Array<{ mode: DoneColumnSortMode; label: string }> = [ { mode: "completion-date-desc", label: t("column.doneSortCompletionDateDesc", "Completion date (newest first)") }, @@ -584,6 +591,11 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktree setIsMenuOpen(false); }, [onDoneSortModeChange]); + const handlePlanAutoApproveToggle = useCallback(() => { + onTogglePlanAutoApprove?.(); + setIsMenuOpen(false); + }, [onTogglePlanAutoApprove]); + const handleArchiveAll = useCallback(async () => { setIsMenuOpen(false); if (!onArchiveAllDone) return; @@ -664,6 +676,25 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktree {isMenuOpen && (
+ {hasPlanAutoApproveAction && ( + + )} {showDoneSortControl && (
{doneSortOptions.map((option) => ( diff --git a/packages/dashboard/app/components/__tests__/Board.test.tsx b/packages/dashboard/app/components/__tests__/Board.test.tsx index 75ee4bafd4..1cee9c8ec8 100644 --- a/packages/dashboard/app/components/__tests__/Board.test.tsx +++ b/packages/dashboard/app/components/__tests__/Board.test.tsx @@ -59,6 +59,8 @@ vi.mock("../Column", () => ({ onQuickCreate, onNewTask, onToggleAutoMerge, + planAutoApproveEnabled, + onTogglePlanAutoApprove, onArchiveAllDone, favoriteProviders, favoriteModels, @@ -83,6 +85,8 @@ vi.mock("../Column", () => ({ onQuickCreate?: unknown; onNewTask?: unknown; onToggleAutoMerge?: () => void; + planAutoApproveEnabled?: boolean; + onTogglePlanAutoApprove?: () => void; onArchiveAllDone?: unknown; favoriteProviders?: string[]; favoriteModels?: string[]; @@ -101,7 +105,7 @@ vi.mock("../Column", () => ({ }) => { columnRenderCounts[column] = (columnRenderCounts[column] ?? 0) + 1; return ( -
workflow.id))} data-default-workflow-id={defaultWorkflowId ?? ""} data-column-display-name={columnDisplayName ?? ""} data-has-can-drop={canDropTask ? "yes" : "no"} data-has-planning={onPlanningMode ? "yes" : "no"} data-has-subtask={onSubtaskBreakdown ? "yes" : "no"}> +
workflow.id))} data-default-workflow-id={defaultWorkflowId ?? ""} data-column-display-name={columnDisplayName ?? ""} data-has-can-drop={canDropTask ? "yes" : "no"} data-has-planning={onPlanningMode ? "yes" : "no"} data-has-subtask={onSubtaskBreakdown ? "yes" : "no"}> {onQuickCreate ? (