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:
Timothy Laurent
2026-05-04 21:32:41 -07:00
parent 54f2832691
commit b061e2bfe6
3 changed files with 54 additions and 7 deletions

View 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.

View File

@@ -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]);

View File

@@ -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");
});
});