FN-6247: refresh PR controls from live auto-merge setting

Keep task detail PR and review affordances synchronized with the current project auto-merge setting.

- Thread the live auto-merge value into task detail modals and list split-pane detail views.
- Prefer the live auto-merge setting over the stale fetched modal snapshot while preserving task-level overrides.
- Cover PR and review tab behavior for auto-merge on/off and document the dashboard behavior.
- Evict flaky engine gate entries and add a patch changeset for the published CLI package.

Files changed:
 .changeset/fn-6247-automerge-off-modal-stale.md    |   5 +
 docs/dashboard-guide.md                            |   1 +
 packages/dashboard/app/App.tsx                     |   3 +-
 packages/dashboard/app/components/AppModals.tsx    |   2 +
 packages/dashboard/app/components/ListView.tsx     |   3 +
 .../dashboard/app/components/TaskDetailModal.tsx   |   4 +-
 .../__tests__/TaskDetailModal.create-pr.test.tsx   | 171 ++++++++++++++++++++-
 packages/engine/vitest.config.ts                   |   2 -
 8 files changed, 182 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-6247

Fusion-Task-Lineage: 1321c03a-216d-4b15-bf4f-95621d68c9ae
This commit is contained in:
gsxdsm
2026-06-11 22:11:44 -07:00
parent 65251d2e6b
commit d23c5d901f
8 changed files with 182 additions and 9 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Fix task detail Pull Request and Review surfaces so they use the live project auto-merge setting instead of a stale modal-open snapshot. Create PR / manual merge affordances now appear immediately when auto-merge is toggled off, and the automatic auto-merge hint returns when it is toggled back on.

View File

@@ -635,6 +635,7 @@ Inspect task definition, logs, review feedback, comments, documents, workflow ou
- In shared task edit/create forms, GitHub Tracking appears at the bottom of **More options**, after **Workflow Steps**.
- From this section you can explicitly enable/disable tracking and manage a per-task repo override (`owner/repo`). Clearing the override saves `null` and falls back to project/global defaults.
- In `in-review`, pull-request controls/status (including stall badges) are in a dedicated **Pull Request** tab instead of the Definition tab.
- Task Detail and list split-pane PR affordances follow the live project auto-merge setting: when auto-merge is off, manual **Create PR** / merge actions are shown; when it is on, the tab shows the automatic auto-merge hint unless a per-task override changes the effective behavior.
- The **Create Pull Request** modal now offers in-app remediation for every blocking preflight check. If `branchOnRemote` is false, use **Push branch to remote** and Fusion will publish `fusion/<task-id-lower>` to `origin` and refresh preflight. If `conflictsWithBase` is true, use **Resolve conflicts with AI** and Fusion will use an AI coding agent to resolve merge markers on the task branch, commit the result, push the branch, and refresh preflight so normal PR creation can continue once all checks pass.
- The modal shell renders immediately: preflight checks and PR options load independently of AI-generated title/body metadata, so slow AI suggestions no longer block base-branch selection, diagnostics, or manual PR authoring.
- AI title/body generation is bounded to 60 seconds and is canceled if the dialog request disconnects; on timeout/cancel, Fusion falls back to deterministic task-based PR title/body content instead of leaving the spinner stuck forever.

View File

@@ -1815,6 +1815,7 @@ function AppInner() {
searchQuery={searchQuery}
lastFetchTimeMs={lastFetchTimeMs}
prAuthAvailable={prAuthAvailable}
autoMerge={autoMerge}
onCreateWorkflow={openCreateWorkflowWithNav}
/>
</PageErrorBoundary>
@@ -2136,7 +2137,7 @@ function AppInner() {
}}
taskOperations={{ moveTask, deleteTask, mergeTask, archiveTask, retryTask, resetTask, duplicateTask }}
deepLink={{ handleDetailClose }}
settings={{ prAuthAvailable, themeMode, colorTheme, dashboardFontScalePct, setThemeMode, setColorTheme, setDashboardFontScalePct }}
settings={{ prAuthAvailable, autoMerge, themeMode, colorTheme, dashboardFontScalePct, setThemeMode, setColorTheme, setDashboardFontScalePct }}
onSettingsClose={handleSettingsCloseWithNav}
onReopenOnboarding={reopenOnboardingWithNav}
onOpenApprovals={(_approvalId) => handleTaskViewChange("mailbox")}

View File

