Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createFileRoute } from "@tanstack/react-router";
import { NewWorkspaceScreen } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/NewWorkspaceScreen";
import { DashboardNewWorkspaceDraftProvider } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceDraftContext";
import { newWorkspaceAttachmentsStore } from "renderer/stores/new-workspace-attachments";
import { resetNewWorkspaceDraftOnRouteLeave } from "renderer/stores/new-workspace-draft";

export const Route = createFileRoute(
"/_authenticated/_dashboard/new-workspace/",
Expand All @@ -14,6 +15,9 @@ export const Route = createFileRoute(
typeof search.projectId === "string" ? search.projectId : undefined,
session: search.session === true ? true : undefined,
}),
// The handoff draft stays available for this route's entire lifetime and is
// cleared only once navigation actually abandons the create flow.
onLeave: resetNewWorkspaceDraftOnRouteLeave,
component: NewWorkspacePage,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ export function DashboardNewWorkspaceModal() {
// "+" button, hotkey, onboarding hand-off) redirect to the route instead.
useEffect(() => {
if (!isScreen || !isOpen) return;
closeModal();
// Keep the seeded draft (setup-script prompt, linked issue/PR): the
// /new-workspace page consumes it after the handoff, so this close
// must not run the #5372 dismissal cleanup.
closeModal({ resetDraft: false });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Abandoning the full-page create flow can still prefill the next New Workspace: this handoff opts out of cleanup, but the destination's close handler is a no-op and leaving it does not reset the global draft. Preserve the draft only for the active handoff, then clear it when the page is abandoned or scope the handoff draft to that route.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceModal.tsx, line 58:

<comment>Abandoning the full-page create flow can still prefill the next New Workspace: this handoff opts out of cleanup, but the destination's close handler is a no-op and leaving it does not reset the global draft. Preserve the draft only for the active handoff, then clear it when the page is abandoned or scope the handoff draft to that route.</comment>

<file context>
@@ -52,7 +52,10 @@ export function DashboardNewWorkspaceModal() {
+		// Keep the seeded draft (setup-script prompt, linked issue/PR): the
+		// /new-workspace page consumes it after the handoff, so this close
+		// must not run the #5372 dismissal cleanup.
+		closeModal({ resetDraft: false });
 		void navigate({
 			to: "/new-workspace",
</file context>

void navigate({
to: "/new-workspace",
search: preSelectedSession
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/renderer/stores/new-workspace-draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,8 @@ export const useNewWorkspaceDraftStore = create<NewWorkspaceDraftState>(
})),
}),
);

/** Clears any draft preserved for the full-page handoff once that route is left. */
export function resetNewWorkspaceDraftOnRouteLeave() {
useNewWorkspaceDraftStore.getState().resetDraft();
}
76 changes: 76 additions & 0 deletions apps/desktop/src/renderer/stores/new-workspace-modal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { afterEach, describe, expect, test } from "bun:test";
import {
resetNewWorkspaceDraftOnRouteLeave,
useNewWorkspaceDraftStore,
} from "./new-workspace-draft";
import { useNewWorkspaceModalStore } from "./new-workspace-modal";

const SETUP_SCRIPT_PROMPT = "Write a setup script for this project";

describe("new workspace modal store (#5372)", () => {
afterEach(() => {
// Leave both stores in a clean state for the next test.
useNewWorkspaceModalStore.getState().closeModal();
useNewWorkspaceDraftStore.getState().resetDraft();
});

test("dismissing the modal clears a seeded draft prompt", () => {
// Mirrors V2SetupScriptCard.handleConfigure: seed the draft with the
// setup-script prompt, then open the modal.
const draftStore = useNewWorkspaceDraftStore.getState();
draftStore.resetDraft();
draftStore.updateDraft({ prompt: SETUP_SCRIPT_PROMPT });
useNewWorkspaceModalStore.getState().openModal("project-1");

expect(useNewWorkspaceDraftStore.getState().prompt).toBe(
SETUP_SCRIPT_PROMPT,
);

// Esc / outside click → closeModal (the dismiss path).
useNewWorkspaceModalStore.getState().closeModal();

expect(useNewWorkspaceDraftStore.getState().prompt).toBe("");
expect(useNewWorkspaceModalStore.getState().isOpen).toBeFalse();
});

test("a fresh open after dismiss shows no leftover prompt", () => {
const draftStore = useNewWorkspaceDraftStore.getState();
draftStore.updateDraft({ prompt: SETUP_SCRIPT_PROMPT });
useNewWorkspaceModalStore.getState().openModal("project-1");
useNewWorkspaceModalStore.getState().closeModal();

// The next "New Workspace" click (no seed) must start empty.
useNewWorkspaceModalStore.getState().openModal("project-2");
expect(useNewWorkspaceDraftStore.getState().prompt).toBe("");
});

test("full-page handoff preserves the seeded draft", () => {
// DashboardNewWorkspaceModal test arm: closes the store modal with
// { resetDraft: false } before navigating to /new-workspace, where
// the destination screen consumes the seeded prompt.
const draftStore = useNewWorkspaceDraftStore.getState();
draftStore.resetDraft();
draftStore.updateDraft({ prompt: SETUP_SCRIPT_PROMPT });
useNewWorkspaceModalStore.getState().openModal("project-1");

useNewWorkspaceModalStore.getState().closeModal({ resetDraft: false });

expect(useNewWorkspaceDraftStore.getState().prompt).toBe(
SETUP_SCRIPT_PROMPT,
);
expect(useNewWorkspaceModalStore.getState().isOpen).toBeFalse();
});

test("leaving the full-page flow clears the preserved handoff draft", () => {
const draftStore = useNewWorkspaceDraftStore.getState();
draftStore.updateDraft({ prompt: SETUP_SCRIPT_PROMPT });
useNewWorkspaceModalStore.getState().openModal("project-1");
useNewWorkspaceModalStore.getState().closeModal({ resetDraft: false });

resetNewWorkspaceDraftOnRouteLeave();

expect(useNewWorkspaceDraftStore.getState().prompt).toBe("");
useNewWorkspaceModalStore.getState().openModal("project-2");
expect(useNewWorkspaceDraftStore.getState().prompt).toBe("");
});
});
19 changes: 17 additions & 2 deletions apps/desktop/src/renderer/stores/new-workspace-modal.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import { useNewWorkspaceDraftStore } from "./new-workspace-draft";

interface PendingWorkspace {
id: string;
Expand Down Expand Up @@ -33,7 +34,7 @@ interface NewWorkspaceModalState {
stashedDraft: StashedDraft | null;
openModal: (projectId?: string) => void;
openSessionModal: () => void;
closeModal: () => void;
closeModal: (options?: { resetDraft?: boolean }) => void;
setPendingWorkspace: (workspace: PendingWorkspace | null) => void;
clearPendingWorkspace: (id: string) => void;
setPendingWorkspaceStatus: (
Expand Down Expand Up @@ -70,12 +71,26 @@ export const useNewWorkspaceModalStore = create<NewWorkspaceModalState>()(
});
},

closeModal: () => {
closeModal: (options?: { resetDraft?: boolean }) => {
set({
isOpen: false,
preSelectedProjectId: null,
preSelectedSession: false,
});
// #5372: a seeded draft (e.g. the Setup-scripts prompt written
// by V2SetupScriptCard) must not survive dismissing the modal —
// otherwise the next "New Workspace" opens pre-filled with a
// prompt the user never asked for. Every seed path calls
// resetDraft() before updateDraft(), so resetting here is safe
// and makes the dismiss the single cleanup point.
//
// The full-page handoff (DashboardNewWorkspaceModal test arm)
// closes the store modal BEFORE navigating to /new-workspace,
// where the destination consumes the seeded draft — so it opts
// out with { resetDraft: false } (greptile/cubic P1).
if (options?.resetDraft !== false) {
useNewWorkspaceDraftStore.getState().resetDraft();
}
},

setPendingWorkspace: (workspace: PendingWorkspace | null) => {
Expand Down