feat(FN-3975): keep interview-stage missions visible in mission list

Made interview-stage missions visible in the mission list (MissionManager) and documented the behavior in the missions reference. Tests added for the new visibility logic.

Fusion-Task-Id: FN-3975
This commit is contained in:
Fusion
2026-05-10 21:44:29 -07:00
committed by gsxdsm
parent 6ce7f9f0df
commit fb7b62a35a
3 changed files with 73 additions and 6 deletions

View File

@@ -49,7 +49,8 @@ The dashboard supports mission planning workflows where you can:
- Break work into milestones/slices/features - Break work into milestones/slices/features
- Associate features to executable tasks - Associate features to executable tasks
- Track progress at each layer - Track progress at each layer
- Resume in-progress mission interview sessions directly from the main missions list (generating, awaiting input, or error) before a mission record is created - Persisted missions with `interviewState: "in_progress"` remain visible as interview-styled mission cards in the main mission list so planning work does not disappear after reloads
- Resume in-progress mission interview sessions directly from separate transient session rows in the main missions list (`mission_interview` sessions in `generating`, `awaiting_input`, or `error`) before a mission record is created
### Auto-Generated Assertions ### Auto-Generated Assertions

View File

@@ -3546,7 +3546,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
); );
}); });
const renderMissionListItems = () => missions.map((mission) => { const renderMissionListItems = (missionList: MissionWithSummary[], options?: { interviewStyle?: boolean }) => missionList.map((mission) => {
const m = mission; const m = mission;
const isSelected = selectedMission?.id === m.id; const isSelected = selectedMission?.id === m.id;
const statusColors = missionStatusColors[m.status as MissionStatus] || { bg: "", text: "" }; const statusColors = missionStatusColors[m.status as MissionStatus] || { bg: "", text: "" };
@@ -3559,11 +3559,12 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
const tasksFailed = health?.tasksFailed ?? 0; const tasksFailed = health?.tasksFailed ?? 0;
const progressPercent = health?.estimatedCompletionPercent ?? summary?.progressPercent ?? 0; const progressPercent = health?.estimatedCompletionPercent ?? summary?.progressPercent ?? 0;
const showSummaryBlock = hasContent || totalTasks > 0 || tasksFailed > 0 || Boolean(health?.lastActivityAt); const showSummaryBlock = hasContent || totalTasks > 0 || tasksFailed > 0 || Boolean(health?.lastActivityAt);
const isInterviewStyle = options?.interviewStyle === true;
return ( return (
<div <div
key={m.id} key={m.id}
className={`mission-list__item ${isSelected ? "mission-list__item--selected" : ""}`} className={`mission-list__item ${isSelected ? "mission-list__item--selected" : ""} ${isInterviewStyle ? "mission-list__item--interview" : ""}`}
onClick={() => handleSelectMission(mission)} onClick={() => handleSelectMission(mission)}
> >
<div className="mission-list__item-content"> <div className="mission-list__item-content">
@@ -3589,10 +3590,17 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
> >
{m.status} {m.status}
</span> </span>
{isInterviewStyle && (
<span className="mission-status-badge mission-status-badge--sm mission-interview-status mission-interview-status--awaiting_input">
Interview in progress
</span>
)}
</div> </div>
{m.description && ( {isInterviewStyle ? (
<p className="mission-list__item-description">Mission interview is still in progress. Open this mission to continue planning.</p>
) : m.description ? (
<p className="mission-list__item-description">{m.description}</p> <p className="mission-list__item-description">{m.description}</p>
)} ) : null}
{showSummaryBlock && ( {showSummaryBlock && (
<div className="mission-list__item-summary"> <div className="mission-list__item-summary">
{hasContent && ( {hasContent && (
@@ -3685,6 +3693,8 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
}); });
const renderMissionListContent = ({ hideBottomButtons = false }: { hideBottomButtons?: boolean } = {}) => { const renderMissionListContent = ({ hideBottomButtons = false }: { hideBottomButtons?: boolean } = {}) => {
const persistedInterviewMissions = missions.filter((mission) => mission.interviewState === "in_progress");
const standardMissions = missions.filter((mission) => mission.interviewState !== "in_progress");
const showMobileTopPlanButton = isMobile && missions.length > 0 && !isCreatingMission; const showMobileTopPlanButton = isMobile && missions.length > 0 && !isCreatingMission;
const showBottomPlanButton = !hideBottomButtons && !showMobileTopPlanButton; const showBottomPlanButton = !hideBottomButtons && !showMobileTopPlanButton;
@@ -3732,7 +3742,8 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
)} )}
{/* Mission and interview items */} {/* Mission and interview items */}
{renderMissionListItems()} {renderMissionListItems(persistedInterviewMissions, { interviewStyle: true })}
{renderMissionListItems(standardMissions)}
{renderInterviewSessionItems()} {renderInterviewSessionItems()}
{/* Edit mission form */} {/* Edit mission form */}

View File

@@ -76,6 +76,7 @@ const mockMissions = [
title: "Build Auth System", title: "Build Auth System",
description: "Complete authentication flow", description: "Complete authentication flow",
status: "planning", status: "planning",
interviewState: "not_started",
milestones: [], milestones: [],
createdAt: "2026-01-01T00:00:00.000Z", createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z",
@@ -85,6 +86,7 @@ const mockMissions = [
title: "API Redesign", title: "API Redesign",
description: "Redesign the REST API", description: "Redesign the REST API",
status: "active", status: "active",
interviewState: "not_started",
autopilotEnabled: true, autopilotEnabled: true,
autopilotState: "watching", autopilotState: "watching",
milestones: [], milestones: [],
@@ -1651,6 +1653,59 @@ describe("MissionManager", () => {
}); });
}); });
it("keeps persisted interview-stage missions visible with interview styling and mission selection behavior", async () => {
const missionsWithInterview = [
{
id: "M-INTERVIEW",
title: "Reliability planning draft",
description: "Should remain visible while interview planning is in progress",
status: "planning",
interviewState: "in_progress",
milestones: [],
createdAt: "2026-01-06T00:00:00.000Z",
updatedAt: "2026-01-06T00:00:00.000Z",
},
...mockMissions,
];
mockFetchAiSessions.mockResolvedValueOnce([]);
globalThis.fetch = createFetchMockWithHealth(missionsWithInterview as Array<Record<string, unknown>>, {
...mockMissionHealthById,
"M-INTERVIEW": {
missionId: "M-INTERVIEW",
status: "planning",
tasksCompleted: 0,
tasksFailed: 0,
tasksInFlight: 0,
totalTasks: 0,
estimatedCompletionPercent: 0,
autopilotState: "inactive",
autopilotEnabled: false,
},
});
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
const interviewMissionTitle = await screen.findByText("Reliability planning draft");
const interviewMissionRow = interviewMissionTitle.closest(".mission-list__item");
expect(interviewMissionRow).toBeTruthy();
expect(interviewMissionRow).toHaveClass("mission-list__item--interview");
expect(within(interviewMissionRow as HTMLElement).getByText("Interview in progress")).toBeInTheDocument();
expect(
within(interviewMissionRow as HTMLElement).getByText(
"Mission interview is still in progress. Open this mission to continue planning.",
),
).toBeInTheDocument();
fireEvent.click(interviewMissionTitle);
await waitFor(() => {
expect(screen.getByText("Database Schema")).toBeInTheDocument();
});
});
it("shows in-progress interview sessions in the main mission list without footer resume duplication", async () => { it("shows in-progress interview sessions in the main mission list without footer resume duplication", async () => {
mockFetchAiSessions.mockResolvedValueOnce([ mockFetchAiSessions.mockResolvedValueOnce([
{ {