diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts index 278072e61cd..5afb68f0fa8 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts @@ -826,6 +826,7 @@ export const createWorkspacesRouter = () => { name: string; color: string; tabOrder: number; + mainRepoPath: string; }; workspaces: Array<{ id: string; @@ -852,6 +853,7 @@ export const createWorkspacesRouter = () => { color: project.color, // biome-ignore lint/style/noNonNullAssertion: filter guarantees tabOrder is not null tabOrder: project.tabOrder!, + mainRepoPath: project.mainRepoPath, }, workspaces: [], }); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx index ec36067e449..64bfb467109 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx @@ -1,31 +1,104 @@ +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, + ContextMenuTrigger, +} from "@superset/ui/context-menu"; +import { toast } from "@superset/ui/sonner"; import { cn } from "@superset/ui/utils"; +import { LuFolderOpen, LuSettings, LuX } from "react-icons/lu"; +import { trpc } from "renderer/lib/trpc"; +import { useOpenSettings } from "renderer/stores/app-state"; interface ProjectHeaderProps { + projectId: string; projectName: string; + mainRepoPath: string; isCollapsed: boolean; onToggleCollapse: () => void; workspaceCount: number; } export function ProjectHeader({ + projectId, projectName, + mainRepoPath, isCollapsed, onToggleCollapse, workspaceCount, }: ProjectHeaderProps) { + const utils = trpc.useUtils(); + const openSettings = useOpenSettings(); + + const closeProject = trpc.projects.close.useMutation({ + onSuccess: (data) => { + utils.workspaces.getAllGrouped.invalidate(); + utils.workspaces.getActive.invalidate(); + utils.projects.getRecents.invalidate(); + if (data.terminalWarning) { + toast.warning(data.terminalWarning); + } + }, + onError: (error) => { + toast.error(`Failed to close project: ${error.message}`); + }, + }); + + const openInFinder = trpc.external.openInFinder.useMutation({ + onError: (error) => toast.error(`Failed to open: ${error.message}`), + }); + + const handleCloseProject = () => { + closeProject.mutate({ id: projectId }); + }; + + const handleOpenInFinder = () => { + openInFinder.mutate(mainRepoPath); + }; + + const handleOpenSettings = () => { + openSettings("project"); + }; + return ( - + + + + + + + + Open in Finder + + + + Project Settings + + + + + {closeProject.isPending ? "Closing..." : "Close Project"} + + + ); } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx index fe109d9971e..a0e4152f94f 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectSection.tsx @@ -28,6 +28,7 @@ interface Workspace { interface ProjectSectionProps { projectId: string; projectName: string; + mainRepoPath: string; workspaces: Workspace[]; activeWorkspaceId: string | null; /** Base index for keyboard shortcuts (0-based) */ @@ -37,6 +38,7 @@ interface ProjectSectionProps { export function ProjectSection({ projectId, projectName, + mainRepoPath, workspaces, activeWorkspaceId, shortcutBaseIndex, @@ -67,7 +69,9 @@ export function ProjectSection({ return (
toggleProjectCollapsed(projectId)} workspaceCount={workspaces.length} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx index c7fb51da427..81340b17eaf 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx @@ -26,6 +26,7 @@ import { useWorkspaceDeleteHandler, } from "renderer/react-query/workspaces"; import { useWorkspaceRename } from "renderer/screens/main/hooks/useWorkspaceRename"; +import { useCloseWorkspacesList } from "renderer/stores/app-state"; import { useTabsStore } from "renderer/stores/tabs/store"; import { extractPaneIdsFromLayout } from "renderer/stores/tabs/utils"; import { @@ -72,6 +73,7 @@ export function WorkspaceListItem({ const isBranchWorkspace = type === "branch"; const setActiveWorkspace = useSetActiveWorkspace(); const reorderWorkspaces = useReorderWorkspaces(); + const closeWorkspacesList = useCloseWorkspacesList(); const [hasHovered, setHasHovered] = useState(false); const rename = useWorkspaceRename(id, name); const tabs = useTabsStore((s) => s.tabs); @@ -120,6 +122,8 @@ export function WorkspaceListItem({ if (!rename.isRenaming) { setActiveWorkspace.mutate({ id }); clearWorkspaceAttention(id); + // Close workspaces list view if open, to show the workspace's terminal view + closeWorkspacesList(); } }; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx index 805144283ce..4ffc0650a78 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx @@ -31,6 +31,7 @@ export function WorkspaceSidebar() { key={group.project.id} projectId={group.project.id} projectName={group.project.name} + mainRepoPath={group.project.mainRepoPath} workspaces={group.workspaces} activeWorkspaceId={activeWorkspaceId} shortcutBaseIndex={projectShortcutIndices[index]} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarHeader/WorkspaceSidebarHeader.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarHeader/WorkspaceSidebarHeader.tsx index a5aff06d598..9103919cf38 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarHeader/WorkspaceSidebarHeader.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarHeader/WorkspaceSidebarHeader.tsx @@ -1,17 +1,44 @@ +import { cn } from "@superset/ui/utils"; import { LuLayers } from "react-icons/lu"; +import { + useCloseWorkspacesList, + useCurrentView, + useOpenWorkspacesList, +} from "renderer/stores/app-state"; import { NewWorkspaceButton } from "./NewWorkspaceButton"; export function WorkspaceSidebarHeader() { + const currentView = useCurrentView(); + const openWorkspacesList = useOpenWorkspacesList(); + const closeWorkspacesList = useCloseWorkspacesList(); + + const isWorkspacesListOpen = currentView === "workspaces-list"; + + const handleClick = () => { + if (isWorkspacesListOpen) { + closeWorkspacesList(); + } else { + openWorkspacesList(); + } + }; + return (
-
+
+ Workspaces +
); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx new file mode 100644 index 00000000000..8f06de0e4f7 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx @@ -0,0 +1,147 @@ +import { cn } from "@superset/ui/utils"; +import { useState } from "react"; +import { + LuArrowRight, + LuGitBranch, + LuGitFork, + LuRotateCw, +} from "react-icons/lu"; +import { trpc } from "renderer/lib/trpc"; +import type { WorkspaceItem } from "../types"; +import { getRelativeTime } from "../utils"; + +const GITHUB_STATUS_STALE_TIME = 5 * 60 * 1000; // 5 minutes + +interface WorkspaceRowProps { + workspace: WorkspaceItem; + isActive: boolean; + onSwitch: () => void; + onReopen: () => void; + isOpening?: boolean; +} + +export function WorkspaceRow({ + workspace, + isActive, + onSwitch, + onReopen, + isOpening, +}: WorkspaceRowProps) { + const isBranch = workspace.type === "branch"; + const [hasHovered, setHasHovered] = useState(false); + + // Lazy-load GitHub status on hover to avoid N+1 queries + const { data: githubStatus } = trpc.workspaces.getGitHubStatus.useQuery( + { workspaceId: workspace.workspaceId ?? "" }, + { + enabled: + hasHovered && workspace.type === "worktree" && !!workspace.workspaceId, + staleTime: GITHUB_STATUS_STALE_TIME, + }, + ); + + const pr = githubStatus?.pr; + const showDiffStats = pr && (pr.additions > 0 || pr.deletions > 0); + + const timeText = workspace.isOpen + ? `Opened ${getRelativeTime(workspace.lastOpenedAt)}` + : `Created ${getRelativeTime(workspace.createdAt)}`; + + const handleClick = () => { + if (workspace.isOpen) { + onSwitch(); + } else { + onReopen(); + } + }; + + return ( + + ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/index.ts new file mode 100644 index 00000000000..0a45a4a8467 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/index.ts @@ -0,0 +1 @@ +export * from "./WorkspaceRow"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspacesListView.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspacesListView.tsx new file mode 100644 index 00000000000..8a3d50a2cbb --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspacesListView.tsx @@ -0,0 +1,282 @@ +import { Button } from "@superset/ui/button"; +import { Input } from "@superset/ui/input"; +import { toast } from "@superset/ui/sonner"; +import { cn } from "@superset/ui/utils"; +import { useMemo, useState } from "react"; +import { LuSearch, LuX } from "react-icons/lu"; +import { trpc } from "renderer/lib/trpc"; +import { useSetActiveWorkspace } from "renderer/react-query/workspaces"; +import { useCloseWorkspacesList } from "renderer/stores/app-state"; +import type { FilterMode, ProjectGroup, WorkspaceItem } from "./types"; +import { WorkspaceRow } from "./WorkspaceRow"; + +const FILTER_OPTIONS: { value: FilterMode; label: string }[] = [ + { value: "all", label: "All" }, + { value: "active", label: "Active" }, + { value: "closed", label: "Closed" }, +]; + +export function WorkspacesListView() { + const [searchQuery, setSearchQuery] = useState(""); + const [filterMode, setFilterMode] = useState("all"); + const utils = trpc.useUtils(); + + // Fetch all data + const { data: groups = [] } = trpc.workspaces.getAllGrouped.useQuery(); + const { data: allProjects = [] } = trpc.projects.getRecents.useQuery(); + const { data: activeWorkspace } = trpc.workspaces.getActive.useQuery(); + + // Fetch worktrees for all projects + const worktreeQueries = trpc.useQueries((t) => + allProjects.map((project) => + t.workspaces.getWorktreesByProject({ projectId: project.id }), + ), + ); + + const setActiveWorkspace = useSetActiveWorkspace(); + const closeWorkspacesList = useCloseWorkspacesList(); + + const openWorktree = trpc.workspaces.openWorktree.useMutation({ + onSuccess: () => { + utils.workspaces.getAllGrouped.invalidate(); + utils.workspaces.getActive.invalidate(); + closeWorkspacesList(); + }, + onError: (error) => { + toast.error(`Failed to open workspace: ${error.message}`); + }, + }); + + // Combine open workspaces and closed worktrees into a single list + const allItems = useMemo(() => { + const items: WorkspaceItem[] = []; + + // First, add all open workspaces from groups + for (const group of groups) { + for (const ws of group.workspaces) { + items.push({ + uniqueId: ws.id, + workspaceId: ws.id, + worktreeId: null, + projectId: ws.projectId, + projectName: group.project.name, + worktreePath: ws.worktreePath, + type: ws.type, + branch: ws.branch, + name: ws.name, + lastOpenedAt: ws.lastOpenedAt, + createdAt: ws.createdAt, + isUnread: ws.isUnread, + isOpen: true, + }); + } + } + + // Add closed worktrees (those without active workspaces) + for (let i = 0; i < allProjects.length; i++) { + const project = allProjects[i]; + const worktrees = worktreeQueries[i]?.data; + + if (!worktrees) continue; + + for (const wt of worktrees) { + // Skip if this worktree has an active workspace + if (wt.hasActiveWorkspace) continue; + + items.push({ + uniqueId: `wt-${wt.id}`, + workspaceId: null, + worktreeId: wt.id, + projectId: project.id, + projectName: project.name, + worktreePath: wt.path, + type: "worktree", + branch: wt.branch, + name: wt.branch, + lastOpenedAt: wt.createdAt, + createdAt: wt.createdAt, + isUnread: false, + isOpen: false, + }); + } + } + + return items; + }, [groups, allProjects, worktreeQueries]); + + // Filter by search query and filter mode + const filteredItems = useMemo(() => { + let items = allItems; + + // Apply filter mode + if (filterMode === "active") { + items = items.filter((ws) => ws.isOpen); + } else if (filterMode === "closed") { + items = items.filter((ws) => !ws.isOpen); + } + + // Apply search filter + if (searchQuery.trim()) { + const query = searchQuery.toLowerCase(); + items = items.filter( + (ws) => + ws.name.toLowerCase().includes(query) || + ws.projectName.toLowerCase().includes(query) || + ws.branch.toLowerCase().includes(query), + ); + } + + return items; + }, [allItems, searchQuery, filterMode]); + + // Group by project + const projectGroups = useMemo(() => { + const groupsMap = new Map(); + + for (const item of filteredItems) { + if (!groupsMap.has(item.projectId)) { + groupsMap.set(item.projectId, { + projectId: item.projectId, + projectName: item.projectName, + workspaces: [], + }); + } + groupsMap.get(item.projectId)?.workspaces.push(item); + } + + // Sort workspaces within each group: active first, then by lastOpenedAt + for (const group of groupsMap.values()) { + group.workspaces.sort((a, b) => { + // Active workspaces first + if (a.isOpen !== b.isOpen) return a.isOpen ? -1 : 1; + // Then by most recently opened/created + return b.lastOpenedAt - a.lastOpenedAt; + }); + } + + // Sort groups by most recent activity + return Array.from(groupsMap.values()).sort((a, b) => { + const aRecent = Math.max(...a.workspaces.map((w) => w.lastOpenedAt)); + const bRecent = Math.max(...b.workspaces.map((w) => w.lastOpenedAt)); + return bRecent - aRecent; + }); + }, [filteredItems]); + + const handleSwitch = (item: WorkspaceItem) => { + if (item.workspaceId) { + setActiveWorkspace.mutate({ id: item.workspaceId }); + closeWorkspacesList(); + } + }; + + const handleReopen = (item: WorkspaceItem) => { + if (item.worktreeId) { + openWorktree.mutate({ worktreeId: item.worktreeId }); + } + }; + + // Count stats for filter badges + const activeCount = allItems.filter((w) => w.isOpen).length; + const closedCount = allItems.filter((w) => !w.isOpen).length; + + return ( +
+ {/* Header */} +
+ {/* Filter toggle */} +
+ {FILTER_OPTIONS.map((option) => { + const count = + option.value === "all" + ? allItems.length + : option.value === "active" + ? activeCount + : closedCount; + return ( + + ); + })} +
+ + {/* Search */} +
+ + setSearchQuery(e.target.value)} + className="pl-9 h-8 bg-background/50" + /> +
+ + {/* Close button */} + +
+ + {/* Workspaces list grouped by project */} +
+ {projectGroups.map((group) => ( +
+ {/* Project header */} +
+ + {group.projectName} + + + {group.workspaces.length} + +
+ + {/* Workspaces in this project */} + {group.workspaces.map((ws) => ( + handleSwitch(ws)} + onReopen={() => handleReopen(ws)} + isOpening={ + openWorktree.isPending && + openWorktree.variables?.worktreeId === ws.worktreeId + } + /> + ))} +
+ ))} + + {filteredItems.length === 0 && ( +
+ {searchQuery + ? "No workspaces match your search" + : filterMode === "active" + ? "No active workspaces" + : filterMode === "closed" + ? "No closed workspaces" + : "No workspaces yet"} +
+ )} +
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/index.ts new file mode 100644 index 00000000000..7d55b16cb61 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/index.ts @@ -0,0 +1 @@ +export { WorkspacesListView } from "./WorkspacesListView"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/types.ts b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/types.ts new file mode 100644 index 00000000000..587000def29 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/types.ts @@ -0,0 +1,26 @@ +export interface WorkspaceItem { + // Unique identifier - either workspace id or worktree id for closed ones + uniqueId: string; + // If open, this is the workspace id + workspaceId: string | null; + // For closed worktrees, this is the worktree id + worktreeId: string | null; + projectId: string; + projectName: string; + worktreePath: string; + type: "worktree" | "branch"; + branch: string; + name: string; + lastOpenedAt: number; + createdAt: number; + isUnread: boolean; + isOpen: boolean; +} + +export interface ProjectGroup { + projectId: string; + projectName: string; + workspaces: WorkspaceItem[]; +} + +export type FilterMode = "all" | "active" | "closed"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/utils.ts b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/utils.ts new file mode 100644 index 00000000000..1640b168863 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/utils.ts @@ -0,0 +1,42 @@ +// Time unit constants (in milliseconds) +const MS_PER_SECOND = 1000; +const MS_PER_MINUTE = MS_PER_SECOND * 60; +const MS_PER_HOUR = MS_PER_MINUTE * 60; +const MS_PER_DAY = MS_PER_HOUR * 24; + +// Time threshold constants (in their respective units) +const MINUTES_PER_HOUR = 60; +const HOURS_PER_DAY = 24; +const DAYS_PER_WEEK = 7; +const DAYS_PER_MONTH = 30; +const DAYS_PER_YEAR = 365; + +// Relative time display thresholds (in days) +const TWO_WEEKS_DAYS = 14; +const TWO_MONTHS_DAYS = 60; + +/** + * Returns a human-readable relative time string + * e.g., "2 hours ago", "yesterday", "3 days ago" + */ +export function getRelativeTime(timestamp: number): string { + const now = Date.now(); + const diff = now - timestamp; + + const minutes = Math.floor(diff / MS_PER_MINUTE); + const hours = Math.floor(diff / MS_PER_HOUR); + const days = Math.floor(diff / MS_PER_DAY); + + if (minutes < 1) return "just now"; + if (minutes < MINUTES_PER_HOUR) return `${minutes}m ago`; + if (hours < HOURS_PER_DAY) return `${hours}h ago`; + if (days === 1) return "yesterday"; + if (days < DAYS_PER_WEEK) return `${days} days ago`; + if (days < TWO_WEEKS_DAYS) return "1 week ago"; + if (days < DAYS_PER_MONTH) + return `${Math.floor(days / DAYS_PER_WEEK)} weeks ago`; + if (days < TWO_MONTHS_DAYS) return "1 month ago"; + if (days < DAYS_PER_YEAR) + return `${Math.floor(days / DAYS_PER_MONTH)} months ago`; + return "over a year ago"; +} diff --git a/apps/desktop/src/renderer/screens/main/index.tsx b/apps/desktop/src/renderer/screens/main/index.tsx index e153796a46c..e339381a8ec 100644 --- a/apps/desktop/src/renderer/screens/main/index.tsx +++ b/apps/desktop/src/renderer/screens/main/index.tsx @@ -30,6 +30,7 @@ import { TasksView } from "./components/TasksView"; import { TopBar } from "./components/TopBar"; import { WorkspaceInitEffects } from "./components/WorkspaceInitEffects"; import { ResizableWorkspaceSidebar } from "./components/WorkspaceSidebar"; +import { WorkspacesListView } from "./components/WorkspacesListView"; import { WorkspaceView } from "./components/WorkspaceView"; function LoadingSpinner() { @@ -295,6 +296,9 @@ export function MainScreen() { if (currentView === "tasks" && hasTasksAccess) { return ; } + if (currentView === "workspaces-list") { + return ; + } return ; }; diff --git a/apps/desktop/src/renderer/stores/app-state.ts b/apps/desktop/src/renderer/stores/app-state.ts index 296752c34d8..ba6ffcd1cc9 100644 --- a/apps/desktop/src/renderer/stores/app-state.ts +++ b/apps/desktop/src/renderer/stores/app-state.ts @@ -1,7 +1,7 @@ import { create } from "zustand"; import { devtools } from "zustand/middleware"; -export type AppView = "workspace" | "settings" | "tasks"; +export type AppView = "workspace" | "settings" | "tasks" | "workspaces-list"; export type SettingsSection = | "account" | "project" @@ -16,6 +16,7 @@ interface AppState { currentView: AppView; isSettingsTabOpen: boolean; isTasksTabOpen: boolean; + isWorkspacesListOpen: boolean; settingsSection: SettingsSection; setView: (view: AppView) => void; openSettings: (section?: SettingsSection) => void; @@ -24,6 +25,8 @@ interface AppState { setSettingsSection: (section: SettingsSection) => void; openTasks: () => void; closeTasks: () => void; + openWorkspacesList: () => void; + closeWorkspacesList: () => void; } export const useAppStore = create()( @@ -32,6 +35,7 @@ export const useAppStore = create()( currentView: "workspace", isSettingsTabOpen: false, isTasksTabOpen: false, + isWorkspacesListOpen: false, settingsSection: "project", setView: (view) => { @@ -65,6 +69,14 @@ export const useAppStore = create()( closeTasks: () => { set({ currentView: "workspace", isTasksTabOpen: false }); }, + + openWorkspacesList: () => { + set({ currentView: "workspaces-list", isWorkspacesListOpen: true }); + }, + + closeWorkspacesList: () => { + set({ currentView: "workspace", isWorkspacesListOpen: false }); + }, }), { name: "AppStore" }, ), @@ -87,3 +99,7 @@ export const useCloseSettingsTab = () => useAppStore((state) => state.closeSettingsTab); export const useOpenTasks = () => useAppStore((state) => state.openTasks); export const useCloseTasks = () => useAppStore((state) => state.closeTasks); +export const useOpenWorkspacesList = () => + useAppStore((state) => state.openWorkspacesList); +export const useCloseWorkspacesList = () => + useAppStore((state) => state.closeWorkspacesList);