diff --git a/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/useWorkspaceHostUrl.ts b/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/useWorkspaceHostUrl.ts index e2778705ce1..b8c59821c34 100644 --- a/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/useWorkspaceHostUrl.ts +++ b/apps/desktop/src/renderer/hooks/host-service/useWorkspaceHostUrl/useWorkspaceHostUrl.ts @@ -34,6 +34,7 @@ export function useWorkspaceHostTarget( .select(({ workspaces }) => ({ organizationId: workspaces.organizationId, hostId: workspaces.hostId, + isSynced: workspaces.$synced, })), [collections, workspaceId], ); @@ -43,6 +44,7 @@ export function useWorkspaceHostTarget( return useMemo(() => { if (!workspaceId || !isReady) return { status: "loading" }; if (!match) return { status: "not-found" }; + if (!match.isSynced) return { status: "loading" }; if (machineId && match.hostId === machineId) { if (activeHostUrl) { return { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts index 22049436e5f..667090cbcfc 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog/hooks/useDestroyDialogState/useDestroyDialogState.ts @@ -10,6 +10,8 @@ import { } from "renderer/hooks/host-service/useDestroyWorkspace"; import { useV2UserPreferences } from "renderer/hooks/useV2UserPreferences/useV2UserPreferences"; import { useNavigateAwayFromWorkspace } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useNavigateAwayFromWorkspace"; +import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import { waitForWorkspaceDeleted } from "renderer/routes/_authenticated/providers/CollectionsProvider/workspaceSyncWaits"; import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider"; interface UseDestroyDialogStateOptions { @@ -34,6 +36,7 @@ export function useDestroyDialogState({ onDeleted, }: UseDestroyDialogStateOptions) { const { destroy, inspect, hostTarget } = useDestroyWorkspace(workspaceId); + const collections = useCollections(); const { markDeleting, clearDeleting } = useDeletingWorkspaces(); const { navigateAwayFromWorkspace } = useNavigateAwayFromWorkspace(); @@ -109,6 +112,7 @@ export function useDestroyDialogState({ async (force: boolean) => { if (inFlight.current) return; inFlight.current = true; + let keepDeleting = false; setError(null); onOpenChange(false); @@ -136,6 +140,20 @@ export function useDestroyDialogState({ throw firstErr; } } + try { + await waitForWorkspaceDeleted(collections.v2Workspaces, workspaceId); + } catch (syncErr) { + keepDeleting = true; + onDeleted?.(); + console.warn("[workspace-delete] delete synced slowly", { + workspaceId, + err: syncErr, + }); + toast.warning( + `Deleted ${workspaceName}, but sync is taking longer than expected.`, + ); + return; + } for (const warning of result.warnings) toast.warning(warning); onDeleted?.(); } catch (err) { @@ -151,7 +169,9 @@ export function useDestroyDialogState({ ); } } finally { - clearDeleting(workspaceId); + if (!keepDeleting) { + clearDeleting(workspaceId); + } inFlight.current = false; } }, @@ -165,6 +185,7 @@ export function useDestroyDialogState({ markDeleting, clearDeleting, navigateAwayFromWorkspace, + collections, ], ); 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 1bf8bc301cd..66da282a0bf 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,6 @@ 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 { useWorkspaceCreatesStore } from "renderer/stores/workspace-creates"; import { useDashboardSidebarHover } from "../../providers/DashboardSidebarHoverProvider"; import type { DashboardSidebarWorkspace } from "../../types"; import { DashboardSidebarDeleteDialog } from "../DashboardSidebarDeleteDialog"; @@ -36,7 +35,7 @@ export function DashboardSidebarWorkspaceItem({ hostIsOnline, name, branch, - creationStatus, + isSynced, pullRequest, } = workspace; const isMainWorkspace = workspace.type === "main"; @@ -77,16 +76,11 @@ export function DashboardSidebarWorkspaceItem({ const handleAfterBranchRename = (newBranchName: string) => { v2WorkspaceActions.updateWorkspace(id, { branch: newBranchName }); }; - const isPending = !!creationStatus; - const isFailedInFlight = creationStatus === "failed"; + const isPending = !isSynced; // Keep the delete dialog outside the hidden wrapper below — the destroy // flow reopens it into an error pane on conflict/teardown-failed. const isDeleting = useDeletingWorkspaces().isDeleting(id); - const handleDismissInFlight = useCallback(() => { - useWorkspaceCreatesStore.getState().remove(id); - }, [id]); - const { hoveredId: hoverHoveredId, requestOpen: hoverRequestOpen, @@ -142,11 +136,9 @@ export function DashboardSidebarWorkspaceItem({ isActive={isActive} workspaceStatus={workspaceStatus} onClick={handleClick} - creationStatus={creationStatus} + isSynced={isSynced} pullRequestState={pullRequest?.state ?? null} - aria-label={ - creationStatus ? `Creating workspace: ${name}` : undefined - } + aria-label={isPending ? `Creating workspace: ${name}` : undefined} /> ); @@ -226,11 +218,7 @@ export function DashboardSidebarWorkspaceItem({ onClick={handleClick} onDoubleClick={isPending ? undefined : startRename} onRemoveFromSidebarClick={handleRemoveFromSidebar} - onCloseWorkspaceClick={ - isFailedInFlight - ? handleDismissInFlight - : () => setIsDeleteDialogOpen(true) - } + onCloseWorkspaceClick={() => setIsDeleteDialogOpen(true)} onRenameValueChange={setRenameValue} onSubmitRename={submitRename} onCancelRename={cancelRename} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/DashboardSidebarCollapsedWorkspaceButton.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/DashboardSidebarCollapsedWorkspaceButton.tsx index db5a250b970..c0a77fa7084 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/DashboardSidebarCollapsedWorkspaceButton.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarCollapsedWorkspaceButton/DashboardSidebarCollapsedWorkspaceButton.tsx @@ -15,7 +15,7 @@ interface DashboardSidebarCollapsedWorkspaceButtonProps hostIsOnline: boolean | null; isActive: boolean; workspaceStatus?: ActivePaneStatus | null; - creationStatus?: "preparing" | "generating-branch" | "creating" | "failed"; + isSynced: boolean; pullRequestState?: DashboardSidebarWorkspacePullRequest["state"] | null; } @@ -30,7 +30,7 @@ export const DashboardSidebarCollapsedWorkspaceButton = forwardRef< hostIsOnline, isActive, workspaceStatus = null, - creationStatus, + isSynced, pullRequestState = null, className, ...props @@ -56,7 +56,7 @@ export const DashboardSidebarCollapsedWorkspaceButton = forwardRef< isActive={isActive} variant="collapsed" workspaceStatus={workspaceStatus} - creationStatus={creationStatus} + isSynced={isSynced} pullRequestState={pullRequestState} /> diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx index 2eda5ee39c1..00547e49f2b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx @@ -4,7 +4,6 @@ import { type ComponentPropsWithoutRef, forwardRef, useEffect, - useMemo, useRef, } from "react"; import { HiMiniMinus, HiMiniXMark } from "react-icons/hi2"; @@ -17,7 +16,6 @@ import type { DashboardSidebarWorkspace, DashboardSidebarWorkspacePullRequest, } from "../../../../types"; -import { getCreationStatusText } from "../../utils/getCreationStatusText"; import { DashboardSidebarWorkspaceDiffStats } from "../DashboardSidebarWorkspaceDiffStats"; import { DashboardSidebarWorkspaceIcon } from "../DashboardSidebarWorkspaceIcon"; @@ -83,8 +81,9 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< name, branch, pullRequest, - creationStatus, + isSynced, } = workspace; + const isPending = !isSynced; const showsStandaloneActiveStripe = accentColor == null; const localRef = useRef(null); const openUrl = electronTrpc.external.openUrl.useMutation(); @@ -98,10 +97,7 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< } }, [isActive]); - const creationStatusText = useMemo( - () => getCreationStatusText(creationStatus), - [creationStatus], - ); + const creationStatusText = isPending ? "Creating…" : null; const isMainWorkspace = workspace.type === "main"; const workspaceKindTitle = isMainWorkspace ? "Main workspace" @@ -115,7 +111,7 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef<
{ localRef.current = node; if (typeof ref === "function") ref(node); @@ -174,7 +170,7 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< isActive={isActive} variant="expanded" workspaceStatus={workspaceStatus} - creationStatus={creationStatus} + isSynced={isSynced} pullRequestState={pullRequest.state} /> @@ -187,7 +183,7 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< isActive={isActive} variant="expanded" workspaceStatus={workspaceStatus} - creationStatus={creationStatus} + isSynced={isSynced} pullRequestState={null} />
@@ -256,14 +252,7 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef<
{creationStatusText ? ( - + {creationStatusText} ) : ( @@ -276,9 +265,9 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< /> ) )} - {(!creationStatus || creationStatus === "failed") && ( + {isSynced && (
- {shortcutLabel && !creationStatus && ( + {shortcutLabel && ( {shortcutLabel} @@ -330,24 +319,16 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< } }} className="flex items-center justify-center text-muted-foreground hover:text-foreground" - aria-label={ - creationStatus === "failed" - ? "Dismiss" - : "Close workspace" - } + aria-label="Close workspace" > - {creationStatus === "failed" ? ( - "Dismiss" - ) : ( - - )} + )} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/DashboardSidebarWorkspaceIcon.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/DashboardSidebarWorkspaceIcon.tsx index ed268448037..22b734ae3c7 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/DashboardSidebarWorkspaceIcon.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarWorkspaceIcon/DashboardSidebarWorkspaceIcon.tsx @@ -1,6 +1,5 @@ import { cn } from "@superset/ui/utils"; import { CgLaptop } from "react-icons/cg"; -import { HiExclamationTriangle } from "react-icons/hi2"; import { LuGitMerge, LuGitPullRequest, @@ -25,7 +24,7 @@ interface DashboardSidebarWorkspaceIconProps { isActive: boolean; variant: "collapsed" | "expanded"; workspaceStatus?: ActivePaneStatus | null; - creationStatus?: "preparing" | "generating-branch" | "creating" | "failed"; + isSynced: boolean; pullRequestState?: DashboardSidebarWorkspacePullRequest["state"] | null; } @@ -55,7 +54,7 @@ export function DashboardSidebarWorkspaceIcon({ isActive, variant, workspaceStatus = null, - creationStatus, + isSynced, pullRequestState = null, }: DashboardSidebarWorkspaceIconProps) { const overlayPosition = OVERLAY_POSITION[variant]; @@ -103,9 +102,7 @@ export function DashboardSidebarWorkspaceIcon({ return ( <> - {creationStatus === "failed" ? ( - - ) : creationStatus || workspaceStatus === "working" ? ( + {!isSynced || workspaceStatus === "working" ? ( ) : ( renderPrimaryIcon() diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/getCreationStatusText.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/getCreationStatusText.ts deleted file mode 100644 index d98c17efbf8..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/getCreationStatusText.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { DashboardSidebarWorkspace } from "../../../types"; - -const CREATION_STATUS_LABELS: Record< - NonNullable, - string -> = { - preparing: "Preparing...", - "generating-branch": "Generating...", - creating: "Creating...", - failed: "Failed", -} as const; - -export function getCreationStatusText( - status: DashboardSidebarWorkspace["creationStatus"], -): string | null { - return status ? CREATION_STATUS_LABELS[status] : null; -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/index.ts deleted file mode 100644 index 5298c6aa549..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { getCreationStatusText } from "./getCreationStatusText"; 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 cfa7f03fbff..4e9798fb00f 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,6 @@ 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 { useWorkspaceCreatesStore } from "renderer/stores/workspace-creates"; import type { DashboardSidebarProject, DashboardSidebarProjectChild, @@ -21,9 +20,6 @@ import { type PullRequestQueryTarget, } from "./derivePullRequestQueryTargets"; -// Sits above every real workspace so the pending row lines up with the real one, -// which is inserted via getPrependTabOrder. -const PENDING_WORKSPACE_TAB_ORDER = Number.MIN_SAFE_INTEGER; const MAIN_WORKSPACE_TAB_ORDER = Number.MIN_SAFE_INTEGER; type SidebarPullRequest = DashboardSidebarWorkspace["pullRequest"]; @@ -133,33 +129,6 @@ export function useDashboardSidebarData() { const { toggleProjectCollapsed } = useDashboardSidebarState(); const queryClient = useQueryClient(); - // 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). Entries that have already - // resolved on the host service carry `cloudRow`; those are surfaced as - // real synced rows below so the sidebar doesn't stick on "creating" when - // Electric is slow. - const inFlightEntries = useWorkspaceCreatesStore((store) => store.entries); - const inFlightSidebarRows = useMemo( - () => - inFlightEntries - .filter((entry) => entry.snapshot.id !== undefined) - // Entries with a cloudRow are rendered via the synced fallback below. - .filter((entry) => !(entry.state === "creating" && entry.cloudRow)) - .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( (q) => q.from({ hosts: collections.v2Hosts }).select(({ hosts }) => ({ @@ -253,6 +222,7 @@ export function useDashboardSidebarData() { taskId: workspaces.taskId, createdAt: workspaces.createdAt, updatedAt: workspaces.updatedAt, + isSynced: workspaces.$synced, tabOrder: sidebarWorkspaces.sidebarState.tabOrder, sectionId: sidebarWorkspaces.sidebarState.sectionId, isHidden: sidebarWorkspaces.sidebarState.isHidden, @@ -293,6 +263,7 @@ export function useDashboardSidebarData() { taskId: workspaces.taskId, createdAt: workspaces.createdAt, updatedAt: workspaces.updatedAt, + isSynced: workspaces.$synced, tabOrder: MAIN_WORKSPACE_TAB_ORDER, sectionId: null as string | null, })), @@ -307,42 +278,6 @@ export function useDashboardSidebarData() { [hostsByMachineId, rawLocalMainWorkspaces], ); - // Cloud-row fallback: when workspaces.create has resolved on the host - // service but Electric hasn't yet delivered the v2Workspaces row, surface - // the cloud row cached on the in-flight entry so the sidebar renders the - // workspace as fully synced. Manager.tsx removes the entry once Electric - // catches up, at which point the live query takes over seamlessly. - const cloudRowFallbackWorkspaces = useMemo(() => { - if (inFlightEntries.length === 0) return []; - const rows = inFlightEntries.flatMap((entry) => { - const cloudRow = entry.cloudRow; - if (!cloudRow) return []; - // Electric already delivered; let the live query own this row. - if (localStateWorkspaceIds.has(cloudRow.id)) return []; - const localState = collections.v2WorkspaceLocalState.get(cloudRow.id); - const host = hostsByMachineId.get(cloudRow.hostId); - return [ - { - id: cloudRow.id, - projectId: localState?.sidebarState.projectId ?? cloudRow.projectId, - hostId: cloudRow.hostId, - type: cloudRow.type, - hostIsOnline: host?.isOnline ?? false, - name: cloudRow.name, - branch: cloudRow.branch, - taskId: cloudRow.taskId, - createdAt: cloudRow.createdAt, - updatedAt: cloudRow.updatedAt, - tabOrder: - localState?.sidebarState.tabOrder ?? PENDING_WORKSPACE_TAB_ORDER, - sectionId: localState?.sidebarState.sectionId ?? null, - isHidden: localState?.sidebarState.isHidden ?? false, - }, - ]; - }); - return getVisibleSidebarWorkspaces(rows); - }, [collections, hostsByMachineId, inFlightEntries, localStateWorkspaceIds]); - const visibleSidebarWorkspaces = useMemo(() => { const sidebarProjectIds = new Set( sidebarProjects.map((project) => project.id), @@ -354,13 +289,8 @@ export function useDashboardSidebarData() { sidebarProjectIds.has(workspace.projectId), ); - return [ - ...autoLocalMainWorkspaces, - ...sidebarWorkspaces, - ...cloudRowFallbackWorkspaces, - ]; + return [...autoLocalMainWorkspaces, ...sidebarWorkspaces]; }, [ - cloudRowFallbackWorkspaces, localMainWorkspaces, localStateWorkspaceIds, machineId, @@ -504,6 +434,7 @@ export function useDashboardSidebarData() { createdAt: workspace.createdAt, updatedAt: workspace.updatedAt, taskId: workspace.taskId, + isSynced: workspace.isSynced, }; if (workspace.sectionId) { @@ -526,47 +457,6 @@ export function useDashboardSidebarData() { }); } - // 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 pendingItem: DashboardSidebarWorkspace = { - id: pw.id, - projectId: pw.projectId, - hostId: "", - hostType: "local-device", - type: "worktree", - hostIsOnline: null, - accentColor: null, - name: pw.name, - branch: pw.branchName, - pullRequest: null, - repoUrl: - project.githubOwner && project.githubRepoName - ? `https://github.com/${project.githubOwner}/${project.githubRepoName}` - : null, - branchExistsOnRemote: false, - previewUrl: null, - needsRebase: null, - behindCount: null, - createdAt: new Date(), - updatedAt: new Date(), - taskId: null, - creationStatus: pw.status, - }; - - project.childEntries.push({ - tabOrder: PENDING_WORKSPACE_TAB_ORDER, - child: { - type: "workspace", - workspace: pendingItem, - }, - }); - } - return sidebarProjects.flatMap((project) => { const resolvedProject = projectsById.get(project.id); if (!resolvedProject) return []; @@ -617,8 +507,6 @@ export function useDashboardSidebarData() { }, [ machineId, pullRequestsByWorkspaceId, - inFlightSidebarRows, - localStateWorkspaceIds, sidebarProjects, sidebarSections, visibleSidebarWorkspaces, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts index 82413df4232..6e32551f2ac 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts @@ -3,6 +3,7 @@ import { useCallback, useMemo, useRef } from "react"; import { useHotkey } from "renderer/hotkeys"; import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; +import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider"; import type { DashboardSidebarProject } from "../../types"; import { getProjectChildrenWorkspaces } from "../../utils/projectChildren"; @@ -53,12 +54,13 @@ export function useDashboardSidebarShortcuts( const navigate = useNavigate(); const { toggleProjectCollapsed, toggleSectionCollapsed } = useDashboardSidebarState(); + const { isDeleting } = useDeletingWorkspaces(); const flattenedWorkspaces = useMemo( () => groups .flatMap((project) => getProjectChildrenWorkspaces(project.children)) - .filter((workspace) => !workspace.creationStatus), - [groups], + .filter((workspace) => workspace.isSynced && !isDeleting(workspace.id)), + [groups, isDeleting], ); const workspaceShortcutLabels = useStableWorkspaceShortcutLabels(flattenedWorkspaces); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts index c9be0b9edd1..d09d3720862 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/types.ts @@ -41,7 +41,7 @@ export interface DashboardSidebarWorkspace { createdAt: Date; updatedAt: Date; taskId: string | null; - creationStatus?: "preparing" | "generating-branch" | "creating" | "failed"; + isSynced: boolean; } export interface DashboardSidebarSection { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceTitle/V2WorkspaceTitle.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceTitle/V2WorkspaceTitle.tsx index e2552fddd92..e14cad7fd14 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceTitle/V2WorkspaceTitle.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceTitle/V2WorkspaceTitle.tsx @@ -3,7 +3,6 @@ import { eq } from "@tanstack/db"; import { useLiveQuery } from "@tanstack/react-db"; import { ChevronRight, GitBranch } from "lucide-react"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; -import { useWorkspaceCreatesStore } from "renderer/stores/workspace-creates"; interface V2WorkspaceTitleProps { workspaceId: string; @@ -22,20 +21,9 @@ export function V2WorkspaceTitle({ workspaceId }: V2WorkspaceTitleProps) { })), [collections, workspaceId], ); - const syncedWorkspace = workspaces[0] ?? null; - const inFlight = useWorkspaceCreatesStore((store) => - store.entries.find((entry) => entry.snapshot.id === workspaceId), - ); - const name = - syncedWorkspace?.name ?? - inFlight?.cloudRow?.name ?? - inFlight?.snapshot.name ?? - null; - const branch = - syncedWorkspace?.branch ?? - inFlight?.cloudRow?.branch ?? - inFlight?.snapshot.branch ?? - null; + const workspace = workspaces[0] ?? null; + const name = workspace?.name ?? null; + const branch = workspace?.branch ?? null; if (!name && !branch) { return null; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx index c1c0822a9ee..0634c2439ee 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx @@ -20,7 +20,6 @@ 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, @@ -191,7 +190,6 @@ function DashboardLayout() { return (
- {sidebarOutsideColumn && sidebarPanel}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/OpenInWorkspaceV2/OpenInWorkspaceV2.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/OpenInWorkspaceV2/OpenInWorkspaceV2.tsx index fc0c44460f4..559d343b9dc 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/OpenInWorkspaceV2/OpenInWorkspaceV2.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/$taskId/components/PropertiesSidebar/components/OpenInWorkspaceV2/OpenInWorkspaceV2.tsx @@ -247,7 +247,7 @@ export function OpenInWorkspaceV2({ task }: OpenInWorkspaceV2Props) { params: { workspaceId: snapshotId }, }); - const promise = submit({ + const { completed } = submit({ hostId, snapshot: { id: snapshotId, @@ -257,34 +257,17 @@ export function OpenInWorkspaceV2({ task }: OpenInWorkspaceV2Props) { taskId: task.id, agents, }, - }).then((result) => { - if (!result.ok) { - // We optimistically navigated to the snapshot URL — bounce back to - // the task on failure so the user isn't stranded on a dead route. - void navigate({ - to: "/tasks/$taskId", - params: { taskId: task.id }, - replace: true, - }); - throw new Error(result.error); - } - if (result.workspaceId !== snapshotId) { + }); + + void completed.then((outcome) => { + if (!outcome.ok) return; + if (outcome.workspaceId !== snapshotId) { void navigate({ to: "/v2-workspace/$workspaceId", - params: { workspaceId: result.workspaceId }, + params: { workspaceId: outcome.workspaceId }, replace: true, }); } - return result; - }); - - toast.promise(promise, { - loading: "Creating workspace...", - success: (result) => - result.alreadyExists - ? "Opened existing workspace" - : "Workspace created", - error: (err) => (err instanceof Error ? err.message : String(err)), }); }; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunInWorkspacePopoverV2/RunInWorkspacePopoverV2.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunInWorkspacePopoverV2/RunInWorkspacePopoverV2.tsx index d758e23487e..6d150dcffd1 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunInWorkspacePopoverV2/RunInWorkspacePopoverV2.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunInWorkspacePopoverV2/RunInWorkspacePopoverV2.tsx @@ -227,7 +227,7 @@ export function RunInWorkspacePopoverV2({ return; } - const submissions = tasks.map((task) => + const handles = tasks.map((task) => submit({ hostId, snapshot: { @@ -249,18 +249,20 @@ export function RunInWorkspacePopoverV2({ }), ); - const promise = Promise.all(submissions).then((results) => { - const failed = results.filter((r) => !r.ok).length; - if (failed > 0) { - const firstFailure = results.find((result) => !result.ok); - const details = - firstFailure && !firstFailure.ok ? `: ${firstFailure.error}` : ""; - throw new Error( - `${results.length - failed} of ${results.length} succeeded${details}`, - ); - } - return results.length; - }); + const promise = Promise.all(handles.map((handle) => handle.completed)).then( + (outcomes) => { + const failed = outcomes.filter((outcome) => !outcome.ok).length; + if (failed > 0) { + const firstFailure = outcomes.find((outcome) => !outcome.ok); + const details = + firstFailure && !firstFailure.ok ? `: ${firstFailure.error}` : ""; + throw new Error( + `${outcomes.length - failed} of ${outcomes.length} succeeded${details}`, + ); + } + return outcomes.length; + }, + ); toast.promise(promise, { loading: `Creating ${tasks.length} workspace${tasks.length === 1 ? "" : "s"}...`, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunIssuesInWorkspacePopover/RunIssuesInWorkspacePopover.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunIssuesInWorkspacePopover/RunIssuesInWorkspacePopover.tsx index b2023e809f6..0f0e6584105 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunIssuesInWorkspacePopover/RunIssuesInWorkspacePopover.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/RunIssuesInWorkspacePopover/RunIssuesInWorkspacePopover.tsx @@ -226,7 +226,7 @@ export function RunIssuesInWorkspacePopover({ setLastProjectId(selectedProjectId); - const submissions = issues.map((issue) => + const handles = issues.map((issue) => submit({ hostId, snapshot: { @@ -250,18 +250,20 @@ export function RunIssuesInWorkspacePopover({ }), ); - const promise = Promise.all(submissions).then((results) => { - const failed = results.filter((r) => !r.ok).length; - if (failed > 0) { - const firstFailure = results.find((result) => !result.ok); - const details = - firstFailure && !firstFailure.ok ? `: ${firstFailure.error}` : ""; - throw new Error( - `${results.length - failed} of ${results.length} succeeded${details}`, - ); - } - return results.length; - }); + const promise = Promise.all(handles.map((handle) => handle.completed)).then( + (outcomes) => { + const failed = outcomes.filter((outcome) => !outcome.ok).length; + if (failed > 0) { + const firstFailure = outcomes.find((outcome) => !outcome.ok); + const details = + firstFailure && !firstFailure.ok ? `: ${firstFailure.error}` : ""; + throw new Error( + `${outcomes.length - failed} of ${outcomes.length} succeeded${details}`, + ); + } + return outcomes.length; + }, + ); toast.promise(promise, { loading: `Creating ${issues.length} workspace${issues.length === 1 ? "" : "s"}...`, 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 282c9f5c5c9..5b35c471f10 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,26 +1,44 @@ import { Button } from "@superset/ui/button"; import { useNavigate } from "@tanstack/react-router"; import { AlertCircle, GitBranch } from "lucide-react"; +import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import type { FailedWorkspaceCreateRow } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal"; import { useWorkspaceCreates } from "renderer/stores/workspace-creates"; interface WorkspaceCreateErrorStateProps { - workspaceId: string; - name?: string; - branch?: string; - error: string; + entry: FailedWorkspaceCreateRow; } export function WorkspaceCreateErrorState({ - workspaceId, - name, - branch, - error, + entry, }: WorkspaceCreateErrorStateProps) { const navigate = useNavigate(); - const { retry, dismiss } = useWorkspaceCreates(); + const collections = useCollections(); + const { submit } = useWorkspaceCreates(); + + const name = entry.input.name; + const branch = entry.input.branch; + + const handleRetry = () => { + const { workspaceId, completed } = submit({ + hostId: entry.hostId, + snapshot: entry.input, + }); + void completed.then((outcome) => { + if (outcome.ok && outcome.workspaceId !== workspaceId) { + void navigate({ + to: "/v2-workspace/$workspaceId", + params: { workspaceId: outcome.workspaceId }, + replace: true, + }); + } + }); + }; const handleDismiss = () => { - dismiss(workspaceId); + if (collections.failedWorkspaceCreates.get(entry.id)) { + collections.failedWorkspaceCreates.delete(entry.id); + } void navigate({ to: "/v2-workspaces" }); }; @@ -61,12 +79,12 @@ export function WorkspaceCreateErrorState({

- {error} + {entry.error}

-