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]);
|
||||
|
||||
// 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
|
||||
useEffect(() => {
|
||||
if (isOpen && initialGoalProp && !hasAutoStartedRef.current && view.type === "initial") {
|
||||
@@ -354,12 +367,6 @@ export function MissionInterviewModal({
|
||||
handleStartInterview(initialGoalProp);
|
||||
}, 0);
|
||||
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]);
|
||||
|
||||
|
||||
@@ -30,9 +30,11 @@ vi.mock("../../api", () => ({
|
||||
fetchModels: (...args: any[]) => mockFetchModels(...args),
|
||||
}));
|
||||
|
||||
const mockGetMissionGoal = vi.fn(() => "");
|
||||
|
||||
vi.mock("../../hooks/modalPersistence", () => ({
|
||||
saveMissionGoal: vi.fn(),
|
||||
getMissionGoal: vi.fn(() => ""),
|
||||
getMissionGoal: (...args: any[]) => mockGetMissionGoal(...args),
|
||||
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