From 744f1ff565c3c48372c49e855b5eaf4214953694 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 7 Apr 2026 16:36:34 -0700 Subject: [PATCH 01/12] diff icons --- .../ChangesFileList/ChangesFileList.tsx | 54 ++++++++++++------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx index a1b1f3f9d99..9b488dabad1 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx @@ -1,7 +1,15 @@ import type { AppRouter } from "@superset/host-service"; import type { inferRouterOutputs } from "@trpc/server"; import { ChevronDown, ChevronRight } from "lucide-react"; +import type { ReactNode } from "react"; import { useMemo, useState } from "react"; +import { + VscCopy, + VscDiffAdded, + VscDiffModified, + VscDiffRemoved, + VscDiffRenamed, +} from "react-icons/vsc"; import { FileIcon } from "renderer/screens/main/components/WorkspaceView/RightSidebar/FilesView/utils"; type ChangedFile = @@ -10,24 +18,34 @@ type FileStatus = ChangedFile["status"]; type ChangeCategory = "against-base" | "staged" | "unstaged"; const STATUS_COLORS: Record = { - added: "text-green-400", - copied: "text-purple-400", - changed: "text-yellow-400", - deleted: "text-red-400", - modified: "text-yellow-400", - renamed: "text-blue-400", - untracked: "text-green-400", + added: "text-green-600 dark:text-green-400", + copied: "text-purple-600 dark:text-purple-400", + changed: "text-yellow-600 dark:text-yellow-400", + deleted: "text-red-600 dark:text-red-400", + modified: "text-yellow-600 dark:text-yellow-400", + renamed: "text-blue-600 dark:text-blue-400", + untracked: "text-green-600 dark:text-green-400", }; -const STATUS_LETTERS: Record = { - added: "A", - copied: "C", - changed: "T", - deleted: "D", - modified: "M", - renamed: "R", - untracked: "U", -}; +function getStatusIcon(status: FileStatus): ReactNode { + const iconClass = "w-3 h-3"; + switch (status) { + case "added": + case "untracked": + return ; + case "modified": + case "changed": + return ; + case "deleted": + return ; + case "renamed": + return ; + case "copied": + return ; + default: + return null; + } +} function groupByFolder( files: ChangedFile[], @@ -48,8 +66,8 @@ function groupByFolder( function StatusIndicator({ status }: { status: FileStatus }) { return ( - - {STATUS_LETTERS[status]} + + {getStatusIcon(status)} ); } From 6a9077c656a8f0450b4b98c1a1475bd736fc825c Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 7 Apr 2026 17:25:27 -0700 Subject: [PATCH 02/12] Handle changes --- .../hooks/useChangesTab/useChangesTab.tsx | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx index 9dc4f1418a1..213c01a6922 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx @@ -3,7 +3,7 @@ import { toast } from "@superset/ui/sonner"; import { workspaceTrpc } from "@superset/workspace-client"; import type { inferRouterOutputs } from "@trpc/server"; import { GitBranch, Pencil } from "lucide-react"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useWorkspaceEvent } from "renderer/hooks/host-service/useWorkspaceEvent"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import type { ChangesFilter } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema"; @@ -232,10 +232,28 @@ export function useChangesTab({ { refetchInterval: 30_000, refetchOnWindowFocus: true }, ); - useWorkspaceEvent("git:changed", workspaceId, () => { + const invalidateGitQueries = useCallback(() => { void statusUtils.git.getStatus.invalidate({ workspaceId }); void statusUtils.git.listCommits.invalidate({ workspaceId }); + }, [statusUtils, workspaceId]); + + useWorkspaceEvent("git:changed", workspaceId, invalidateGitQueries); + + // Working-tree edits don't touch .git, so git:changed won't fire for them. + // Debounce fs:events to catch unstaged changes without spamming git status. + const fsDebounceRef = useRef | null>(null); + useWorkspaceEvent("fs:events", workspaceId, () => { + if (fsDebounceRef.current) clearTimeout(fsDebounceRef.current); + fsDebounceRef.current = setTimeout(() => { + fsDebounceRef.current = null; + invalidateGitQueries(); + }, 300); }); + useEffect(() => { + return () => { + if (fsDebounceRef.current) clearTimeout(fsDebounceRef.current); + }; + }, []); const renameBranchMutation = workspaceTrpc.git.renameBranch.useMutation(); From 7821fda6659cc518a772d9576ceab65773edf6b1 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 7 Apr 2026 17:30:32 -0700 Subject: [PATCH 03/12] debounce --- .../hooks/useChangesTab/useChangesTab.tsx | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx index 213c01a6922..f3a7d87cf16 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx @@ -78,8 +78,7 @@ function ChangesHeader({ return (
- {/* Branch name */} -
+
{isEditing ? ( - {/* Commits from base */} -
+
{commitCount} {commitCount === 1 ? "commit" : "commits"} from{" "}
- {/* Remote status */} {currentBranch.aheadCount > 0 && currentBranch.behindCount > 0 && (
Your branch and
@@ -156,8 +153,7 @@ function ChangesHeader({
)} - {/* Filter + stats */} -
+
| null>(null); - useWorkspaceEvent("fs:events", workspaceId, () => { - if (fsDebounceRef.current) clearTimeout(fsDebounceRef.current); - fsDebounceRef.current = setTimeout(() => { - fsDebounceRef.current = null; + // Shared debounce for git:changed and fs:events — batches rapid events + // from either source into a single git status refresh. + const debounceRef = useRef | null>(null); + const debouncedInvalidate = useCallback(() => { + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => { + debounceRef.current = null; invalidateGitQueries(); }, 300); - }); + }, [invalidateGitQueries]); useEffect(() => { return () => { - if (fsDebounceRef.current) clearTimeout(fsDebounceRef.current); + if (debounceRef.current) clearTimeout(debounceRef.current); }; }, []); + useWorkspaceEvent("git:changed", workspaceId, debouncedInvalidate); + useWorkspaceEvent("fs:events", workspaceId, debouncedInvalidate); + const renameBranchMutation = workspaceTrpc.git.renameBranch.useMutation(); const handleRenameBranch = useCallback( @@ -278,7 +275,7 @@ export function useChangesTab({ [workspaceId, status.data?.currentBranch.name, renameBranchMutation], ); - // Can only rename if branch hasn't been pushed (aheadCount === total commits means nothing pushed) + // Only allow rename for branches with no upstream (never pushed) const canRenameBranch = !status.data?.currentBranch.upstream; const commitFilesInput = @@ -301,7 +298,7 @@ export function useChangesTab({ if (filter.kind === "commit" || filter.kind === "range") { return commitFiles.data?.files ?? []; } - // "all" — deduplicate by path + // Deduplicate — a file can appear in multiple categories const map = new Map(); for (const f of status.data.againstBase) map.set(f.path, f); for (const f of status.data.staged) map.set(f.path, f); @@ -350,8 +347,6 @@ export function useChangesTab({ /> ); } else { - // Merge all files into a single flat list, deduplicating by path - // (a file can appear in both againstBase and staged/unstaged) const allFilesMap = new Map< string, (typeof status.data.againstBase)[number] From 06487e28de32887af957181216f09891334a26d6 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 7 Apr 2026 17:52:33 -0700 Subject: [PATCH 04/12] Lint --- .../WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx index f3a7d87cf16..367cf45fe71 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx @@ -78,7 +78,7 @@ function ChangesHeader({ return (
-
+
{isEditing ? ( -
+
{commitCount} {commitCount === 1 ? "commit" : "commits"} from{" "} )} -
+
Date: Tue, 7 Apr 2026 17:56:20 -0700 Subject: [PATCH 05/12] fix: clear debounce timer on workspaceId change --- .../WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx index 367cf45fe71..d386c284592 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx @@ -247,7 +247,7 @@ export function useChangesTab({ return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; - }, []); + }, [workspaceId]); useWorkspaceEvent("git:changed", workspaceId, debouncedInvalidate); useWorkspaceEvent("fs:events", workspaceId, debouncedInvalidate); From c4712052360f4a5c6154ff4a3b43bec1ae872159 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 7 Apr 2026 18:01:36 -0700 Subject: [PATCH 06/12] Refactor --- .../ChangesFileList/ChangesFileList.tsx | 19 +- .../hooks/useChangesTab/useChangesTab.tsx | 208 ++++++++++-------- 2 files changed, 122 insertions(+), 105 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx index 9b488dabad1..d2a41f95ce5 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx @@ -1,7 +1,7 @@ import type { AppRouter } from "@superset/host-service"; import type { inferRouterOutputs } from "@trpc/server"; import { ChevronDown, ChevronRight } from "lucide-react"; -import type { ReactNode } from "react"; +import { type ReactNode, memo } from "react"; import { useMemo, useState } from "react"; import { VscCopy, @@ -72,7 +72,7 @@ function StatusIndicator({ status }: { status: FileStatus }) { ); } -function FileRow({ +const FileRow = memo(function FileRow({ file, category, onSelect, @@ -107,9 +107,9 @@ function FileRow({ ); -} +}); -function FolderGroup({ +const FolderGroup = memo(function FolderGroup({ folder, files, category, @@ -142,7 +142,7 @@ function FolderGroup({ ))}
); -} +}); function Section({ title, @@ -203,7 +203,7 @@ interface ChangesFileListProps { onSelectFile?: (path: string, category: ChangeCategory) => void; } -export function ChangesFileList({ +export const ChangesFileList = memo(function ChangesFileList({ files, staged, unstaged, @@ -212,6 +212,8 @@ export function ChangesFileList({ category = "against-base", onSelectFile, }: ChangesFileListProps) { + const groups = useMemo(() => groupByFolder(files), [files]); + if (isLoading) { return (
@@ -231,7 +233,6 @@ export function ChangesFileList({ ); } - // If staged/unstaged are provided, show three sections if (staged !== undefined && unstaged !== undefined) { return (
@@ -260,8 +261,6 @@ export function ChangesFileList({ ); } - // Single list (filtered by commit or uncommitted) - const groups = groupByFolder(files); return (
{groups.map((group) => ( @@ -275,4 +274,4 @@ export function ChangesFileList({ ))}
); -} +}); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx index d386c284592..543bcdfabe4 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx @@ -3,7 +3,7 @@ import { toast } from "@superset/ui/sonner"; import { workspaceTrpc } from "@superset/workspace-client"; import type { inferRouterOutputs } from "@trpc/server"; import { GitBranch, Pencil } from "lucide-react"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useWorkspaceEvent } from "renderer/hooks/host-service/useWorkspaceEvent"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import type { ChangesFilter } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema"; @@ -179,6 +179,96 @@ function ChangesHeader({ ); } +type ChangedFile = + RouterOutputs["git"]["getStatus"]["againstBase"][number]; + +interface ChangesTabContentProps { + status: { data: RouterOutputs["git"]["getStatus"] | undefined; isLoading: boolean }; + commits: { data: RouterOutputs["git"]["listCommits"] | undefined }; + branches: { data: RouterOutputs["git"]["listBranches"] | undefined }; + commitFiles: { data: { files: ChangedFile[] } | undefined; isLoading: boolean }; + filter: ChangesFilter; + filteredFiles: ChangedFile[]; + fileCategory: "against-base" | "staged" | "unstaged"; + totalChanges: number; + totalAdditions: number; + totalDeletions: number; + onSelectFile?: (path: string, category: "against-base" | "staged" | "unstaged") => void; + onFilterChange: (filter: ChangesFilter) => void; + onBaseBranchChange: (branchName: string) => void; + onRenameBranch: (newName: string) => void; + canRenameBranch: boolean; +} + +const ChangesTabContent = memo(function ChangesTabContent({ + status, + commits, + branches, + commitFiles, + filter, + filteredFiles, + fileCategory, + totalChanges, + totalAdditions, + totalDeletions, + onSelectFile, + onFilterChange, + onBaseBranchChange, + onRenameBranch, + canRenameBranch, +}: ChangesTabContentProps) { + if (status.isLoading) { + return ( +
+ Loading changes... +
+ ); + } + + if (!status.data) { + return ( +
+ Unable to load git status +
+ ); + } + + return ( +
+ +
+ +
+
+ ); +}); + export function useChangesTab({ workspaceId, onSelectFile, @@ -310,100 +400,28 @@ export function useChangesTab({ const totalAdditions = filteredFiles.reduce((sum, f) => sum + f.additions, 0); const totalDeletions = filteredFiles.reduce((sum, f) => sum + f.deletions, 0); - const content = useMemo(() => { - if (status.isLoading) { - return ( -
- Loading changes... -
- ); - } - - if (!status.data) { - return ( -
- Unable to load git status -
- ); - } - - let fileList: React.ReactNode; - - if (filter.kind === "commit" || filter.kind === "range") { - fileList = ( - - ); - } else if (filter.kind === "uncommitted") { - fileList = ( - - ); - } else { - const allFilesMap = new Map< - string, - (typeof status.data.againstBase)[number] - >(); - for (const f of status.data.againstBase) allFilesMap.set(f.path, f); - for (const f of status.data.staged) allFilesMap.set(f.path, f); - for (const f of status.data.unstaged) allFilesMap.set(f.path, f); - - fileList = ( - - ); - } - - return ( -
- -
{fileList}
-
- ); - }, [ - status.data, - status.isLoading, - filter, - commitFiles.data, - commitFiles.isLoading, - commits.data, - totalChanges, - totalAdditions, - totalDeletions, - onSelectFile, - setFilter, - branches.data?.branches, - canRenameBranch, - handleRenameBranch, - setBaseBranch, - ]); + const fileCategory: "against-base" | "staged" | "unstaged" = + filter.kind === "uncommitted" ? "unstaged" : "against-base"; + + const content = ( + + ); return { id: "changes", From 5c4626abe2708ce974ff66038372ad7da53c18be Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 7 Apr 2026 18:10:47 -0700 Subject: [PATCH 07/12] memoize --- .../ChangesFileList/ChangesFileList.tsx | 3 +- .../ChangesHeader/ChangesHeader.tsx | 160 +++++++++++ .../components/ChangesHeader/index.ts | 1 + .../ChangesTabContent/ChangesTabContent.tsx | 105 +++++++ .../components/ChangesTabContent/index.ts | 1 + .../hooks/useChangesTab/types.ts | 9 + .../hooks/useChangesTab/useChangesTab.tsx | 258 +----------------- 7 files changed, 279 insertions(+), 258 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesHeader/ChangesHeader.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesHeader/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesTabContent/ChangesTabContent.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesTabContent/index.ts create mode 100644 apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/types.ts diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx index d2a41f95ce5..3070bdb959d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx @@ -1,8 +1,7 @@ import type { AppRouter } from "@superset/host-service"; import type { inferRouterOutputs } from "@trpc/server"; import { ChevronDown, ChevronRight } from "lucide-react"; -import { type ReactNode, memo } from "react"; -import { useMemo, useState } from "react"; +import { memo, type ReactNode, useMemo, useState } from "react"; import { VscCopy, VscDiffAdded, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesHeader/ChangesHeader.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesHeader/ChangesHeader.tsx new file mode 100644 index 00000000000..b043fbb2b65 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesHeader/ChangesHeader.tsx @@ -0,0 +1,160 @@ +import { GitBranch, Pencil } from "lucide-react"; +import { useRef, useState } from "react"; +import type { ChangesFilter } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema"; +import type { Branch, Commit } from "../../types"; +import { BaseBranchSelector } from "../BaseBranchSelector"; +import { CommitFilterDropdown } from "../CommitFilterDropdown"; + +interface ChangesHeaderProps { + currentBranch: { name: string; aheadCount: number; behindCount: number }; + defaultBranchName: string; + commitCount: number; + totalFiles: number; + totalAdditions: number; + totalDeletions: number; + filter: ChangesFilter; + onFilterChange: (filter: ChangesFilter) => void; + commits: Commit[]; + uncommittedCount: number; + branches: Branch[]; + onBaseBranchChange: (branchName: string) => void; + onRenameBranch: (newName: string) => void; + canRename: boolean; +} + +export function ChangesHeader({ + currentBranch, + defaultBranchName, + commitCount, + totalFiles, + totalAdditions, + totalDeletions, + onRenameBranch, + canRename, + filter, + onFilterChange, + commits, + uncommittedCount, + branches, + onBaseBranchChange, +}: ChangesHeaderProps) { + const [isEditing, setIsEditing] = useState(false); + const [editValue, setEditValue] = useState(currentBranch.name); + const inputRef = useRef(null); + + const startEditing = () => { + setEditValue(currentBranch.name); + setIsEditing(true); + requestAnimationFrame(() => inputRef.current?.select()); + }; + + const handleSubmit = () => { + const trimmed = editValue.trim(); + if (trimmed && trimmed !== currentBranch.name) { + onRenameBranch(trimmed); + } + setIsEditing(false); + }; + + return ( +
+
+ + {isEditing ? ( + setEditValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleSubmit(); + if (e.key === "Escape") setIsEditing(false); + }} + onBlur={handleSubmit} + className="min-w-0 flex-1 truncate bg-transparent font-medium outline-none ring-1 ring-ring rounded-sm px-1" + /> + ) : ( + <> + {currentBranch.name} + {canRename && ( + + )} + + )} +
+ +
+ {commitCount} {commitCount === 1 ? "commit" : "commits"} from{" "} + +
+ + {currentBranch.aheadCount > 0 && currentBranch.behindCount > 0 && ( +
+
Your branch and
+
+ origin/{currentBranch.name} +
+
have diverged
+
+ {currentBranch.aheadCount} local not pushed,{" "} + {currentBranch.behindCount} remote to pull +
+
+ )} + {currentBranch.aheadCount > 0 && currentBranch.behindCount === 0 && ( +
+
+ {currentBranch.aheadCount}{" "} + {currentBranch.aheadCount === 1 ? "commit" : "commits"} ahead of +
+
+ origin/{currentBranch.name} +
+
+ )} + {currentBranch.behindCount > 0 && currentBranch.aheadCount === 0 && ( +
+
+ {currentBranch.behindCount}{" "} + {currentBranch.behindCount === 1 ? "commit" : "commits"} behind +
+
+ origin/{currentBranch.name} +
+
+ )} + +
+ +
+ {totalFiles} files changed + {(totalAdditions > 0 || totalDeletions > 0) && ( + + {totalAdditions > 0 && ( + +{totalAdditions} + )} + {totalAdditions > 0 && totalDeletions > 0 && " "} + {totalDeletions > 0 && ( + -{totalDeletions} + )} + + )} +
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesHeader/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesHeader/index.ts new file mode 100644 index 00000000000..2d44c6bf794 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesHeader/index.ts @@ -0,0 +1 @@ +export { ChangesHeader } from "./ChangesHeader"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesTabContent/ChangesTabContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesTabContent/ChangesTabContent.tsx new file mode 100644 index 00000000000..10bb81530f3 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesTabContent/ChangesTabContent.tsx @@ -0,0 +1,105 @@ +import type { AppRouter } from "@superset/host-service"; +import type { inferRouterOutputs } from "@trpc/server"; +import { memo } from "react"; +import type { ChangesFilter } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema"; +import type { ChangedFile } from "../../types"; +import { ChangesFileList } from "../ChangesFileList"; +import { ChangesHeader } from "../ChangesHeader"; + +type RouterOutputs = inferRouterOutputs; + +interface ChangesTabContentProps { + status: { + data: RouterOutputs["git"]["getStatus"] | undefined; + isLoading: boolean; + }; + commits: { data: RouterOutputs["git"]["listCommits"] | undefined }; + branches: { data: RouterOutputs["git"]["listBranches"] | undefined }; + commitFiles: { + data: { files: ChangedFile[] } | undefined; + isLoading: boolean; + }; + filter: ChangesFilter; + filteredFiles: ChangedFile[]; + fileCategory: "against-base" | "staged" | "unstaged"; + totalChanges: number; + totalAdditions: number; + totalDeletions: number; + onSelectFile?: ( + path: string, + category: "against-base" | "staged" | "unstaged", + ) => void; + onFilterChange: (filter: ChangesFilter) => void; + onBaseBranchChange: (branchName: string) => void; + onRenameBranch: (newName: string) => void; + canRenameBranch: boolean; +} + +export const ChangesTabContent = memo(function ChangesTabContent({ + status, + commits, + branches, + commitFiles, + filter, + filteredFiles, + fileCategory, + totalChanges, + totalAdditions, + totalDeletions, + onSelectFile, + onFilterChange, + onBaseBranchChange, + onRenameBranch, + canRenameBranch, +}: ChangesTabContentProps) { + if (status.isLoading) { + return ( +
+ Loading changes... +
+ ); + } + + if (!status.data) { + return ( +
+ Unable to load git status +
+ ); + } + + return ( +
+ +
+ +
+
+ ); +}); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesTabContent/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesTabContent/index.ts new file mode 100644 index 00000000000..a8468c8187b --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesTabContent/index.ts @@ -0,0 +1 @@ +export { ChangesTabContent } from "./ChangesTabContent"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/types.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/types.ts new file mode 100644 index 00000000000..727d7505698 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/types.ts @@ -0,0 +1,9 @@ +import type { AppRouter } from "@superset/host-service"; +import type { inferRouterOutputs } from "@trpc/server"; + +type RouterOutputs = inferRouterOutputs; + +export type Commit = RouterOutputs["git"]["listCommits"]["commits"][number]; +export type Branch = RouterOutputs["git"]["listBranches"]["branches"][number]; +export type ChangedFile = + RouterOutputs["git"]["getStatus"]["againstBase"][number]; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx index 543bcdfabe4..f5d572214e8 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx @@ -1,22 +1,14 @@ -import type { AppRouter } from "@superset/host-service"; import { toast } from "@superset/ui/sonner"; import { workspaceTrpc } from "@superset/workspace-client"; -import type { inferRouterOutputs } from "@trpc/server"; -import { GitBranch, Pencil } from "lucide-react"; -import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef } from "react"; import { useWorkspaceEvent } from "renderer/hooks/host-service/useWorkspaceEvent"; import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; import type { ChangesFilter } from "renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema"; import type { SidebarTabDefinition } from "../../types"; -import { BaseBranchSelector } from "./components/BaseBranchSelector"; -import { ChangesFileList } from "./components/ChangesFileList"; -import { CommitFilterDropdown } from "./components/CommitFilterDropdown"; +import { ChangesTabContent } from "./components/ChangesTabContent"; export type { ChangesFilter }; -type RouterOutputs = inferRouterOutputs; -type Commit = RouterOutputs["git"]["listCommits"]["commits"][number]; - interface UseChangesTabParams { workspaceId: string; onSelectFile?: ( @@ -25,250 +17,6 @@ interface UseChangesTabParams { ) => void; } -type Branch = RouterOutputs["git"]["listBranches"]["branches"][number]; - -function ChangesHeader({ - currentBranch, - defaultBranchName, - commitCount, - totalFiles, - totalAdditions, - totalDeletions, - onRenameBranch, - canRename, - filter, - onFilterChange, - commits, - uncommittedCount, - branches, - onBaseBranchChange, -}: { - currentBranch: { name: string; aheadCount: number; behindCount: number }; - defaultBranchName: string; - commitCount: number; - totalFiles: number; - totalAdditions: number; - totalDeletions: number; - filter: ChangesFilter; - onFilterChange: (filter: ChangesFilter) => void; - commits: Commit[]; - uncommittedCount: number; - branches: Branch[]; - onBaseBranchChange: (branchName: string) => void; - onRenameBranch: (newName: string) => void; - canRename: boolean; -}) { - const [isEditing, setIsEditing] = useState(false); - const [editValue, setEditValue] = useState(currentBranch.name); - const inputRef = useRef(null); - - const startEditing = () => { - setEditValue(currentBranch.name); - setIsEditing(true); - requestAnimationFrame(() => inputRef.current?.select()); - }; - - const handleSubmit = () => { - const trimmed = editValue.trim(); - if (trimmed && trimmed !== currentBranch.name) { - onRenameBranch(trimmed); - } - setIsEditing(false); - }; - - return ( -
-
- - {isEditing ? ( - setEditValue(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") handleSubmit(); - if (e.key === "Escape") setIsEditing(false); - }} - onBlur={handleSubmit} - className="min-w-0 flex-1 truncate bg-transparent font-medium outline-none ring-1 ring-ring rounded-sm px-1" - /> - ) : ( - <> - {currentBranch.name} - {canRename && ( - - )} - - )} -
- -
- {commitCount} {commitCount === 1 ? "commit" : "commits"} from{" "} - -
- - {currentBranch.aheadCount > 0 && currentBranch.behindCount > 0 && ( -
-
Your branch and
-
- origin/{currentBranch.name} -
-
have diverged
-
- {currentBranch.aheadCount} local not pushed,{" "} - {currentBranch.behindCount} remote to pull -
-
- )} - {currentBranch.aheadCount > 0 && currentBranch.behindCount === 0 && ( -
-
- {currentBranch.aheadCount}{" "} - {currentBranch.aheadCount === 1 ? "commit" : "commits"} ahead of -
-
- origin/{currentBranch.name} -
-
- )} - {currentBranch.behindCount > 0 && currentBranch.aheadCount === 0 && ( -
-
- {currentBranch.behindCount}{" "} - {currentBranch.behindCount === 1 ? "commit" : "commits"} behind -
-
- origin/{currentBranch.name} -
-
- )} - -
- -
- {totalFiles} files changed - {(totalAdditions > 0 || totalDeletions > 0) && ( - - {totalAdditions > 0 && ( - +{totalAdditions} - )} - {totalAdditions > 0 && totalDeletions > 0 && " "} - {totalDeletions > 0 && ( - -{totalDeletions} - )} - - )} -
-
-
- ); -} - -type ChangedFile = - RouterOutputs["git"]["getStatus"]["againstBase"][number]; - -interface ChangesTabContentProps { - status: { data: RouterOutputs["git"]["getStatus"] | undefined; isLoading: boolean }; - commits: { data: RouterOutputs["git"]["listCommits"] | undefined }; - branches: { data: RouterOutputs["git"]["listBranches"] | undefined }; - commitFiles: { data: { files: ChangedFile[] } | undefined; isLoading: boolean }; - filter: ChangesFilter; - filteredFiles: ChangedFile[]; - fileCategory: "against-base" | "staged" | "unstaged"; - totalChanges: number; - totalAdditions: number; - totalDeletions: number; - onSelectFile?: (path: string, category: "against-base" | "staged" | "unstaged") => void; - onFilterChange: (filter: ChangesFilter) => void; - onBaseBranchChange: (branchName: string) => void; - onRenameBranch: (newName: string) => void; - canRenameBranch: boolean; -} - -const ChangesTabContent = memo(function ChangesTabContent({ - status, - commits, - branches, - commitFiles, - filter, - filteredFiles, - fileCategory, - totalChanges, - totalAdditions, - totalDeletions, - onSelectFile, - onFilterChange, - onBaseBranchChange, - onRenameBranch, - canRenameBranch, -}: ChangesTabContentProps) { - if (status.isLoading) { - return ( -
- Loading changes... -
- ); - } - - if (!status.data) { - return ( -
- Unable to load git status -
- ); - } - - return ( -
- -
- -
-
- ); -}); - export function useChangesTab({ workspaceId, onSelectFile, @@ -365,7 +113,6 @@ export function useChangesTab({ [workspaceId, status.data?.currentBranch.name, renameBranchMutation], ); - // Only allow rename for branches with no upstream (never pushed) const canRenameBranch = !status.data?.currentBranch.upstream; const commitFilesInput = @@ -388,7 +135,6 @@ export function useChangesTab({ if (filter.kind === "commit" || filter.kind === "range") { return commitFiles.data?.files ?? []; } - // Deduplicate — a file can appear in multiple categories const map = new Map(); for (const f of status.data.againstBase) map.set(f.path, f); for (const f of status.data.staged) map.set(f.path, f); From 68aee023a69a962691ccf094cb914177565fadb6 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 7 Apr 2026 18:14:10 -0700 Subject: [PATCH 08/12] Lint --- .../WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx index f5d572214e8..2b3e6cf134a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx @@ -85,7 +85,7 @@ export function useChangesTab({ return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; - }, [workspaceId]); + }, []); useWorkspaceEvent("git:changed", workspaceId, debouncedInvalidate); useWorkspaceEvent("fs:events", workspaceId, debouncedInvalidate); From f491ec922985b3424e7f84c1cd9a64f056323742 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 7 Apr 2026 18:28:36 -0700 Subject: [PATCH 09/12] Update colors --- apps/desktop/src/renderer/globals.css | 15 +++++++++++++++ .../ChangesFileList/ChangesFileList.tsx | 14 +++++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/renderer/globals.css b/apps/desktop/src/renderer/globals.css index a21a41cd21f..abf835ab0fb 100644 --- a/apps/desktop/src/renderer/globals.css +++ b/apps/desktop/src/renderer/globals.css @@ -53,6 +53,11 @@ --sidebar-ring: #3a3837; --highlight-match: rgba(224, 120, 80, 0.2); --highlight-active: rgba(224, 120, 80, 0.5); + --diff-added: oklch(0.77 0.2 155); + --diff-modified: oklch(0.82 0.2 95); + --diff-deleted: oklch(0.65 0.2 25); + --diff-renamed: oklch(0.7 0.17 260); + --diff-copied: oklch(0.7 0.2 300); } /* Light theme fallback values - applied before hydration if user has light theme saved */ @@ -93,6 +98,11 @@ --sidebar-ring: oklch(0.708 0 0); --highlight-match: rgba(255, 211, 61, 0.35); --highlight-active: rgba(255, 150, 50, 0.55); + --diff-added: oklch(0.55 0.18 155); + --diff-modified: oklch(0.65 0.18 85); + --diff-deleted: oklch(0.52 0.22 25); + --diff-renamed: oklch(0.52 0.2 260); + --diff-copied: oklch(0.5 0.22 300); } @theme inline { @@ -134,6 +144,11 @@ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); --color-sidebar-border: var(--sidebar-border); --color-sidebar-ring: var(--sidebar-ring); + --color-diff-added: var(--diff-added); + --color-diff-modified: var(--diff-modified); + --color-diff-deleted: var(--diff-deleted); + --color-diff-renamed: var(--diff-renamed); + --color-diff-copied: var(--diff-copied); } @layer base { diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx index 3070bdb959d..3c2ca0b5cd6 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx @@ -17,13 +17,13 @@ type FileStatus = ChangedFile["status"]; type ChangeCategory = "against-base" | "staged" | "unstaged"; const STATUS_COLORS: Record = { - added: "text-green-600 dark:text-green-400", - copied: "text-purple-600 dark:text-purple-400", - changed: "text-yellow-600 dark:text-yellow-400", - deleted: "text-red-600 dark:text-red-400", - modified: "text-yellow-600 dark:text-yellow-400", - renamed: "text-blue-600 dark:text-blue-400", - untracked: "text-green-600 dark:text-green-400", + added: "text-diff-added", + copied: "text-diff-copied", + changed: "text-diff-modified", + deleted: "text-diff-deleted", + modified: "text-diff-modified", + renamed: "text-diff-renamed", + untracked: "text-diff-added", }; function getStatusIcon(status: FileStatus): ReactNode { From b9ec2ef8b47e2c75f322a2de5eaeee4a34df42cc Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Tue, 7 Apr 2026 18:33:46 -0700 Subject: [PATCH 10/12] clean up --- .../BaseBranchSelector/BaseBranchSelector.tsx | 4 ++-- .../components/ChangesHeader/ChangesHeader.tsx | 17 ++++++++++++++--- .../hooks/useChangesTab/useChangesTab.tsx | 3 ++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/BaseBranchSelector/BaseBranchSelector.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/BaseBranchSelector/BaseBranchSelector.tsx index 33d324d6779..153bbee0483 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/BaseBranchSelector/BaseBranchSelector.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/BaseBranchSelector/BaseBranchSelector.tsx @@ -39,7 +39,7 @@ export function BaseBranchSelector({ - +
- +
{filtered.map((branch) => ( - +
- +
{filtered.map((branch) => (