diff --git a/.changeset/fn-8275-mission-merge-summary.md b/.changeset/fn-8275-mission-merge-summary.md new file mode 100644 index 0000000000..a6fa13bf4e --- /dev/null +++ b/.changeset/fn-8275-mission-merge-summary.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Mission auto-merge controls now explain merge behavior and show shared branch PR status. +category: feature +dev: Mission detail resolves its group from linked task branchContext data and adds engine cascade coverage. diff --git a/docs/missions.md b/docs/missions.md index 992e7db329..b64e329d35 100644 --- a/docs/missions.md +++ b/docs/missions.md @@ -99,7 +99,7 @@ The Mission Manager create/edit form exposes this as **Branch strategy** plus a ### Mission auto-merge override -The **Merge behavior** control can inherit the project default, explicitly enable auto-merge, or select **Single pull request**. The latter persists `autoMerge: false` on the mission and stamps newly triaged feature tasks with the same false override, while preserving the mission's shared branch group. Returning the control to inherited clears the mission override. +The **Merge behavior** control can inherit the project default, explicitly enable auto-merge, or select **Single pull request**. In-context help explains that auto-merge lands each feature individually while a single pull request retains all features on a shared branch for joint review. The latter persists `autoMerge: false` on the mission and stamps newly triaged feature tasks with the same false override, while preserving the mission's shared branch group. Returning the control to inherited clears the mission override. Once a shared branch has members, Mission detail displays its branch name, member count, and PR state. ### Shared branch-group invariant across entry points diff --git a/packages/dashboard/app/components/MissionManager.tsx b/packages/dashboard/app/components/MissionManager.tsx index 512da211e3..9847eab6c4 100644 --- a/packages/dashboard/app/components/MissionManager.tsx +++ b/packages/dashboard/app/components/MissionManager.tsx @@ -100,8 +100,11 @@ import { fetchAiSession, fetchMissionInterviewDrafts, discardMissionInterviewDraft, + fetchTaskDetail, + apiGetBranchGroup, api, type AiSessionSummary, + type BranchGroupSummary, } from "../api"; import type { AutopilotState, MissionInterviewDraftSummary } from "./mission-types"; import { readCache, SWR_CACHE_KEYS, writeCache } from "../utils/swrCache"; @@ -640,6 +643,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr const initialMissions = readCache(missionsCacheKey); const [missions, setMissions] = useState(() => (Array.isArray(initialMissions) ? initialMissions : [])); const [selectedMission, setSelectedMission] = useState(null); + const [selectedMissionBranchGroup, setSelectedMissionBranchGroup] = useState(null); const [loading, setLoading] = useState(!(Array.isArray(initialMissions) && initialMissions.length > 0)); const hasHydratedRef = useRef(Array.isArray(initialMissions) && initialMissions.length > 0); const [detailLoading, setDetailLoading] = useState(false); @@ -1121,6 +1125,41 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr } }, [addToast, loadAssertionsForMilestone, projectId]); + useEffect(() => { + let cancelled = false; + setSelectedMissionBranchGroup(null); + + const selectedMissionId = selectedMission?.id; + const taskIds = selectedMission?.milestones.flatMap((milestone) => + milestone.slices.flatMap((slice) => slice.features.flatMap((feature) => feature.taskId ? [feature.taskId] : [])), + ) ?? []; + if (!selectedMissionId || taskIds.length === 0) return () => { cancelled = true; }; + + void (async () => { + for (const taskId of taskIds) { + try { + const task = await fetchTaskDetail(taskId, projectId); + const groupId = task.branchContext?.source === "mission" ? task.branchContext.groupId : undefined; + if (!groupId) continue; + const { group } = await apiGetBranchGroup(groupId, projectId); + /* + FNXC:MissionAutoMerge 2026-07-19-00:00: + A task can retain a stale or colliding branch-group id. Only show a group that + is owned by the selected mission so its detail pane never reports another + mission's branch or PR state. + */ + if (group?.sourceType !== "mission" || group.sourceId !== selectedMissionId) continue; + if (!cancelled) setSelectedMissionBranchGroup(group); + return; + } catch { + // A stale/deleted linked task or unavailable group must not break mission detail rendering. + } + } + })(); + + return () => { cancelled = true; }; + }, [projectId, selectedMission]); + useEffect(() => { if (!isActive || !selectedMilestoneId) { setValidationTelemetry(null); @@ -2739,6 +2778,29 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr )} + {/* + FNXC:MissionAutoMerge 2026-07-19-00:00: + A mission detail must show its actual shared branch, member count, and PR state. Resolve the group through linked member tasks' branchContext.groupId rather than source ownership because branch-name collisions can reuse another mission's group. + */} + {selectedMissionBranchGroup && ( +
+
+ {t("missions.sharedBranch", "Shared branch")}: {selectedMissionBranchGroup.branchName} + + {selectedMissionBranchGroup.prState === "none" + ? t("missions.noPullRequest", "No PR") + : t(`missions.prState.${selectedMissionBranchGroup.prState}`, selectedMissionBranchGroup.prState)} + +
+ + {t("missions.sharedBranchMembers", "{{count}} member", { count: selectedMissionBranchGroup.completion.total })} + +
+ )} +
{selectedMission.status === "active" && ( @@ -2859,6 +2921,13 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr + {/* + FNXC:MissionAutoMerge 2026-07-19-00:00: + Operators need in-context guidance for the per-mission merge choice: auto-merge lands each feature independently, while a single pull request keeps all features on one shared branch for joint review. + */} + + {t("missions.autoMergeOverrideDescription", "Auto-merge merges each feature as it passes. Single pull request keeps every feature on one shared branch to review and merge together.")} + {(missionForm.branchStrategy.mode === "existing" || missionForm.branchStrategy.mode === "custom-new") && ( {(missionForm.branchStrategy.mode === "existing" || missionForm.branchStrategy.mode === "custom-new") && ( {(missionForm.branchStrategy.mode === "existing" || missionForm.branchStrategy.mode === "custom-new") && (