fix(FN-001): fix stale goal and unable to type in Plan Mission With AI modal (#36)
Split the combined useEffect that handled both auto-start and persisted-goal restoration into two separate effects: - Goal restoration: depends only on [isOpen], runs once on modal open - Auto-start: keeps handleStartInterview dep for initialGoalProp flow Previously the single effect depended on handleStartInterview, which recreates on every missionGoal change. Every keystroke re-triggered the effect, which read the stale persisted value from localStorage and overwrote user input — making the textarea uneditable and showing stale data from the last planned mission. Co-authored-by: AI <ai@runfusion.ai> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
5
.changeset/fix-mission-interview-stale-goal-cant-type.md
Normal file
5
.changeset/fix-mission-interview-stale-goal-cant-type.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix Plan Mission With AI modal: stale goal text and unable to type in textarea. The persisted-goal restoration effect depended on `handleStartInterview`, which recreates on every keystroke via `missionGoal` — causing the effect to re-fire and overwrite user input with stale localStorage data on each character typed.
|
||||||
@@ -345,6 +345,19 @@ export function MissionInterviewModal({
|
|||||||
}
|
}
|
||||||
}, [isOpen, view.type]);
|
}, [isOpen, view.type]);
|
||||||
|
|
||||||
|
// Restore persisted goal from localStorage ONCE when the modal opens.
|
||||||
|
// Must NOT depend on handleStartInterview or missionGoal — otherwise every
|
||||||
|
// keystroke recreates handleStartInterview, re-triggers this effect, and
|
||||||
|
// overwrites what the user just typed (cursor jumps to end, can't edit).
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen && !initialGoalProp && !resumeSessionId && !hasAutoStartedRef.current && view.type === "initial") {
|
||||||
|
const persisted = getMissionGoal(projectId);
|
||||||
|
if (persisted) {
|
||||||
|
setMissionGoal(persisted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isOpen]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
// Auto-start when initialGoal prop is provided
|
// Auto-start when initialGoal prop is provided
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && initialGoalProp && !hasAutoStartedRef.current && view.type === "initial") {
|
if (isOpen && initialGoalProp && !hasAutoStartedRef.current && view.type === "initial") {
|
||||||
@@ -354,12 +367,6 @@ export function MissionInterviewModal({
|
|||||||
handleStartInterview(initialGoalProp);
|
handleStartInterview(initialGoalProp);
|
||||||
}, 0);
|
}, 0);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
} else if (isOpen && !initialGoalProp && !hasAutoStartedRef.current && view.type === "initial") {
|
|
||||||
// Check localStorage for persisted goal when no prop provided
|
|
||||||
const persisted = getMissionGoal(projectId);
|
|
||||||
if (persisted) {
|
|
||||||
setMissionGoal(persisted);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [isOpen, initialGoalProp, view.type, handleStartInterview]);
|
}, [isOpen, initialGoalProp, view.type, handleStartInterview]);
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ vi.mock("../../api", () => ({
|
|||||||
fetchModels: (...args: any[]) => mockFetchModels(...args),
|
fetchModels: (...args: any[]) => mockFetchModels(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const mockGetMissionGoal = vi.fn(() => "");
|
||||||
|
|
||||||
vi.mock("../../hooks/modalPersistence", () => ({
|
vi.mock("../../hooks/modalPersistence", () => ({
|
||||||
saveMissionGoal: vi.fn(),
|
saveMissionGoal: vi.fn(),
|
||||||
getMissionGoal: vi.fn(() => ""),
|
getMissionGoal: (...args: any[]) => mockGetMissionGoal(...args),
|
||||||
clearMissionGoal: vi.fn(),
|
clearMissionGoal: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -324,4 +326,37 @@ describe("MissionInterviewModal", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("restores persisted goal from localStorage on open", () => {
|
||||||
|
mockGetMissionGoal.mockReturnValue("Previous mission goal");
|
||||||
|
|
||||||
|
renderModal();
|
||||||
|
|
||||||
|
const textarea = screen.getByLabelText("What do you want to build?");
|
||||||
|
expect(textarea).toHaveValue("Previous mission goal");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows typing in textarea without resetting to stale persisted goal", async () => {
|
||||||
|
// Simulate a stale persisted goal from a previous session
|
||||||
|
mockGetMissionGoal.mockReturnValue("Old stale goal");
|
||||||
|
|
||||||
|
renderModal();
|
||||||
|
|
||||||
|
const textarea = screen.getByLabelText("What do you want to build?");
|
||||||
|
expect(textarea).toHaveValue("Old stale goal");
|
||||||
|
|
||||||
|
// User starts typing a new goal
|
||||||
|
fireEvent.change(textarea, { target: { value: "New mission" } });
|
||||||
|
expect(textarea).toHaveValue("New mission");
|
||||||
|
|
||||||
|
// Type more characters — the stale value should NOT overwrite
|
||||||
|
fireEvent.change(textarea, { target: { value: "New mission idea" } });
|
||||||
|
expect(textarea).toHaveValue("New mission idea");
|
||||||
|
|
||||||
|
// Even after a re-render cycle, user input should persist
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
});
|
||||||
|
expect(textarea).toHaveValue("New mission idea");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user