diff --git a/apps/desktop/docs/OPTIMISTIC_ELECTRIC_UPDATES.md b/apps/desktop/docs/OPTIMISTIC_ELECTRIC_UPDATES.md index bf40a83d511..6ed0d810a75 100644 --- a/apps/desktop/docs/OPTIMISTIC_ELECTRIC_UPDATES.md +++ b/apps/desktop/docs/OPTIMISTIC_ELECTRIC_UPDATES.md @@ -38,7 +38,7 @@ These collections have Electric mutation handlers in `CollectionsProvider/collec | --- | --- | --- | --- | | `tasks` | insert, update, delete | `useOptimisticCollectionActions().tasks` for update/delete; create dialog still uses `task.createFromUi` directly | Optimistic for task edits/deletes; collection handlers return `{ txid }`. | | `v2Projects` | update | `useOptimisticCollectionActions().v2Projects` for rename/repository updates | Optimistic for project row edits; create/delete remain API-confirmed. | -| `v2Workspaces` | update | `useOptimisticCollectionActions().v2Workspaces` for rename-style updates | Optimistic for workspace row edits; create/delete remain host-service sagas. | +| `v2Workspaces` | insert, update | `useOptimisticCollectionActions().v2Workspaces` for rename-style updates; `useWorkspaceCreates().submit` for optimistic insert | Optimistic for workspace creation and row edits; delete remains host-service saga. The `onInsert` handler calls host-service `workspaces.create`, returns `{ txid }`, and rolls back to the canonical ID when the server assigns a different one. | | `chatSessions` | delete | `useOptimisticCollectionActions().chatSessions` for chat session deletion | Optimistic delete; create remains server-confirmed because the chat runtime coordinates session creation. | | `agentCommands` | update | `useCommandWatcher` | Background optimistic update; caller awaits `tx.isPersisted.promise` and retries on failure. | @@ -65,12 +65,11 @@ These are Electric-backed in the renderer but have no collection mutation handle - `automations` - `automationRuns` -Workspace create/delete flows do not use `collections.v2Workspaces.insert/delete`. They go through host-service or tRPC APIs and then Electric streams the confirmed row back: +Workspace create now uses `collections.v2Workspaces.insert` with an `onInsert` handler that calls host-service `workspaces.create`. The renderer does an optimistic insert, the handler persists through host-service, returns `{ txid }`, and the caller awaits `waitForSyncedWorkspaceRow` before navigating. If the server assigns a different canonical ID, the handler awaits the txid then throws a rollback sentinel so TanStack DB replaces the optimistic row with the canonical one. -- workspace create/checkout/adopt writes a local `pendingWorkspaces` row, then the pending page calls host-service -- workspace delete calls host-service `workspaceCleanup.destroy`; the sidebar hides the row through `DeletingWorkspacesProvider` while the saga runs +Workspace delete still calls host-service `workspaceCleanup.destroy`; the sidebar hides the row through `DeletingWorkspacesProvider` while the saga runs. -Workspace rename does use `collections.v2Workspaces.update` via `useOptimisticCollectionActions().v2Workspaces`, backed by `v2Workspace.update` returning `{ txid }` from the same Postgres transaction. +Workspace rename uses `collections.v2Workspaces.update` via `useOptimisticCollectionActions().v2Workspaces`, backed by `v2Workspace.update` returning `{ txid }` from the same Postgres transaction. ### LocalStorage collections @@ -80,7 +79,7 @@ These are client-local TanStack DB collections. They are synchronous local persi - `v2WorkspaceLocalState` — sidebar placement, pane layout, viewed files, changes tab - `v2SidebarSections` — user-created sidebar sections and ordering - `v2TerminalPresets` — local terminal presets -- `pendingWorkspaces` — durable local bus for workspace creation progress and launch handoff +- `pendingWorkspaces` — legacy; previously used for workspace creation progress (now handled by `v2Workspaces` optimistic insert) - `v2UserPreferences` — local v2 preferences such as link behavior and delete-branch default LocalStorage mutations can still throw for schema/storage errors, but they do not have remote persistence confirmation or Electric rollback semantics. diff --git a/apps/desktop/src/renderer/commandPalette/core/ContextProvider.tsx b/apps/desktop/src/renderer/commandPalette/core/ContextProvider.tsx index e63b1cb16f2..a38bb9761a6 100644 --- a/apps/desktop/src/renderer/commandPalette/core/ContextProvider.tsx +++ b/apps/desktop/src/renderer/commandPalette/core/ContextProvider.tsx @@ -15,6 +15,7 @@ import { } from "react"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; import type { CommandContext } from "./types"; @@ -32,6 +33,7 @@ export function CommandContextProvider({ children }: { children: ReactNode }) { hostServiceStatus, machineId, } = useLocalHostService(); + const { isDeleting } = useDeletingWorkspaces(); const navigateTo = useCallback( (path: string) => { @@ -54,10 +56,16 @@ export function CommandContextProvider({ children }: { children: ReactNode }) { projectId: workspaces.projectId, type: workspaces.type, hostId: workspaces.hostId, + isSynced: workspaces.$synced, })), [collections, v2WorkspaceId], ); - const v2Workspace = v2WorkspaceId ? (v2WorkspaceRows[0] ?? null) : null; + const v2Workspace = + v2WorkspaceId && + v2WorkspaceRows[0]?.isSynced === true && + !isDeleting(v2WorkspaceId) + ? v2WorkspaceRows[0] + : null; const projectId = v2Workspace?.projectId ?? null; const { data: preferredAppRows = [] } = useLiveQuery( diff --git a/apps/desktop/src/renderer/hooks/host-service/useDestroyWorkspace/useDestroyWorkspace.ts b/apps/desktop/src/renderer/hooks/host-service/useDestroyWorkspace/useDestroyWorkspace.ts index 846b1c6236c..dd5f25dc1b0 100644 --- a/apps/desktop/src/renderer/hooks/host-service/useDestroyWorkspace/useDestroyWorkspace.ts +++ b/apps/desktop/src/renderer/hooks/host-service/useDestroyWorkspace/useDestroyWorkspace.ts @@ -20,6 +20,7 @@ export interface DestroyWorkspaceSuccess { worktreeRemoved: boolean; branchDeleted: boolean; cloudDeleted: boolean; + cloudDeleteTxid: number | null; warnings: string[]; } 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..53b48f27c80 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,9 @@ 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 { ELECTRIC_WRITE_SYNC_TIMEOUT_MS } from "renderer/routes/_authenticated/providers/CollectionsProvider/collections"; +import { waitForWorkspaceDeleted } from "renderer/routes/_authenticated/providers/CollectionsProvider/workspaceSyncWaits"; import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider"; interface UseDestroyDialogStateOptions { @@ -34,6 +37,7 @@ export function useDestroyDialogState({ onDeleted, }: UseDestroyDialogStateOptions) { const { destroy, inspect, hostTarget } = useDestroyWorkspace(workspaceId); + const collections = useCollections(); const { markDeleting, clearDeleting } = useDeletingWorkspaces(); const { navigateAwayFromWorkspace } = useNavigateAwayFromWorkspace(); @@ -109,6 +113,7 @@ export function useDestroyDialogState({ async (force: boolean) => { if (inFlight.current) return; inFlight.current = true; + let keepDeleting = false; setError(null); onOpenChange(false); @@ -136,6 +141,26 @@ export function useDestroyDialogState({ throw firstErr; } } + try { + if (typeof result.cloudDeleteTxid === "number") { + await collections.v2Workspaces.utils.awaitTxId( + result.cloudDeleteTxid, + ELECTRIC_WRITE_SYNC_TIMEOUT_MS, + ); + } + 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 +176,9 @@ export function useDestroyDialogState({ ); } } finally { - clearDeleting(workspaceId); + if (!keepDeleting) { + clearDeleting(workspaceId); + } inFlight.current = false; } }, @@ -165,6 +192,7 @@ export function useDestroyDialogState({ markDeleting, clearDeleting, navigateAwayFromWorkspace, + collections, ], ); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.ts index 0c269d0f249..0abc4905c2c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarPortsList/hooks/useDashboardSidebarPortsData/useDashboardSidebarPortsData.ts @@ -9,6 +9,7 @@ import { useRelayUrl } from "renderer/hooks/useRelayUrl"; import { getHostServiceWsToken } from "renderer/lib/host-service-auth"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; +import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; import { applyPortEventsToHostPortsResult, @@ -36,6 +37,7 @@ export function useDashboardSidebarPortsData(): { const collections = useCollections(); const queryClient = useQueryClient(); const { activeHostUrl, machineId } = useLocalHostService(); + const { isDeleting } = useDeletingWorkspaces(); const relayUrl = useRelayUrl(); const { data: hosts = [] } = useLiveQuery( @@ -56,10 +58,19 @@ export function useDashboardSidebarPortsData(): { id: workspaces.id, name: workspaces.name, hostId: workspaces.hostId, + isSynced: workspaces.$synced, })), [collections], ); + const queryableWorkspaces = useMemo( + () => + workspaces.filter( + (workspace) => workspace.isSynced === true && !isDeleting(workspace.id), + ), + [workspaces, isDeleting], + ); + const hostsToQuery = useMemo( () => deriveHostPortQueryTargets({ @@ -67,9 +78,9 @@ export function useDashboardSidebarPortsData(): { hosts, machineId, relayUrl, - workspaces, + workspaces: queryableWorkspaces, }), - [activeHostUrl, hosts, machineId, relayUrl, workspaces], + [activeHostUrl, hosts, machineId, relayUrl, queryableWorkspaces], ); const queries = useQueries({ @@ -154,9 +165,9 @@ export function useDashboardSidebarPortsData(): { () => groupDashboardSidebarPorts({ hostPortResults: queries.map((query) => query.data), - workspaces, + workspaces: queryableWorkspaces, }), - [queries, workspaces], + [queries, queryableWorkspaces], ); const totalPortCount = workspacePortGroups.reduce( 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 0f4a79be2bb..4ff91ddc5de 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,6 @@ export function DashboardSidebarWorkspaceItem({ hostIsOnline, name, branch, - creationStatus, pullRequest, } = workspace; const isMainWorkspace = workspace.type === "main"; @@ -77,16 +75,10 @@ export function DashboardSidebarWorkspaceItem({ const handleAfterBranchRename = (newBranchName: string) => { v2WorkspaceActions.updateWorkspace(id, { branch: newBranchName }); }; - const isPending = !!creationStatus; - const isFailedInFlight = creationStatus === "failed"; // 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, @@ -94,20 +86,18 @@ export function DashboardSidebarWorkspaceItem({ syncIfHovered: hoverSyncIfHovered, } = useDashboardSidebarHover(); const rowRef = useRef(null); - const hoverEligible = !isPending; const hoverPayload = useMemo( () => ({ workspace, onEditBranchClick: setRenameBranchTarget }), [workspace], ); const handleMouseEnter = useCallback(() => { - if (!hoverEligible || !rowRef.current) return; + if (!rowRef.current) return; hoverRequestOpen(id, rowRef.current, hoverPayload); - }, [hoverEligible, hoverRequestOpen, id, hoverPayload]); + }, [hoverRequestOpen, id, hoverPayload]); const handleMouseLeave = useCallback(() => { - if (!hoverEligible) return; hoverRequestClose(id); - }, [hoverEligible, hoverRequestClose, id]); + }, [hoverRequestClose, id]); const isHovered = hoverHoveredId === id; useEffect(() => { @@ -142,11 +132,7 @@ export function DashboardSidebarWorkspaceItem({ isActive={isActive} workspaceStatus={workspaceStatus} onClick={handleClick} - creationStatus={creationStatus} pullRequestState={pullRequest?.state ?? null} - aria-label={ - creationStatus ? `Creating workspace: ${name}` : undefined - } /> ); @@ -154,35 +140,31 @@ export function DashboardSidebarWorkspaceItem({ return ( <> - {!isPending && !isMainWorkspace && ( + {!isMainWorkspace && ( setIsDeleteDialogOpen(true) - } + onCloseWorkspaceClick={() => setIsDeleteDialogOpen(true)} onRenameValueChange={setRenameValue} onSubmitRename={submitRename} onCancelRename={cancelRename} @@ -240,35 +218,31 @@ export function DashboardSidebarWorkspaceItem({ return ( <> - {!isPending && !isMainWorkspace && ( + {!isMainWorkspace && ( 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..e9d11a0dcbe 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,7 +81,6 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< name, branch, pullRequest, - creationStatus, } = workspace; const showsStandaloneActiveStripe = accentColor == null; const localRef = useRef(null); @@ -98,10 +95,6 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< } }, [isActive]); - const creationStatusText = useMemo( - () => getCreationStatusText(creationStatus), - [creationStatus], - ); const isMainWorkspace = workspace.type === "main"; const workspaceKindTitle = isMainWorkspace ? "Main workspace" @@ -115,7 +108,6 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef<
{ localRef.current = node; if (typeof ref === "function") ref(node); @@ -174,7 +166,6 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< isActive={isActive} variant="expanded" workspaceStatus={workspaceStatus} - creationStatus={creationStatus} pullRequestState={pullRequest.state} /> @@ -187,7 +178,6 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< isActive={isActive} variant="expanded" workspaceStatus={workspaceStatus} - creationStatus={creationStatus} pullRequestState={null} />
@@ -255,104 +245,81 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< )}
- {creationStatusText ? ( - - {creationStatusText} - - ) : ( - diffStats && + {diffStats && (diffStats.additions > 0 || diffStats.deletions > 0) && ( - ) - )} - {(!creationStatus || creationStatus === "failed") && ( -
- {shortcutLabel && !creationStatus && ( - - {shortcutLabel} - - )} - {isMainWorkspace ? ( - - - - - - - - - ) : ( - - - + + + + + + ) : ( + + + - - - {creationStatus === "failed" ? ( - "Dismiss" - ) : ( - - )} - - - )} -
- )} + }} + className="flex items-center justify-center text-muted-foreground hover:text-foreground" + aria-label="Close workspace" + > + + + + + + + + )} +
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..3f6d920e8d6 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,6 @@ interface DashboardSidebarWorkspaceIconProps { isActive: boolean; variant: "collapsed" | "expanded"; workspaceStatus?: ActivePaneStatus | null; - creationStatus?: "preparing" | "generating-branch" | "creating" | "failed"; pullRequestState?: DashboardSidebarWorkspacePullRequest["state"] | null; } @@ -55,7 +53,6 @@ export function DashboardSidebarWorkspaceIcon({ isActive, variant, workspaceStatus = null, - creationStatus, pullRequestState = null, }: DashboardSidebarWorkspaceIconProps) { const overlayPosition = OVERLAY_POSITION[variant]; @@ -103,9 +100,7 @@ export function DashboardSidebarWorkspaceIcon({ return ( <> - {creationStatus === "failed" ? ( - - ) : creationStatus || workspaceStatus === "working" ? ( + {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..c730d2b2c2e 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 @@ -7,8 +7,8 @@ import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { getVisibleSidebarWorkspaces } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal"; +import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider"; import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider"; -import { useWorkspaceCreatesStore } from "renderer/stores/workspace-creates"; import type { DashboardSidebarProject, DashboardSidebarProjectChild, @@ -21,9 +21,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"]; @@ -129,37 +126,11 @@ function useStableDashboardSidebarProjects( export function useDashboardSidebarData() { const collections = useCollections(); const { machineId, activeHostUrl } = useLocalHostService(); + const { isDeleting } = useDeletingWorkspaces(); const relayUrl = useRelayUrl(); 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 +224,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 +265,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,48 +280,14 @@ 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), ); const autoLocalMainWorkspaces = localMainWorkspaces.filter( (workspace) => + workspace.isSynced && + !isDeleting(workspace.id) && !localStateWorkspaceIds.has(workspace.id) && workspace.hostId === machineId && sidebarProjectIds.has(workspace.projectId), @@ -356,11 +295,12 @@ export function useDashboardSidebarData() { return [ ...autoLocalMainWorkspaces, - ...sidebarWorkspaces, - ...cloudRowFallbackWorkspaces, + ...sidebarWorkspaces.filter( + (workspace) => workspace.isSynced && !isDeleting(workspace.id), + ), ]; }, [ - cloudRowFallbackWorkspaces, + isDeleting, localMainWorkspaces, localStateWorkspaceIds, machineId, @@ -504,6 +444,7 @@ export function useDashboardSidebarData() { createdAt: workspace.createdAt, updatedAt: workspace.updatedAt, taskId: workspace.taskId, + isSynced: workspace.isSynced, }; if (workspace.sectionId) { @@ -526,47 +467,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 +517,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..e08c4132a6e 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 @@ -55,9 +55,9 @@ export function useDashboardSidebarShortcuts( useDashboardSidebarState(); const flattenedWorkspaces = useMemo( () => - groups - .flatMap((project) => getProjectChildrenWorkspaces(project.children)) - .filter((workspace) => !workspace.creationStatus), + groups.flatMap((project) => + getProjectChildrenWorkspaces(project.children), + ), [groups], ); const workspaceShortcutLabels = 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/ResourceConsumption/ResourceConsumption.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/ResourceConsumption.tsx index b3eeb5ace77..495442d7247 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/ResourceConsumption.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/ResourceConsumption/ResourceConsumption.tsx @@ -30,6 +30,7 @@ import { } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import { getVisibleSidebarWorkspaces } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal"; +import { useDeletingWorkspaces } from "renderer/routes/_authenticated/providers/DeletingWorkspacesProvider"; import { useTabsStore } from "renderer/stores/tabs/store"; import { AppResourceSection } from "./components/AppResourceSection"; import { MetricBadge } from "./components/MetricBadge"; @@ -172,6 +173,7 @@ function ResourceConsumptionContent({ const setActiveTab = useTabsStore((state) => state.setActiveTab); const setFocusedPane = useTabsStore((state) => state.setFocusedPane); const collections = useCollections(); + const { isDeleting } = useDeletingWorkspaces(); const isV2 = surface === "v2"; const { data: session } = authClient.useSession(); const organizationId = session?.session?.activeOrganizationId ?? undefined; @@ -241,6 +243,7 @@ function ResourceConsumptionContent({ id: workspace.id, projectId: workspace.projectId, name: workspace.name, + isSynced: workspace.$synced, })), [collections], ); @@ -288,29 +291,36 @@ function ResourceConsumptionContent({ rawV2Projects.map((project) => [project.id, project]), ); const workspaceById = new Map( - rawV2Workspaces.map((workspace) => [workspace.id, workspace]), + rawV2Workspaces + .filter( + (workspace) => + workspace.isSynced === true && !isDeleting(workspace.id), + ) + .map((workspace) => [workspace.id, workspace]), ); return { ...normalized, - workspaces: normalized.workspaces.map((workspace) => { - const v2Workspace = workspaceById.get(workspace.workspaceId); - const projectId = v2Workspace?.projectId ?? workspace.projectId; - const project = projectById.get(projectId); - return { - ...workspace, - projectId, - projectName: project?.name ?? workspace.projectName, - workspaceName: v2Workspace?.name ?? workspace.workspaceName, - sessions: workspace.sessions.map((session) => ({ - ...session, - title: - terminalTitleOverrides.get(session.paneId) ?? - session.title ?? - null, - })), - }; - }), + workspaces: normalized.workspaces + .filter((workspace) => workspaceById.has(workspace.workspaceId)) + .map((workspace) => { + const v2Workspace = workspaceById.get(workspace.workspaceId); + const projectId = v2Workspace?.projectId ?? workspace.projectId; + const project = projectById.get(projectId); + return { + ...workspace, + projectId, + projectName: project?.name ?? workspace.projectName, + workspaceName: v2Workspace?.name ?? workspace.workspaceName, + sessions: workspace.sessions.map((session) => ({ + ...session, + title: + terminalTitleOverrides.get(session.paneId) ?? + session.title ?? + null, + })), + }; + }), }; }, [ snapshot, @@ -319,6 +329,7 @@ function ResourceConsumptionContent({ rawV2Projects, rawV2Workspaces, terminalTitleOverrides, + isDeleting, ]); const getPaneName = (session: SessionMetrics): string => { 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..25878c82769 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]; + 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 07dd12a22c1..1c74364a7ae 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx @@ -15,7 +15,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, @@ -139,7 +138,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..6fcd30a75a0 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 @@ -238,15 +238,6 @@ export function OpenInWorkspaceV2({ task }: OpenInWorkspaceV2Props) { }, ]; - // Navigate optimistically — the host service uses our supplied id for new - // workspaces, so the route is correct in the common case. If the server - // found an existing workspace under a different id, the success handler - // replaces the URL. - void navigate({ - to: "/v2-workspace/$workspaceId", - params: { workspaceId: snapshotId }, - }); - const promise = submit({ hostId, snapshot: { @@ -259,22 +250,12 @@ export function OpenInWorkspaceV2({ task }: OpenInWorkspaceV2Props) { }, }).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 navigate({ - to: "/v2-workspace/$workspaceId", - params: { workspaceId: result.workspaceId }, - replace: true, - }); - } + void navigate({ + to: "/v2-workspace/$workspaceId", + params: { workspaceId: result.workspaceId }, + }); return result; }); 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 deleted file mode 100644 index 282c9f5c5c9..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/components/WorkspaceCreateErrorState/WorkspaceCreateErrorState.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { Button } from "@superset/ui/button"; -import { useNavigate } from "@tanstack/react-router"; -import { AlertCircle, GitBranch } from "lucide-react"; -import { useWorkspaceCreates } from "renderer/stores/workspace-creates"; - -interface WorkspaceCreateErrorStateProps { - workspaceId: string; - name?: string; - branch?: string; - error: string; -} - -export function WorkspaceCreateErrorState({ - workspaceId, - name, - branch, - error, -}: WorkspaceCreateErrorStateProps) { - const navigate = useNavigate(); - const { retry, dismiss } = useWorkspaceCreates(); - - const handleDismiss = () => { - dismiss(workspaceId); - void navigate({ to: "/v2-workspaces" }); - }; - - return ( -
-
-
-
- ); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/components/WorkspaceCreateErrorState/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/components/WorkspaceCreateErrorState/index.ts deleted file mode 100644 index 39b3ccfa90c..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/components/WorkspaceCreateErrorState/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { WorkspaceCreateErrorState } from "./WorkspaceCreateErrorState"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/components/WorkspaceCreatingState/WorkspaceCreatingState.css b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/components/WorkspaceCreatingState/WorkspaceCreatingState.css deleted file mode 100644 index a3ebf3d4fda..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/components/WorkspaceCreatingState/WorkspaceCreatingState.css +++ /dev/null @@ -1,60 +0,0 @@ -.wcs-bar-track { - position: relative; - height: 1px; - width: 100%; - overflow: hidden; - background: hsl(var(--border) / 0.6); -} - -.wcs-bar-fill { - position: absolute; - inset-block: 0; - left: 0; - background: hsl(var(--foreground) / 0.75); - transition: width 0.7s cubic-bezier(0.22, 1, 0.36, 1); -} - -.wcs-bar-sweep { - position: absolute; - inset: 0; - background: linear-gradient( - 90deg, - transparent, - hsl(var(--foreground) / 0.25) 50%, - transparent - ); - transform: translateX(-100%); - animation: wcs-sweep 2.4s cubic-bezier(0.4, 0, 0.2, 1) infinite; -} - -@keyframes wcs-sweep { - 0% { - transform: translateX(-100%); - } - 100% { - transform: translateX(100%); - } -} - -.wcs-active-ring { - animation: wcs-pulse 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite; -} - -@keyframes wcs-pulse { - 0%, - 100% { - opacity: 0.35; - transform: scale(1); - } - 50% { - opacity: 0.85; - transform: scale(1.4); - } -} - -@media (prefers-reduced-motion: reduce) { - .wcs-bar-sweep, - .wcs-active-ring { - animation: none; - } -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/components/WorkspaceCreatingState/WorkspaceCreatingState.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/components/WorkspaceCreatingState/WorkspaceCreatingState.tsx deleted file mode 100644 index e74404b030d..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/components/WorkspaceCreatingState/WorkspaceCreatingState.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { cn } from "@superset/ui/utils"; -import { Check, GitBranch, Loader2 } from "lucide-react"; -import { useEffect, useState } from "react"; -import "./WorkspaceCreatingState.css"; - -interface Step { - id: string; - label: string; - /** Cumulative seconds at which this step is considered complete. */ - doneAt: number; -} - -// Mirrors the v1 init step order in shared/types/workspace-init.ts so the -// labels feel real — v2 workspaces.create runs the same git work server-side -// without streaming progress events, so timings here are estimates. -const STEPS: readonly Step[] = [ - { id: "preparing", label: "Preparing", doneAt: 1 }, - { id: "syncing", label: "Syncing with remote", doneAt: 4 }, - { id: "verifying", label: "Verifying base branch", doneAt: 5 }, - { id: "fetching", label: "Fetching latest changes", doneAt: 15 }, - { id: "creating_worktree", label: "Creating git worktree", doneAt: 18 }, - { id: "copying_config", label: "Copying configuration", doneAt: 20 }, - { id: "finalizing", label: "Finalizing setup", doneAt: 23 }, -] as const; - -const TOTAL_SECONDS = STEPS[STEPS.length - 1].doneAt; -// Cap synthetic progress so the bar never claims completion before the real -// workspaces.create mutation resolves. -const PROGRESS_CAP = 0.94; - -interface WorkspaceCreatingStateProps { - name?: string; - branch?: string; - startedAt?: number; -} - -export function WorkspaceCreatingState({ - name, - branch, - startedAt, -}: WorkspaceCreatingStateProps) { - const elapsed = useElapsedSeconds(startedAt); - const activeIndex = getActiveIndex(elapsed); - const progress = Math.min(elapsed / TOTAL_SECONDS, PROGRESS_CAP); - - return ( -
-
-