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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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([
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user