@@ -73,6 +73,7 @@ interface AppModalsProps {
};
settings: {
prAuthAvailable: boolean;
autoMerge: boolean;
themeMode: ThemeMode;
colorTheme: ColorTheme;
dashboardFontScalePct: number;
@@ -298,6 +299,7 @@ export function AppModals({
onTaskUpdated={modalManager.updateDetailTask}
addToast={addToast}
prAuthAvailable={settings.prAuthAvailable}
autoMergeEnabled={settings.autoMerge}
onOpenWorkflowEditor={() => modalManager.openWorkflowEditor()}
initialTab={modalManager.detailTaskInitialTab}
/>

View File

@@ -233,6 +233,7 @@ interface ListViewProps {
/** Timestamp (ms) when task data was last confirmed fresh from the server. Used for freshness-aware stuck detection. */
lastFetchTimeMs?: number;
prAuthAvailable?: boolean;
autoMerge?: boolean;
onCreateWorkflow?: () => void;
}
@@ -296,6 +297,7 @@ export function ListView({
searchQuery = "",
lastFetchTimeMs,
prAuthAvailable,
autoMerge,
onCreateWorkflow,
}: ListViewProps) {
const { t } = useTranslation("app");
@@ -2332,6 +2334,7 @@ export function ListView({
}}
addToast={addToast}
prAuthAvailable={prAuthAvailable}
autoMergeEnabled={autoMerge}
/>
</div>
)}

View File

@@ -368,6 +368,7 @@ export interface TaskDetailModalProps {
onTaskUpdated?: (task: Task) => void;
addToast: (message: string, type?: ToastType) => void;
prAuthAvailable?: boolean;
autoMergeEnabled?: boolean;
onOpenWorkflowEditor?: () => void;
/** Open the modal with this tab active instead of "definition" */
initialTab?: TabId;
@@ -549,6 +550,7 @@ export function TaskDetailContent({
onTaskUpdated,
addToast,
prAuthAvailable,
autoMergeEnabled: autoMergeEnabledProp,
onOpenWorkflowEditor,
initialTab = "definition",
mobileHeaderMode = "close",
@@ -2605,7 +2607,7 @@ export function TaskDetailContent({
};
const prAutomationLabel = task.status ? prAutomationStatusLabels[task.status] : undefined;
const mergeStrategy = settings?.mergeStrategy ?? "direct";
const autoMergeEnabled = settings?.autoMerge ?? false;
const autoMergeEnabled = autoMergeEnabledProp ?? (settings?.autoMerge ?? false);
const effectiveAutoMerge = resolveEffectiveAutoMerge({ autoMerge: task.autoMerge }, { autoMerge: autoMergeEnabled });
const isManualPrFlow = mergeStrategy === "pull-request" && !autoMergeEnabled;

View File

@@ -5,6 +5,7 @@ import type { PrInfo } from "@fusion/core";
const prPanelState = vi.hoisted(() => ({
latestPrInfo: undefined as PrInfo | undefined,
latestAutoMerge: undefined as boolean | undefined,
latestIsManualPrFlow: undefined as boolean | undefined,
}));
const prCreateModalState = vi.hoisted(() => ({
@@ -19,11 +20,16 @@ vi.mock("../PrPanel", () => ({
PrPanel: (props: any) => {
prPanelState.latestPrInfo = props.prInfo;
prPanelState.latestAutoMerge = props.autoMerge;
prPanelState.latestIsManualPrFlow = props.isManualPrFlow;
return (
<div>
<button type="button" onClick={() => props.onRequestCreatePr?.()}>
Create PR
</button>
{props.autoMerge ? (
<div>Auto-merge will handle this task automatically.</div>
) : (
<button type="button" onClick={() => props.onRequestCreatePr?.()}>
Create PR
</button>
)}
<div data-testid="pr-panel-pr-number">{props.prInfo?.number ?? "none"}</div>
</div>
);
@@ -65,11 +71,18 @@ vi.mock("../PrCreateModal", () => ({
vi.mock("../TaskReviewTab", () => ({
TaskReviewTab: (props: any) => {
taskReviewTabState.latestProps = props;
return (
const effectiveAutoMerge = props.task.autoMerge ?? props.autoMergeEnabled;
const showCreatePr =
props.task.column === "in-review" &&
!props.task.prInfo &&
props.prAuthAvailable === true &&
!effectiveAutoMerge &&
typeof props.onRequestCreatePr === "function";
return showCreatePr ? (
<button type="button" data-testid="task-review-create-pr" onClick={() => props.onRequestCreatePr?.()}>
Review create PR
</button>
);
) : null;
},
}));
@@ -92,6 +105,7 @@ describe("TaskDetailModal create-PR wiring", () => {
vi.clearAllMocks();
prPanelState.latestPrInfo = undefined;
prPanelState.latestAutoMerge = undefined;
prPanelState.latestIsManualPrFlow = undefined;
prCreateModalState.latestProps = null;
taskReviewTabState.latestProps = null;
});
@@ -234,4 +248,151 @@ describe("TaskDetailModal create-PR wiring", () => {
expect(screen.queryByTestId("pr-create-modal-stub")).toBeNull();
expect(prCreateModalState.latestProps?.open).toBe(false);
});
it("prefers live auto-merge off over a stale fetched snapshot for PR surfaces", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
modelPresets: [],
autoSelectModelPreset: false,
defaultPresetBySize: {},
autoMerge: true,
});
render(
<TaskDetailModal
task={makeTask({ id: "FN-6247-OFF", prInfo: undefined, column: "in-review", autoMerge: undefined })}
projectId="project-123"
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={vi.fn()}
prAuthAvailable
autoMergeEnabled={false}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Pull Request" }));
await waitFor(() => expect(prPanelState.latestAutoMerge).toBe(false));
expect(screen.getByRole("button", { name: "Create PR" })).toBeInTheDocument();
expect(screen.queryByText("Auto-merge will handle this task automatically.")).toBeNull();
fireEvent.click(screen.getByRole("button", { name: "Review" }));
await waitFor(() => expect(taskReviewTabState.latestProps?.autoMergeEnabled).toBe(false));
expect(screen.getByTestId("task-review-create-pr")).toBeInTheDocument();
});
it("prefers live auto-merge on over a stale fetched snapshot for PR surfaces", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
modelPresets: [],
autoSelectModelPreset: false,
defaultPresetBySize: {},
autoMerge: false,
});
render(
<TaskDetailModal
task={makeTask({ id: "FN-6247-ON", prInfo: undefined, column: "in-review", autoMerge: undefined })}
projectId="project-123"
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={vi.fn()}
prAuthAvailable
autoMergeEnabled
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Pull Request" }));
await waitFor(() => expect(prPanelState.latestAutoMerge).toBe(true));
expect(screen.getByText("Auto-merge will handle this task automatically.")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Create PR" })).toBeNull();
fireEvent.click(screen.getByRole("button", { name: "Review" }));
await waitFor(() => expect(taskReviewTabState.latestProps?.autoMergeEnabled).toBe(true));
expect(screen.queryByTestId("task-review-create-pr")).toBeNull();
});
it.each([
{ taskAutoMerge: undefined, liveAutoMerge: false, expectedEffective: false },
{ taskAutoMerge: undefined, liveAutoMerge: true, expectedEffective: true },
{ taskAutoMerge: true, liveAutoMerge: false, expectedEffective: true },
{ taskAutoMerge: true, liveAutoMerge: true, expectedEffective: true },
{ taskAutoMerge: false, liveAutoMerge: false, expectedEffective: false },
{ taskAutoMerge: false, liveAutoMerge: true, expectedEffective: false },
])(
"resolves effective auto-merge for task override $taskAutoMerge with live global $liveAutoMerge",
async ({ taskAutoMerge, liveAutoMerge, expectedEffective }) => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
modelPresets: [],
autoSelectModelPreset: false,
defaultPresetBySize: {},
autoMerge: !liveAutoMerge,
});
render(
<TaskDetailModal
task={makeTask({ id: `FN-6247-${String(taskAutoMerge)}-${String(liveAutoMerge)}`, prInfo: undefined, column: "in-review", autoMerge: taskAutoMerge })}
projectId="project-123"
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={vi.fn()}
prAuthAvailable
autoMergeEnabled={liveAutoMerge}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Pull Request" }));
await waitFor(() => expect(prPanelState.latestAutoMerge).toBe(expectedEffective));
if (expectedEffective) {
expect(screen.getByText("Auto-merge will handle this task automatically.")).toBeInTheDocument();
} else {
expect(screen.queryByText("Auto-merge will handle this task automatically.")).toBeNull();
expect(screen.getByRole("button", { name: "Create PR" })).toBeInTheDocument();
}
fireEvent.click(screen.getByRole("button", { name: "Review" }));
await waitFor(() => expect(taskReviewTabState.latestProps?.autoMergeEnabled).toBe(liveAutoMerge));
if (expectedEffective) {
expect(screen.queryByTestId("task-review-create-pr")).toBeNull();
} else {
expect(screen.getByTestId("task-review-create-pr")).toBeInTheDocument();
}
},
);
it("keeps manual PR flow driven by live global auto-merge rather than effective override", async () => {
(fetchSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
modelPresets: [],
autoSelectModelPreset: false,
defaultPresetBySize: {},
autoMerge: true,
mergeStrategy: "pull-request",
});
render(
<TaskDetailModal
task={makeTask({ id: "FN-6247-MANUAL", prInfo: undefined, column: "in-review", autoMerge: true })}
projectId="project-123"
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={vi.fn()}
prAuthAvailable
autoMergeEnabled={false}
/>,
);
fireEvent.click(screen.getByRole("button", { name: "Pull Request" }));
await waitFor(() => expect(prPanelState.latestAutoMerge).toBe(true));
expect(prPanelState.latestIsManualPrFlow).toBe(true);
expect(screen.getByText("Auto-merge will handle this task automatically.")).toBeInTheDocument();
});
});

View File

@@ -74,13 +74,11 @@ export default defineConfig({
"src/__tests__/executor-recovery.test.ts",
"src/__tests__/executor-base-commit-capture.test.ts",
"src/__tests__/executor-capture-modified-files-attribution.test.ts",
"src/__tests__/triage.test.ts",
"src/__tests__/triage-preflight.test.ts",
"src/__tests__/scheduler.test.ts",
"src/__tests__/scheduler-node-routing.test.ts",
"src/__tests__/scheduler-overlap-requeue.test.ts",
"src/__tests__/mission-scheduler.test.ts",
"src/__tests__/self-healing.test.ts",
"src/__tests__/heartbeat-monitor.test.ts",
"src/__tests__/workflow-node-handlers.test.ts",
"src/__tests__/workflow-policy-ownership-map.test.ts",