diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts index e6381a711d7..09f9311fb6e 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/projects.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/projects.ts @@ -23,6 +23,7 @@ import { refreshDefaultBranch, } from "../workspaces/utils/git"; import { assignRandomColor } from "./utils/colors"; +import { fetchGitHubOwner, getGitHubAvatarUrl } from "./utils/github"; type Project = SelectProject; @@ -710,6 +711,59 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { return { success: true, terminalWarning }; }), + + getGitHubAvatar: publicProcedure + .input(z.object({ id: z.string() })) + .query(async ({ input }) => { + const project = localDb + .select() + .from(projects) + .where(eq(projects.id, input.id)) + .get(); + + if (!project) { + console.log("[getGitHubAvatar] Project not found:", input.id); + return null; + } + + // If we already have the github owner cached, return the avatar URL + if (project.githubOwner) { + console.log( + "[getGitHubAvatar] Using cached owner:", + project.githubOwner, + ); + return { + owner: project.githubOwner, + avatarUrl: getGitHubAvatarUrl(project.githubOwner), + }; + } + + // Fetch the owner from GitHub + console.log( + "[getGitHubAvatar] Fetching owner for:", + project.mainRepoPath, + ); + const owner = await fetchGitHubOwner(project.mainRepoPath); + + if (!owner) { + console.log("[getGitHubAvatar] Failed to fetch owner"); + return null; + } + + console.log("[getGitHubAvatar] Fetched owner:", owner); + + // Cache the owner + localDb + .update(projects) + .set({ githubOwner: owner }) + .where(eq(projects.id, input.id)) + .run(); + + return { + owner, + avatarUrl: getGitHubAvatarUrl(owner), + }; + }), }); }; diff --git a/apps/desktop/src/lib/trpc/routers/projects/utils/github.ts b/apps/desktop/src/lib/trpc/routers/projects/utils/github.ts new file mode 100644 index 00000000000..7c9341b94c3 --- /dev/null +++ b/apps/desktop/src/lib/trpc/routers/projects/utils/github.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; +import { execWithShellEnv } from "../../workspaces/utils/shell-env"; + +const GHRepoOwnerResponseSchema = z.object({ + owner: z.object({ + login: z.string(), + }), +}); + +/** + * Fetches the GitHub owner (user or org) for a repository using the `gh` CLI. + * Returns null if `gh` is not installed, not authenticated, or on error. + */ +export async function fetchGitHubOwner( + repoPath: string, +): Promise { + try { + console.log("[fetchGitHubOwner] Running gh repo view in:", repoPath); + const { stdout, stderr } = await execWithShellEnv( + "gh", + ["repo", "view", "--json", "owner"], + { cwd: repoPath }, + ); + if (stderr) { + console.log("[fetchGitHubOwner] stderr:", stderr); + } + console.log("[fetchGitHubOwner] stdout:", stdout); + const raw = JSON.parse(stdout); + const result = GHRepoOwnerResponseSchema.safeParse(raw); + if (!result.success) { + console.error("[GitHub] Owner schema validation failed:", result.error); + return null; + } + console.log("[fetchGitHubOwner] Parsed owner:", result.data.owner.login); + return result.data.owner.login; + } catch (error) { + console.error("[fetchGitHubOwner] Error:", error); + return null; + } +} + +/** + * Constructs the GitHub avatar URL for a user or organization. + * GitHub serves avatars at https://github.com/{owner}.png + */ +export function getGitHubAvatarUrl(owner: string): string { + return `https://github.com/${owner}.png`; +} diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts b/apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts index 5afb68f0fa8..8c991ac9a17 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; + githubOwner: string | null; mainRepoPath: string; }; workspaces: Array<{ @@ -853,6 +854,7 @@ export const createWorkspacesRouter = () => { color: project.color, // biome-ignore lint/style/noNonNullAssertion: filter guarantees tabOrder is not null tabOrder: project.tabOrder!, + githubOwner: project.githubOwner ?? null, mainRepoPath: project.mainRepoPath, }, workspaces: [], diff --git a/apps/desktop/src/renderer/index.html b/apps/desktop/src/renderer/index.html index 4a223a2b13c..04ede98b8ec 100644 --- a/apps/desktop/src/renderer/index.html +++ b/apps/desktop/src/renderer/index.html @@ -12,10 +12,10 @@ - script-src 'self' 'wasm-unsafe-eval' https://*.posthog.com: Allow scripts from same origin + WebAssembly (for xterm ImageAddon) + PostHog - style-src 'self' 'unsafe-inline': Allow styles from same origin + inline (needed for CSS-in-JS) - connect-src 'self' ws: wss: %NEXT_PUBLIC_API_URL% https://*.posthog.com https://*.sentry.io sentry-ipc:: Allow WebSocket + API (includes Electric proxy) + PostHog + Sentry - - img-src 'self' data: https://*.public.blob.vercel-storage.com: Allow images from same origin + data URIs + Vercel blob storage (avatars) + - img-src 'self' data: https://*.public.blob.vercel-storage.com https://github.com https://avatars.githubusercontent.com: Allow images from same origin + data URIs + Vercel blob storage + GitHub avatars - font-src 'self': Allow fonts from same origin --> - + diff --git a/apps/desktop/src/renderer/screens/main/components/SidebarControl/SidebarControl.tsx b/apps/desktop/src/renderer/screens/main/components/SidebarControl/SidebarControl.tsx index d44739bc584..d5f190a5fbd 100644 --- a/apps/desktop/src/renderer/screens/main/components/SidebarControl/SidebarControl.tsx +++ b/apps/desktop/src/renderer/screens/main/components/SidebarControl/SidebarControl.tsx @@ -1,7 +1,7 @@ import { Button } from "@superset/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { cn } from "@superset/ui/utils"; -import { LuGitBranch } from "react-icons/lu"; +import { LuGitCompareArrows } from "react-icons/lu"; import { HotkeyTooltipContent } from "renderer/components/HotkeyTooltipContent"; import { useSidebarStore } from "renderer/stores"; @@ -22,11 +22,11 @@ export function SidebarControl() { className={cn( "no-drag gap-1.5", isSidebarOpen - ? "font-semibold text-foreground" + ? "font-semibold text-foreground bg-accent" : "text-muted-foreground hover:text-foreground", )} > - + Changes diff --git a/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceSidebarControl.tsx b/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceSidebarControl.tsx index a5a517901f6..7396e789a63 100644 --- a/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceSidebarControl.tsx +++ b/apps/desktop/src/renderer/screens/main/components/TopBar/WorkspaceSidebarControl.tsx @@ -10,7 +10,20 @@ import { } from "shared/hotkeys"; export function WorkspaceSidebarControl() { - const { isOpen, toggleOpen } = useWorkspaceSidebarStore(); + const { isOpen, isCollapsed, toggleCollapsed, setOpen } = + useWorkspaceSidebarStore(); + + const handleToggle = () => { + if (!isOpen) { + // If sidebar is closed, open it to collapsed state + setOpen(true); + } else { + // If sidebar is open, toggle between collapsed and expanded + toggleCollapsed(); + } + }; + + const sidebarCollapsed = isCollapsed(); return ( @@ -18,11 +31,11 @@ export function WorkspaceSidebarControl() { + + + + {projectName} + + {workspaceCount} workspace{workspaceCount !== 1 ? "s" : ""} + + + + + + + Open in Finder + + + + Project Settings + + + + + {closeProject.isPending ? "Closing..." : "Close Project"} + + + + ); + } + return ( - + {/* Main clickable area */} + + + {/* Add workspace button */} +
+ + + + + + + + + Add workspace + + + e.stopPropagation()} + > + + + New Workspace + + + + Quick Create + + + +
+ + {/* Collapse chevron */} + +
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 a0e4152f94f..5e15f7126e1 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 @@ -1,13 +1,6 @@ -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@superset/ui/dropdown-menu"; import { toast } from "@superset/ui/sonner"; import { AnimatePresence, motion } from "framer-motion"; import { useState } from "react"; -import { HiMiniPlus, HiOutlineBolt } from "react-icons/hi2"; import { useCreateWorkspace } from "renderer/react-query/workspaces"; import { useWorkspaceSidebarStore } from "renderer/stores"; import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal"; @@ -28,20 +21,25 @@ interface Workspace { interface ProjectSectionProps { projectId: string; projectName: string; + githubOwner: string | null; mainRepoPath: string; workspaces: Workspace[]; activeWorkspaceId: string | null; /** Base index for keyboard shortcuts (0-based) */ shortcutBaseIndex: number; + /** Whether the sidebar is in collapsed mode */ + isCollapsed?: boolean; } export function ProjectSection({ projectId, projectName, + githubOwner, mainRepoPath, workspaces, activeWorkspaceId, shortcutBaseIndex, + isCollapsed: isSidebarCollapsed = false, }: ProjectSectionProps) { const [dropdownOpen, setDropdownOpen] = useState(false); const { isProjectCollapsed, toggleProjectCollapsed } = @@ -66,15 +64,75 @@ export function ProjectSection({ openModal(projectId); }; + // When sidebar is collapsed, show compact view with just thumbnail and workspace icons + if (isSidebarCollapsed) { + return ( +
+ toggleProjectCollapsed(projectId)} + workspaceCount={workspaces.length} + onNewWorkspace={handleNewWorkspace} + onQuickCreate={handleQuickCreate} + isCreating={createWorkspace.isPending} + dropdownOpen={dropdownOpen} + onDropdownOpenChange={setDropdownOpen} + /> + + {!isCollapsed && ( + +
+ {workspaces.map((workspace, index) => ( + + ))} +
+
+ )} +
+
+ ); + } + return (
toggleProjectCollapsed(projectId)} workspaceCount={workspaces.length} + onNewWorkspace={handleNewWorkspace} + onQuickCreate={handleQuickCreate} + isCreating={createWorkspace.isPending} + dropdownOpen={dropdownOpen} + onDropdownOpenChange={setDropdownOpen} /> @@ -102,39 +160,6 @@ export function ProjectSection({ shortcutIndex={shortcutBaseIndex + index} /> ))} - - - - - - - - New Workspace - - - - Quick Create - - -
)} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectThumbnail/ProjectThumbnail.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectThumbnail/ProjectThumbnail.tsx new file mode 100644 index 00000000000..f1796bbbb0e --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectThumbnail/ProjectThumbnail.tsx @@ -0,0 +1,68 @@ +import { cn } from "@superset/ui/utils"; +import { useState } from "react"; +import { trpc } from "renderer/lib/trpc"; + +interface ProjectThumbnailProps { + projectId: string; + projectName: string; + githubOwner: string | null; + className?: string; +} + +function getGitHubAvatarUrl(owner: string): string { + return `https://github.com/${owner}.png?size=64`; +} + +export function ProjectThumbnail({ + projectId, + projectName, + githubOwner, + className, +}: ProjectThumbnailProps) { + const [imageError, setImageError] = useState(false); + + // Always fetch to ensure we get the latest - the backend caches it + const { data: avatarData } = trpc.projects.getGitHubAvatar.useQuery( + { id: projectId }, + { + staleTime: 1000 * 60 * 5, // Consider stale after 5 minutes + refetchOnWindowFocus: false, + }, + ); + + // Prefer fetched data, fall back to prop + const owner = avatarData?.owner ?? githubOwner; + const firstLetter = projectName.charAt(0).toUpperCase(); + + // Show avatar if we have an owner and no image loading error + if (owner && !imageError) { + return ( +
+ {`${projectName} setImageError(true)} + /> +
+ ); + } + + // Fallback: show first letter with subtle background + return ( +
+ {firstLetter} +
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectThumbnail/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectThumbnail/index.ts new file mode 100644 index 00000000000..ddda77440d6 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectThumbnail/index.ts @@ -0,0 +1 @@ +export { ProjectThumbnail } from "./ProjectThumbnail"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ResizableWorkspaceSidebar.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ResizableWorkspaceSidebar.tsx index 526fa283d2c..33df9877692 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ResizableWorkspaceSidebar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ResizableWorkspaceSidebar.tsx @@ -1,14 +1,14 @@ import { cn } from "@superset/ui/utils"; import { useCallback, useEffect, useRef } from "react"; import { + COLLAPSED_WORKSPACE_SIDEBAR_WIDTH, MAX_WORKSPACE_SIDEBAR_WIDTH, - MIN_WORKSPACE_SIDEBAR_WIDTH, useWorkspaceSidebarStore, } from "renderer/stores"; import { WorkspaceSidebar } from "./WorkspaceSidebar"; export function ResizableWorkspaceSidebar() { - const { isOpen, width, setWidth, isResizing, setIsResizing } = + const { isOpen, width, setWidth, isResizing, setIsResizing, isCollapsed } = useWorkspaceSidebarStore(); const startXRef = useRef(0); @@ -30,11 +30,8 @@ export function ResizableWorkspaceSidebar() { const delta = e.clientX - startXRef.current; const newWidth = startWidthRef.current + delta; - const clampedWidth = Math.max( - MIN_WORKSPACE_SIDEBAR_WIDTH, - Math.min(MAX_WORKSPACE_SIDEBAR_WIDTH, newWidth), - ); - setWidth(clampedWidth); + // Let setWidth handle the snapping logic + setWidth(newWidth); }, [isResizing, setWidth], ); @@ -65,12 +62,14 @@ export function ResizableWorkspaceSidebar() { return null; } + const sidebarCollapsed = isCollapsed(); + return (
- + {/* Resize handle */} {/* biome-ignore lint/a11y/useSemanticElements:
is not appropriate for interactive resize handles */} @@ -78,7 +77,7 @@ export function ResizableWorkspaceSidebar() { role="separator" aria-orientation="vertical" aria-valuenow={width} - aria-valuemin={MIN_WORKSPACE_SIDEBAR_WIDTH} + aria-valuemin={COLLAPSED_WORKSPACE_SIDEBAR_WIDTH} aria-valuemax={MAX_WORKSPACE_SIDEBAR_WIDTH} tabIndex={0} onMouseDown={handleMouseDown} 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 81340b17eaf..11c44051f0a 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 @@ -18,7 +18,7 @@ import { cn } from "@superset/ui/utils"; import { useState } from "react"; import { useDrag, useDrop } from "react-dnd"; import { HiMiniXMark } from "react-icons/hi2"; -import { LuEye, LuEyeOff, LuGitBranch } from "react-icons/lu"; +import { LuEye, LuEyeOff, LuFolder, LuFolderGit2 } from "react-icons/lu"; import { trpc } from "renderer/lib/trpc"; import { useReorderWorkspaces, @@ -56,6 +56,8 @@ interface WorkspaceListItemProps { isUnread?: boolean; index: number; shortcutIndex?: number; + /** Whether the sidebar is in collapsed mode (icon-only view) */ + isCollapsed?: boolean; } export function WorkspaceListItem({ @@ -69,6 +71,7 @@ export function WorkspaceListItem({ isUnread = false, index, shortcutIndex, + isCollapsed = false, }: WorkspaceListItemProps) { const isBranchWorkspace = type === "branch"; const setActiveWorkspace = useSetActiveWorkspace(); @@ -178,6 +181,71 @@ export function WorkspaceListItem({ const pr = githubStatus?.pr; const showDiffStats = pr && (pr.additions > 0 || pr.deletions > 0); + // Determine if we should show the branch subtitle + const showBranchSubtitle = + !isBranchWorkspace && name && name !== branch && !rename.isRenaming; + + // 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 ( + + {collapsedButton} + + {name || branch} + + Local workspace + + + + ); + } + + // Worktree workspaces get the full hover card + return ( + + {collapsedButton} + + + + + ); + } + const content = ( - - - Close or delete - - - )} + {/* Close button for worktree workspaces */} + {!isBranchWorkspace && ( + + )} +
); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/BranchSwitcher/BranchSwitcher.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/BranchSwitcher/BranchSwitcher.tsx index 0bb7e432b36..acd747b4b99 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/BranchSwitcher/BranchSwitcher.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/BranchSwitcher/BranchSwitcher.tsx @@ -9,7 +9,7 @@ import { Input } from "@superset/ui/input"; import { toast } from "@superset/ui/sonner"; import { cn } from "@superset/ui/utils"; import { useMemo, useState } from "react"; -import { HiCheck, HiChevronDown } from "react-icons/hi2"; +import { HiCheck, HiChevronUpDown } from "react-icons/hi2"; import { LuGitBranch, LuGitFork, LuLoader } from "react-icons/lu"; import { trpc } from "renderer/lib/trpc"; import { useSetActiveWorkspace } from "renderer/react-query/workspaces"; @@ -117,12 +117,13 @@ export function BranchSwitcher({ - +
{groups.map((group, index) => ( @@ -31,14 +37,16 @@ export function WorkspaceSidebar() { key={group.project.id} projectId={group.project.id} projectName={group.project.name} + githubOwner={group.project.githubOwner} mainRepoPath={group.project.mainRepoPath} workspaces={group.workspaces} activeWorkspaceId={activeWorkspaceId} shortcutBaseIndex={projectShortcutIndices[index]} + isCollapsed={isCollapsed} /> ))} - {groups.length === 0 && ( + {groups.length === 0 && !isCollapsed && (
No workspaces yet Add a project to get started @@ -46,9 +54,9 @@ export function WorkspaceSidebar() { )}
- + {!isCollapsed && } - +
); } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarFooter.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarFooter.tsx index 3a0b6d1c7cf..47b9f1527d2 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarFooter.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarFooter.tsx @@ -1,10 +1,17 @@ import { Button } from "@superset/ui/button"; import { toast } from "@superset/ui/sonner"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { LuFolderOpen } from "react-icons/lu"; import { useOpenNew } from "renderer/react-query/projects"; import { useCreateBranchWorkspace } from "renderer/react-query/workspaces"; -export function WorkspaceSidebarFooter() { +interface WorkspaceSidebarFooterProps { + isCollapsed?: boolean; +} + +export function WorkspaceSidebarFooter({ + isCollapsed = false, +}: WorkspaceSidebarFooterProps) { const openNew = useOpenNew(); const createBranchWorkspace = useCreateBranchWorkspace(); @@ -45,6 +52,27 @@ export function WorkspaceSidebarFooter() { } }; + if (isCollapsed) { + return ( +
+ + + + + Add repository + +
+ ); + } + return (
+ + New Workspace + + ); + } + return ( + + Toggle sidebar + + + {/* Workspaces button */} + + + + + Workspaces + + + +
+ ); + } + return ( -
+
+ {/* Toggle sidebar button */} + + + + + Toggle sidebar + + + {/* Workspaces button */} +
); diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupItem.tsx index 5154b0d0188..1af0963416e 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupItem.tsx @@ -62,8 +62,8 @@ export function GroupItem({ onClose(); }} className={cn( - "absolute right-1 top-1/2 -translate-y-1/2 cursor-pointer size-5 group-hover:opacity-100", - isActive ? "opacity-90" : "opacity-0", + "absolute right-1 top-1/2 -translate-y-1/2 cursor-pointer size-5 bg-muted hover:bg-background", + isActive ? "opacity-100" : "opacity-0 group-hover:opacity-100", )} aria-label="Close group" > diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx index 93bdc31325b..66e3893ede4 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/GroupStrip/GroupStrip.tsx @@ -125,7 +125,7 @@ export function GroupStrip() {
{tabs.length > 0 && (
{tabs.map((tab) => ( diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/ChangesView.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/ChangesView.tsx index 43fa741ee5f..6647a5b4a42 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/ChangesView.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/ChangesView.tsx @@ -36,7 +36,6 @@ export function ChangesView({ onFileOpen }: ChangesViewProps) { const { data: status, isLoading, - isFetching, refetch, } = trpc.changes.getStatus.useQuery( { worktreePath: worktreePath || "", defaultBranch: effectiveBaseBranch }, @@ -160,7 +159,7 @@ export function ChangesView({ onFileOpen }: ChangesViewProps) { if (!worktreePath) { return ( -
+
No workspace selected
); @@ -168,7 +167,7 @@ export function ChangesView({ onFileOpen }: ChangesViewProps) { if (isLoading) { return ( -
+
Loading changes...
); @@ -183,7 +182,7 @@ export function ChangesView({ onFileOpen }: ChangesViewProps) { !status.untracked ) { return ( -
+
Unable to load changes
); @@ -210,9 +209,6 @@ export function ChangesView({ onFileOpen }: ChangesViewProps) { return (
{/* Section header */}
- {isExpanded ? ( - - ) : ( - - )} - - {title} - - - ({count}) + + {title} + + {count} - {actions &&
{actions}
} + {actions &&
{actions}
}
{/* Section content */} - + {children} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/ChangesHeader/ChangesHeader.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/ChangesHeader/ChangesHeader.tsx index e1e2b84212d..d29454f002b 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/ChangesHeader/ChangesHeader.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/ChangesHeader/ChangesHeader.tsx @@ -7,6 +7,7 @@ import { SelectValue, } from "@superset/ui/select"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { useEffect, useRef, useState } from "react"; import { HiArrowPath } from "react-icons/hi2"; import { LuLoaderCircle } from "react-icons/lu"; import { trpc } from "renderer/lib/trpc"; @@ -17,9 +18,6 @@ import type { ChangesViewMode } from "../../types"; import { ViewModeToggle } from "../ViewModeToggle"; interface ChangesHeaderProps { - ahead: number; - behind: number; - isRefreshing: boolean; onRefresh: () => void; viewMode: ChangesViewMode; onViewModeChange: (mode: ChangesViewMode) => void; @@ -28,15 +26,38 @@ interface ChangesHeaderProps { } export function ChangesHeader({ - ahead: _ahead, - behind: _behind, - isRefreshing, onRefresh, viewMode, onViewModeChange, worktreePath, workspaceId, }: ChangesHeaderProps) { + const [isManualRefresh, setIsManualRefresh] = useState(false); + const timeoutRef = useRef(null); + + const handleRefresh = () => { + setIsManualRefresh(true); + onRefresh(); + // Clear any existing timeout + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + // Stop spinning after a short delay + timeoutRef.current = setTimeout(() => { + setIsManualRefresh(false); + }, 600); + }; + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + }; + }, []); + const { baseBranch, setBaseBranch } = useChangesStore(); const { data: branchData, isLoading } = trpc.changes.getBranches.useQuery( @@ -66,10 +87,13 @@ export function ChangesHeader({ }; return ( -
-
+
+
+ + Base: + {isLoading || !branchData ? ( - + {effectiveBaseBranch} ) : ( @@ -78,7 +102,7 @@ export function ChangesHeader({ @@ -103,6 +127,8 @@ export function ChangesHeader({ )} +
+
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/CommitInput/CommitInput.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/CommitInput/CommitInput.tsx index b94dceaf9da..de8863d752f 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/CommitInput/CommitInput.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/Sidebar/ChangesView/components/CommitInput/CommitInput.tsx @@ -211,12 +211,12 @@ export function CommitInput({ : null; return ( -
+