Skip to content
Merged
2 changes: 2 additions & 0 deletions apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,6 +826,7 @@ export const createWorkspacesRouter = () => {
name: string;
color: string;
tabOrder: number;
mainRepoPath: string;
};
workspaces: Array<{
id: string;
Expand All @@ -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: [],
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<button
type="button"
onClick={onToggleCollapse}
aria-expanded={!isCollapsed}
className={cn(
"flex items-center gap-2 w-full px-3 py-2 text-sm font-medium",
"hover:bg-muted/50 transition-colors",
"text-left cursor-pointer",
)}
>
<span className="truncate flex-1">{projectName}</span>
<span className="text-xs text-muted-foreground">{workspaceCount}</span>
</button>
<ContextMenu>
<ContextMenuTrigger asChild>
<button
type="button"
onClick={onToggleCollapse}
aria-expanded={!isCollapsed}
className={cn(
"flex items-center gap-2 w-full px-3 py-2 text-sm font-medium",
"hover:bg-muted/50 transition-colors",
"text-left cursor-pointer",
)}
>
<span className="truncate flex-1">{projectName}</span>
<span className="text-xs text-muted-foreground">
{workspaceCount}
</span>
</button>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onSelect={handleOpenInFinder}>
<LuFolderOpen className="size-4 mr-2" />
Open in Finder
</ContextMenuItem>
<ContextMenuItem onSelect={handleOpenSettings}>
<LuSettings className="size-4 mr-2" />
Project Settings
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem
onSelect={handleCloseProject}
disabled={closeProject.isPending}
className="text-destructive focus:text-destructive"
>
<LuX className="size-4 mr-2" />
{closeProject.isPending ? "Closing..." : "Close Project"}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) */
Expand All @@ -37,6 +38,7 @@ interface ProjectSectionProps {
export function ProjectSection({
projectId,
projectName,
mainRepoPath,
workspaces,
activeWorkspaceId,
shortcutBaseIndex,
Expand Down Expand Up @@ -67,7 +69,9 @@ export function ProjectSection({
return (
<div className="border-b border-border last:border-b-0">
<ProjectHeader
projectId={projectId}
projectName={projectName}
mainRepoPath={mainRepoPath}
isCollapsed={isCollapsed}
onToggleCollapse={() => toggleProjectCollapsed(projectId)}
workspaceCount={workspaces.length}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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();
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col border-b border-border px-2 pt-2 pb-2">
<div className="flex items-center gap-2 px-2 py-1.5">
<button
type="button"
onClick={handleClick}
className={cn(
"flex items-center gap-2 px-2 py-1.5 w-full rounded-md transition-colors",
isWorkspacesListOpen
? "text-foreground bg-accent"
: "text-muted-foreground hover:text-foreground hover:bg-accent/50",
)}
>
<div className="flex items-center justify-center size-5">
<LuLayers className="size-4 text-muted-foreground" />
<LuLayers className="size-4" />
</div>
<span className="text-sm font-medium text-muted-foreground">
Workspaces
</span>
</div>
<span className="text-sm font-medium flex-1 text-left">Workspaces</span>
</button>
<NewWorkspaceButton />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<button
type="button"
onClick={handleClick}
disabled={isOpening}
onMouseEnter={() => !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 */}
<div
className={cn(
"flex items-center justify-center size-6 rounded shrink-0",
isBranch ? "bg-primary/20" : "bg-background/80",
!workspace.isOpen && "opacity-50",
)}
>
{isBranch ? (
<LuGitBranch className="size-3.5 text-primary" />
) : (
<LuGitFork className="size-3.5 text-foreground/60" />
)}
</div>

{/* Workspace/branch name */}
<span
className={cn(
"text-sm truncate",
isActive ? "text-foreground font-medium" : "text-foreground/80",
!workspace.isOpen && "text-foreground/50",
)}
>
{workspace.name}
</span>

{/* Active indicator */}
{isActive && (
<span className="size-1.5 rounded-full bg-emerald-500 shrink-0" />
)}

{/* Unread indicator */}
{workspace.isUnread && !isActive && (
<span className="relative flex size-2 shrink-0">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75" />
<span className="relative inline-flex size-2 rounded-full bg-red-500" />
</span>
)}

{/* Diff stats */}
{showDiffStats && (
<div className="flex items-center gap-1 text-[10px] font-mono shrink-0">
<span className="text-emerald-500">+{pr.additions}</span>
<span className="text-destructive-foreground">-{pr.deletions}</span>
</div>
)}

{/* Spacer */}
<div className="flex-1" />

{/* Time context */}
<span className="text-xs text-foreground/40 shrink-0 group-hover:hidden">
{timeText}
</span>

{/* Action indicator - visible on hover */}
<div className="hidden group-hover:flex items-center gap-1.5 text-xs shrink-0">
{isOpening ? (
<>
<LuRotateCw className="size-3 animate-spin text-foreground/60" />
<span className="text-foreground/60">Opening...</span>
</>
) : workspace.isOpen ? (
<>
<span className="font-medium text-foreground/80">Switch to</span>
<LuArrowRight className="size-3 text-foreground/80" />
</>
) : (
<>
<span className="font-medium text-foreground/80">Reopen</span>
<LuArrowRight className="size-3 text-foreground/80" />
</>
)}
</div>
</button>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./WorkspaceRow";
Loading
Loading