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) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/FN-8026-interview-reconnecting-hint.md
Normal file
7
.changeset/FN-8026-interview-reconnecting-hint.md
Normal file
@@ -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.
|
||||
@@ -439,7 +439,11 @@ export function MilestoneSliceInterviewModal({
|
||||
|
||||
<div className="planning-modal-body">
|
||||
{error && <div className="form-error planning-error">{error}</div>}
|
||||
{isReconnecting && <div className="form-hint text-muted">{t("interview.reconnecting", "Reconnecting…")}</div>}
|
||||
{/*
|
||||
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" && <div className="form-hint text-muted">{t("interview.reconnecting", "Reconnecting…")}</div>}
|
||||
|
||||
{view.type === "initial" && (
|
||||
<div className="planning-initial">
|
||||
|
||||
@@ -811,7 +811,11 @@ export function MissionInterviewModal({
|
||||
|
||||
<div className="planning-modal-body">
|
||||
{error && <div className="form-error planning-error">{error}</div>}
|
||||
{isReconnecting && <div className="form-hint text-muted">{t("missions.reconnecting", "Reconnecting…")}</div>}
|
||||
{/*
|
||||
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" && <div className="form-hint text-muted">{t("missions.reconnecting", "Reconnecting…")}</div>}
|
||||
|
||||
{view.type === "initial" && (
|
||||
<div className="planning-initial">
|
||||
|
||||
@@ -613,7 +613,11 @@ export function SubtaskBreakdownModal({ isOpen, onClose, initialDescription, onT
|
||||
|
||||
<div className="planning-modal-body">
|
||||
{error && <div className="form-error planning-error">{error}</div>}
|
||||
{isReconnecting && <div className="form-hint text-muted">{t("subtasks.reconnecting", "Reconnecting…")}</div>}
|
||||
{/*
|
||||
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" && <div className="form-hint text-muted">{t("subtasks.reconnecting", "Reconnecting…")}</div>}
|
||||
|
||||
{view.type === "initial" && (
|
||||
<div className="planning-loading" data-testid="subtask-progress-state">
|
||||
|
||||
@@ -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(
|
||||
<MilestoneSliceInterviewModal
|
||||
isOpen={true}
|
||||
onClose={vi.fn()}
|
||||
onApplied={vi.fn()}
|
||||
targetType="milestone"
|
||||
targetId="MS-001"
|
||||
targetTitle="Test Milestone"
|
||||
projectId="test-project"
|
||||
/>,
|
||||
);
|
||||
|
||||
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", () => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user