Skip to content

Commit f60ee34

Browse files
PR Batch Testerclaude
andcommitted
fix(web): removing a project no longer opens a new thread for it
The delete command returns before the shell stream drops the project, so the index route's draft landing could pick the project that was just removed. Each removal path now waits for the project to leave the store before clearing its drafts or leaving for `/`. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
1 parent 250e052 commit f60ee34

4 files changed

Lines changed: 20 additions & 1 deletion

File tree

‎apps/web/src/components/LegacySidebar.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import {
9090
useProjects,
9191
useThreadShells,
9292
useThreadShellsForProjectRefs,
93+
waitForProjectRemoval,
9394
} from "../state/entities";
9495
import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore";
9596
import { useThreadRunningTerminalIds } from "../state/terminalSessions";
@@ -1529,6 +1530,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
15291530
if (result._tag === "Failure") {
15301531
return result;
15311532
}
1533+
await waitForProjectRemoval(memberProjectRef);
15321534
const draftStore = useComposerDraftStore.getState();
15331535
releaseProjectDraftUploads(
15341536
memberProjectRef,

‎apps/web/src/components/settings/ProjectSettingsPanel.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
type SidebarProjectSnapshot,
2222
} from "../../sidebarProjectGrouping";
2323
import { useEnvironments, usePrimaryEnvironmentId } from "../../state/environments";
24-
import { useThreadShells } from "../../state/entities";
24+
import { useThreadShells, waitForProjectRemoval } from "../../state/entities";
2525
import { projectEnvironment } from "../../state/projects";
2626
import { useAtomCommand } from "../../state/use-atom-command";
2727
import { ProjectFavicon } from "../ProjectFavicon";
@@ -358,6 +358,7 @@ function ProjectDetail({
358358
return;
359359
}
360360
const projectRef = scopeProjectRef(member.environmentId, member.id);
361+
await waitForProjectRemoval(projectRef);
361362
releaseProjectDraftUploads(
362363
projectRef,
363364
memberThreads.map((thread) => scopeThreadRef(thread.environmentId, thread.id)),

‎apps/web/src/hooks/useRemoveClonedProject.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useCallback } from "react";
66
import { useComposerDraftStore } from "../composerDraftStore";
77
import { resolveThreadRouteTarget } from "../threadRoutes";
88
import { releaseProjectDraftUploads } from "../lib/composerDraftUploads";
9+
import { waitForProjectRemoval } from "../state/entities";
910
import { projectEnvironment } from "../state/projects";
1011
import { useAtomCommand } from "../state/use-atom-command";
1112
import { stackedThreadToast, toastManager } from "../components/ui/toast";
@@ -41,6 +42,7 @@ export function useRemoveClonedProject() {
4142
);
4243
return false;
4344
}
45+
await waitForProjectRemoval(projectRef);
4446
// Read the route after the await: the user may have moved on while the
4547
// delete was in flight, and only a draft of this project needs to go.
4648
const routeParams = router.state.matches[router.state.matches.length - 1]?.params ?? {};

‎apps/web/src/state/entities.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,20 @@ export function waitForProject(
181181
});
182182
}
183183

184+
/**
185+
* Resolves once a deleted project leaves the live client store. The delete
186+
* command returns before the shell stream does, so anything that picks a
187+
* project in between (the index route's draft landing) still sees it.
188+
*/
189+
export function waitForProjectRemoval(ref: ScopedProjectRef, timeoutMs = 5_000): Promise<boolean> {
190+
return waitForAtomValue({
191+
registry: appAtomRegistry,
192+
atom: environmentProjects.projectAtom(ref),
193+
predicate: (project) => project === null,
194+
timeoutMs,
195+
});
196+
}
197+
184198
export function readThreadShell(ref: ScopedThreadRef): EnvironmentThreadShell | null {
185199
return appAtomRegistry.get(environmentThreadShells.threadShellAtom(ref));
186200
}

0 commit comments

Comments
 (0)