From e7a37798127e764bee6143c9ff470a5ffb5fe749 Mon Sep 17 00:00:00 2001 From: z3thon Date: Tue, 17 Mar 2026 17:00:41 +0700 Subject: [PATCH 01/33] feat: add per-project worktree mode setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `worktreeMode` setting (global + per-project override) with three values: "always" (current behavior), "optional" (user chooses per workspace via a toggle in the New Workspace modal), and "disabled" (never create worktrees — open directly in main repo). - Add WorktreeMode type and WORKTREE_MODES constant to local-db schema - Add worktree_mode column to both settings and projects tables - Add getWorktreeMode/setWorktreeMode tRPC procedures for global setting - Add worktreeMode to project update procedure for per-project override - Modify workspace create procedure to respect effective worktree mode - Add Worktree Mode dropdown to global Git & Worktrees settings page - Add per-project Worktree Mode override in project settings - Register GIT_WORKTREE_MODE in settings search - Show worktree toggle in New Workspace modal when mode is "optional" - Hide branch prefix/worktree location settings when mode is "disabled" --- .../src/lib/trpc/routers/projects/projects.ts | 5 + .../src/lib/trpc/routers/settings/index.ts | 21 ++++ .../routers/workspaces/procedures/create.ts | 83 +++++++++++++- .../components/PromptGroup/PromptGroup.tsx | 40 ++++++- .../components/GitSettings/GitSettings.tsx | 72 +++++++++++- .../ProjectSettings/ProjectSettings.tsx | 105 ++++++++++++++---- .../utils/settings-search/settings-search.ts | 19 ++++ .../drizzle/0037_add_worktree_mode.sql | 2 + packages/local-db/drizzle/meta/_journal.json | 7 ++ packages/local-db/src/schema/schema.ts | 3 + packages/local-db/src/schema/zod.ts | 7 ++ 11 files changed, 335 insertions(+), 29 deletions(-) create mode 100644 packages/local-db/drizzle/0037_add_worktree_mode.sql diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts index 17f671a1944..ddc1b507dd4 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/projects.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/projects.ts @@ -7,6 +7,7 @@ import { projects, type SelectProject, settings, + WORKTREE_MODES, workspaceSections, workspaces, worktrees, @@ -1230,6 +1231,7 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { worktreeBaseDir: z.string().nullable().optional(), hideImage: z.boolean().optional(), defaultApp: z.enum(EXTERNAL_APPS).nullable().optional(), + worktreeMode: z.enum(WORKTREE_MODES).nullable().optional(), }), }), ) @@ -1268,6 +1270,9 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => { ...(input.patch.defaultApp !== undefined && { defaultApp: input.patch.defaultApp, }), + ...(input.patch.worktreeMode !== undefined && { + worktreeMode: input.patch.worktreeMode, + }), lastOpenedAt: Date.now(), }) .where(eq(projects.id, input.id)) diff --git a/apps/desktop/src/lib/trpc/routers/settings/index.ts b/apps/desktop/src/lib/trpc/routers/settings/index.ts index 26ffdc0cc04..58db837db3f 100644 --- a/apps/desktop/src/lib/trpc/routers/settings/index.ts +++ b/apps/desktop/src/lib/trpc/routers/settings/index.ts @@ -9,6 +9,7 @@ import { settings, TERMINAL_LINK_BEHAVIORS, type TerminalPreset, + WORKTREE_MODES, } from "@superset/local-db"; import { AGENT_PRESET_COMMANDS, @@ -749,6 +750,26 @@ export const createSettingsRouter = () => { return { success: true }; }), + getWorktreeMode: publicProcedure.query(() => { + const row = getSettings(); + return row.worktreeMode ?? "always"; + }), + + setWorktreeMode: publicProcedure + .input(z.object({ mode: z.enum(WORKTREE_MODES) })) + .mutation(({ input }) => { + localDb + .insert(settings) + .values({ id: 1, worktreeMode: input.mode }) + .onConflictDoUpdate({ + target: settings.id, + set: { worktreeMode: input.mode }, + }) + .run(); + + return { success: true }; + }), + getOpenLinksInApp: publicProcedure.query(() => { const row = getSettings(); return row.openLinksInApp ?? DEFAULT_OPEN_LINKS_IN_APP; 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 97285fc73ad..d1faf7e2fdb 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts @@ -295,6 +295,7 @@ export const createCreateProcedures = () => { baseBranch: z.string().optional(), useExistingBranch: z.boolean().optional(), applyPrefix: z.boolean().optional().default(true), + useWorktree: z.boolean().optional(), }), ) .mutation(async ({ input }) => { @@ -307,6 +308,87 @@ export const createCreateProcedures = () => { throw new Error(`Project ${input.projectId} not found`); } + // Resolve effective worktree mode + const globalSettings = localDb.select().from(settings).get(); + const effectiveWorktreeMode = + project.worktreeMode ?? globalSettings?.worktreeMode ?? "always"; + + // If worktrees are disabled (or user explicitly chose no worktree), + // open directly in the main repo + if ( + effectiveWorktreeMode === "disabled" || + input.useWorktree === false + ) { + const branch = + input.branchName?.trim() || + (await getCurrentBranch(project.mainRepoPath)); + if (!branch) { + throw new Error("Could not determine current branch"); + } + + const existing = getBranchWorkspace(input.projectId); + + if (existing) { + if (existing.branch !== branch) { + localDb + .update(workspaces) + .set({ branch }) + .where(eq(workspaces.id, existing.id)) + .run(); + } + touchWorkspace(existing.id); + setLastActiveWorkspace(existing.id); + return { + workspace: { + ...existing, + branch, + lastOpenedAt: Date.now(), + }, + worktreePath: project.mainRepoPath, + projectId: project.id, + isInitializing: false, + wasExisting: true, + }; + } + + const maxTabOrder = getMaxProjectChildTabOrder(input.projectId); + const workspace = localDb + .insert(workspaces) + .values({ + projectId: input.projectId, + type: "branch", + branch, + name: input.name ?? branch, + tabOrder: maxTabOrder + 1, + }) + .onConflictDoNothing() + .returning() + .all(); + + const ws = workspace[0] ?? getBranchWorkspace(input.projectId); + if (!ws) { + throw new Error("Failed to create or find branch workspace"); + } + + setLastActiveWorkspace(ws.id); + activateProject(project); + + track("workspace_created", { + workspace_id: ws.id, + project_id: project.id, + type: "branch", + }); + + return { + workspace: ws, + initialCommands: null, + worktreePath: project.mainRepoPath, + projectId: project.id, + isInitializing: false, + wasExisting: false, + }; + } + let existingBranchName: string | undefined; if (input.useExistingBranch) { existingBranchName = input.branchName?.trim(); @@ -332,7 +414,6 @@ export const createCreateProcedures = () => { let branchPrefix: string | undefined; if (input.applyPrefix) { - const globalSettings = localDb.select().from(settings).get(); const projectOverrides = project.branchPrefixMode != null; const prefixMode = projectOverrides ? project.branchPrefixMode diff --git a/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx b/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx index 314b246558d..a86d5685ef3 100644 --- a/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx +++ b/apps/desktop/src/renderer/components/NewWorkspaceModal/components/PromptGroup/PromptGroup.tsx @@ -27,8 +27,10 @@ import { DropdownMenuTrigger, } from "@superset/ui/dropdown-menu"; import { Input } from "@superset/ui/input"; +import { Label } from "@superset/ui/label"; import { Popover, PopoverContent, PopoverTrigger } from "@superset/ui/popover"; import { toast } from "@superset/ui/sonner"; +import { Switch } from "@superset/ui/switch"; import { cn } from "@superset/ui/utils"; import { AnimatePresence, motion } from "framer-motion"; import { @@ -471,6 +473,12 @@ function PromptGroupInner({ const { data: globalBranchPrefix } = electronTrpc.settings.getBranchPrefix.useQuery(); const { data: gitInfo } = electronTrpc.settings.getGitInfo.useQuery(); + const { data: globalWorktreeMode } = + electronTrpc.settings.getWorktreeMode.useQuery(); + + const effectiveWorktreeMode = + project?.worktreeMode ?? globalWorktreeMode ?? "always"; + const [useWorktreeToggle, setUseWorktreeToggle] = useState(true); const resolvedPrefix = useMemo(() => { const projectOverrides = project?.branchPrefixMode != null; @@ -637,6 +645,12 @@ function PromptGroupInner({ ? sanitizeBranchNameWithMaxLength(branchName.trim()) : branchSlug) || undefined, baseBranch: baseBranch || undefined, + ...(effectiveWorktreeMode === "optional" && { + useWorktree: useWorktreeToggle, + }), + ...(effectiveWorktreeMode === "disabled" && { + useWorktree: false, + }), }, { agentLaunchRequest: launchRequest ?? undefined, @@ -661,10 +675,12 @@ function PromptGroupInner({ convertBlobUrlToDataUrl, createFromPr, createWorkspace, + effectiveWorktreeMode, linkedPR, projectId, runAsyncAction, trimmedPrompt, + useWorktreeToggle, workspaceName, workspaceNameEdited, ]); @@ -903,9 +919,27 @@ function PromptGroupInner({ )} - - {modKey}+↵ to create - +
+ {effectiveWorktreeMode === "optional" && ( +
+ + +
+ )} + + {modKey}+↵ to create + +
); diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/git/components/GitSettings/GitSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/git/components/GitSettings/GitSettings.tsx index 39a190cef10..618f9020014 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/git/components/GitSettings/GitSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/git/components/GitSettings/GitSettings.tsx @@ -1,4 +1,4 @@ -import type { BranchPrefixMode } from "@superset/local-db"; +import type { BranchPrefixMode, WorktreeMode } from "@superset/local-db"; import { Input } from "@superset/ui/input"; import { Label } from "@superset/ui/label"; import { @@ -23,11 +23,21 @@ import { type SettingItemId, } from "../../../utils/settings-search"; +const WORKTREE_MODE_LABELS: Record = { + always: "Always use worktrees", + optional: "Ask each time", + disabled: "Never use worktrees", +}; + interface GitSettingsProps { visibleItems?: SettingItemId[] | null; } export function GitSettings({ visibleItems }: GitSettingsProps) { + const showWorktreeMode = isItemVisible( + SETTING_ITEM_ID.GIT_WORKTREE_MODE, + visibleItems, + ); const showDeleteLocalBranch = isItemVisible( SETTING_ITEM_ID.GIT_DELETE_LOCAL_BRANCH, visibleItems, @@ -43,6 +53,28 @@ export function GitSettings({ visibleItems }: GitSettingsProps) { const utils = electronTrpc.useUtils(); + const { data: worktreeMode, isLoading: isWorktreeModeLoading } = + electronTrpc.settings.getWorktreeMode.useQuery(); + const setWorktreeMode = electronTrpc.settings.setWorktreeMode.useMutation({ + onMutate: async ({ mode }) => { + await utils.settings.getWorktreeMode.cancel(); + const previous = utils.settings.getWorktreeMode.getData(); + utils.settings.getWorktreeMode.setData(undefined, mode); + return { previous }; + }, + onError: (_err, _vars, context) => { + if (context?.previous !== undefined) { + utils.settings.getWorktreeMode.setData(undefined, context.previous); + } + }, + onSettled: () => { + utils.settings.getWorktreeMode.invalidate(); + }, + }); + + const effectiveWorktreeMode = worktreeMode ?? "always"; + const isWorktreeDisabled = effectiveWorktreeMode === "disabled"; + const { data: deleteLocalBranch, isLoading: isDeleteBranchLoading } = electronTrpc.settings.getDeleteLocalBranch.useQuery(); const setDeleteLocalBranch = @@ -154,7 +186,41 @@ export function GitSettings({ visibleItems }: GitSettingsProps) {
- {showDeleteLocalBranch && ( + {showWorktreeMode && ( +
+
+ +

+ Control whether new workspaces use git worktrees +

+
+ +
+ )} + + {showDeleteLocalBranch && !isWorktreeDisabled && (
@@ -59,13 +62,29 @@ export function CloseProjectDialog({ Cancel + {hasWorktrees && ( + + )} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx index a71eaa1342f..ab52990195a 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx @@ -12,7 +12,7 @@ import { toast } from "@superset/ui/sonner"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { cn } from "@superset/ui/utils"; import { useNavigate, useParams } from "@tanstack/react-router"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { HiChevronRight, HiMiniPlus } from "react-icons/hi2"; import { LuFolderOpen, @@ -52,6 +52,18 @@ interface ProjectHeaderProps { onToggleCollapse: () => void; workspaceCount: number; onNewWorkspace: () => void; + /** Whether this project has worktree-type workspaces on disk */ + hasWorktrees?: boolean; + /** True when the project has only a single branch workspace (no worktrees) */ + isBranchOnly?: boolean; + /** Branch name for inline display on branch-only projects */ + branchOnlyBranch?: string; + /** Worktree path for diff stats on branch-only projects */ + branchOnlyWorktreePath?: string; + /** Whether this branch-only project's workspace is currently active */ + isActive?: boolean; + /** Called when clicking a branch-only project to navigate directly */ + onNavigateToWorkspace?: () => void; } export function ProjectHeader({ @@ -67,6 +79,12 @@ export function ProjectHeader({ onToggleCollapse, workspaceCount, onNewWorkspace, + hasWorktrees = false, + isBranchOnly = false, + branchOnlyBranch, + branchOnlyWorktreePath, + isActive = false, + onNavigateToWorkspace, }: ProjectHeaderProps) { const utils = electronTrpc.useUtils(); const navigate = useNavigate(); @@ -74,6 +92,24 @@ export function ProjectHeader({ const [isCloseDialogOpen, setIsCloseDialogOpen] = useState(false); const rename = useProjectRename(projectId, projectName); + // Diff stats for branch-only inline display + const { data: branchOnlyChanges } = electronTrpc.changes.getStatus.useQuery( + { worktreePath: branchOnlyWorktreePath ?? "" }, + { enabled: isBranchOnly && !!branchOnlyWorktreePath, staleTime: 5000 }, + ); + const branchOnlyDiffStats = useMemo(() => { + if (!branchOnlyChanges) return null; + const files = [ + ...branchOnlyChanges.staged, + ...branchOnlyChanges.unstaged, + ...branchOnlyChanges.untracked, + ]; + const additions = files.reduce((sum, f) => sum + (f.additions || 0), 0); + const deletions = files.reduce((sum, f) => sum + (f.deletions || 0), 0); + if (additions === 0 && deletions === 0) return null; + return { additions, deletions }; + }, [branchOnlyChanges]); + const closeProject = electronTrpc.projects.close.useMutation({ onMutate: async ({ id }) => { let shouldNavigate = false; @@ -128,8 +164,11 @@ export function ProjectHeader({ setIsCloseDialogOpen(true); }; - const handleConfirmClose = () => { - closeProject.mutate({ id: projectId }); + const handleConfirmClose = (options: { deleteWorktrees: boolean }) => { + closeProject.mutate({ + id: projectId, + deleteWorktrees: options.deleteWorktrees, + }); }; const handleOpenInFinder = () => { @@ -268,6 +307,7 @@ export function ProjectHeader({ {rename.isRenaming ? ( @@ -307,58 +348,88 @@ export function ProjectHeader({ ) : ( )} - - + {!isBranchOnly && ( + <> + + + + + + New workspace + + + - - - New workspace - - - - + + )}
@@ -406,6 +477,7 @@ export function ProjectHeader({ sum + s.workspaces.length, 0); + const isBranchOnly = worktreeMode === "disabled"; + const hasWorktrees = !isBranchOnly; + + const matchRoute = useMatchRoute(); + const isBranchOnlyActive = + isBranchOnly && + workspaces.length > 0 && + !!matchRoute({ + to: "/workspace/$workspaceId", + params: { workspaceId: workspaces[0].id }, + }); + + const handleNavigateToWorkspace = () => { + if (isBranchOnly && workspaces.length > 0) { + navigateToWorkspace(workspaces[0].id, navigate); + } + }; + const { orderedWorkspaceIds, topLevelChildren } = useMemo(() => { const topLevelWorkspacesById = new Map( workspaces.map((workspace) => [workspace.id, workspace]), @@ -197,6 +217,9 @@ export function ProjectSection({ }, }); + // For branch-only projects, always show workspace (no collapse needed) + const showWorkspaces = isBranchOnly || !isCollapsed; + if (isSidebarCollapsed) { return (
toggleProjectCollapsed(projectId)} workspaceCount={totalWorkspaceCount} onNewWorkspace={handleNewWorkspace} + hasWorktrees={hasWorktrees} + isBranchOnly={isBranchOnly} + isActive={isBranchOnlyActive} + onNavigateToWorkspace={handleNavigateToWorkspace} /> - {!isCollapsed && ( + {showWorkspaces && ( toggleProjectCollapsed(projectId)} workspaceCount={totalWorkspaceCount} onNewWorkspace={handleNewWorkspace} + hasWorktrees={hasWorktrees} + isBranchOnly={isBranchOnly} + branchOnlyBranch={ + isBranchOnly && workspaces.length > 0 + ? workspaces[0].branch + : undefined + } + branchOnlyWorktreePath={ + isBranchOnly && workspaces.length > 0 + ? workspaces[0].worktreePath + : undefined + } + isActive={isBranchOnlyActive} + onNavigateToWorkspace={handleNavigateToWorkspace} /> - - {!isCollapsed && ( - -
- {topLevelChildren.length === 0 && ( -
- )} - {topLevelChildren.map((item) => - item.kind === "workspace" ? ( - - ) : ( - + {showWorkspaces && ( + +
+ {!isBranchOnly && topLevelChildren.length === 0 && ( +
- ), - )} -
- - )} - + )} + {topLevelChildren.map((item) => + item.kind === "workspace" ? ( + + ) : ( + + ), + )} +
+
+ )} + + )}
); } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceContextMenu.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceContextMenu.tsx index 513c4e02590..4d7f8f8a2ca 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceContextMenu.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceContextMenu.tsx @@ -24,6 +24,7 @@ import { LuFolderPlus, LuMinus, LuPencil, + LuX, } from "react-icons/lu"; import { useCreateSectionFromWorkspaces, @@ -48,6 +49,7 @@ interface WorkspaceContextMenuProps { onCopyPath: () => void; onSetUnread: (isUnread: boolean) => void; onResetStatus: () => void; + onClose: () => void; children: React.ReactNode; } @@ -64,6 +66,7 @@ export function WorkspaceContextMenu({ onCopyPath, onSetUnread, onResetStatus, + onClose, children, }: WorkspaceContextMenuProps) { const [isContextMenuOpen, setIsContextMenuOpen] = useState(false); @@ -180,7 +183,20 @@ export function WorkspaceContextMenu({ return ( {children} - {commonContextMenuItems} + + {commonContextMenuItems} + + + + Close Workspace + + ); } @@ -202,6 +218,17 @@ export function WorkspaceContextMenu({ {commonContextMenuItems} + + + + Close Workspace + 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 313355ff83a..b5cfa76aa2c 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,6 +18,7 @@ import { useWorkspaceSelectionStore } from "renderer/stores/workspace-selection" import { getHighestPriorityStatus } from "shared/tabs-types"; import { CollapsedWorkspaceItem } from "./CollapsedWorkspaceItem"; import { DeleteWorkspaceDialog } from "./components"; +import { CloseWorkspaceDialog } from "./components/CloseWorkspaceDialog"; import { GITHUB_STATUS_STALE_TIME, MAX_KEYBOARD_SHORTCUT_INDEX, @@ -120,6 +121,26 @@ export function WorkspaceListItem({ const { showDeleteDialog, setShowDeleteDialog, handleDeleteClick } = useWorkspaceDeleteHandler(); + const [showCloseDialog, setShowCloseDialog] = useState(false); + const closeWorkspace = electronTrpc.workspaces.close.useMutation({ + onSuccess: () => utils.workspaces.getAllGrouped.invalidate(), + onError: (error) => + toast.error(`Failed to close workspace: ${error.message}`), + }); + const deleteWorkspace = electronTrpc.workspaces.delete.useMutation({ + onSuccess: () => utils.workspaces.getAllGrouped.invalidate(), + onError: (error) => + toast.error(`Failed to close workspace: ${error.message}`), + }); + + const handleCloseConfirm = (options: { moveToTrash: boolean }) => { + if (options.moveToTrash) { + deleteWorkspace.mutate({ id }); + } else { + closeWorkspace.mutate({ id }); + } + }; + const { data: githubStatus } = electronTrpc.workspaces.getGitHubStatus.useQuery( { workspaceId: id }, @@ -436,6 +457,7 @@ export function WorkspaceListItem({ onCopyPath={handleCopyPath} onSetUnread={(unread) => setUnread.mutate({ id, isUnread: unread })} onResetStatus={() => resetWorkspaceStatus(id)} + onClose={() => setShowCloseDialog(true)} > {content} @@ -446,6 +468,13 @@ export function WorkspaceListItem({ open={showDeleteDialog} onOpenChange={setShowDeleteDialog} /> + ); } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/CloseWorkspaceDialog/CloseWorkspaceDialog.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/CloseWorkspaceDialog/CloseWorkspaceDialog.tsx new file mode 100644 index 00000000000..a82eb3fc150 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/CloseWorkspaceDialog/CloseWorkspaceDialog.tsx @@ -0,0 +1,88 @@ +import { + AlertDialog, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@superset/ui/alert-dialog"; +import { Button } from "@superset/ui/button"; + +interface CloseWorkspaceDialogProps { + workspaceName: string; + isWorktree: boolean; + open: boolean; + onOpenChange: (open: boolean) => void; + onConfirm: (options: { moveToTrash: boolean }) => void; +} + +export function CloseWorkspaceDialog({ + workspaceName, + isWorktree, + open, + onOpenChange, + onConfirm, +}: CloseWorkspaceDialogProps) { + return ( + + + + + Close workspace "{workspaceName}"? + + +
+ + This will kill all active terminals in this workspace. + + {isWorktree && ( + + Close Workspace removes the workspace from + the sidebar. Worktree files stay on disk. +
+ Recycle Worktree closes the workspace and + moves the worktree folder to Trash. +
+ )} +
+
+
+ + + + + {isWorktree && ( + + )} + +
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/CloseWorkspaceDialog/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/CloseWorkspaceDialog/index.ts new file mode 100644 index 00000000000..e6b9ead2ed6 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/CloseWorkspaceDialog/index.ts @@ -0,0 +1 @@ +export { CloseWorkspaceDialog } from "./CloseWorkspaceDialog"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx index 4198ed8636c..04c14dd0731 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx @@ -88,6 +88,7 @@ export function WorkspaceSidebar({ mainRepoPath={group.project.mainRepoPath} hideImage={group.project.hideImage} iconUrl={group.project.iconUrl} + worktreeMode={group.project.worktreeMode} workspaces={group.workspaces} sections={group.sections ?? []} topLevelItems={group.topLevelItems} diff --git a/apps/desktop/src/renderer/stores/worktree-choice-dialog.ts b/apps/desktop/src/renderer/stores/worktree-choice-dialog.ts new file mode 100644 index 00000000000..bc4884d8c88 --- /dev/null +++ b/apps/desktop/src/renderer/stores/worktree-choice-dialog.ts @@ -0,0 +1,32 @@ +import { create } from "zustand"; +import { devtools } from "zustand/middleware"; + +interface WorktreeChoiceDialogState { + isOpen: boolean; + projectName: string; + onChoice: ((enableWorktrees: boolean) => void) | null; + open: (params: { + projectName: string; + onChoice: (enableWorktrees: boolean) => void; + }) => void; + close: () => void; +} + +export const useWorktreeChoiceDialogStore = create()( + devtools( + (set) => ({ + isOpen: false, + projectName: "", + onChoice: null, + + open: ({ projectName, onChoice }) => { + set({ isOpen: true, projectName, onChoice }); + }, + + close: () => { + set({ isOpen: false, projectName: "", onChoice: null }); + }, + }), + { name: "WorktreeChoiceDialogStore" }, + ), +); From 6867bd235b4d538f6c1929f8222e59cbc7888534 Mon Sep 17 00:00:00 2001 From: z3thon Date: Thu, 19 Mar 2026 15:52:56 +0700 Subject: [PATCH 03/33] fix(desktop): worktree recycle uses Trash, sidebar refreshes on project open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `trash` option to workspaces.delete procedure — uses shell.trashItem() + git worktree prune instead of permanent delete - "Disable & Recycle" in project settings now sends worktrees to Trash - "Recycle Worktree" in workspace close dialog also uses Trash - Invalidate getAllGrouped + getRecents after worktree choice dialog so new projects appear in the sidebar immediately - Capitalize "Worktrees" in dialog titles, use "Enable"/"Disable" button labels instead of "With/Without Worktrees" --- .../routers/workspaces/procedures/delete.ts | 30 +++-- .../WorktreeChoiceDialog.tsx | 16 +-- .../react-query/projects/useOpenProject.tsx | 3 +- .../ProjectSettings/ProjectSettings.tsx | 106 ++++++++++++++++++ .../WorkspaceListItem/WorkspaceListItem.tsx | 2 +- 5 files changed, 140 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts index 21cf02c02b1..ea36d470f79 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts @@ -154,6 +154,7 @@ export const createDeleteProcedures = () => { id: z.string(), deleteLocalBranch: z.boolean().optional(), force: z.boolean().optional(), + trash: z.boolean().optional(), }), ) .mutation(async ({ input }) => { @@ -252,13 +253,28 @@ export const createDeleteProcedures = () => { await workspaceInitManager.acquireProjectLock(project.id); try { - const removeResult = await removeWorktreeFromDisk({ - mainRepoPath: project.mainRepoPath, - worktreePath: worktree.path, - }); - if (!removeResult.success) { - clearWorkspaceDeletingStatus(input.id); - return removeResult; + if (input.trash) { + // Move to Trash (recoverable) instead of permanent delete + const { existsSync } = await import("node:fs"); + if (existsSync(worktree.path)) { + const { shell } = await import("electron"); + await shell.trashItem(worktree.path); + } + // Clean up stale git worktree references + const { getSimpleGitWithShellPath } = await import( + "../utils/git-client" + ); + const git = await getSimpleGitWithShellPath(project.mainRepoPath); + await git.raw(["worktree", "prune"]); + } else { + const removeResult = await removeWorktreeFromDisk({ + mainRepoPath: project.mainRepoPath, + worktreePath: worktree.path, + }); + if (!removeResult.success) { + clearWorkspaceDeletingStatus(input.id); + return removeResult; + } } } finally { workspaceInitManager.releaseProjectLock(project.id); diff --git a/apps/desktop/src/renderer/components/WorktreeChoiceDialog/WorktreeChoiceDialog.tsx b/apps/desktop/src/renderer/components/WorktreeChoiceDialog/WorktreeChoiceDialog.tsx index 2449af88396..2f7238771b3 100644 --- a/apps/desktop/src/renderer/components/WorktreeChoiceDialog/WorktreeChoiceDialog.tsx +++ b/apps/desktop/src/renderer/components/WorktreeChoiceDialog/WorktreeChoiceDialog.tsx @@ -26,18 +26,18 @@ export function WorktreeChoiceDialog({ - Enable worktrees for "{projectName}"? + Enable Worktrees for "{projectName}"?
- With Worktrees — each workspace gets its own - isolated copy of the repo. You can run multiple agents in - parallel without conflicts. + Enable — each workspace gets its own isolated + copy of the repo. You can run multiple agents in parallel + without conflicts. - Without Worktrees — work directly in the - project folder. One workspace, no copies. + Disable — work directly in the project folder. + One workspace, no copies.
@@ -53,7 +53,7 @@ export function WorktreeChoiceDialog({ onChoice(false); }} > - Without Worktrees + Disable
diff --git a/apps/desktop/src/renderer/react-query/projects/useOpenProject.tsx b/apps/desktop/src/renderer/react-query/projects/useOpenProject.tsx index 13cd2ede75b..52827280ed9 100644 --- a/apps/desktop/src/renderer/react-query/projects/useOpenProject.tsx +++ b/apps/desktop/src/renderer/react-query/projects/useOpenProject.tsx @@ -90,8 +90,9 @@ export function useOpenProject() { id: project.id, patch: { worktreeMode: "disabled" }, }); - await utils.workspaces.getAllGrouped.invalidate(); } + await utils.workspaces.getAllGrouped.invalidate(); + await utils.projects.getRecents.invalidate(); resolveChoice(); }, }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx index e9ad8cc4bd3..eab89d1a9c1 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx @@ -213,7 +213,29 @@ export function ProjectSettings({ const { data: globalWorktreeMode } = electronTrpc.settings.getWorktreeMode.useQuery(); + const [showDisableWorktreeDialog, setShowDisableWorktreeDialog] = + useState(false); + + const deleteWorkspace = electronTrpc.workspaces.delete.useMutation({ + onSettled: () => utils.workspaces.getAllGrouped.invalidate(), + }); + const closeWorkspaceMutation = electronTrpc.workspaces.close.useMutation({ + onSettled: () => utils.workspaces.getAllGrouped.invalidate(), + }); + + // Get worktree workspaces for this project + const { data: allGroups = [] } = + electronTrpc.workspaces.getAllGrouped.useQuery(); + const projectGroup = allGroups.find((g) => g.project.id === projectId); + const worktreeWorkspaces = (projectGroup?.workspaces ?? []).filter( + (w) => w.type === "worktree", + ); + const handleWorktreeModeChange = (value: string) => { + if (value === "disabled" && worktreeWorkspaces.length > 0) { + setShowDisableWorktreeDialog(true); + return; + } updateProject.mutate({ id: projectId, patch: { @@ -222,6 +244,29 @@ export function ProjectSettings({ }); }; + const handleDisableWorktrees = async (recycleWorktrees: boolean) => { + // Close/delete all worktree workspaces + for (const ws of worktreeWorkspaces) { + try { + if (recycleWorktrees) { + await deleteWorkspace.mutateAsync({ id: ws.id, trash: true }); + } else { + await closeWorkspaceMutation.mutateAsync({ id: ws.id }); + } + } catch (error) { + console.error( + `[ProjectSettings] Failed to close worktree workspace ${ws.id}:`, + error, + ); + } + } + // Now disable worktrees on the project + updateProject.mutate({ + id: projectId, + patch: { worktreeMode: "disabled" }, + }); + }; + const effectiveWorktreeMode = project?.worktreeMode ?? globalWorktreeMode ?? "always"; const isWorktreeDisabled = effectiveWorktreeMode === "disabled"; @@ -703,6 +748,67 @@ export function ProjectSettings({
+ + {/* Disable worktrees confirmation dialog */} + + + + + Disable Worktrees for "{project.name}"? + + +
+ + This project has {worktreeWorkspaces.length} worktree + workspace{worktreeWorkspaces.length !== 1 ? "s" : ""}. + + + Disable removes worktree workspaces from the + sidebar. Files stay on disk. +
+ Disable & Recycle also moves worktree folders + to Trash. +
+
+
+
+ + + + + +
+
); } 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 b5cfa76aa2c..32b7c80843f 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 @@ -135,7 +135,7 @@ export function WorkspaceListItem({ const handleCloseConfirm = (options: { moveToTrash: boolean }) => { if (options.moveToTrash) { - deleteWorkspace.mutate({ id }); + deleteWorkspace.mutate({ id, trash: true }); } else { closeWorkspace.mutate({ id }); } From 60c9f87e8d618a4b5af940892a081df7a7de6528 Mon Sep 17 00:00:00 2001 From: z3thon Date: Thu, 19 Mar 2026 16:04:58 +0700 Subject: [PATCH 04/33] feat(desktop): add hover shortcut overlay for branch-only projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Branch-only projects now show the same hover UX as worktree workspaces: diff stats fade out and keyboard shortcut (⌘1-9) fades in on hover. Uses the same CSS grid overlay pattern as WorkspaceListItem. --- .../ProjectSection/ProjectHeader.tsx | 39 ++++++++++++++----- .../ProjectSection/ProjectSection.tsx | 1 + 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx index ab52990195a..efee4891d83 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx @@ -60,6 +60,8 @@ interface ProjectHeaderProps { branchOnlyBranch?: string; /** Worktree path for diff stats on branch-only projects */ branchOnlyWorktreePath?: string; + /** Keyboard shortcut index for branch-only projects */ + shortcutIndex?: number; /** Whether this branch-only project's workspace is currently active */ isActive?: boolean; /** Called when clicking a branch-only project to navigate directly */ @@ -83,6 +85,7 @@ export function ProjectHeader({ isBranchOnly = false, branchOnlyBranch, branchOnlyWorktreePath, + shortcutIndex, isActive = false, onNavigateToWorkspace, }: ProjectHeaderProps) { @@ -322,7 +325,7 @@ export function ProjectHeader({
)} - {isBranchOnly && branchOnlyDiffStats && ( -
-
- - +{branchOnlyDiffStats.additions} - - - -{branchOnlyDiffStats.deletions} - + {isBranchOnly && ( +
+ {branchOnlyDiffStats && ( +
+
+ + +{branchOnlyDiffStats.additions} + + + -{branchOnlyDiffStats.deletions} + +
+
+ )} +
+ {shortcutIndex !== undefined && shortcutIndex < 9 && ( + + ⌘{shortcutIndex + 1} + + )}
)} 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 b7676013e48..c49c1617440 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 @@ -339,6 +339,7 @@ export function ProjectSection({ ? workspaces[0].worktreePath : undefined } + shortcutIndex={isBranchOnly ? shortcutBaseIndex : undefined} isActive={isBranchOnlyActive} onNavigateToWorkspace={handleNavigateToWorkspace} /> From 5c8d78362966a37dbde3e2caa152e3ccd4a9c8b6 Mon Sep 17 00:00:00 2001 From: z3thon Date: Thu, 19 Mar 2026 18:17:38 +0700 Subject: [PATCH 05/33] feat(desktop): add agent status ring to branch-only project thumbnails When an agent is running in a branch-only project's workspace, the project thumbnail in the sidebar gets a colored ring: - Working (amber): pulsing ring while agent is processing - Permission (red): pulsing ring when agent needs user input - Review (green): static ring when agent finished but user hasn't seen it Uses the same tabs store status aggregation as WorkspaceListItem. --- .../ProjectSection/ProjectHeader.tsx | 48 +++++++++++++++---- .../ProjectSection/ProjectSection.tsx | 3 ++ 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx index efee4891d83..c49066ea594 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx @@ -28,10 +28,13 @@ import { electronTrpc } from "renderer/lib/electron-trpc"; import { useUpdateProject } from "renderer/react-query/projects/useUpdateProject"; import { navigateToWorkspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { useProjectRename } from "renderer/screens/main/hooks/useProjectRename"; +import { useTabsStore } from "renderer/stores/tabs/store"; +import { extractPaneIdsFromLayout } from "renderer/stores/tabs/utils"; import { PROJECT_COLOR_DEFAULT, PROJECT_COLORS, } from "shared/constants/project-colors"; +import { getHighestPriorityStatus } from "shared/tabs-types"; import { STROKE_WIDTH } from "../constants"; import { RenameInput } from "../RenameInput"; import { CloseProjectDialog } from "./CloseProjectDialog"; @@ -62,6 +65,8 @@ interface ProjectHeaderProps { branchOnlyWorktreePath?: string; /** Keyboard shortcut index for branch-only projects */ shortcutIndex?: number; + /** Workspace ID for branch-only status tracking */ + branchOnlyWorkspaceId?: string; /** Whether this branch-only project's workspace is currently active */ isActive?: boolean; /** Called when clicking a branch-only project to navigate directly */ @@ -86,6 +91,7 @@ export function ProjectHeader({ branchOnlyBranch, branchOnlyWorktreePath, shortcutIndex, + branchOnlyWorkspaceId, isActive = false, onNavigateToWorkspace, }: ProjectHeaderProps) { @@ -95,6 +101,20 @@ export function ProjectHeader({ const [isCloseDialogOpen, setIsCloseDialogOpen] = useState(false); const rename = useProjectRename(projectId, projectName); + // Agent status for branch-only projects (pulsing ring around thumbnail) + const branchOnlyStatus = useTabsStore((state) => { + if (!branchOnlyWorkspaceId) return null; + function* paneStatuses() { + for (const tab of state.tabs) { + if (tab.workspaceId !== branchOnlyWorkspaceId) continue; + for (const paneId of extractPaneIdsFromLayout(tab.layout)) { + yield state.panes[paneId]?.status; + } + } + } + return getHighestPriorityStatus(paneStatuses()); + }); + // Diff stats for branch-only inline display const { data: branchOnlyChanges } = electronTrpc.changes.getStatus.useQuery( { worktreePath: branchOnlyWorktreePath ?? "" }, @@ -361,14 +381,26 @@ export function ProjectHeader({ )} >
- +
+ +
{projectName} {!isBranchOnly && ( 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 c49c1617440..29e2263b4b0 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 @@ -340,6 +340,9 @@ export function ProjectSection({ : undefined } shortcutIndex={isBranchOnly ? shortcutBaseIndex : undefined} + branchOnlyWorkspaceId={ + isBranchOnly && workspaces.length > 0 ? workspaces[0].id : undefined + } isActive={isBranchOnlyActive} onNavigateToWorkspace={handleNavigateToWorkspace} /> From f878bcda5ee62ae711a601bc2c9a0710fd8cdbb9 Mon Sep 17 00:00:00 2001 From: z3thon Date: Thu, 19 Mar 2026 18:20:47 +0700 Subject: [PATCH 06/33] feat(desktop): auto-discover project favicon on open When a project is opened or created, automatically search for favicon/logo files in the repo (favicon.ico, logo.png, public/favicon, etc.) and set it as the project icon. Only runs if no icon is already set. Manually uploaded icons take priority. Fire-and-forget so it doesn't block project creation. --- .../src/lib/trpc/routers/projects/projects.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/apps/desktop/src/lib/trpc/routers/projects/projects.ts b/apps/desktop/src/lib/trpc/routers/projects/projects.ts index 2a7ff963a85..42a6f42b88a 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/projects.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/projects.ts @@ -117,6 +117,27 @@ function upsertProject(mainRepoPath: string, defaultBranch: string): Project { .set({ lastOpenedAt: Date.now(), defaultBranch }) .where(eq(projects.id, existing.id)) .run(); + + // Discover favicon if no icon is set yet (fire-and-forget) + if (!existing.iconUrl) { + discoverAndSaveProjectIcon({ + projectId: existing.id, + repoPath: mainRepoPath, + }) + .then((iconUrl) => { + if (iconUrl) { + localDb + .update(projects) + .set({ iconUrl }) + .where(eq(projects.id, existing.id)) + .run(); + } + }) + .catch((err) => { + console.error("[upsertProject] Favicon discovery failed:", err); + }); + } + return { ...existing, lastOpenedAt: Date.now(), defaultBranch }; } @@ -131,6 +152,24 @@ function upsertProject(mainRepoPath: string, defaultBranch: string): Project { .returning() .get(); + // Discover favicon for new project (fire-and-forget) + discoverAndSaveProjectIcon({ + projectId: project.id, + repoPath: mainRepoPath, + }) + .then((iconUrl) => { + if (iconUrl) { + localDb + .update(projects) + .set({ iconUrl }) + .where(eq(projects.id, project.id)) + .run(); + } + }) + .catch((err) => { + console.error("[upsertProject] Favicon discovery failed:", err); + }); + return project; } From 797c2268d0dccb7ce4615d5ab30da3f389e51caf Mon Sep 17 00:00:00 2001 From: z3thon Date: Thu, 19 Mar 2026 18:21:49 +0700 Subject: [PATCH 07/33] fix(desktop): match status ring radius to project thumbnail (rounded) --- .../WorkspaceSidebar/ProjectSection/ProjectHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx index c49066ea594..4492aa77744 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx @@ -383,7 +383,7 @@ export function ProjectHeader({
Date: Thu, 19 Mar 2026 18:30:14 +0700 Subject: [PATCH 08/33] fix(desktop): expand favicon discovery to find nested app icons Add patterns for Next.js app directory (app/favicon.ico, app/icon.png), monorepo nested public directories (**/public/favicon.*), and increase glob depth to 4 levels. Fixes discovery for projects where the favicon is in a subdirectory like NEXTapp/app/ or peliguard-website/app/. --- .../projects/utils/favicon-discovery.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/trpc/routers/projects/utils/favicon-discovery.ts b/apps/desktop/src/lib/trpc/routers/projects/utils/favicon-discovery.ts index f985b7c2eb1..b005699236d 100644 --- a/apps/desktop/src/lib/trpc/routers/projects/utils/favicon-discovery.ts +++ b/apps/desktop/src/lib/trpc/routers/projects/utils/favicon-discovery.ts @@ -6,8 +6,9 @@ import { saveProjectIconFromFile, } from "main/lib/project-icons"; -/** Common favicon file names to search for in project roots */ +/** Common favicon file names to search for in project roots and subdirectories */ const FAVICON_PATTERNS = [ + // Root level "favicon.ico", "favicon.png", "favicon.svg", @@ -17,6 +18,7 @@ const FAVICON_PATTERNS = [ "icon.svg", ".github/logo.png", ".github/logo.svg", + // Common static/public directories "public/favicon.ico", "public/favicon.png", "public/favicon.svg", @@ -28,6 +30,19 @@ const FAVICON_PATTERNS = [ "assets/favicon.ico", "assets/favicon.png", "assets/icon.png", + // Next.js / app directory patterns (up to 2 levels deep) + "app/favicon.ico", + "app/icon.png", + "app/icon.svg", + "**/app/favicon.ico", + "**/app/icon.png", + "**/app/icon.svg", + // Deeper public directories in monorepos/nested projects + "**/public/favicon.ico", + "**/public/favicon.png", + "**/public/favicon.svg", + "**/public/logo.png", + "**/public/logo.svg", ]; /** Max file size for discovered favicons: 256KB */ @@ -48,6 +63,7 @@ export async function discoverAndSaveProjectIcon({ const matches = await fg(FAVICON_PATTERNS, { cwd: repoPath, absolute: true, + deep: 4, ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**"], onlyFiles: true, }); From 8dfa3ce40444c561680fb674c78d01b73b552632 Mon Sep 17 00:00:00 2001 From: z3thon Date: Thu, 19 Mar 2026 18:32:47 +0700 Subject: [PATCH 09/33] style(desktop): workspace count in badge tile instead of parentheses --- .../WorkspaceSidebar/ProjectSection/ProjectHeader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx index 4492aa77744..ffe2dce4daa 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectHeader.tsx @@ -403,8 +403,8 @@ export function ProjectHeader({
{projectName} {!isBranchOnly && ( - - ({workspaceCount}) + + {workspaceCount} )} {isBranchOnly && ( From 5b9a80fe14373d41efd1b455b865d6e63cfebd11 Mon Sep 17 00:00:00 2001 From: z3thon Date: Thu, 19 Mar 2026 18:58:36 +0700 Subject: [PATCH 10/33] fix(desktop): clear review status when clicking branch-only project --- .../WorkspaceSidebar/ProjectSection/ProjectSection.tsx | 6 ++++++ 1 file changed, 6 insertions(+) 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 29e2263b4b0..040fe0b535b 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 @@ -9,6 +9,7 @@ import { useReorderProjects } from "renderer/react-query/projects"; import { navigateToWorkspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { useWorkspaceSidebarStore } from "renderer/stores"; import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal"; +import { useTabsStore } from "renderer/stores/tabs/store"; import { useSectionDropZone } from "../hooks"; import type { SidebarSection, SidebarWorkspace } from "../types"; import { WorkspaceListItem } from "../WorkspaceListItem"; @@ -92,8 +93,13 @@ export function ProjectSection({ params: { workspaceId: workspaces[0].id }, }); + const clearWorkspaceAttentionStatus = useTabsStore( + (s) => s.clearWorkspaceAttentionStatus, + ); + const handleNavigateToWorkspace = () => { if (isBranchOnly && workspaces.length > 0) { + clearWorkspaceAttentionStatus(workspaces[0].id); navigateToWorkspace(workspaces[0].id, navigate); } }; From 3ecf401a7207d5ef28962c7b87614b583f71cf84 Mon Sep 17 00:00:00 2001 From: z3thon Date: Thu, 19 Mar 2026 19:03:05 +0700 Subject: [PATCH 11/33] fix(desktop): show worktree choice dialog on drag-and-drop project open --- .../renderer/react-query/projects/useOpenProject.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/renderer/react-query/projects/useOpenProject.tsx b/apps/desktop/src/renderer/react-query/projects/useOpenProject.tsx index 52827280ed9..7ade642ecc5 100644 --- a/apps/desktop/src/renderer/react-query/projects/useOpenProject.tsx +++ b/apps/desktop/src/renderer/react-query/projects/useOpenProject.tsx @@ -220,7 +220,10 @@ export function useOpenProject() { showDialog({ paths: [result.selectedPath], immediateSuccesses: [], - resolve: (projects) => resolve(projects[0] ?? null), + resolve: async (projects) => { + await maybePromptWorktreeChoice(projects); + resolve(projects[0] ?? null); + }, }); return; } @@ -231,7 +234,9 @@ export function useOpenProject() { } if ("project" in result) { - resolve(result.project); + maybePromptWorktreeChoice([result.project]).then(() => { + resolve(result.project); + }); return; } @@ -244,7 +249,7 @@ export function useOpenProject() { ); }); }, - [openFromPathMutation, showDialog], + [maybePromptWorktreeChoice, openFromPathMutation, showDialog], ); return { From 93a53050463079060c5846dc3efce3f8045f0f6d Mon Sep 17 00:00:00 2001 From: z3thon Date: Thu, 19 Mar 2026 19:06:56 +0700 Subject: [PATCH 12/33] fix(desktop): navigate to workspace after drag-and-drop project open --- .../SidebarDropZone/SidebarDropZone.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/SidebarDropZone/SidebarDropZone.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/SidebarDropZone/SidebarDropZone.tsx index 519c4f5b819..550d51573f2 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/SidebarDropZone/SidebarDropZone.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/SidebarDropZone/SidebarDropZone.tsx @@ -3,6 +3,7 @@ import { useNavigate } from "@tanstack/react-router"; import { AnimatePresence, motion } from "framer-motion"; import { type ReactNode, useCallback, useEffect, useState } from "react"; import { LuFolderPlus, LuLoader, LuX } from "react-icons/lu"; +import { electronTrpc } from "renderer/lib/electron-trpc"; import { useOpenProject } from "renderer/react-query/projects"; interface SidebarDropZoneProps { @@ -16,6 +17,7 @@ export function SidebarDropZone({ children, className }: SidebarDropZoneProps) { const [error, setError] = useState(null); const { openFromPath, isPending } = useOpenProject(); + const utils = electronTrpc.useUtils(); useEffect(() => { if (!error) return; @@ -97,16 +99,24 @@ export function SidebarDropZone({ children, className }: SidebarDropZoneProps) { try { const project = await openFromPath(filePath); if (project) { - navigate({ - to: "/project/$projectId", - params: { projectId: project.id }, - }); + // Refresh sidebar and navigate to the project's workspace + await utils.workspaces.getAllGrouped.invalidate(); + const groups = await utils.workspaces.getAllGrouped.fetch(); + const workspace = groups + .flatMap((g) => g.workspaces) + .find((w) => w.projectId === project.id); + if (workspace) { + navigate({ + to: "/workspace/$workspaceId", + params: { workspaceId: workspace.id }, + }); + } } } catch (err) { setError(err instanceof Error ? err.message : "Failed to open project"); } }, - [openFromPath, isPending, navigate], + [openFromPath, isPending, navigate, utils], ); return ( From d30adb6f680cc07ba6fb32d37b2b84ff851088a6 Mon Sep 17 00:00:00 2001 From: z3thon Date: Tue, 17 Mar 2026 22:57:03 +0700 Subject: [PATCH 13/33] feat: consolidate project settings into a dedicated Projects page Replace the individual project collapsibles in the settings sidebar with a single "Projects" link that opens a clean project list page. Selecting a project navigates to its settings. This scales much better when users have many active projects. - Replace per-project collapsibles in sidebar with single "Projects" nav item - Create /settings/projects route with a searchable project list - Each project shows name, color indicator, and repo path - Clicking a project navigates to /settings/project/$projectId/general --- .../SettingsSidebar/ProjectsSettings.tsx | 153 +++++------------- .../SettingsSidebar/SettingsSidebar.tsx | 2 +- .../_authenticated/settings/projects/page.tsx | 64 ++++++++ 3 files changed, 109 insertions(+), 110 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/ProjectsSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/ProjectsSettings.tsx index 3b66d265989..5f43b538519 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/ProjectsSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/ProjectsSettings.tsx @@ -1,32 +1,25 @@ import { FEATURE_FLAGS } from "@superset/shared/constants"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@superset/ui/collapsible"; import { cn } from "@superset/ui/utils"; import { Link, useMatchRoute } from "@tanstack/react-router"; import { useFeatureFlagEnabled } from "posthog-js/react"; -import { useMemo } from "react"; -import { HiChevronDown, HiChevronRight } from "react-icons/hi2"; +import { HiOutlineFolder } from "react-icons/hi2"; import { electronTrpc } from "renderer/lib/electron-trpc"; -import { getMatchCountBySection } from "../../utils/settings-search"; +import type { SettingsSection } from "renderer/stores/settings-state"; interface ProjectsSettingsProps { searchQuery: string; + matchCounts: Partial> | null; } -export function ProjectsSettings({ searchQuery }: ProjectsSettingsProps) { +export function ProjectsSettings({ + searchQuery, + matchCounts, +}: ProjectsSettingsProps) { const { data: groups = [] } = electronTrpc.workspaces.getAllGrouped.useQuery(); const matchRoute = useMatchRoute(); const hasCloudAccess = useFeatureFlagEnabled(FEATURE_FLAGS.CLOUD_ACCESS); - const matchCounts = useMemo(() => { - if (!searchQuery) return null; - return getMatchCountBySection(searchQuery); - }, [searchQuery]); - const hasProjectMatches = (matchCounts?.project ?? 0) > 0; if (searchQuery && !hasProjectMatches) { @@ -37,105 +30,47 @@ export function ProjectsSettings({ searchQuery }: ProjectsSettingsProps) { return null; } + // Check if we're on the projects list or any project settings page + const isProjectsListActive = matchRoute({ to: "/settings/projects" }); + const isAnyProjectActive = groups.some( + (group) => + matchRoute({ + to: "/settings/project/$projectId/general", + params: { projectId: group.project.id }, + }) || + (hasCloudAccess && + matchRoute({ + to: "/settings/project/$projectId/cloud/secrets", + params: { projectId: group.project.id }, + })), + ); + const isActive = !!isProjectsListActive || isAnyProjectActive; + + const count = matchCounts?.project; + return ( -
+

Projects - {searchQuery && hasProjectMatches && ( - - {matchCounts?.project ?? 0} - - )}

-
+ {/* ── Workspaces ── */} +
+

+ Workspaces +

+
+ {projectWorkspaces.length === 0 ? ( +

+ No workspaces yet. +

+ ) : ( +
+ {projectWorkspaces.map((ws) => ( +
+ +
+

+ {ws.type === "branch" ? "local" : ws.name || ws.branch} +

+

+ {ws.branch} +

+
+ + {ws.type} + +
+ ))} +
+ )} +
+
+ {/* ── Workspace Settings ── */} {!isWorktreeDisabled && (
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx index 8f40231e3aa..e464e2a0189 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx @@ -1,6 +1,7 @@ import { cn } from "@superset/ui/utils"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { HiChevronRight } from "react-icons/hi2"; +import { LuFolderOpen, LuGitBranch } from "react-icons/lu"; import { electronTrpc } from "renderer/lib/electron-trpc"; export const Route = createFileRoute("/_authenticated/settings/projects/")({ @@ -12,6 +13,13 @@ function ProjectsListPage() { electronTrpc.workspaces.getAllGrouped.useQuery(); const navigate = useNavigate(); + const navigateToProject = (projectId: string) => { + navigate({ + to: "/settings/project/$projectId/general", + params: { projectId }, + }); + }; + return (
@@ -27,36 +35,99 @@ function ProjectsListPage() {

) : (
- {groups.map((group) => ( - + + {/* Workspace list (worktree-enabled projects only) */} + {!isBranchOnly && group.workspaces.length > 0 && ( +
+ {branchWorkspace && ( +
+ + + {branchWorkspace.branch} + + local +
+ )} + {worktreeWorkspaces.map((ws) => ( +
+ + {ws.name || ws.branch} + + {ws.branch} + +
+ ))} +
+ )} + + {/* Branch-only: show single branch inline */} + {isBranchOnly && branchWorkspace && ( +
+
+ + + {branchWorkspace.branch} + +
+
+ )}
- - - ))} + ); + })}
)}
From 44c055f7493a76c795ea1600993ee7e787578b0a Mon Sep 17 00:00:00 2001 From: z3thon Date: Thu, 19 Mar 2026 15:34:24 +0700 Subject: [PATCH 15/33] feat(desktop): polish projects settings page - Convert projects page to master-detail split view (project list on left, settings on right) instead of two-page drill-down - Always expand workspace/worktree sub-items under each project - Branch-only projects show collapsed (no sub-items, just project row) - Worktree items indented with left border line showing hierarchy - Move "Projects" into the "Editor & Workflow" sidebar group right after "Git & Worktrees" instead of its own separate section - Add workspace listing section to project settings detail view --- .../SettingsSidebar/GeneralSettings.tsx | 8 + .../SettingsSidebar/SettingsSidebar.tsx | 2 - .../_authenticated/settings/projects/page.tsx | 149 ++++++++---------- 3 files changed, 78 insertions(+), 81 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx index 1062c0e578a..fc80c239596 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx @@ -7,6 +7,7 @@ import { HiOutlineCpuChip, HiOutlineCreditCard, HiOutlineDevicePhoneMobile, + HiOutlineFolder, HiOutlineKey, HiOutlinePaintBrush, HiOutlinePuzzlePiece, @@ -30,6 +31,7 @@ type SettingsRoute = | "/settings/keyboard" | "/settings/behavior" | "/settings/git" + | "/settings/projects" | "/settings/agents" | "/settings/terminal" | "/settings/models" @@ -97,6 +99,12 @@ const SECTION_GROUPS: SectionGroup[] = [ label: "Git & Worktrees", icon: , }, + { + id: "/settings/projects", + section: "project", + label: "Projects", + icon: , + }, { id: "/settings/agents", section: "agents", diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/SettingsSidebar.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/SettingsSidebar.tsx index fa87f713de6..648b070528b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/SettingsSidebar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/SettingsSidebar.tsx @@ -13,7 +13,6 @@ import { } from "renderer/stores/settings-state"; import { getMatchCountBySection } from "../../utils/settings-search"; import { GeneralSettings } from "./GeneralSettings"; -import { ProjectsSettings } from "./ProjectsSettings"; export function SettingsSidebar() { const searchQuery = useSettingsSearchQuery(); @@ -58,7 +57,6 @@ export function SettingsSidebar() {
-
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx index e464e2a0189..0cf3d465896 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx @@ -1,42 +1,40 @@ import { cn } from "@superset/ui/utils"; -import { createFileRoute, useNavigate } from "@tanstack/react-router"; -import { HiChevronRight } from "react-icons/hi2"; +import { createFileRoute } from "@tanstack/react-router"; +import { useState } from "react"; import { LuFolderOpen, LuGitBranch } from "react-icons/lu"; import { electronTrpc } from "renderer/lib/electron-trpc"; +import { ProjectSettings } from "../project/$projectId/components/ProjectSettings"; export const Route = createFileRoute("/_authenticated/settings/projects/")({ - component: ProjectsListPage, + component: ProjectsPage, }); -function ProjectsListPage() { +function ProjectsPage() { const { data: groups = [] } = electronTrpc.workspaces.getAllGrouped.useQuery(); - const navigate = useNavigate(); + const [selectedProjectId, setSelectedProjectId] = useState( + groups[0]?.project.id ?? null, + ); - const navigateToProject = (projectId: string) => { - navigate({ - to: "/settings/project/$projectId/general", - params: { projectId }, - }); - }; + // Auto-select first project if none selected + const effectiveSelectedId = + selectedProjectId && groups.some((g) => g.project.id === selectedProjectId) + ? selectedProjectId + : (groups[0]?.project.id ?? null); return ( -
-
-

Projects

-

- Select a project to configure its settings -

-
- - {groups.length === 0 ? ( -

- No projects yet. Import a repository to get started. -

- ) : ( -
+
+ {/* Left: Project/workspace list */} +
+
+

+ Projects +

+
+
{groups.map((group) => { const isBranchOnly = group.project.worktreeMode === "disabled"; + const isSelected = group.project.id === effectiveSelectedId; const worktreeWorkspaces = group.workspaces.filter( (w) => w.type === "worktree", ); @@ -45,91 +43,84 @@ function ProjectsListPage() { ); return ( -
- {/* Project header row */} +
+ {/* Project row */} - {/* Workspace list (worktree-enabled projects only) */} + {/* Workspace sub-items (worktree-enabled projects only) */} {!isBranchOnly && group.workspaces.length > 0 && ( -
+
{branchWorkspace && ( -
+
{branchWorkspace.branch} - local
)} {worktreeWorkspaces.map((ws) => (
{ws.name || ws.branch} - - {ws.branch} -
))}
)} - - {/* Branch-only: show single branch inline */} - {isBranchOnly && branchWorkspace && ( -
-
- - - {branchWorkspace.branch} - -
-
- )}
); })} + + {groups.length === 0 && ( +

+ No projects yet. +

+ )}
- )} +
+ + {/* Right: Selected project settings */} +
+ {effectiveSelectedId ? ( + + ) : ( +
+ Select a project to view its settings +
+ )} +
); } From 433d3d7d6f95d07cf596c4b7753274af30a42f9a Mon Sep 17 00:00:00 2001 From: z3thon Date: Thu, 19 Mar 2026 15:55:24 +0700 Subject: [PATCH 16/33] fix: deduplicate allGroups query after rebase --- .../components/ProjectSettings/ProjectSettings.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx index e064baabe73..ab4647f75f6 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx @@ -100,6 +100,9 @@ export function ProjectSettings({ electronTrpc.workspaces.getAllGrouped.useQuery(); const projectGroup = allGroups.find((g) => g.project.id === projectId); const projectWorkspaces = projectGroup?.workspaces ?? []; + const worktreeWorkspaces = projectWorkspaces.filter( + (w) => w.type === "worktree", + ); const { data: branchData, isLoading: isBranchDataLoading } = electronTrpc.projects.getBranches.useQuery( { projectId }, @@ -227,14 +230,6 @@ export function ProjectSettings({ onSettled: () => utils.workspaces.getAllGrouped.invalidate(), }); - // Get worktree workspaces for this project - const { data: allGroups = [] } = - electronTrpc.workspaces.getAllGrouped.useQuery(); - const projectGroup = allGroups.find((g) => g.project.id === projectId); - const worktreeWorkspaces = (projectGroup?.workspaces ?? []).filter( - (w) => w.type === "worktree", - ); - const handleWorktreeModeChange = (value: string) => { if (value === "disabled" && worktreeWorkspaces.length > 0) { setShowDisableWorktreeDialog(true); From 85d74162fc295f5cabff0ebf73062125d541de7b Mon Sep 17 00:00:00 2001 From: z3thon Date: Thu, 19 Mar 2026 18:40:34 +0700 Subject: [PATCH 17/33] feat(desktop): project icons in settings list + detect/refresh button - Replace color dots with ProjectThumbnail in the settings project list (shows favicon, GitHub avatar, or letter badge) - Add "Detect" button to project icon section that clears the current icon and re-discovers favicon from the project directory - Spinning icon while detection is in progress - Upload and Remove buttons still available alongside Detect --- .../ProjectSettings/ProjectSettings.tsx | 46 ++++++++++++++++++- .../_authenticated/settings/projects/page.tsx | 13 ++++-- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx index ab4647f75f6..42448f595e9 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx @@ -30,7 +30,12 @@ import { HiOutlineFolderOpen, HiOutlinePaintBrush, } from "react-icons/hi2"; -import { LuFolderOpen, LuImagePlus, LuTrash2 } from "react-icons/lu"; +import { + LuFolderOpen, + LuImagePlus, + LuRefreshCw, + LuTrash2, +} from "react-icons/lu"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { useImportAllWorktrees, @@ -146,6 +151,26 @@ export function ProjectSettings({ }, }); + const discoverIcon = + electronTrpc.projects.triggerFaviconDiscovery.useMutation({ + onSettled: () => { + utils.projects.get.invalidate({ id: projectId }); + utils.workspaces.getAllGrouped.invalidate(); + }, + }); + + const handleRefreshIcon = useCallback(() => { + // Clear existing icon first so discovery runs fresh + setProjectIcon.mutate( + { id: projectId, icon: null }, + { + onSuccess: () => { + discoverIcon.mutate({ id: projectId }); + }, + }, + ); + }, [projectId, setProjectIcon, discoverIcon]); + const fileInputRef = useRef(null); const handleIconUpload = useCallback(() => { @@ -467,6 +492,25 @@ export function ProjectSettings({ className="hidden" onChange={handleFileChange} /> + - {project.iconUrl && ( - - )}
From 028c388fb151929879edbe86f31fd2b842328ded Mon Sep 17 00:00:00 2001 From: z3thon Date: Thu, 19 Mar 2026 18:46:35 +0700 Subject: [PATCH 19/33] fix(desktop): show Remove button when any icon is set --- .../ProjectSettings/ProjectSettings.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx index 5f187c4031f..7f3ccfc8f75 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx @@ -514,6 +514,21 @@ export function ProjectSettings({ Upload + {project.iconUrl && ( + + )}
From cb8f280d8cdb682470721c46168b6476b1251100 Mon Sep 17 00:00:00 2001 From: z3thon Date: Thu, 19 Mar 2026 18:51:25 +0700 Subject: [PATCH 20/33] fix(desktop): only show Remove icon button after manual upload --- .../components/ProjectSettings/ProjectSettings.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx index 7f3ccfc8f75..57f5a080cb7 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettings/ProjectSettings.tsx @@ -156,6 +156,7 @@ export function ProjectSettings({ const handleRefreshIcon = useCallback(() => { // Clear existing icon first so discovery runs fresh + setIconManuallyUploaded(false); setProjectIcon.mutate( { id: projectId, icon: null }, { @@ -166,6 +167,7 @@ export function ProjectSettings({ ); }, [projectId, setProjectIcon, discoverIcon]); + const [iconManuallyUploaded, setIconManuallyUploaded] = useState(false); const fileInputRef = useRef(null); const handleIconUpload = useCallback(() => { @@ -183,6 +185,7 @@ export function ProjectSettings({ reader.onload = () => { const dataUrl = reader.result as string; setProjectIcon.mutate({ id: projectId, icon: dataUrl }); + setIconManuallyUploaded(true); }; reader.readAsDataURL(file); @@ -514,12 +517,13 @@ export function ProjectSettings({ Upload - {project.iconUrl && ( + {project.iconUrl && iconManuallyUploaded && ( + > + + Detect + + )}
+ } + /> + ), + [], + ); + + return ( + + splitPaneHorizontal(tabId, paneId, path)} + onSplitVertical={() => splitPaneVertical(tabId, paneId, path)} + onSplitWithNewChat={() => + splitPaneVertical(tabId, paneId, path, { + paneType: "chat-mastra", + }) + } + onSplitWithNewBrowser={() => + splitPaneVertical(tabId, paneId, path, { + paneType: "webview", + }) + } + onSplitWithFileTree={() => + splitPaneVertical(tabId, paneId, path, { + paneType: "file-tree", + }) + } + onEqualizePaneSplits={() => equalizePaneSplits(tabId)} + onClosePane={handleRemove} + currentTabId={tabId} + availableTabs={availableTabs} + onMoveToTab={onMoveToTab} + onMoveToNewTab={onMoveToNewTab} + closeLabel="Close File Tree" + > +
+ +
+
+
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileTreePane/index.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileTreePane/index.ts new file mode 100644 index 00000000000..8afccb4a199 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileTreePane/index.ts @@ -0,0 +1 @@ +export { FileTreePane } from "./FileTreePane"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx index c478887c772..bfd13c7cc85 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/TabPane.tsx @@ -128,6 +128,9 @@ export function TabPane({ onSplitWithNewBrowser={() => splitPaneVertical(tabId, paneId, path, { paneType: "webview" }) } + onSplitWithFileTree={() => + splitPaneVertical(tabId, paneId, path, { paneType: "file-tree" }) + } onEqualizePaneSplits={() => equalizePaneSplits(tabId)} onClosePane={() => removePane(paneId)} onClearTerminal={handleClearTerminal} diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx index cee85350766..0fe84ed4233 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/index.tsx @@ -22,6 +22,7 @@ import { BrowserPane } from "./BrowserPane"; import { ChatMastraPane } from "./ChatMastraPane"; import { MosaicSplitOverlay } from "./components"; import { DevToolsPane } from "./DevToolsPane"; +import { FileTreePane } from "./FileTreePane"; import { FileViewerPane } from "./FileViewerPane"; import { TabPane } from "./TabPane"; @@ -226,6 +227,25 @@ export function TabView({ tab }: TabViewProps) { ); } + // Route file-tree panes + if (paneInfo.type === "file-tree") { + return ( + movePaneToTab(paneId, targetTabId)} + onMoveToNewTab={() => movePaneToNewTab(paneId)} + /> + ); + } + // Route devtools panes if (paneInfo.type === "devtools" && paneInfo.devtools) { return ( diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PaneContextMenuItems/PaneContextMenuItems.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PaneContextMenuItems/PaneContextMenuItems.tsx index 72ec42c56ac..45b70cd56aa 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PaneContextMenuItems/PaneContextMenuItems.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/components/PaneContextMenuItems/PaneContextMenuItems.tsx @@ -9,6 +9,7 @@ import { import { LuColumns2, LuEqual, + LuFolderTree, LuGlobe, LuMessageSquare, LuMoveRight, @@ -24,6 +25,7 @@ export interface PaneContextMenuActions { onSplitVertical: () => void; onSplitWithNewChat?: () => void; onSplitWithNewBrowser?: () => void; + onSplitWithFileTree?: () => void; onEqualizePaneSplits?: () => void; onClosePane: () => void; currentTabId: string; @@ -80,6 +82,12 @@ export function PaneContextMenuItems({ {renderShortcut(splitWithBrowserShortcut)} )} + {actions.onSplitWithFileTree && ( + + + Split with File Tree + + )} {actions.onEqualizePaneSplits && ( diff --git a/apps/desktop/src/renderer/stores/tabs/store.ts b/apps/desktop/src/renderer/stores/tabs/store.ts index e63c3bf0f8e..2140fe4e296 100644 --- a/apps/desktop/src/renderer/stores/tabs/store.ts +++ b/apps/desktop/src/renderer/stores/tabs/store.ts @@ -30,6 +30,7 @@ import { createChatMastraPane, createChatMastraTabWithPane, createDevToolsPane, + createFileTreePane, createFileViewerPane, createPane, createTabWithPane, @@ -1278,13 +1279,17 @@ export const useTabsStore = create()( ? createChatMastraPane(tabId) : paneType === "webview" ? createBrowserPane(tabId) - : createPane(tabId, "terminal", options); + : paneType === "file-tree" + ? createFileTreePane(tabId) + : createPane(tabId, "terminal", options); const panelType = paneType === "chat-mastra" ? "chat" : paneType === "webview" ? "browser" - : "terminal"; + : paneType === "file-tree" + ? "file-tree" + : "terminal"; let newLayout: MosaicNode; if (path && path.length > 0) { @@ -1347,13 +1352,17 @@ export const useTabsStore = create()( ? createChatMastraPane(tabId) : paneType === "webview" ? createBrowserPane(tabId) - : createPane(tabId, "terminal", options); + : paneType === "file-tree" + ? createFileTreePane(tabId) + : createPane(tabId, "terminal", options); const panelType = paneType === "chat-mastra" ? "chat" : paneType === "webview" ? "browser" - : "terminal"; + : paneType === "file-tree" + ? "file-tree" + : "terminal"; let newLayout: MosaicNode; if (path && path.length > 0) { diff --git a/apps/desktop/src/renderer/stores/tabs/types.ts b/apps/desktop/src/renderer/stores/tabs/types.ts index 45968114e68..337da2d94ad 100644 --- a/apps/desktop/src/renderer/stores/tabs/types.ts +++ b/apps/desktop/src/renderer/stores/tabs/types.ts @@ -50,7 +50,7 @@ export interface AddTabOptions { export interface SplitPaneOptions { initialCwd?: string; - paneType?: "terminal" | "chat-mastra" | "webview"; + paneType?: "terminal" | "chat-mastra" | "webview" | "file-tree"; } export interface AddChatMastraTabOptions { diff --git a/apps/desktop/src/renderer/stores/tabs/utils.ts b/apps/desktop/src/renderer/stores/tabs/utils.ts index 33d5dd17292..8b04ba39892 100644 --- a/apps/desktop/src/renderer/stores/tabs/utils.ts +++ b/apps/desktop/src/renderer/stores/tabs/utils.ts @@ -229,6 +229,19 @@ export const createFileViewerPane = ( }; }; +/** + * Creates a new file-tree pane + */ +export const createFileTreePane = (tabId: string): Pane => { + const id = generateId("pane"); + return { + id, + tabId, + type: "file-tree", + name: "File Tree", + }; +}; + export const createChatMastraPane = ( tabId: string, options?: AddChatMastraTabOptions, diff --git a/apps/desktop/src/shared/tabs-types.ts b/apps/desktop/src/shared/tabs-types.ts index ce0c741e62e..299f0ef2853 100644 --- a/apps/desktop/src/shared/tabs-types.ts +++ b/apps/desktop/src/shared/tabs-types.ts @@ -12,6 +12,7 @@ export type PaneType = | "terminal" | "webview" | "file-viewer" + | "file-tree" | "chat-mastra" | "devtools"; From 534c2f65576982ea34ea69166598cf6da34f4050 Mon Sep 17 00:00:00 2001 From: z3thon Date: Thu, 19 Mar 2026 16:23:29 +0700 Subject: [PATCH 23/33] fix(desktop): wrap FileTreePane toolbar in div for react-dnd compat react-dnd's connectDragSource requires native DOM elements, not React components. Wrapping PaneToolbarActions in a
fixes the error. --- .../TabView/FileTreePane/FileTreePane.tsx | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileTreePane/FileTreePane.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileTreePane/FileTreePane.tsx index d0b130e15f9..a38487d0b2e 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileTreePane/FileTreePane.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/TabView/FileTreePane/FileTreePane.tsx @@ -58,17 +58,19 @@ export function FileTreePane({ const renderToolbar = useCallback( (handlers: PaneHandlers) => ( - - - Files -
- } - /> +
+ + + Files +
+ } + /> +
), [], ); From b758f1f96d5ee7f76002d70b2e5cacc65a0cc919 Mon Sep 17 00:00:00 2001 From: z3thon Date: Thu, 19 Mar 2026 16:37:47 +0700 Subject: [PATCH 24/33] feat(desktop): movable sidebar panels and expand button relocation - Right-click Changes or Files tab to move it to left or right side - Each tab can be independently positioned (e.g. Files on left, Changes on right, or both on one side) - Left panel renders with its own resizable handle - Close left panel: tabs return to right side - Close right panel with lone tab: resets all tabs to right - Expand/collapse button moved from tab bar into ChangesHeader toolbar (only relevant to Changes view) - Tab positions persisted in sidebar store --- .../RightSidebar/ChangesView/ChangesView.tsx | 6 + .../ChangesHeader/ChangesHeader.tsx | 26 +++ .../WorkspaceView/RightSidebar/index.tsx | 168 ++++++++++++------ .../WorkspaceLayout/WorkspaceLayout.tsx | 33 +++- .../src/renderer/stores/sidebar-state.ts | 28 +++ 5 files changed, 207 insertions(+), 54 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/ChangesView.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/ChangesView.tsx index 0b7a042fd43..1b492278f1b 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/ChangesView.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/ChangesView.tsx @@ -37,6 +37,8 @@ interface ChangesViewProps { ) => void; isExpandedView?: boolean; isActive?: boolean; + isExpanded?: boolean; + onExpandToggle?: () => void; } const INACTIVE_BRANCH_REFETCH_INTERVAL_MS = 10_000; @@ -76,6 +78,8 @@ export function ChangesView({ onFileOpen, isExpandedView, isActive = true, + isExpanded, + onExpandToggle, }: ChangesViewProps) { const { workspaceId } = useParams({ strict: false }); const trpcUtils = electronTrpc.useUtils(); @@ -663,6 +667,8 @@ export function ChangesView({ stashIncludeUntrackedMutation.isPending || stashPopMutation.isPending } + isExpanded={isExpanded} + onExpandToggle={onExpandToggle} />
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/ChangesHeader/ChangesHeader.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/ChangesHeader/ChangesHeader.tsx index 2fe38495de4..a6fe2140279 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/ChangesHeader/ChangesHeader.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/components/ChangesHeader/ChangesHeader.tsx @@ -17,6 +17,7 @@ import { import { Popover, PopoverContent, PopoverTrigger } from "@superset/ui/popover"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { useEffect, useMemo, useRef, useState } from "react"; +import { LuExpand, LuShrink } from "react-icons/lu"; import { VscCheck, VscGitStash, @@ -44,6 +45,8 @@ interface ChangesHeaderProps { onStashIncludeUntracked: () => void; onStashPop: () => void; isStashPending: boolean; + isExpanded?: boolean; + onExpandToggle?: () => void; } function BaseBranchSelector({ worktreePath }: { worktreePath: string }) { @@ -288,6 +291,8 @@ export function ChangesHeader({ onStashIncludeUntracked, onStashPop, isStashPending, + isExpanded, + onExpandToggle, }: ChangesHeaderProps) { return (
@@ -300,6 +305,27 @@ export function ChangesHeader({ /> + {onExpandToggle && ( + + + + + + {isExpanded ? "Collapse" : "Expand"} + + + )} {pr && pr.state === "open" && ( s.setRightSidebarTab); const toggleSidebar = useSidebarStore((s) => s.toggleSidebar); const setMode = useSidebarStore((s) => s.setMode); + const setTabPosition = useSidebarStore((s) => s.setTabPosition); + const tabPositions = useSidebarStore((s) => s.tabPositions); const sidebarWidth = useSidebarStore((s) => s.sidebarWidth); + const leftPanelWidth = useSidebarStore((s) => s.leftPanelWidth); const isExpanded = currentMode === SidebarMode.Changes; - const compactTabs = sidebarWidth < 250; - const showChangesTab = !!worktreePath; + const panelWidth = side === "left" ? leftPanelWidth : sidebarWidth; + const compactTabs = panelWidth < 250; + const showChangesTab = + !!worktreePath && tabPositions[RightSidebarTab.Changes] === side; + const showFilesTab = tabPositions[RightSidebarTab.Files] === side; + const oppositeSide: PanelSide = side === "left" ? "right" : "left"; const handleExpandToggle = () => { setMode(isExpanded ? SidebarMode.Tabs : SidebarMode.Changes); }; + const handleClosePanel = () => { + if (side === "right") { + // Move any lone tab back to right before closing + const tabsOnRight = Object.entries(tabPositions).filter( + ([, s]) => s === "right", + ); + if (tabsOnRight.length <= 1) { + // Reset all tabs to right so nothing is stranded + setTabPosition(RightSidebarTab.Changes, "right"); + setTabPosition(RightSidebarTab.Files, "right"); + } + toggleSidebar(); + } else { + // Left panel: move all tabs on the left back to right, panel disappears + for (const [tab, tabSide] of Object.entries(tabPositions)) { + if (tabSide === "left") { + setTabPosition(tab as RightSidebarTab, "right"); + } + } + } + }; + const addFileViewerPane = useTabsStore((s) => s.addFileViewerPane); const trpcUtils = electronTrpc.useUtils(); const { scrollToFile } = useScrollContext(); @@ -169,21 +209,63 @@ export function RightSidebar() {
{showChangesTab && ( - setRightSidebarTab(RightSidebarTab.Changes)} - icon={} - label="Changes" - compact={compactTabs} - /> + + +
+ setRightSidebarTab(RightSidebarTab.Changes)} + icon={} + label="Changes" + compact={compactTabs} + /> +
+
+ + + setTabPosition(RightSidebarTab.Changes, oppositeSide) + } + > + {oppositeSide === "left" ? ( + + ) : ( + + )} + Move to {oppositeSide === "left" ? "Left" : "Right"} + + +
+ )} + {showFilesTab && ( + + +
+ setRightSidebarTab(RightSidebarTab.Files)} + icon={} + label="Files" + compact={compactTabs} + /> +
+
+ + + setTabPosition(RightSidebarTab.Files, oppositeSide) + } + > + {oppositeSide === "left" ? ( + + ) : ( + + )} + Move to {oppositeSide === "left" ? "Left" : "Right"} + + +
)} - setRightSidebarTab(RightSidebarTab.Files)} - icon={} - label="Files" - compact={compactTabs} - />
@@ -192,29 +274,7 @@ export function RightSidebar() { - - - - - - - -
)} -
- -
+ {showFilesTab && ( +
+ +
+ )} ); } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceLayout/WorkspaceLayout.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceLayout/WorkspaceLayout.tsx index 94f8b5dabea..c366a438950 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceLayout/WorkspaceLayout.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceLayout/WorkspaceLayout.tsx @@ -3,6 +3,7 @@ import { DEFAULT_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH, MIN_SIDEBAR_WIDTH, + RightSidebarTab, SidebarMode, useSidebarStore, } from "renderer/stores/sidebar-state"; @@ -27,14 +28,42 @@ export function WorkspaceLayout({ const isSidebarOpen = useSidebarStore((s) => s.isSidebarOpen); const sidebarWidth = useSidebarStore((s) => s.sidebarWidth); const setSidebarWidth = useSidebarStore((s) => s.setSidebarWidth); + const leftPanelWidth = useSidebarStore((s) => s.leftPanelWidth); + const setLeftPanelWidth = useSidebarStore((s) => s.setLeftPanelWidth); const isResizing = useSidebarStore((s) => s.isResizing); const setIsResizing = useSidebarStore((s) => s.setIsResizing); const currentMode = useSidebarStore((s) => s.currentMode); + const tabPositions = useSidebarStore((s) => s.tabPositions); const isExpanded = currentMode === SidebarMode.Changes; + // Determine which tabs are on which side + const hasLeftTabs = + tabPositions[RightSidebarTab.Changes] === "left" || + tabPositions[RightSidebarTab.Files] === "left"; + const hasRightTabs = + tabPositions[RightSidebarTab.Changes] === "right" || + tabPositions[RightSidebarTab.Files] === "right"; + + const showLeftPanel = isSidebarOpen && hasLeftTabs; + const showRightPanel = isSidebarOpen && hasRightTabs; + return ( + {showLeftPanel && ( + setLeftPanelWidth(DEFAULT_SIDEBAR_WIDTH)} + > + + + )}
{isExpanded ? ( @@ -46,7 +75,7 @@ export function WorkspaceLayout({ /> )}
- {isSidebarOpen && ( + {showRightPanel && ( setSidebarWidth(DEFAULT_SIDEBAR_WIDTH)} > - + )}
diff --git a/apps/desktop/src/renderer/stores/sidebar-state.ts b/apps/desktop/src/renderer/stores/sidebar-state.ts index f0a17eae120..7030149de5c 100644 --- a/apps/desktop/src/renderer/stores/sidebar-state.ts +++ b/apps/desktop/src/renderer/stores/sidebar-state.ts @@ -15,6 +15,8 @@ export const DEFAULT_SIDEBAR_WIDTH = 250; export const MIN_SIDEBAR_WIDTH = 200; export const MAX_SIDEBAR_WIDTH = 500; +export type PanelSide = "left" | "right"; + interface SidebarState { isSidebarOpen: boolean; sidebarWidth: number; @@ -23,12 +25,18 @@ interface SidebarState { lastMode: SidebarMode; isResizing: boolean; rightSidebarTab: RightSidebarTab; + /** Which side each tab is docked on */ + tabPositions: Record; + /** Width of the left panel (when tabs are docked left) */ + leftPanelWidth: number; toggleSidebar: () => void; setSidebarOpen: (open: boolean) => void; setSidebarWidth: (width: number) => void; setMode: (mode: SidebarMode) => void; setIsResizing: (isResizing: boolean) => void; setRightSidebarTab: (tab: RightSidebarTab) => void; + setTabPosition: (tab: RightSidebarTab, side: PanelSide) => void; + setLeftPanelWidth: (width: number) => void; } export const useSidebarStore = create()( @@ -42,6 +50,11 @@ export const useSidebarStore = create()( lastMode: SidebarMode.Tabs, isResizing: false, rightSidebarTab: RightSidebarTab.Changes, + tabPositions: { + [RightSidebarTab.Changes]: "right", + [RightSidebarTab.Files]: "right", + }, + leftPanelWidth: DEFAULT_SIDEBAR_WIDTH, toggleSidebar: () => { const { isSidebarOpen, lastOpenSidebarWidth, currentMode, lastMode } = @@ -122,6 +135,21 @@ export const useSidebarStore = create()( setRightSidebarTab: (tab) => { set({ rightSidebarTab: tab }); }, + + setTabPosition: (tab, side) => { + const { tabPositions } = get(); + set({ + tabPositions: { ...tabPositions, [tab]: side }, + }); + }, + + setLeftPanelWidth: (width) => { + const clamped = Math.max( + MIN_SIDEBAR_WIDTH, + Math.min(MAX_SIDEBAR_WIDTH, width), + ); + set({ leftPanelWidth: clamped }); + }, }), { name: "sidebar-store", From 48da3605f638b107f56dd2c3d9590aa295c799d0 Mon Sep 17 00:00:00 2001 From: z3thon Date: Fri, 20 Mar 2026 05:42:57 +0700 Subject: [PATCH 25/33] fix(desktop): address PR review feedback - Checkout target branch before creating branch workspace when worktrees are disabled (prevents main repo staying on wrong branch) - Handle async errors in worktree choice dialog to prevent fire-and-forget rejections and ensure dialog closes properly - Wrap useOpenProject's onChoice in try/finally to guarantee resolveChoice() is always called, preventing indefinite pending state --- .../routers/workspaces/procedures/create.ts | 5 +++++ .../ConnectedWorktreeChoiceDialog.tsx | 11 ++++++++--- .../react-query/projects/useOpenProject.tsx | 19 +++++++++++-------- 3 files changed, 24 insertions(+), 11 deletions(-) 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 d1faf7e2fdb..858a5de0acb 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts @@ -326,6 +326,11 @@ export const createCreateProcedures = () => { throw new Error("Could not determine current branch"); } + // Checkout the target branch so the main repo matches the workspace + if (input.branchName?.trim()) { + await safeCheckoutBranch(project.mainRepoPath, branch); + } + const existing = getBranchWorkspace(input.projectId); if (existing) { diff --git a/apps/desktop/src/renderer/components/WorktreeChoiceDialog/ConnectedWorktreeChoiceDialog.tsx b/apps/desktop/src/renderer/components/WorktreeChoiceDialog/ConnectedWorktreeChoiceDialog.tsx index 5b703b6e1a8..2e27588af81 100644 --- a/apps/desktop/src/renderer/components/WorktreeChoiceDialog/ConnectedWorktreeChoiceDialog.tsx +++ b/apps/desktop/src/renderer/components/WorktreeChoiceDialog/ConnectedWorktreeChoiceDialog.tsx @@ -12,9 +12,14 @@ export function ConnectedWorktreeChoiceDialog() { onOpenChange={(open) => { if (!open) close(); }} - onChoice={(enableWorktrees) => { - onChoice?.(enableWorktrees); - close(); + onChoice={async (enableWorktrees) => { + try { + await onChoice?.(enableWorktrees); + } catch (error) { + console.error("[WorktreeChoiceDialog] onChoice failed:", error); + } finally { + close(); + } }} /> ); diff --git a/apps/desktop/src/renderer/react-query/projects/useOpenProject.tsx b/apps/desktop/src/renderer/react-query/projects/useOpenProject.tsx index 7ade642ecc5..ef2dc2919f4 100644 --- a/apps/desktop/src/renderer/react-query/projects/useOpenProject.tsx +++ b/apps/desktop/src/renderer/react-query/projects/useOpenProject.tsx @@ -85,15 +85,18 @@ export function useOpenProject() { useWorktreeChoiceDialogStore.getState().open({ projectName: project.name, onChoice: async (enableWorktrees) => { - if (!enableWorktrees) { - await updateProject.mutateAsync({ - id: project.id, - patch: { worktreeMode: "disabled" }, - }); + try { + if (!enableWorktrees) { + await updateProject.mutateAsync({ + id: project.id, + patch: { worktreeMode: "disabled" }, + }); + } + await utils.workspaces.getAllGrouped.invalidate(); + await utils.projects.getRecents.invalidate(); + } finally { + resolveChoice(); } - await utils.workspaces.getAllGrouped.invalidate(); - await utils.projects.getRecents.invalidate(); - resolveChoice(); }, }); }); From 1fef62b490f6a7fb878d0869c8f47003ad6bac1e Mon Sep 17 00:00:00 2001 From: z3thon Date: Fri, 20 Mar 2026 06:08:55 +0700 Subject: [PATCH 26/33] feat(desktop): independent left/right panel toggles with dedicated buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace complex auto-move sidebar logic with simple independent panels: - Add isLeftPanelOpen/isRightPanelOpen to sidebar store (replaces single isSidebarOpen for panel visibility) - Add toggleLeftPanel()/toggleRightPanel() methods - Add PanelToggleButtons component (LuPanelLeft/LuPanelRight icons) always visible in content header, highlighted when panel is open - Simplify handleClosePanel to just toggle the panel, no tab moving - Each panel's tabs stay where they are when opened/closed - Backward compat: isSidebarOpen/toggleSidebar still work (map to right) - Persist migration v1→v2 initializes new panel states from legacy --- .../PanelToggleButtons/PanelToggleButtons.tsx | 54 +++++++++++++++++++ .../components/PanelToggleButtons/index.ts | 1 + .../ChangesContent/ChangesContent.tsx | 2 +- .../WorkspaceView/ContentView/index.tsx | 8 +-- .../WorkspaceView/RightSidebar/index.tsx | 21 ++------ .../WorkspaceLayout/WorkspaceLayout.tsx | 7 +-- .../src/renderer/stores/sidebar-state.ts | 54 +++++++++++++++---- 7 files changed, 111 insertions(+), 36 deletions(-) create mode 100644 apps/desktop/src/renderer/screens/main/components/PanelToggleButtons/PanelToggleButtons.tsx create mode 100644 apps/desktop/src/renderer/screens/main/components/PanelToggleButtons/index.ts diff --git a/apps/desktop/src/renderer/screens/main/components/PanelToggleButtons/PanelToggleButtons.tsx b/apps/desktop/src/renderer/screens/main/components/PanelToggleButtons/PanelToggleButtons.tsx new file mode 100644 index 00000000000..385eb4be2c8 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/PanelToggleButtons/PanelToggleButtons.tsx @@ -0,0 +1,54 @@ +import { cn } from "@superset/ui/utils"; +import { LuPanelLeft, LuPanelRight } from "react-icons/lu"; +import { + RightSidebarTab, + useSidebarStore, +} from "renderer/stores/sidebar-state"; + +export function PanelToggleButtons() { + const isLeftPanelOpen = useSidebarStore((s) => s.isLeftPanelOpen); + const isRightPanelOpen = useSidebarStore((s) => s.isRightPanelOpen); + const toggleLeftPanel = useSidebarStore((s) => s.toggleLeftPanel); + const toggleRightPanel = useSidebarStore((s) => s.toggleRightPanel); + const tabPositions = useSidebarStore((s) => s.tabPositions); + + const hasLeftTabs = + tabPositions[RightSidebarTab.Changes] === "left" || + tabPositions[RightSidebarTab.Files] === "left"; + const hasRightTabs = + tabPositions[RightSidebarTab.Changes] === "right" || + tabPositions[RightSidebarTab.Files] === "right"; + + return ( +
+ {hasLeftTabs && ( + + )} + {hasRightTabs && ( + + )} +
+ ); +} diff --git a/apps/desktop/src/renderer/screens/main/components/PanelToggleButtons/index.ts b/apps/desktop/src/renderer/screens/main/components/PanelToggleButtons/index.ts new file mode 100644 index 00000000000..424f06fd7f2 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/PanelToggleButtons/index.ts @@ -0,0 +1 @@ +export { PanelToggleButtons } from "./PanelToggleButtons"; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/ChangesContent.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/ChangesContent.tsx index 4bdae7e68c7..15dcf749079 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/ChangesContent.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/ChangesContent.tsx @@ -10,7 +10,7 @@ import { InfiniteScrollView } from "./components/InfiniteScrollView"; export function ChangesContent() { const { workspaceId } = useParams({ strict: false }); const isChangesSidebarVisible = useSidebarStore( - (s) => s.isSidebarOpen && s.rightSidebarTab === RightSidebarTab.Changes, + (s) => s.isRightPanelOpen && s.rightSidebarTab === RightSidebarTab.Changes, ); const { data: workspace } = electronTrpc.workspaces.get.useQuery( { id: workspaceId ?? "" }, diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/index.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/index.tsx index 54ef3f42524..2a333bfcf26 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/index.tsx @@ -1,7 +1,6 @@ import type { ExternalApp } from "@superset/local-db"; import { electronTrpc } from "renderer/lib/electron-trpc"; -import { useSidebarStore } from "renderer/stores/sidebar-state"; -import { SidebarControl } from "../../SidebarControl"; +import { PanelToggleButtons } from "../../PanelToggleButtons"; import { ContentHeader } from "./ContentHeader"; import { PresetsBar } from "./components/PresetsBar"; import { TabsContent } from "./TabsContent"; @@ -18,15 +17,12 @@ export function ContentView({ onOpenInApp, onOpenQuickOpen, }: ContentViewProps) { - const isSidebarOpen = useSidebarStore((s) => s.isSidebarOpen); const { data: showPresetsBar } = electronTrpc.settings.getShowPresetsBar.useQuery(); return (
- : undefined} - > + }> {showPresetsBar && } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/index.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/index.tsx index c634dfe18ec..4ed8bd1248a 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/index.tsx @@ -99,7 +99,8 @@ export function RightSidebar({ side = "right" }: RightSidebarProps) { const currentMode = useSidebarStore((s) => s.currentMode); const rightSidebarTab = useSidebarStore((s) => s.rightSidebarTab); const setRightSidebarTab = useSidebarStore((s) => s.setRightSidebarTab); - const toggleSidebar = useSidebarStore((s) => s.toggleSidebar); + const toggleLeftPanel = useSidebarStore((s) => s.toggleLeftPanel); + const toggleRightPanel = useSidebarStore((s) => s.toggleRightPanel); const setMode = useSidebarStore((s) => s.setMode); const setTabPosition = useSidebarStore((s) => s.setTabPosition); const tabPositions = useSidebarStore((s) => s.tabPositions); @@ -119,23 +120,9 @@ export function RightSidebar({ side = "right" }: RightSidebarProps) { const handleClosePanel = () => { if (side === "right") { - // Move any lone tab back to right before closing - const tabsOnRight = Object.entries(tabPositions).filter( - ([, s]) => s === "right", - ); - if (tabsOnRight.length <= 1) { - // Reset all tabs to right so nothing is stranded - setTabPosition(RightSidebarTab.Changes, "right"); - setTabPosition(RightSidebarTab.Files, "right"); - } - toggleSidebar(); + toggleRightPanel(); } else { - // Left panel: move all tabs on the left back to right, panel disappears - for (const [tab, tabSide] of Object.entries(tabPositions)) { - if (tabSide === "left") { - setTabPosition(tab as RightSidebarTab, "right"); - } - } + toggleLeftPanel(); } }; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceLayout/WorkspaceLayout.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceLayout/WorkspaceLayout.tsx index c366a438950..a8e7a26b90e 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceLayout/WorkspaceLayout.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/WorkspaceLayout/WorkspaceLayout.tsx @@ -25,7 +25,8 @@ export function WorkspaceLayout({ onOpenQuickOpen, }: WorkspaceLayoutProps) { useBrowserLifecycle(); - const isSidebarOpen = useSidebarStore((s) => s.isSidebarOpen); + const isLeftPanelOpen = useSidebarStore((s) => s.isLeftPanelOpen); + const isRightPanelOpen = useSidebarStore((s) => s.isRightPanelOpen); const sidebarWidth = useSidebarStore((s) => s.sidebarWidth); const setSidebarWidth = useSidebarStore((s) => s.setSidebarWidth); const leftPanelWidth = useSidebarStore((s) => s.leftPanelWidth); @@ -45,8 +46,8 @@ export function WorkspaceLayout({ tabPositions[RightSidebarTab.Changes] === "right" || tabPositions[RightSidebarTab.Files] === "right"; - const showLeftPanel = isSidebarOpen && hasLeftTabs; - const showRightPanel = isSidebarOpen && hasRightTabs; + const showLeftPanel = isLeftPanelOpen && hasLeftTabs; + const showRightPanel = isRightPanelOpen && hasRightTabs; return ( diff --git a/apps/desktop/src/renderer/stores/sidebar-state.ts b/apps/desktop/src/renderer/stores/sidebar-state.ts index 7030149de5c..48cc9e168a8 100644 --- a/apps/desktop/src/renderer/stores/sidebar-state.ts +++ b/apps/desktop/src/renderer/stores/sidebar-state.ts @@ -18,6 +18,11 @@ export const MAX_SIDEBAR_WIDTH = 500; export type PanelSide = "left" | "right"; interface SidebarState { + /** Whether the left panel is open */ + isLeftPanelOpen: boolean; + /** Whether the right panel is open */ + isRightPanelOpen: boolean; + /** @deprecated Use isRightPanelOpen — kept for backward compat */ isSidebarOpen: boolean; sidebarWidth: number; lastOpenSidebarWidth: number; @@ -29,6 +34,9 @@ interface SidebarState { tabPositions: Record; /** Width of the left panel (when tabs are docked left) */ leftPanelWidth: number; + toggleLeftPanel: () => void; + toggleRightPanel: () => void; + /** @deprecated Use toggleRightPanel — toggles the right panel for hotkey compat */ toggleSidebar: () => void; setSidebarOpen: (open: boolean) => void; setSidebarWidth: (width: number) => void; @@ -43,6 +51,8 @@ export const useSidebarStore = create()( devtools( persist( (set, get) => ({ + isLeftPanelOpen: false, + isRightPanelOpen: true, isSidebarOpen: true, sidebarWidth: DEFAULT_SIDEBAR_WIDTH, lastOpenSidebarWidth: DEFAULT_SIDEBAR_WIDTH, @@ -56,11 +66,20 @@ export const useSidebarStore = create()( }, leftPanelWidth: DEFAULT_SIDEBAR_WIDTH, - toggleSidebar: () => { - const { isSidebarOpen, lastOpenSidebarWidth, currentMode, lastMode } = - get(); - if (isSidebarOpen) { + toggleLeftPanel: () => { + set((s) => ({ isLeftPanelOpen: !s.isLeftPanelOpen })); + }, + + toggleRightPanel: () => { + const { + isRightPanelOpen, + lastOpenSidebarWidth, + currentMode, + lastMode, + } = get(); + if (isRightPanelOpen) { set({ + isRightPanelOpen: false, isSidebarOpen: false, sidebarWidth: 0, lastMode: currentMode, @@ -68,6 +87,7 @@ export const useSidebarStore = create()( }); } else { set({ + isRightPanelOpen: true, isSidebarOpen: true, sidebarWidth: lastOpenSidebarWidth, currentMode: lastMode, @@ -75,16 +95,23 @@ export const useSidebarStore = create()( } }, + toggleSidebar: () => { + // Backward compat — toggles right panel + get().toggleRightPanel(); + }, + setSidebarOpen: (open) => { const { lastOpenSidebarWidth, currentMode, lastMode } = get(); if (open) { set({ + isRightPanelOpen: true, isSidebarOpen: true, sidebarWidth: lastOpenSidebarWidth, currentMode: lastMode, }); } else { set({ + isRightPanelOpen: false, isSidebarOpen: false, sidebarWidth: 0, lastMode: currentMode, @@ -100,23 +127,26 @@ export const useSidebarStore = create()( ); if (width > 0) { - const { sidebarWidth, lastOpenSidebarWidth, isSidebarOpen } = get(); + const { sidebarWidth, lastOpenSidebarWidth, isRightPanelOpen } = + get(); if ( sidebarWidth === clampedWidth && lastOpenSidebarWidth === clampedWidth && - isSidebarOpen + isRightPanelOpen ) { return; } set({ sidebarWidth: clampedWidth, lastOpenSidebarWidth: clampedWidth, + isRightPanelOpen: true, isSidebarOpen: true, }); } else { const { currentMode } = get(); set({ sidebarWidth: 0, + isRightPanelOpen: false, isSidebarOpen: false, lastMode: currentMode, currentMode: SidebarMode.Tabs, @@ -153,16 +183,22 @@ export const useSidebarStore = create()( }), { name: "sidebar-store", - migrate: (persistedState: unknown, _version: number) => { + migrate: (persistedState: unknown, version: number) => { const state = persistedState as Partial; - // Convert old percentage-based values (<100) to pixel widths + // v0→v1: Convert old percentage-based values (<100) to pixel widths if (state.sidebarWidth !== undefined && state.sidebarWidth < 100) { state.sidebarWidth = DEFAULT_SIDEBAR_WIDTH; state.lastOpenSidebarWidth = DEFAULT_SIDEBAR_WIDTH; } + // v1→v2: Initialize independent panel open states from legacy isSidebarOpen + if (version < 2) { + const wasOpen = state.isSidebarOpen ?? true; + state.isRightPanelOpen = wasOpen; + state.isLeftPanelOpen = false; + } return state as SidebarState; }, - version: 1, + version: 2, }, ), { name: "SidebarStore" }, From 96d15b2aa304687d674531ca6b6f20c58b621008 Mon Sep 17 00:00:00 2001 From: z3thon Date: Fri, 20 Mar 2026 06:36:14 +0700 Subject: [PATCH 27/33] feat(desktop): move panel toggle buttons to top nav bar --- .../_dashboard/components/TopBar/TopBar.tsx | 2 + .../PanelToggleButtons/PanelToggleButtons.tsx | 67 +++++++------------ .../WorkspaceView/ContentView/index.tsx | 3 +- 3 files changed, 29 insertions(+), 43 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx index 538d0702678..407444b2c95 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/TopBar.tsx @@ -3,6 +3,7 @@ import { HiOutlineWifi } from "react-icons/hi2"; import { useOnlineStatus } from "renderer/hooks/useOnlineStatus"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { getWorkspaceDisplayName } from "renderer/lib/getWorkspaceDisplayName"; +import { PanelToggleButtons } from "renderer/screens/main/components/PanelToggleButtons"; import { NavigationControls } from "./components/NavigationControls"; import { OpenInMenuButton } from "./components/OpenInMenuButton"; import { OrganizationDropdown } from "./components/OrganizationDropdown"; @@ -60,6 +61,7 @@ export function TopBar() { Offline
)} + {workspace?.worktreePath && ( s.isLeftPanelOpen); const isRightPanelOpen = useSidebarStore((s) => s.isRightPanelOpen); const toggleLeftPanel = useSidebarStore((s) => s.toggleLeftPanel); const toggleRightPanel = useSidebarStore((s) => s.toggleRightPanel); - const tabPositions = useSidebarStore((s) => s.tabPositions); - - const hasLeftTabs = - tabPositions[RightSidebarTab.Changes] === "left" || - tabPositions[RightSidebarTab.Files] === "left"; - const hasRightTabs = - tabPositions[RightSidebarTab.Changes] === "right" || - tabPositions[RightSidebarTab.Files] === "right"; return ( -
- {hasLeftTabs && ( - - )} - {hasRightTabs && ( - - )} +
+ +
); } diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/index.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/index.tsx index 2a333bfcf26..4a7938c9f4d 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/index.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/index.tsx @@ -1,6 +1,5 @@ import type { ExternalApp } from "@superset/local-db"; import { electronTrpc } from "renderer/lib/electron-trpc"; -import { PanelToggleButtons } from "../../PanelToggleButtons"; import { ContentHeader } from "./ContentHeader"; import { PresetsBar } from "./components/PresetsBar"; import { TabsContent } from "./TabsContent"; @@ -22,7 +21,7 @@ export function ContentView({ return (
- }> + {showPresetsBar && } From f9a6c6ca38f119766f9bc6ef5ee756c3770d339a Mon Sep 17 00:00:00 2001 From: z3thon Date: Fri, 20 Mar 2026 07:08:25 +0700 Subject: [PATCH 28/33] fix(desktop): restore split-pane project settings page lost during merge --- .../_authenticated/settings/projects/page.tsx | 161 ++++++++++++------ 1 file changed, 113 insertions(+), 48 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx index 8f40231e3aa..a7a16000938 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/projects/page.tsx @@ -1,64 +1,129 @@ import { cn } from "@superset/ui/utils"; -import { createFileRoute, useNavigate } from "@tanstack/react-router"; -import { HiChevronRight } from "react-icons/hi2"; +import { createFileRoute } from "@tanstack/react-router"; +import { useState } from "react"; +import { LuFolderOpen, LuGitBranch } from "react-icons/lu"; import { electronTrpc } from "renderer/lib/electron-trpc"; +import { ProjectThumbnail } from "renderer/screens/main/components/WorkspaceSidebar/ProjectSection/ProjectThumbnail"; +import { ProjectSettings } from "../project/$projectId/components/ProjectSettings"; export const Route = createFileRoute("/_authenticated/settings/projects/")({ - component: ProjectsListPage, + component: ProjectsPage, }); -function ProjectsListPage() { +function ProjectsPage() { const { data: groups = [] } = electronTrpc.workspaces.getAllGrouped.useQuery(); - const navigate = useNavigate(); + const [selectedProjectId, setSelectedProjectId] = useState( + groups[0]?.project.id ?? null, + ); + + // Auto-select first project if none selected + const effectiveSelectedId = + selectedProjectId && groups.some((g) => g.project.id === selectedProjectId) + ? selectedProjectId + : (groups[0]?.project.id ?? null); return ( -
-
-

Projects

-

- Select a project to configure its settings -

-
+
+ {/* Left: Project/workspace list */} +
+
+

+ Projects +

+
+
+ {groups.map((group) => { + const isBranchOnly = group.project.worktreeMode === "disabled"; + const isSelected = group.project.id === effectiveSelectedId; + const worktreeWorkspaces = group.workspaces.filter( + (w) => w.type === "worktree", + ); + const branchWorkspace = group.workspaces.find( + (w) => w.type === "branch", + ); + + return ( +
+ {/* Project row */} + - {groups.length === 0 ? ( -

- No projects yet. Import a repository to get started. -

- ) : ( -
- {groups.map((group) => ( - - ))} + ); + })} + + {groups.length === 0 && ( +

+ No projects yet. +

+ )}
- )} +
+ + {/* Right: Selected project settings */} +
+ {effectiveSelectedId ? ( + + ) : ( +
+ Select a project to view its settings +
+ )} +
); } From 241716312421f44ceec2703fc5f8193df96366f0 Mon Sep 17 00:00:00 2001 From: z3thon Date: Fri, 20 Mar 2026 07:10:15 +0700 Subject: [PATCH 29/33] fix(desktop): remove duplicate Projects section from settings sidebar bottom --- .../settings/components/SettingsSidebar/SettingsSidebar.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/SettingsSidebar.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/SettingsSidebar.tsx index fa87f713de6..648b070528b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/SettingsSidebar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/SettingsSidebar.tsx @@ -13,7 +13,6 @@ import { } from "renderer/stores/settings-state"; import { getMatchCountBySection } from "../../utils/settings-search"; import { GeneralSettings } from "./GeneralSettings"; -import { ProjectsSettings } from "./ProjectsSettings"; export function SettingsSidebar() { const searchQuery = useSettingsSearchQuery(); @@ -58,7 +57,6 @@ export function SettingsSidebar() {
-
From b824b4b7e4a4f2d140124bb88a77369dbce67772 Mon Sep 17 00:00:00 2001 From: z3thon Date: Fri, 20 Mar 2026 07:11:05 +0700 Subject: [PATCH 30/33] fix(desktop): remove back button from project settings (now inline in split-pane) --- .../ProjectSettingsHeader.tsx | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettingsHeader/ProjectSettingsHeader.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettingsHeader/ProjectSettingsHeader.tsx index f1ff9248290..7fcc307012c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettingsHeader/ProjectSettingsHeader.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/components/ProjectSettingsHeader/ProjectSettingsHeader.tsx @@ -1,7 +1,4 @@ -import { Button } from "@superset/ui/button"; -import { Link } from "@tanstack/react-router"; import type { ReactNode } from "react"; -import { HiArrowLeft } from "react-icons/hi2"; interface ProjectSettingsHeaderProps { title: string; @@ -13,18 +10,9 @@ export function ProjectSettingsHeader({ children, }: ProjectSettingsHeaderProps) { return ( -
- - -
-

{title}

- {children &&
{children}
} -
+
+

{title}

+ {children &&
{children}
}
); } From d34f945a12e277eae856d133f001a56e550cc782 Mon Sep 17 00:00:00 2001 From: z3thon Date: Fri, 20 Mar 2026 07:18:43 +0700 Subject: [PATCH 31/33] feat(desktop): match panel toggle buttons to existing sidebar toggle design --- .../PanelToggleButtons/PanelToggleButtons.tsx | 93 +++++++++++++------ 1 file changed, 66 insertions(+), 27 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/PanelToggleButtons/PanelToggleButtons.tsx b/apps/desktop/src/renderer/screens/main/components/PanelToggleButtons/PanelToggleButtons.tsx index aeb5e80b364..7c018161b1b 100644 --- a/apps/desktop/src/renderer/screens/main/components/PanelToggleButtons/PanelToggleButtons.tsx +++ b/apps/desktop/src/renderer/screens/main/components/PanelToggleButtons/PanelToggleButtons.tsx @@ -1,5 +1,13 @@ -import { cn } from "@superset/ui/utils"; -import { LuPanelLeft, LuPanelRight } from "react-icons/lu"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { + LuPanelLeft, + LuPanelLeftClose, + LuPanelLeftOpen, + LuPanelRight, + LuPanelRightClose, + LuPanelRightOpen, +} from "react-icons/lu"; +import { HotkeyTooltipContent } from "renderer/components/HotkeyTooltipContent"; import { useSidebarStore } from "renderer/stores/sidebar-state"; export function PanelToggleButtons() { @@ -9,31 +17,62 @@ export function PanelToggleButtons() { const toggleRightPanel = useSidebarStore((s) => s.toggleRightPanel); return ( -
- - +
+ + + + + + + + + + + + + + + + +
); } From c4a03244e9813bbcef174a47bba0cecf8459e584 Mon Sep 17 00:00:00 2001 From: z3thon Date: Fri, 20 Mar 2026 07:22:37 +0700 Subject: [PATCH 32/33] fix(desktop): X button on workspace uses close dialog with recycle option --- .../WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 32b7c80843f..5ce885ac7a6 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 @@ -402,7 +402,7 @@ export function WorkspaceListItem({ type="button" onClick={(e) => { e.stopPropagation(); - handleDeleteClick(); + setShowCloseDialog(true); }} className="flex items-center justify-center text-muted-foreground hover:text-foreground" aria-label="Close workspace" From d14d19dc4667adfe138c2dcb596ca1b8eabd0783 Mon Sep 17 00:00:00 2001 From: z3thon Date: Fri, 20 Mar 2026 23:58:05 +0700 Subject: [PATCH 33/33] fix(desktop): add missing settings import in workspace create procedure --- .../src/lib/trpc/routers/workspaces/procedures/create.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6164aa93aff..40b79aa4d27 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts @@ -1,4 +1,4 @@ -import { projects, workspaces, worktrees } from "@superset/local-db"; +import { projects, settings, workspaces, worktrees } from "@superset/local-db"; import { and, eq, isNull, not } from "drizzle-orm"; import { track } from "main/lib/analytics"; import { localDb } from "main/lib/local-db";