diff --git a/apps/desktop/src/renderer/commandPalette/ui/DeleteWorkspaceMount/DeleteWorkspaceMount.tsx b/apps/desktop/src/renderer/commandPalette/ui/DeleteWorkspaceMount/DeleteWorkspaceMount.tsx index 6565fb5f8f7..bc80d5c1381 100644 --- a/apps/desktop/src/renderer/commandPalette/ui/DeleteWorkspaceMount/DeleteWorkspaceMount.tsx +++ b/apps/desktop/src/renderer/commandPalette/ui/DeleteWorkspaceMount/DeleteWorkspaceMount.tsx @@ -1,10 +1,12 @@ import { useCallback } from "react"; import { DashboardSidebarDeleteDialog } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog"; +import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; import { useDeleteWorkspaceIntent } from "renderer/stores/delete-workspace-intent"; export function DeleteWorkspaceMount() { const target = useDeleteWorkspaceIntent((s) => s.target); const close = useDeleteWorkspaceIntent((s) => s.close); + const { hideWorkspaceInSidebar } = useDashboardSidebarState(); const handleOpenChange = useCallback( (open: boolean) => { @@ -20,6 +22,10 @@ export function DeleteWorkspaceMount() { workspaceName={target.workspaceName} open onOpenChange={handleOpenChange} + onDeleted={() => { + hideWorkspaceInSidebar(target.workspaceId); + close(); + }} /> ); } diff --git a/apps/desktop/src/renderer/commandPalette/ui/RemoveFromSidebarMount/RemoveFromSidebarMount.tsx b/apps/desktop/src/renderer/commandPalette/ui/RemoveFromSidebarMount/RemoveFromSidebarMount.tsx index 7d6aa148fe5..d021e21e332 100644 --- a/apps/desktop/src/renderer/commandPalette/ui/RemoveFromSidebarMount/RemoveFromSidebarMount.tsx +++ b/apps/desktop/src/renderer/commandPalette/ui/RemoveFromSidebarMount/RemoveFromSidebarMount.tsx @@ -15,8 +15,7 @@ import { useRemoveFromSidebarIntent } from "renderer/stores/remove-workspace-fro export function RemoveFromSidebarMount() { const target = useRemoveFromSidebarIntent((s) => s.target); const clear = useRemoveFromSidebarIntent((s) => s.clear); - const { hideWorkspaceInSidebar, removeWorkspaceFromSidebar } = - useDashboardSidebarState(); + const { hideWorkspaceInSidebar } = useDashboardSidebarState(); const { navigateAwayFromWorkspace } = useNavigateAwayFromWorkspace(); const handleOpenChange = useCallback( @@ -29,19 +28,9 @@ export function RemoveFromSidebarMount() { const handleConfirm = useCallback(() => { if (!target) return; navigateAwayFromWorkspace(target.workspaceId); - if (target.isMain) { - hideWorkspaceInSidebar(target.workspaceId, target.projectId); - } else { - removeWorkspaceFromSidebar(target.workspaceId); - } + hideWorkspaceInSidebar(target.workspaceId); clear(); - }, [ - target, - navigateAwayFromWorkspace, - hideWorkspaceInSidebar, - removeWorkspaceFromSidebar, - clear, - ]); + }, [target, navigateAwayFromWorkspace, hideWorkspaceInSidebar, clear]); return ( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts index 6a7920423cb..3675ad5190c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/hooks/useDashboardSidebarWorkspaceItemActions/useDashboardSidebarWorkspaceItemActions.ts @@ -42,7 +42,7 @@ export function useDashboardSidebarWorkspaceItemActions({ ); const setManualUnread = useV2NotificationStore((s) => s.setManualUnread); const isUnread = useV2WorkspaceIsUnread(workspaceId); - const { createSection, moveWorkspaceToSection, removeWorkspaceFromSidebar } = + const { createSection, hideWorkspaceInSidebar, moveWorkspaceToSection } = useDashboardSidebarState(); const [isRenaming, setIsRenaming] = useState(false); @@ -82,7 +82,7 @@ export function useDashboardSidebarWorkspaceItemActions({ }; const handleDeleted = () => { - removeWorkspaceFromSidebar(workspaceId); + hideWorkspaceInSidebar(workspaceId); }; const handleRemoveFromSidebar = () => { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/sidebarDefaultVisibility.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/sidebarDefaultVisibility.test.ts new file mode 100644 index 00000000000..51ccd349eb5 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/sidebarDefaultVisibility.test.ts @@ -0,0 +1,190 @@ +import { describe, expect, it } from "bun:test"; +import { + buildSidebarProjects, + type SidebarProjectRow, + shouldIncludeSidebarWorkspace, +} from "./sidebarDefaultVisibility"; + +const userId = "user-1"; + +describe("shouldIncludeSidebarWorkspace", () => { + it("includes workspaces with explicit local sidebar state", () => { + expect( + shouldIncludeSidebarWorkspace( + { + type: "main", + createdByUserId: null, + hasUserHostAccess: false, + hasLocalSidebarState: true, + }, + userId, + ), + ).toBe(true); + }); + + it("defaults current-user worktree workspaces visible when host is accessible", () => { + expect( + shouldIncludeSidebarWorkspace( + { + type: "worktree", + createdByUserId: userId, + hasUserHostAccess: true, + hasLocalSidebarState: false, + }, + userId, + ), + ).toBe(true); + }); + + it("does not default main, inaccessible, or other-user workspaces visible", () => { + expect( + shouldIncludeSidebarWorkspace( + { + type: "main", + createdByUserId: userId, + hasUserHostAccess: true, + hasLocalSidebarState: false, + }, + userId, + ), + ).toBe(false); + expect( + shouldIncludeSidebarWorkspace( + { + type: "worktree", + createdByUserId: userId, + hasUserHostAccess: false, + hasLocalSidebarState: false, + }, + userId, + ), + ).toBe(false); + expect( + shouldIncludeSidebarWorkspace( + { + type: "worktree", + createdByUserId: "user-2", + hasUserHostAccess: true, + hasLocalSidebarState: false, + }, + userId, + ), + ).toBe(false); + expect( + shouldIncludeSidebarWorkspace( + { + type: "worktree", + createdByUserId: null, + hasUserHostAccess: true, + hasLocalSidebarState: false, + }, + null, + ), + ).toBe(false); + }); +}); + +describe("buildSidebarProjects", () => { + const date = new Date("2026-01-01T00:00:00.000Z"); + const explicitProject: SidebarProjectRow = { + id: "project-explicit", + name: "Explicit", + slug: "explicit", + githubRepositoryId: null, + githubOwner: null, + githubRepoName: null, + iconUrl: null, + createdAt: date, + updatedAt: date, + isCollapsed: true, + tabOrder: 1, + }; + const { tabOrder: _explicitTabOrder, ...expectedExplicitProject } = + explicitProject; + + it("adds default project rows without replacing explicit projects", () => { + expect( + buildSidebarProjects( + [explicitProject], + [ + { + projectId: "project-explicit", + projectName: "Ignored", + projectSlug: "ignored", + projectGithubRepositoryId: "ignored-repo", + projectGithubOwner: "ignored-owner", + projectGithubRepoName: "ignored-name", + projectIconUrl: null, + projectCreatedAt: date, + projectUpdatedAt: date, + }, + { + projectId: "project-default", + projectName: "Default", + projectSlug: "default", + projectGithubRepositoryId: "repo-1", + projectGithubOwner: "owner", + projectGithubRepoName: "repo", + projectIconUrl: "https://example.com/icon.png", + projectCreatedAt: date, + projectUpdatedAt: date, + }, + ], + ), + ).toEqual([ + expectedExplicitProject, + { + id: "project-default", + name: "Default", + slug: "default", + githubRepositoryId: "repo-1", + githubOwner: "owner", + githubRepoName: "repo", + iconUrl: "https://example.com/icon.png", + createdAt: date, + updatedAt: date, + isCollapsed: false, + }, + ]); + }); + + it("sorts explicit projects by tab order and default projects by name after them", () => { + const laterProject: SidebarProjectRow = { + ...explicitProject, + id: "project-later", + name: "Later", + slug: "later", + tabOrder: 2, + }; + + expect( + buildSidebarProjects( + [laterProject, explicitProject], + [ + { + projectId: "project-z", + projectName: "Zulu", + projectSlug: "zulu", + projectGithubRepositoryId: null, + projectGithubOwner: null, + projectGithubRepoName: null, + projectIconUrl: null, + projectCreatedAt: date, + projectUpdatedAt: date, + }, + { + projectId: "project-a", + projectName: "Alpha", + projectSlug: "alpha", + projectGithubRepositoryId: null, + projectGithubOwner: null, + projectGithubRepoName: null, + projectIconUrl: null, + projectCreatedAt: date, + projectUpdatedAt: date, + }, + ], + ).map((project) => project.id), + ).toEqual(["project-explicit", "project-later", "project-a", "project-z"]); + }); +}); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/sidebarDefaultVisibility.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/sidebarDefaultVisibility.ts new file mode 100644 index 00000000000..e588f3d1b51 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarData/sidebarDefaultVisibility.ts @@ -0,0 +1,77 @@ +import type { + DashboardSidebarProject, + DashboardSidebarWorkspaceType, +} from "../../types"; + +export type SidebarProjectRow = Omit & { + tabOrder: number; +}; + +export interface SidebarWorkspaceVisibilitySource { + type: DashboardSidebarWorkspaceType; + createdByUserId: string | null; + hasUserHostAccess: boolean; + hasLocalSidebarState: boolean; +} + +export interface SidebarProjectWorkspaceSource { + projectId: string; + projectName: string; + projectSlug: string; + projectGithubRepositoryId: string | null; + projectGithubOwner: string | null | undefined; + projectGithubRepoName: string | null | undefined; + projectIconUrl: string | null; + projectCreatedAt: Date; + projectUpdatedAt: Date; +} + +export function shouldIncludeSidebarWorkspace( + workspace: SidebarWorkspaceVisibilitySource, + currentUserId: string | null, +): boolean { + if (workspace.hasLocalSidebarState) return true; + if (currentUserId === null) return false; + return ( + workspace.type === "worktree" && + workspace.createdByUserId === currentUserId && + workspace.hasUserHostAccess + ); +} + +export function buildSidebarProjects( + explicitProjects: readonly SidebarProjectRow[], + workspaces: readonly SidebarProjectWorkspaceSource[], +): Array> { + const explicitProjectIds = new Set( + explicitProjects.map((project) => project.id), + ); + const defaultProjects = new Map(); + + for (const workspace of workspaces) { + if (explicitProjectIds.has(workspace.projectId)) continue; + if (defaultProjects.has(workspace.projectId)) continue; + + defaultProjects.set(workspace.projectId, { + id: workspace.projectId, + name: workspace.projectName, + slug: workspace.projectSlug, + githubRepositoryId: workspace.projectGithubRepositoryId, + githubOwner: workspace.projectGithubOwner ?? null, + githubRepoName: workspace.projectGithubRepoName ?? null, + iconUrl: workspace.projectIconUrl, + createdAt: workspace.projectCreatedAt, + updatedAt: workspace.projectUpdatedAt, + isCollapsed: false, + tabOrder: Number.MAX_SAFE_INTEGER, + }); + } + + return [...explicitProjects, ...defaultProjects.values()] + .sort((left, right) => { + const orderDelta = left.tabOrder - right.tabOrder; + if (orderDelta !== 0) return orderDelta; + return left.name.localeCompare(right.name); + }) + .map(({ tabOrder: _tabOrder, ...project }) => project); +} 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 646b1de8f3e..2fde89d8163 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 @@ -3,6 +3,7 @@ import { useLiveQuery } from "@tanstack/react-db"; import { useQueries, useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useMemo, useRef } from "react"; import { useRelayUrl } from "renderer/hooks/useRelayUrl"; +import { authClient } from "renderer/lib/auth-client"; import { getHostServiceClientByUrl } from "renderer/lib/host-service-client"; import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; @@ -20,6 +21,11 @@ import { getDashboardSidebarPullRequestQueryKey, type PullRequestQueryTarget, } from "./derivePullRequestQueryTargets"; +import { + buildSidebarProjects, + type SidebarProjectRow, + shouldIncludeSidebarWorkspace, +} from "./sidebarDefaultVisibility"; const MAIN_WORKSPACE_TAB_ORDER = Number.MIN_SAFE_INTEGER; @@ -126,6 +132,8 @@ function useStableDashboardSidebarProjects( export function useDashboardSidebarData() { const collections = useCollections(); const { machineId, activeHostUrl } = useLocalHostService(); + const { data: session } = authClient.useSession(); + const currentUserId = session?.user?.id ?? null; const relayUrl = useRelayUrl(); const { toggleProjectCollapsed } = useDashboardSidebarState(); const queryClient = useQueryClient(); @@ -175,20 +183,11 @@ export function useDashboardSidebarData() { createdAt: projects.createdAt, updatedAt: projects.updatedAt, isCollapsed: sidebarProjects.isCollapsed, + tabOrder: sidebarProjects.tabOrder, })), [collections], ); - const sidebarProjects = useMemo( - () => - rawSidebarProjects.map((project) => ({ - ...project, - githubOwner: project.githubOwner ?? null, - githubRepoName: project.githubRepoName ?? null, - })), - [rawSidebarProjects], - ); - const { data: sidebarSections = [] } = useLiveQuery( (q) => q @@ -209,41 +208,101 @@ export function useDashboardSidebarData() { const { data: rawSidebarWorkspaces = [] } = useLiveQuery( (q) => q - .from({ sidebarWorkspaces: collections.v2WorkspaceLocalState }) + .from({ workspaces: collections.v2Workspaces }) .innerJoin( - { workspaces: collections.v2Workspaces }, - ({ sidebarWorkspaces, workspaces }) => - eq(sidebarWorkspaces.workspaceId, workspaces.id), + { projects: collections.v2Projects }, + ({ workspaces, projects }) => eq(workspaces.projectId, projects.id), ) - .orderBy( - ({ sidebarWorkspaces }) => sidebarWorkspaces.sidebarState.tabOrder, - "asc", + .leftJoin( + { repos: collections.githubRepositories }, + ({ projects, repos }) => eq(projects.githubRepositoryId, repos.id), + ) + .leftJoin( + { sidebarWorkspaces: collections.v2WorkspaceLocalState }, + ({ workspaces, sidebarWorkspaces }) => + eq(sidebarWorkspaces.workspaceId, workspaces.id), ) - .select(({ sidebarWorkspaces, workspaces }) => ({ + .select(({ sidebarWorkspaces, workspaces, projects, repos }) => ({ id: workspaces.id, - projectId: sidebarWorkspaces.sidebarState.projectId, + organizationId: workspaces.organizationId, + workspaceProjectId: workspaces.projectId, + sidebarProjectId: sidebarWorkspaces?.sidebarState.projectId ?? null, + projectName: projects.name, + projectSlug: projects.slug, + projectGithubRepositoryId: projects.githubRepositoryId, + projectGithubOwner: repos?.owner ?? null, + projectGithubRepoName: repos?.name ?? null, + projectIconUrl: projects.iconUrl, + projectCreatedAt: projects.createdAt, + projectUpdatedAt: projects.updatedAt, hostId: workspaces.hostId, type: workspaces.type, + createdByUserId: workspaces.createdByUserId, name: workspaces.name, branch: workspaces.branch, taskId: workspaces.taskId, createdAt: workspaces.createdAt, updatedAt: workspaces.updatedAt, isSynced: workspaces.$synced, - tabOrder: sidebarWorkspaces.sidebarState.tabOrder, - sectionId: sidebarWorkspaces.sidebarState.sectionId, - isHidden: sidebarWorkspaces.sidebarState.isHidden, + hasLocalSidebarState: sidebarWorkspaces?.workspaceId != null, + tabOrder: sidebarWorkspaces?.sidebarState.tabOrder ?? null, + sectionId: sidebarWorkspaces?.sidebarState.sectionId ?? null, + isHidden: sidebarWorkspaces?.sidebarState.isHidden ?? false, })), [collections], ); + + // Separate query — TanStack DB join conditions only accept a single + // equality, so we can't filter `v2UsersHosts` by (org, host, user) inline. + const { data: currentUserHosts = [] } = useLiveQuery( + (q) => + q + .from({ userHosts: collections.v2UsersHosts }) + .where(({ userHosts }) => eq(userHosts.userId, currentUserId ?? "")) + .select(({ userHosts }) => ({ + organizationId: userHosts.organizationId, + hostId: userHosts.hostId, + })), + [collections, currentUserId], + ); + const userHostAccessKeys = useMemo( + () => + new Set( + currentUserHosts.map((row) => `${row.organizationId}:${row.hostId}`), + ), + [currentUserHosts], + ); + const rawSidebarWorkspacesWithHostStatus = useMemo( () => - rawSidebarWorkspaces.map((workspace) => ({ - ...workspace, - hostIsOnline: hostsByMachineId.get(workspace.hostId)?.isOnline ?? false, - pendingTransaction: workspaceTransactionsById[workspace.id] ?? null, - })), - [hostsByMachineId, rawSidebarWorkspaces, workspaceTransactionsById], + rawSidebarWorkspaces + .filter((workspace) => + shouldIncludeSidebarWorkspace( + { + ...workspace, + hasUserHostAccess: userHostAccessKeys.has( + `${workspace.organizationId}:${workspace.hostId}`, + ), + }, + currentUserId, + ), + ) + .map((workspace) => ({ + ...workspace, + projectId: workspace.sidebarProjectId ?? workspace.workspaceProjectId, + tabOrder: + workspace.tabOrder ?? new Date(workspace.createdAt).getTime(), + hostIsOnline: + hostsByMachineId.get(workspace.hostId)?.isOnline ?? false, + pendingTransaction: workspaceTransactionsById[workspace.id] ?? null, + })), + [ + currentUserId, + hostsByMachineId, + rawSidebarWorkspaces, + userHostAccessKeys, + workspaceTransactionsById, + ], ); const sidebarWorkspaces = useMemo( @@ -252,10 +311,31 @@ export function useDashboardSidebarData() { ); const localStateWorkspaceIds = useMemo( - () => new Set(rawSidebarWorkspaces.map((workspace) => workspace.id)), + () => + new Set( + rawSidebarWorkspaces + .filter((workspace) => workspace.hasLocalSidebarState) + .map((workspace) => workspace.id), + ), [rawSidebarWorkspaces], ); + const sidebarProjects = useMemo(() => { + const explicitProjects: SidebarProjectRow[] = rawSidebarProjects.map( + (project) => ({ + ...project, + githubOwner: project.githubOwner ?? null, + githubRepoName: project.githubRepoName ?? null, + }), + ); + return buildSidebarProjects(explicitProjects, sidebarWorkspaces); + }, [rawSidebarProjects, sidebarWorkspaces]); + + const sidebarProjectIds = useMemo( + () => new Set(sidebarProjects.map((project) => project.id)), + [sidebarProjects], + ); + const { data: rawLocalMainWorkspaces = [] } = useLiveQuery( (q) => q @@ -305,9 +385,6 @@ export function useDashboardSidebarData() { ]); const visibleSidebarWorkspaces = useMemo(() => { - const sidebarProjectIds = new Set( - sidebarProjects.map((project) => project.id), - ); const autoLocalMainWorkspaces = localMainWorkspaces.filter( (workspace) => !localStateWorkspaceIds.has(workspace.id) && @@ -320,7 +397,7 @@ export function useDashboardSidebarData() { localMainWorkspaces, localStateWorkspaceIds, machineId, - sidebarProjects, + sidebarProjectIds, sidebarWorkspaces, ]); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx index 0634c2439ee..76a2318c948 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/layout.tsx @@ -53,7 +53,7 @@ function DashboardLayout() { const openNewWorkspaceModal = useOpenNewWorkspaceModal(); const isV2CloudEnabled = useIsV2CloudEnabled(); const collections = useCollections(); - const { removeWorkspaceFromSidebar } = useDashboardSidebarState(); + const { hideWorkspaceInSidebar } = useDashboardSidebarState(); useDevSeedV2Sidebar(); // Get current workspace from route to pre-select project in new workspace modal const matchRoute = useMatchRoute(); @@ -224,7 +224,7 @@ function DashboardLayout() { ); }} onDeleted={() => { - removeWorkspaceFromSidebar(deleteTarget.workspaceId); + hideWorkspaceInSidebar(deleteTarget.workspaceId); setDeleteTarget(null); }} /> diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceMissingWorktreeState/WorkspaceMissingWorktreeState.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceMissingWorktreeState/WorkspaceMissingWorktreeState.tsx index 8e8dc3080b9..402bc32ab54 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceMissingWorktreeState/WorkspaceMissingWorktreeState.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceMissingWorktreeState/WorkspaceMissingWorktreeState.tsx @@ -3,6 +3,7 @@ import { Link } from "@tanstack/react-router"; import { ArrowRight, FolderX, RefreshCw, Trash2 } from "lucide-react"; import { useState } from "react"; import { DashboardSidebarDeleteDialog } from "renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarDeleteDialog"; +import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; interface WorkspaceMissingWorktreeStateProps { workspaceId: string; @@ -22,6 +23,7 @@ export function WorkspaceMissingWorktreeState({ isRefreshing = false, }: WorkspaceMissingWorktreeStateProps) { const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const { hideWorkspaceInSidebar } = useDashboardSidebarState(); const displayName = workspaceName || branch; return ( @@ -107,6 +109,9 @@ export function WorkspaceMissingWorktreeState({ workspaceName={displayName} open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen} + onDeleted={() => { + hideWorkspaceInSidebar(workspaceId); + }} /> diff --git a/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts b/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts index 829d82a06ba..76b6f519db1 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/hooks/useDashboardSidebarState/useDashboardSidebarState.ts @@ -25,6 +25,7 @@ type ProjectTopLevelCollections = Pick< AppCollections, "v2SidebarSections" | "v2WorkspaceLocalState" >; +type HiddenWorkspaceCollections = Pick; function compareProjectTopLevelItems( left: ProjectTopLevelItem, @@ -82,6 +83,36 @@ function createEmptyPaneLayout(): WorkspaceState { } satisfies WorkspaceState; } +function writeHiddenWorkspaceSidebarState( + collections: HiddenWorkspaceCollections, + workspaceId: string, + projectId: string, +): void { + const existing = collections.v2WorkspaceLocalState.get(workspaceId); + + if (existing) { + collections.v2WorkspaceLocalState.update(workspaceId, (draft) => { + draft.sidebarState.projectId = projectId; + draft.sidebarState.sectionId = null; + draft.sidebarState.isHidden = true; + draft.paneLayout = createEmptyPaneLayout(); + }); + return; + } + + collections.v2WorkspaceLocalState.insert({ + workspaceId, + createdAt: new Date(), + sidebarState: { + projectId, + tabOrder: 0, + sectionId: null, + isHidden: true, + }, + paneLayout: createEmptyPaneLayout(), + }); +} + /** * Rewrites the flat top-level project lane. Workspace items are explicitly * ungrouped by setting sidebarState.projectId and clearing sidebarState.sectionId. @@ -443,60 +474,47 @@ export function useDashboardSidebarState() { [collections], ); - const removeWorkspaceFromSidebar = useCallback( - (workspaceId: string) => { - const workspace = collections.v2WorkspaceLocalState.get(workspaceId); - if (!workspace) return; - cleanupWorkspacePaneRuntimes([workspace]); - collections.v2WorkspaceLocalState.delete(workspaceId); - }, - [collections], - ); - const hideWorkspaceInSidebar = useCallback( - (workspaceId: string, projectId: string) => { - const workspace = collections.v2WorkspaceLocalState.get(workspaceId); - if (!workspace) { - collections.v2WorkspaceLocalState.insert({ - workspaceId, - createdAt: new Date(), - sidebarState: { - projectId, - tabOrder: 0, - sectionId: null, - isHidden: true, - }, - paneLayout: createEmptyPaneLayout(), - }); - return; + (workspaceId: string) => { + const localState = collections.v2WorkspaceLocalState.get(workspaceId); + if (localState) { + cleanupWorkspacePaneRuntimes([localState]); } - - cleanupWorkspacePaneRuntimes([workspace]); - collections.v2WorkspaceLocalState.update(workspaceId, (draft) => { - draft.sidebarState.projectId = projectId; - draft.sidebarState.sectionId = null; - draft.sidebarState.isHidden = true; - draft.paneLayout = createEmptyPaneLayout(); - }); + const projectId = + localState?.sidebarState.projectId ?? + collections.v2Workspaces.get(workspaceId)?.projectId; + // If neither row exists, the workspace is already gone — the data live + // query will drop it on its own, so we don't need to write a tombstone. + if (!projectId) return; + writeHiddenWorkspaceSidebarState(collections, workspaceId, projectId); }, [collections], ); const removeProjectFromSidebar = useCallback( (projectId: string) => { + const workspaceIds = new Set([ + ...Array.from(collections.v2WorkspaceLocalState.state.values()) + .filter((item) => item.sidebarState.projectId === projectId) + .map((item) => item.workspaceId), + ...Array.from(collections.v2Workspaces.state.values()) + .filter((item) => item.projectId === projectId) + .map((item) => item.id), + ]); const workspaceRows = Array.from( collections.v2WorkspaceLocalState.state.values(), - ).filter((item) => item.sidebarState.projectId === projectId); - const workspaceIds = workspaceRows.map((item) => item.workspaceId); + ).filter((item) => workspaceIds.has(item.workspaceId)); const sectionIds = Array.from( collections.v2SidebarSections.state.values(), ) .filter((item) => item.projectId === projectId) .map((item) => item.sectionId); - if (workspaceIds.length > 0) { + if (workspaceRows.length > 0) { cleanupWorkspacePaneRuntimes(workspaceRows); - collections.v2WorkspaceLocalState.delete(workspaceIds); + } + for (const workspaceId of workspaceIds) { + writeHiddenWorkspaceSidebarState(collections, workspaceId, projectId); } if (sectionIds.length > 0) { collections.v2SidebarSections.delete(sectionIds); @@ -518,7 +536,6 @@ export function useDashboardSidebarState() { moveWorkspaceToSectionAtIndex, removeProjectFromSidebar, reorderProjectChildren, - removeWorkspaceFromSidebar, reorderProjects, reorderWorkspaces, renameSection,