From db05a77fea445d3e5b4cd63850a6ce2f978b8911 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 15 Jul 2026 23:39:12 -0700 Subject: [PATCH] FN-8026: fix idle reconnecting hints in interview modals Keep reconnecting indicators scoped to active interview generation states. - Hide transient reconnecting hints on persisted mission and milestone questions - Limit subtask reconnecting hints to the generating state - Cover active and persisted reconnecting behavior with modal tests - Add a patch changeset for the dashboard fix Files changed: .changeset/FN-8026-interview-reconnecting-hint.md | 7 ++++ .../components/MilestoneSliceInterviewModal.tsx | 6 +++- .../app/components/MissionInterviewModal.tsx | 6 +++- .../app/components/SubtaskBreakdownModal.tsx | 6 +++- .../MilestoneSliceInterviewModal.test.tsx | 37 ++++++++++++++++++++++ .../__tests__/MissionInterviewModal.test.tsx | 20 +++++------- .../__tests__/SubtaskBreakdownModal.test.tsx | 19 ++++++----- 7 files changed, 76 insertions(+), 25 deletions(-) Fusion-Task-Id: FN-8026 Fusion-Task-Lineage: 60b2f741-6761-4fc0-bf50-cbddce5f5b6f Co-authored-by: Fusion (runfusion.ai) --- .../FN-8026-interview-reconnecting-hint.md | 7 ++++ .../MilestoneSliceInterviewModal.tsx | 6 ++- .../app/components/MissionInterviewModal.tsx | 6 ++- .../app/components/SubtaskBreakdownModal.tsx | 6 ++- .../MilestoneSliceInterviewModal.test.tsx | 37 +++++++++++++++++++ .../__tests__/MissionInterviewModal.test.tsx | 22 +++++------ .../__tests__/SubtaskBreakdownModal.test.tsx | 23 ++++++------ 7 files changed, 79 insertions(+), 28 deletions(-) create mode 100644 .changeset/FN-8026-interview-reconnecting-hint.md 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 () => {