From 6aefdc9e2d2f1ba0969e16c0fa9fc11a29a9f301 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 6 May 2026 09:40:11 -0700 Subject: [PATCH] Revert "feat(desktop): optimistic v2 workspace.create (#4120)" This reverts commit 3b6615d961c0b16a5e6228933040cd9ab6c28ddc. --- .../DashboardSidebarWorkspaceItem.tsx | 4 +- .../useDashboardSidebarData.ts | 118 +++++----- .../_authenticated/_dashboard/layout.tsx | 2 + .../WorkspaceCreateErrorState.tsx | 27 +-- .../_dashboard/v2-workspace/layout.tsx | 44 ++-- .../useDashboardSidebarState.ts | 23 +- .../CollectionsProvider/collections.ts | 92 -------- .../dashboardSidebarLocal/index.ts | 1 - .../dashboardSidebarLocal/tabOrder.ts | 22 -- .../stores/workspace-creates/Manager.tsx | 30 +++ .../stores/workspace-creates/index.ts | 6 +- .../stores/workspace-creates/store.ts | 95 ++++----- .../workspace-creates/useWorkspaceCreates.ts | 201 ++++++++++-------- .../shared/adopt-existing-worktree.ts | 5 - .../src/trpc/router/workspaces/workspaces.ts | 22 +- .../src/router/v2-workspace/v2-workspace.ts | 20 +- 16 files changed, 291 insertions(+), 421 deletions(-) delete mode 100644 apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/tabOrder.ts create mode 100644 apps/desktop/src/renderer/stores/workspace-creates/Manager.tsx diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx index e6afb5f8e8d..748ccaa887f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/DashboardSidebarWorkspaceItem.tsx @@ -4,7 +4,7 @@ import { useOptimisticCollectionActions } from "renderer/routes/_authenticated/h import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider"; import { RenameBranchDialog } from "renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components"; import { useV2WorkspaceNotificationStatus } from "renderer/stores/v2-notifications"; -import { useWorkspaceCreateFailuresStore } from "renderer/stores/workspace-creates"; +import { useWorkspaceCreatesStore } from "renderer/stores/workspace-creates"; import { useDashboardSidebarHover } from "../../providers/DashboardSidebarHoverProvider"; import type { DashboardSidebarWorkspace } from "../../types"; import { DashboardSidebarDeleteDialog } from "../DashboardSidebarDeleteDialog"; @@ -83,7 +83,7 @@ export function DashboardSidebarWorkspaceItem({ const isDeleting = useDeletingWorkspaces().isDeleting(id); const handleDismissInFlight = useCallback(() => { - useWorkspaceCreateFailuresStore.getState().clear(id); + useWorkspaceCreatesStore.getState().remove(id); }, [id]); const { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts index f5b427c5def..7d2189cfedb 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/useDashboardSidebarData.ts @@ -8,7 +8,7 @@ import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/u import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { getVisibleSidebarWorkspaces } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; -import { useWorkspaceCreateFailuresStore } from "renderer/stores/workspace-creates"; +import { useWorkspaceCreatesStore } from "renderer/stores/workspace-creates"; import type { DashboardSidebarProject, DashboardSidebarProjectChild, @@ -132,23 +132,26 @@ export function useDashboardSidebarData() { const { toggleProjectCollapsed } = useDashboardSidebarState(); const queryClient = useQueryClient(); - // Failed workspace.create operations — backing v2_workspaces row was rolled - // back, but we keep the snapshot in renderer memory so the user can retry - // from the detail page or dismiss from the sidebar. - const failuresMap = useWorkspaceCreateFailuresStore( - (store) => store.failures, - ); - const failureSidebarRows = useMemo( + // In-flight workspace.create operations. These don't have a backing DB row + // — they're kept in renderer memory until the real v2Workspaces row arrives + // via Electric sync (or until error/dismiss). + const inFlightEntries = useWorkspaceCreatesStore((store) => store.entries); + const inFlightSidebarRows = useMemo( () => - Object.entries(failuresMap).map(([id, entry]) => ({ - id, - projectId: entry.snapshot.projectId, - hostId: entry.hostId, - name: entry.snapshot.name ?? "New workspace", - branchName: - entry.snapshot.branch ?? entry.snapshot.name ?? "New workspace", - })), - [failuresMap], + inFlightEntries + .filter((entry) => entry.snapshot.id !== undefined) + .map((entry) => ({ + id: entry.snapshot.id as string, + projectId: entry.snapshot.projectId, + name: entry.snapshot.name ?? "New workspace", + branchName: + entry.snapshot.branch ?? entry.snapshot.name ?? "New workspace", + status: + entry.state === "creating" + ? ("creating" as const) + : ("failed" as const), + })), + [inFlightEntries], ); const { data: hosts = [] } = useLiveQuery( @@ -243,7 +246,6 @@ export function useDashboardSidebarData() { branch: workspaces.branch, createdAt: workspaces.createdAt, updatedAt: workspaces.updatedAt, - synced: workspaces.$synced, tabOrder: sidebarWorkspaces.sidebarState.tabOrder, sectionId: sidebarWorkspaces.sidebarState.sectionId, isHidden: sidebarWorkspaces.sidebarState.isHidden, @@ -261,13 +263,14 @@ export function useDashboardSidebarData() { [rawSidebarWorkspaces], ); - const { data: localWorkspaceCandidates = [] } = useLiveQuery( + const { data: localMainWorkspaces = [] } = useLiveQuery( (q) => q .from({ workspaces: collections.v2Workspaces }) .innerJoin({ hosts: collections.v2Hosts }, ({ workspaces, hosts }) => eq(workspaces.hostId, hosts.machineId), ) + .where(({ workspaces }) => eq(workspaces.type, "main")) .select(({ workspaces, hosts }) => ({ id: workspaces.id, projectId: workspaces.projectId, @@ -278,7 +281,8 @@ export function useDashboardSidebarData() { branch: workspaces.branch, createdAt: workspaces.createdAt, updatedAt: workspaces.updatedAt, - synced: workspaces.$synced, + tabOrder: MAIN_WORKSPACE_TAB_ORDER, + sectionId: null as string | null, })), [collections], ); @@ -287,40 +291,17 @@ export function useDashboardSidebarData() { const sidebarProjectIds = new Set( sidebarProjects.map((project) => project.id), ); - const autoIncluded = localWorkspaceCandidates - .filter((workspace) => { - if (localStateWorkspaceIds.has(workspace.id)) return false; - if (workspace.hostId !== machineId) return false; - if (!sidebarProjectIds.has(workspace.projectId)) return false; - if (workspace.type === "main") return true; - return workspace.type === "worktree" && workspace.synced === false; - }) - .map((workspace) => ({ - ...workspace, - tabOrder: - workspace.type === "main" - ? MAIN_WORKSPACE_TAB_ORDER - : PENDING_WORKSPACE_TAB_ORDER, - sectionId: null as string | null, - creationStatus: - workspace.synced === false ? ("creating" as const) : undefined, - })); - // Pinned rows (those with v2WorkspaceLocalState) keep showing the - // creating spinner until Electric confirms `$synced`. The detail page - // reads `$synced` directly off the row, so without this the sidebar - // would clear its spinner the moment local state was inserted in - // `onInsert`, while the detail page would still show - // `WorkspaceCreatingState` until the shape stream caught up. - const sidebarWithSyncMeta = sidebarWorkspaces.map((workspace) => ({ - ...workspace, - creationStatus: - workspace.synced === false ? ("creating" as const) : undefined, - })); - - return [...autoIncluded, ...sidebarWithSyncMeta]; + const autoLocalMainWorkspaces = localMainWorkspaces.filter( + (workspace) => + !localStateWorkspaceIds.has(workspace.id) && + workspace.hostId === machineId && + sidebarProjectIds.has(workspace.projectId), + ); + + return [...autoLocalMainWorkspaces, ...sidebarWorkspaces]; }, [ + localMainWorkspaces, localStateWorkspaceIds, - localWorkspaceCandidates, machineId, sidebarProjects, sidebarWorkspaces, @@ -461,7 +442,6 @@ export function useDashboardSidebarData() { behindCount: null, createdAt: workspace.createdAt, updatedAt: workspace.updatedAt, - creationStatus: workspace.creationStatus, }; if (workspace.sectionId) { @@ -484,25 +464,23 @@ export function useDashboardSidebarData() { }); } - // Inject failed workspace.create rows from the renderer failure store. - // The optimistic v2_workspaces row was rolled back, so the only signal - // left is the snapshot we kept in memory for retry/dismiss. - for (const failure of failureSidebarRows) { - if (localStateWorkspaceIds.has(failure.id)) continue; - const project = projectsById.get(failure.projectId); + // Inject in-flight workspaces (creating / failed) from the renderer-side + // in-flight store. + for (const pw of inFlightSidebarRows) { + if (localStateWorkspaceIds.has(pw.id)) continue; + const project = projectsById.get(pw.projectId); if (!project) continue; - const failedItem: DashboardSidebarWorkspace = { - id: failure.id, - projectId: failure.projectId, - hostId: failure.hostId, - hostType: - failure.hostId === machineId ? "local-device" : "remote-device", + const pendingItem: DashboardSidebarWorkspace = { + id: pw.id, + projectId: pw.projectId, + hostId: "", + hostType: "local-device", type: "worktree", hostIsOnline: null, accentColor: null, - name: failure.name, - branch: failure.branchName, + name: pw.name, + branch: pw.branchName, pullRequest: null, repoUrl: project.githubOwner && project.githubRepoName @@ -514,14 +492,14 @@ export function useDashboardSidebarData() { behindCount: null, createdAt: new Date(), updatedAt: new Date(), - creationStatus: "failed", + creationStatus: pw.status, }; project.childEntries.push({ tabOrder: PENDING_WORKSPACE_TAB_ORDER, child: { type: "workspace", - workspace: failedItem, + workspace: pendingItem, }, }); } @@ -564,7 +542,7 @@ export function useDashboardSidebarData() { }, [ machineId, pullRequestsByWorkspaceId, - failureSidebarRows, + inFlightSidebarRows, localStateWorkspaceIds, sidebarProjects, sidebarSections, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx index e0544216216..f3cdabbafce 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx @@ -14,6 +14,7 @@ import { ResizablePanel } from "renderer/screens/main/components/ResizablePanel" import { WorkspaceSidebar } from "renderer/screens/main/components/WorkspaceSidebar"; import { DeleteWorkspaceDialog } from "renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components"; import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal"; +import { WorkspaceCreatesManager } from "renderer/stores/workspace-creates"; import { COLLAPSED_WORKSPACE_SIDEBAR_WIDTH, DEFAULT_WORKSPACE_SIDEBAR_WIDTH, @@ -127,6 +128,7 @@ function DashboardLayout() { return (
+ {sidebarOutsideColumn && sidebarPanel}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/components/WorkspaceCreateErrorState/WorkspaceCreateErrorState.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/components/WorkspaceCreateErrorState/WorkspaceCreateErrorState.tsx index c10b19a9714..282c9f5c5c9 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/components/WorkspaceCreateErrorState/WorkspaceCreateErrorState.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/components/WorkspaceCreateErrorState/WorkspaceCreateErrorState.tsx @@ -1,10 +1,7 @@ import { Button } from "@superset/ui/button"; +import { useNavigate } from "@tanstack/react-router"; import { AlertCircle, GitBranch } from "lucide-react"; -import { useNavigateAwayFromWorkspace } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace"; -import { - useWorkspaceCreateFailuresStore, - useWorkspaceCreates, -} from "renderer/stores/workspace-creates"; +import { useWorkspaceCreates } from "renderer/stores/workspace-creates"; interface WorkspaceCreateErrorStateProps { workspaceId: string; @@ -19,22 +16,12 @@ export function WorkspaceCreateErrorState({ branch, error, }: WorkspaceCreateErrorStateProps) { - const { submit } = useWorkspaceCreates(); - const navigateAway = useNavigateAwayFromWorkspace(); - - const handleRetry = () => { - const failure = - useWorkspaceCreateFailuresStore.getState().failures[workspaceId]; - if (!failure) return; - void submit({ hostId: failure.hostId, snapshot: failure.snapshot }); - }; + const navigate = useNavigate(); + const { retry, dismiss } = useWorkspaceCreates(); const handleDismiss = () => { - useWorkspaceCreateFailuresStore.getState().clear(workspaceId); - // `navigateAway` jumps to the next sidebar workspace when we're viewing - // the one being dismissed — falls back to the top sidebar entry since - // the failed id was never in the sidebar list, then to "/" if empty. - navigateAway(workspaceId); + dismiss(workspaceId); + void navigate({ to: "/v2-workspaces" }); }; return ( @@ -79,7 +66,7 @@ export function WorkspaceCreateErrorState({
-