From ae86d35cee02106f8885eaaa1fd97ecd399cebcd Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sat, 14 Feb 2026 00:28:01 -0800 Subject: [PATCH 1/3] Show behind ahead --- .../routers/workspaces/procedures/create.ts | 4 ++ .../workspaces/procedures/git-status.ts | 38 +++++++++----- .../lib/trpc/routers/workspaces/utils/git.ts | 25 ++++++++++ .../workspaces/utils/workspace-init.ts | 4 ++ .../WorkspaceAheadBehind.tsx | 20 ++++++++ .../WorkspaceListItem/WorkspaceListItem.tsx | 50 +++++++++---------- .../WorkspaceHoverCard/WorkspaceHoverCard.tsx | 13 ++--- .../WorkspaceListItem/constants.ts | 12 +---- packages/local-db/src/schema/zod.ts | 2 + 9 files changed, 110 insertions(+), 58 deletions(-) create mode 100644 apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceAheadBehind.tsx diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts index d47ebeaf354..e2efc41808d 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts @@ -732,6 +732,8 @@ export const createCreateProcedures = () => { gitStatus: { branch: existingWorktree.branch, needsRebase: false, + ahead: 0, + behind: 0, lastRefreshed: Date.now(), }, }) @@ -816,6 +818,8 @@ export const createCreateProcedures = () => { gitStatus: { branch: input.branch, needsRebase: false, + ahead: 0, + behind: 0, lastRefreshed: Date.now(), }, }) diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts index 1f978cb5200..ea55390ffe0 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts @@ -10,8 +10,8 @@ import { updateProjectDefaultBranch, } from "../utils/db-helpers"; import { - checkNeedsRebase, fetchDefaultBranch, + getAheadBehindCount, getDefaultBranch, listExternalWorktrees, refreshDefaultBranch, @@ -42,7 +42,6 @@ export const createGitStatusProcedures = () => { throw new Error(`Project ${workspace.projectId} not found`); } - // Sync with remote in case the default branch changed (e.g. master -> main) const remoteDefaultBranch = await refreshDefaultBranch( project.mainRepoPath, ); @@ -59,22 +58,21 @@ export const createGitStatusProcedures = () => { updateProjectDefaultBranch(project.id, defaultBranch); } - // Fetch default branch to get latest await fetchDefaultBranch(project.mainRepoPath, defaultBranch); - // Check if worktree branch is behind origin/{defaultBranch} - const needsRebase = await checkNeedsRebase( - worktree.path, + const { ahead, behind } = await getAheadBehindCount({ + repoPath: worktree.path, defaultBranch, - ); + }); const gitStatus = { branch: worktree.branch, - needsRebase, + needsRebase: behind > 0, + ahead, + behind, lastRefreshed: Date.now(), }; - // Update worktree in db localDb .update(worktrees) .set({ gitStatus }) @@ -84,6 +82,25 @@ export const createGitStatusProcedures = () => { return { gitStatus, defaultBranch }; }), + getAheadBehind: publicProcedure + .input(z.object({ workspaceId: z.string() })) + .query(async ({ input }) => { + const workspace = getWorkspace(input.workspaceId); + if (!workspace) { + return { ahead: 0, behind: 0 }; + } + + const project = getProject(workspace.projectId); + if (!project) { + return { ahead: 0, behind: 0 }; + } + + return getAheadBehindCount({ + repoPath: project.mainRepoPath, + defaultBranch: workspace.branch, + }); + }), + getGitHubStatus: publicProcedure .input(z.object({ workspaceId: z.string() })) .query(async ({ input }) => { @@ -99,10 +116,8 @@ export const createGitStatusProcedures = () => { return null; } - // Always fetch fresh data on hover const freshStatus = await fetchGitHubPRStatus(worktree.path); - // Update cache if we got data if (freshStatus) { localDb .update(worktrees) @@ -129,7 +144,6 @@ export const createGitStatusProcedures = () => { return null; } - // Extract worktree name from path (last segment) const worktreeName = worktree.path.split("/").pop() ?? worktree.branch; const branchName = worktree.branch; diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts index 70bad22f411..eb480cd3fb0 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts @@ -1010,6 +1010,31 @@ export async function checkNeedsRebase( return Number.parseInt(behindCount.trim(), 10) > 0; } +export async function getAheadBehindCount({ + repoPath, + defaultBranch, +}: { + repoPath: string; + defaultBranch: string; +}): Promise<{ ahead: number; behind: number }> { + const git = simpleGit(repoPath); + try { + const output = await git.raw([ + "rev-list", + "--left-right", + "--count", + `origin/${defaultBranch}...HEAD`, + ]); + const [behindStr, aheadStr] = output.trim().split(/\s+/); + return { + ahead: Number.parseInt(aheadStr || "0", 10), + behind: Number.parseInt(behindStr || "0", 10), + }; + } catch { + return { ahead: 0, behind: 0 }; + } +} + export async function hasUncommittedChanges( worktreePath: string, ): Promise { diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts b/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts index e92733a836b..dde53a635d4 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts @@ -118,6 +118,8 @@ export async function initializeWorkspaceWorktree({ gitStatus: { branch, needsRebase: false, + ahead: 0, + behind: 0, lastRefreshed: Date.now(), }, }) @@ -438,6 +440,8 @@ export async function initializeWorkspaceWorktree({ gitStatus: { branch, needsRebase: false, + ahead: 0, + behind: 0, lastRefreshed: Date.now(), }, }) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceAheadBehind.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceAheadBehind.tsx new file mode 100644 index 00000000000..df5a2c2d702 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceAheadBehind.tsx @@ -0,0 +1,20 @@ +interface WorkspaceAheadBehindProps { + ahead: number; + behind: number; +} + +export function WorkspaceAheadBehind({ + ahead, + behind, +}: WorkspaceAheadBehindProps) { + if (ahead === 0 && behind === 0) { + return null; + } + + return ( +
+ {behind > 0 && ↓{behind}} + {ahead > 0 && ↑{ahead}} +
+ ); +} 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 fd3650a2779..c6d68d15401 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 @@ -44,11 +44,13 @@ import { getHighestPriorityStatus } from "shared/tabs-types"; import { STROKE_WIDTH } from "../constants"; import { DeleteWorkspaceDialog, WorkspaceHoverCardContent } from "./components"; import { + AHEAD_BEHIND_STALE_TIME, GITHUB_STATUS_STALE_TIME, HOVER_CARD_CLOSE_DELAY, HOVER_CARD_OPEN_DELAY, MAX_KEYBOARD_SHORTCUT_INDEX, } from "./constants"; +import { WorkspaceAheadBehind } from "./WorkspaceAheadBehind"; import { WorkspaceDiffStats } from "./WorkspaceDiffStats"; import { WorkspaceStatusBadge } from "./WorkspaceStatusBadge"; @@ -93,7 +95,6 @@ export function WorkspaceListItem({ ); const utils = electronTrpc.useUtils(); - // Derive isActive from route const isActive = !!matchRoute({ to: "/workspace/$workspaceId", params: { workspaceId: id }, @@ -118,11 +119,9 @@ export function WorkspaceListItem({ toast.error(`Failed to update unread status: ${error.message}`), }); - // Shared delete logic const { showDeleteDialog, setShowDeleteDialog, handleDeleteClick } = useWorkspaceDeleteHandler(); - // Lazy-load GitHub status on hover to avoid N+1 queries const { data: githubStatus } = electronTrpc.workspaces.getGitHubStatus.useQuery( { workspaceId: id }, @@ -132,7 +131,6 @@ export function WorkspaceListItem({ }, ); - // Lazy-load local git changes on hover const { data: localChanges } = electronTrpc.changes.getStatus.useQuery( { worktreePath }, { @@ -141,13 +139,21 @@ export function WorkspaceListItem({ }, ); + const { data: aheadBehind } = electronTrpc.workspaces.getAheadBehind.useQuery( + { workspaceId: id }, + { + enabled: isBranchWorkspace, + staleTime: AHEAD_BEHIND_STALE_TIME, + refetchInterval: AHEAD_BEHIND_STALE_TIME, + }, + ); + useBranchSyncInvalidation({ gitBranch: localChanges?.branch, workspaceBranch: branch, workspaceId: id, }); - // Calculate total local changes (staged + unstaged + untracked) const localDiffStats = useMemo(() => { if (!localChanges) return null; const allFiles = [ @@ -161,7 +167,6 @@ export function WorkspaceListItem({ return { additions, deletions }; }, [localChanges]); - // Memoize workspace pane IDs to avoid recalculating on every render const workspacePaneIds = useMemo(() => { const workspaceTabs = tabs.filter((t) => t.workspaceId === id); return new Set( @@ -169,9 +174,7 @@ export function WorkspaceListItem({ ); }, [tabs, id]); - // Compute aggregate status for workspace using shared priority logic const workspaceStatus = useMemo(() => { - // Generator avoids array allocation function* paneStatuses() { for (const paneId of workspacePaneIds) { yield panes[paneId]?.status; @@ -214,7 +217,6 @@ export function WorkspaceListItem({ } }; - // Drag and drop const [{ isDragging }, drag] = useDrag( () => ({ type: WORKSPACE_TYPE, @@ -291,7 +293,6 @@ export function WorkspaceListItem({ }); const pr = githubStatus?.pr; - // Show diff stats from PR if available, otherwise from local changes const diffStats = localDiffStats || (pr && (pr.additions > 0 || pr.deletions > 0) @@ -299,10 +300,11 @@ export function WorkspaceListItem({ : null); const showDiffStats = !!diffStats; - // Determine if we should show the branch subtitle const showBranchSubtitle = isBranchWorkspace || (!!name && name !== branch); + const hasAheadBehind = + !!aheadBehind && (aheadBehind.ahead > 0 || aheadBehind.behind > 0); + const showRow2 = showBranchSubtitle || pr || hasAheadBehind; - // Collapsed sidebar: show just the icon with hover card (worktree) or tooltip (branch) if (isCollapsed) { const collapsedButton = ( ); - // Branch workspaces get a simple tooltip if (isBranchWorkspace) { return ( @@ -367,7 +366,6 @@ export function WorkspaceListItem({ ); } - // Worktree workspaces get the full hover card with context menu return ( <> - {/* Active indicator - left border */} {isActive && (
)} - {/* Icon with status indicator */}
{workspaceStatus === "working" ? ( @@ -497,7 +493,6 @@ export function WorkspaceListItem({ - {/* Content area */}
{rename.isRenaming ? ( ) : (
- {/* Row 1: Title + actions */}
- {/* Keyboard shortcut */} {shortcutIndex !== undefined && shortcutIndex < MAX_KEYBOARD_SHORTCUT_INDEX && ( @@ -537,7 +530,6 @@ export function WorkspaceListItem({ )} - {/* Diff stats (transforms to X on hover) or close button for worktree workspaces */} {!isBranchWorkspace && (showDiffStats && diffStats ? ( - {/* Row 2: Git info (branch + PR badge) */} - {(showBranchSubtitle || pr) && ( + {showRow2 && (
{showBranchSubtitle && ( {branch} )} + {aheadBehind && ( + + )} {pr && ( ); - // Wrap with context menu and hover card if (isBranchWorkspace) { return ( <> diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/WorkspaceHoverCard/WorkspaceHoverCard.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/WorkspaceHoverCard/WorkspaceHoverCard.tsx index 3964861bbfb..5a2506b9f37 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/WorkspaceHoverCard/WorkspaceHoverCard.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/WorkspaceHoverCard/WorkspaceHoverCard.tsx @@ -44,6 +44,7 @@ export function WorkspaceHoverCardContent({ ); const needsRebase = worktreeInfo?.gitStatus?.needsRebase; + const behindCount = worktreeInfo?.gitStatus?.behind; const worktreeName = worktreeInfo?.worktreeName; const branchName = worktreeInfo?.branchName; @@ -52,7 +53,6 @@ export function WorkspaceHoverCardContent({ return (
- {/* Header: Alias + Worktree name + age */}
{hasCustomAlias && (
{workspaceAlias}
@@ -91,18 +91,19 @@ export function WorkspaceHoverCardContent({ )}
- {/* Needs Rebase Warning */} {needsRebase && (
- Behind main, needs rebase + + Behind main by {behindCount ?? "?"} commit + {behindCount !== 1 && "s"}, needs rebase +
)} - {/* PR Section */} {isLoadingGithub ? (
) : pr ? (
- {/* PR Header: Number + Status + Diff Stats */}
@@ -129,10 +129,8 @@ export function WorkspaceHoverCardContent({
- {/* PR Title */}

{pr.title}

- {/* Checks & Review - only for open PRs */} {pr.state === "open" && (
@@ -144,7 +142,6 @@ export function WorkspaceHoverCardContent({
)} - {/* View on GitHub button */} + ); + + if (isBranchWorkspace) { + return ( + + {collapsedButton} + + local + + {branch} + + + + ); + } + + return ( + <> + + + + {collapsedButton} + + + + + Copy Path + + + onDeleteClick()}> + + Close Worktree + + + + + + + + + + ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceDiffStats.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceDiffStats.tsx index 8cda83a4bde..c408e454617 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceDiffStats.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceDiffStats.tsx @@ -24,18 +24,15 @@ export function WorkspaceDiffStats({ : "bg-muted/50 group-hover:bg-transparent", )} > - {/* Diff stats - hidden on card hover when onClose provided */}
+{additions} −{deletions}
- {/* X icon - shown on card hover */} {onClose && ( diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceIcon.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceIcon.tsx new file mode 100644 index 00000000000..73c4f3ac7b4 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceIcon.tsx @@ -0,0 +1,66 @@ +import { cn } from "@superset/ui/utils"; +import { LuFolderGit2, LuLaptop } from "react-icons/lu"; +import { AsciiSpinner } from "renderer/screens/main/components/AsciiSpinner"; +import { StatusIndicator } from "renderer/screens/main/components/StatusIndicator"; +import type { ActivePaneStatus } from "shared/tabs-types"; +import { STROKE_WIDTH } from "../constants"; + +interface WorkspaceIconProps { + isBranchWorkspace: boolean; + isActive: boolean; + isUnread: boolean; + workspaceStatus: ActivePaneStatus | null; + variant: "collapsed" | "expanded"; +} + +const OVERLAY_POSITION = { + collapsed: "top-1 right-1", + expanded: "-top-0.5 -right-0.5", +} as const; + +export function WorkspaceIcon({ + isBranchWorkspace, + isActive, + isUnread, + workspaceStatus, + variant, +}: WorkspaceIconProps) { + const overlayPosition = OVERLAY_POSITION[variant]; + const iconColor = isActive ? "text-foreground" : "text-muted-foreground"; + + return ( + <> + {workspaceStatus === "working" ? ( + + ) : isBranchWorkspace ? ( + + ) : ( + + )} + {workspaceStatus && workspaceStatus !== "working" && ( + + + + )} + {isUnread && !workspaceStatus && ( + + + + )} + + ); +} 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 9454cb59345..f7d9a86bdb2 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 @@ -22,11 +22,8 @@ import { LuCopy, LuEye, LuEyeOff, - LuFolderGit2, LuFolderOpen, - LuLaptop, LuPencil, - LuX, } from "react-icons/lu"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { @@ -34,8 +31,6 @@ import { useWorkspaceDeleteHandler, } from "renderer/react-query/workspaces"; import { navigateToWorkspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; -import { AsciiSpinner } from "renderer/screens/main/components/AsciiSpinner"; -import { StatusIndicator } from "renderer/screens/main/components/StatusIndicator"; import { useBranchSyncInvalidation } from "renderer/screens/main/hooks/useBranchSyncInvalidation"; import { useGitChangesStatus } from "renderer/screens/main/hooks/useGitChangesStatus"; import { useWorkspaceRename } from "renderer/screens/main/hooks/useWorkspaceRename"; @@ -43,6 +38,7 @@ import { useTabsStore } from "renderer/stores/tabs/store"; import { extractPaneIdsFromLayout } from "renderer/stores/tabs/utils"; import { getHighestPriorityStatus } from "shared/tabs-types"; import { STROKE_WIDTH } from "../constants"; +import { CollapsedWorkspaceItem } from "./CollapsedWorkspaceItem"; import { DeleteWorkspaceDialog, WorkspaceHoverCardContent } from "./components"; import { AHEAD_BEHIND_STALE_TIME, @@ -53,9 +49,17 @@ import { } from "./constants"; import { WorkspaceAheadBehind } from "./WorkspaceAheadBehind"; import { WorkspaceDiffStats } from "./WorkspaceDiffStats"; +import { WorkspaceIcon } from "./WorkspaceIcon"; import { WorkspaceStatusBadge } from "./WorkspaceStatusBadge"; -const WORKSPACE_TYPE = "WORKSPACE"; +const WORKSPACE_DND_TYPE = "WORKSPACE"; + +interface DragItem { + id: string; + projectId: string; + index: number; + originalIndex: number; +} interface WorkspaceListItemProps { id: string; @@ -67,7 +71,6 @@ interface WorkspaceListItemProps { isUnread?: boolean; index: number; shortcutIndex?: number; - /** Whether the sidebar is in collapsed mode (icon-only view) */ isCollapsed?: boolean; } @@ -113,9 +116,7 @@ export function WorkspaceListItem({ onError: (error) => toast.error(`Failed to open: ${error.message}`), }); const setUnread = electronTrpc.workspaces.setUnread.useMutation({ - onSuccess: () => { - utils.workspaces.getAllGrouped.invalidate(); - }, + onSuccess: () => utils.workspaces.getAllGrouped.invalidate(), onError: (error) => toast.error(`Failed to update unread status: ${error.message}`), }); @@ -169,21 +170,18 @@ export function WorkspaceListItem({ return { additions, deletions }; }, [localChanges]); - const workspacePaneIds = useMemo(() => { + const workspaceStatus = useMemo(() => { const workspaceTabs = tabs.filter((t) => t.workspaceId === id); - return new Set( + const paneIds = new Set( workspaceTabs.flatMap((t) => extractPaneIdsFromLayout(t.layout)), ); - }, [tabs, id]); - - const workspaceStatus = useMemo(() => { function* paneStatuses() { - for (const paneId of workspacePaneIds) { + for (const paneId of paneIds) { yield panes[paneId]?.status; } } return getHighestPriorityStatus(paneStatuses()); - }, [panes, workspacePaneIds]); + }, [tabs, panes, id]); const handleClick = () => { if (!rename.isRenaming) { @@ -193,103 +191,71 @@ export function WorkspaceListItem({ }; const handleMouseEnter = () => { - if (!hasHovered) { - setHasHovered(true); - } + if (!hasHovered) setHasHovered(true); }; const handleOpenInFinder = () => { - if (worktreePath) { - openInFinder.mutate(worktreePath); - } - }; - - const handleToggleUnread = () => { - setUnread.mutate({ id, isUnread: !isUnread }); + if (worktreePath) openInFinder.mutate(worktreePath); }; const handleCopyPath = async () => { - if (worktreePath) { - try { - await navigator.clipboard.writeText(worktreePath); - toast.success("Path copied to clipboard"); - } catch { - toast.error("Failed to copy path"); - } + if (!worktreePath) return; + try { + await navigator.clipboard.writeText(worktreePath); + toast.success("Path copied to clipboard"); + } catch { + toast.error("Failed to copy path"); } }; + const handleReorder = (item: DragItem) => { + if (item.originalIndex === item.index) return; + reorderWorkspaces.mutate( + { + projectId: item.projectId, + fromIndex: item.originalIndex, + toIndex: item.index, + }, + { + onError: (error) => + toast.error(`Failed to reorder workspace: ${error.message}`), + onSettled: () => utils.workspaces.getAllGrouped.invalidate(), + }, + ); + }; + const [{ isDragging }, drag] = useDrag( () => ({ - type: WORKSPACE_TYPE, + type: WORKSPACE_DND_TYPE, item: { id, projectId, index, originalIndex: index }, end: (item, monitor) => { - if (!item || monitor.didDrop()) return; - if (item.originalIndex !== item.index) { - reorderWorkspaces.mutate( - { - projectId: item.projectId, - fromIndex: item.originalIndex, - toIndex: item.index, - }, - { - onError: (error) => - toast.error(`Failed to reorder workspace: ${error.message}`), - onSettled: () => utils.workspaces.getAllGrouped.invalidate(), - }, - ); - } + if (item && !monitor.didDrop()) handleReorder(item); }, - collect: (monitor) => ({ - isDragging: monitor.isDragging(), - }), + collect: (monitor) => ({ isDragging: monitor.isDragging() }), }), [id, projectId, index, reorderWorkspaces], ); const [, drop] = useDrop({ - accept: WORKSPACE_TYPE, - hover: (item: { - id: string; - projectId: string; - index: number; - originalIndex: number; - }) => { - if (item.projectId === projectId && item.index !== index) { - utils.workspaces.getAllGrouped.setData(undefined, (oldData) => { - if (!oldData) return oldData; - return oldData.map((group) => { - if (group.project.id !== projectId) return group; - const workspaces = [...group.workspaces]; - const [moved] = workspaces.splice(item.index, 1); - workspaces.splice(index, 0, moved); - return { ...group, workspaces }; - }); + accept: WORKSPACE_DND_TYPE, + hover: (item: DragItem) => { + if (item.projectId !== projectId || item.index === index) return; + utils.workspaces.getAllGrouped.setData(undefined, (oldData) => { + if (!oldData) return oldData; + return oldData.map((group) => { + if (group.project.id !== projectId) return group; + const workspaces = [...group.workspaces]; + const [moved] = workspaces.splice(item.index, 1); + workspaces.splice(index, 0, moved); + return { ...group, workspaces }; }); - item.index = index; - } + }); + item.index = index; }, - drop: (item: { - id: string; - projectId: string; - index: number; - originalIndex: number; - }) => { - if (item.projectId !== projectId) return; - if (item.originalIndex !== item.index) { - reorderWorkspaces.mutate( - { - projectId, - fromIndex: item.originalIndex, - toIndex: item.index, - }, - { - onError: (error) => - toast.error(`Failed to reorder workspace: ${error.message}`), - onSettled: () => utils.workspaces.getAllGrouped.invalidate(), - }, - ); - return { reordered: true }; + drop: (item: DragItem) => { + if (item.projectId === projectId) { + handleReorder(item); + if (item.originalIndex !== item.index) return { reordered: true }; } }, }); @@ -300,110 +266,32 @@ export function WorkspaceListItem({ (pr && (pr.additions > 0 || pr.deletions > 0) ? { additions: pr.additions, deletions: pr.deletions } : null); - const showDiffStats = !!diffStats; const showBranchSubtitle = isBranchWorkspace || (!!name && name !== branch); if (isCollapsed) { - const collapsedButton = ( - - ); - - if (isBranchWorkspace) { - return ( - - {collapsedButton} - - local - - {branch} - - - - ); - } - return ( - <> - - - - {collapsedButton} - - - - - Copy Path - - - handleDeleteClick()}> - - Close Worktree - - - - - - - - - + ); } const content = ( - // biome-ignore lint/a11y/useSemanticElements: Can't use