diff --git a/.changeset/FN-8026-interview-reconnecting-hint.md b/.changeset/FN-8026-interview-reconnecting-hint.md new file mode 100644 index 0000000000..aec2ae746b --- /dev/null +++ b/.changeset/FN-8026-interview-reconnecting-hint.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Hide the interview reconnecting hint on persisted question and review screens. +category: fix +dev: Gate the shared reconnecting indicator in MissionInterviewModal/MilestoneSliceInterviewModal to view.type === "loading" and in SubtaskBreakdownModal to view.type === "generating", mirroring FN-8002 so idle awaiting-input screens render purely from persisted state. diff --git a/packages/dashboard/app/components/MilestoneSliceInterviewModal.tsx b/packages/dashboard/app/components/MilestoneSliceInterviewModal.tsx index 4bff31307b..3066db0ada 100644 --- a/packages/dashboard/app/components/MilestoneSliceInterviewModal.tsx +++ b/packages/dashboard/app/components/MilestoneSliceInterviewModal.tsx @@ -439,7 +439,11 @@ export function MilestoneSliceInterviewModal({
{error &&
{error}
} - {isReconnecting &&
{t("interview.reconnecting", "Reconnecting…")}
} + {/* + FNXC:MilestoneSliceInterview 2026-07-15-00:00: + Awaiting-input questions render persisted database state, so transient idle SSE reconnects must not imply that the question is being regenerated. Reserve this hint for the active loading view, mirroring the FN-8002 Planning Mode invariant. + */} + {isReconnecting && view.type === "loading" &&
{t("interview.reconnecting", "Reconnecting…")}
} {view.type === "initial" && (
diff --git a/packages/dashboard/app/components/MissionInterviewModal.tsx b/packages/dashboard/app/components/MissionInterviewModal.tsx index 2f5359c0b2..c1f686c941 100644 --- a/packages/dashboard/app/components/MissionInterviewModal.tsx +++ b/packages/dashboard/app/components/MissionInterviewModal.tsx @@ -811,7 +811,11 @@ export function MissionInterviewModal({
{error &&
{error}
} - {isReconnecting &&
{t("missions.reconnecting", "Reconnecting…")}
} + {/* + FNXC:MissionInterviewModal 2026-07-15-00:00: + Awaiting-input questions render persisted database state, so transient idle SSE reconnects must not imply that the question is being regenerated. Reserve this hint for the active loading view, mirroring the FN-8002 Planning Mode invariant. + */} + {isReconnecting && view.type === "loading" &&
{t("missions.reconnecting", "Reconnecting…")}
} {view.type === "initial" && (
diff --git a/packages/dashboard/app/components/SubtaskBreakdownModal.tsx b/packages/dashboard/app/components/SubtaskBreakdownModal.tsx index 24c6a0b016..b1148d6138 100644 --- a/packages/dashboard/app/components/SubtaskBreakdownModal.tsx +++ b/packages/dashboard/app/components/SubtaskBreakdownModal.tsx @@ -613,7 +613,11 @@ export function SubtaskBreakdownModal({ isOpen, onClose, initialDescription, onT
{error &&
{error}
} - {isReconnecting &&
{t("subtasks.reconnecting", "Reconnecting…")}
} + {/* + FNXC:SubtaskBreakdown 2026-07-15-00:00: + The persisted editing stored-review screen and in-flight creating state render from state, so transient SSE reconnects must not imply regeneration. Reserve the shared hint for active generating, which retains its intentional inline indicator. + */} + {isReconnecting && view.type === "generating" &&
{t("subtasks.reconnecting", "Reconnecting…")}
} {view.type === "initial" && (
diff --git a/packages/dashboard/app/components/__tests__/MilestoneSliceInterviewModal.test.tsx b/packages/dashboard/app/components/__tests__/MilestoneSliceInterviewModal.test.tsx index 17bfc74477..c9e82acdc9 100644 --- a/packages/dashboard/app/components/__tests__/MilestoneSliceInterviewModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/MilestoneSliceInterviewModal.test.tsx @@ -393,6 +393,43 @@ describe("MilestoneSliceInterviewModal", () => { expect(screen.getByText("Pick the size for this feature.")).toBeDefined(); }); }); + + it("shows reconnecting only during active generation, not on persisted questions", async () => { + mockStartMilestoneInterview.mockResolvedValue({ sessionId: "session-123" }); + + render( + , + ); + + fireEvent.click(screen.getByText("Start Interview")); + await waitFor(() => expect(streamHandlers).toBeDefined()); + + act(() => { + streamHandlers.onConnectionStateChange?.("reconnecting"); + }); + expect(screen.getByText("Reconnecting…")).toBeInTheDocument(); + + act(() => { + streamHandlers.onConnectionStateChange?.("connected"); + streamHandlers.onQuestion?.(SAMPLE_QUESTION); + }); + expect(await screen.findByText("What is the target scope?")).toBeInTheDocument(); + + act(() => { + streamHandlers.onConnectionStateChange?.("reconnecting"); + }); + + expect(screen.getByText("What is the target scope?")).toBeInTheDocument(); + expect(screen.queryByText("Reconnecting…")).not.toBeInTheDocument(); + }); }); describe("summary and apply", () => { diff --git a/packages/dashboard/app/components/__tests__/MissionInterviewModal.test.tsx b/packages/dashboard/app/components/__tests__/MissionInterviewModal.test.tsx index 5c802e53de..adcf8c2a35 100644 --- a/packages/dashboard/app/components/__tests__/MissionInterviewModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/MissionInterviewModal.test.tsx @@ -259,7 +259,7 @@ describe("MissionInterviewModal", () => { expect(mockForceAcquireSessionLock).not.toHaveBeenCalled(); }); - it("shows reconnecting indicator without clearing current question", async () => { + it("shows reconnecting only during active generation, not on persisted questions", async () => { renderModal(); fireEvent.change(screen.getByLabelText("What do you want to build?"), { @@ -273,26 +273,22 @@ describe("MissionInterviewModal", () => { }); act(() => { + streamHandlers.onConnectionStateChange?.("reconnecting"); + }); + expect(screen.getByText("Reconnecting…")).toBeInTheDocument(); + + act(() => { + streamHandlers.onConnectionStateChange?.("connected"); streamHandlers.onQuestion?.(SAMPLE_QUESTION); }); - expect(await screen.findByText("What is the target scope?")).toBeInTheDocument(); act(() => { streamHandlers.onConnectionStateChange?.("reconnecting"); }); - expect(screen.getByText("Reconnecting…")).toBeInTheDocument(); - expect(screen.getByText("What is the target scope?")).toBeInTheDocument(); - - act(() => { - streamHandlers.onConnectionStateChange?.("connected"); - }); - - await waitFor(() => { - expect(screen.queryByText("Reconnecting…")).not.toBeInTheDocument(); - }); expect(screen.getByText("What is the target scope?")).toBeInTheDocument(); + expect(screen.queryByText("Reconnecting…")).not.toBeInTheDocument(); }); it("preserves streaming thinking output while reconnecting", async () => { @@ -390,7 +386,7 @@ describe("MissionInterviewModal", () => { streamHandlers.onError?.("Stream error"); }); - expect(await screen.findByText("Reconnecting…")).toBeInTheDocument(); + expect(screen.queryByText("Reconnecting…")).not.toBeInTheDocument(); expect(screen.getByText("What is the target scope?")).toBeInTheDocument(); expect(screen.queryByText("Stream error")).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: "Retry" })).not.toBeInTheDocument(); diff --git a/packages/dashboard/app/components/__tests__/SubtaskBreakdownModal.test.tsx b/packages/dashboard/app/components/__tests__/SubtaskBreakdownModal.test.tsx index d4cfc574e9..18d8bb6897 100644 --- a/packages/dashboard/app/components/__tests__/SubtaskBreakdownModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/SubtaskBreakdownModal.test.tsx @@ -302,28 +302,27 @@ describe("SubtaskBreakdownModal", () => { expect(screen.getByDisplayValue("Do second")).toBeInTheDocument(); }); - it("shows reconnecting indicator without clearing subtask state", async () => { + it("shows reconnecting only during generation, not on persisted subtask review", async () => { renderModal(); await waitFor(() => expect(streamHandlers).toBeDefined()); - streamHandlers.onSubtasks(SAMPLE_SUBTASKS); + act(() => { + streamHandlers.onConnectionStateChange?.("reconnecting"); + }); + expect(screen.getAllByText("Reconnecting…")).toHaveLength(2); + + act(() => { + streamHandlers.onConnectionStateChange?.("connected"); + streamHandlers.onSubtasks(SAMPLE_SUBTASKS); + }); expect(await screen.findByDisplayValue("First")).toBeInTheDocument(); act(() => { streamHandlers.onConnectionStateChange?.("reconnecting"); }); - await waitFor(() => { - expect(screen.getByText("Reconnecting…")).toBeInTheDocument(); - }); - expect(screen.getByDisplayValue("First")).toBeInTheDocument(); - act(() => { - streamHandlers.onConnectionStateChange?.("connected"); - }); - await waitFor(() => { - expect(screen.queryByText("Reconnecting…")).not.toBeInTheDocument(); - }); expect(screen.getByDisplayValue("First")).toBeInTheDocument(); + expect(screen.queryByText("Reconnecting…")).not.toBeInTheDocument(); }); it("preserves thinking output while reconnecting in generating state", async () => {