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 (
-
- {projectName}
- {workspaceCount}
-
+
+
+
+ {projectName}
+
+ {workspaceCount}
+
+
+
+
+
+
+ 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 (
);
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 (
+ !hasHovered && setHasHovered(true)}
+ className={cn(
+ "flex items-center gap-3 w-full px-4 py-2 group text-left",
+ "hover:bg-background/50 transition-colors",
+ isActive && "bg-background/70",
+ isOpening && "opacity-50 cursor-wait",
+ )}
+ >
+ {/* Icon */}
+
+ {isBranch ? (
+
+ ) : (
+
+ )}
+
+
+ {/* Workspace/branch name */}
+
+ {workspace.name}
+
+
+ {/* Active indicator */}
+ {isActive && (
+
+ )}
+
+ {/* Unread indicator */}
+ {workspace.isUnread && !isActive && (
+
+
+
+
+ )}
+
+ {/* Diff stats */}
+ {showDiffStats && (
+
+ +{pr.additions}
+ -{pr.deletions}
+
+ )}
+
+ {/* Spacer */}
+
+
+ {/* Time context */}
+
+ {timeText}
+
+
+ {/* Action indicator - visible on hover */}
+
+ {isOpening ? (
+ <>
+
+ Opening...
+ >
+ ) : workspace.isOpen ? (
+ <>
+ Switch to
+
+ >
+ ) : (
+ <>
+ Reopen
+
+ >
+ )}
+
+
+ );
+}
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 (
+ setFilterMode(option.value)}
+ className={cn(
+ "px-2 py-1 text-xs rounded transition-colors",
+ filterMode === option.value
+ ? "bg-accent text-foreground"
+ : "text-foreground/60 hover:text-foreground",
+ )}
+ >
+ {option.label}
+ {count}
+
+ );
+ })}
+
+
+ {/* 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);