Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DashboardSidebarCollapsedProjectContent } from "./components/DashboardS
import { DashboardSidebarExpandedProjectContent } from "./components/DashboardSidebarExpandedProjectContent";
import { DashboardSidebarProjectContextMenu } from "./components/DashboardSidebarProjectContextMenu";
import { DashboardSidebarProjectRow } from "./components/DashboardSidebarProjectRow";
import { ImportWorktreesDialog } from "./components/ImportWorktreesDialog";
import { useDashboardSidebarProjectSectionActions } from "./hooks/useDashboardSidebarProjectSectionActions";

interface DashboardSidebarProjectSectionProps {
Expand Down Expand Up @@ -40,15 +41,20 @@ export function DashboardSidebarProjectSection({

const {
cancelRename,
confirmImportWorktrees,
confirmRemoveFromSidebar,
deleteSection,
handleImportWorktrees,
handleNewSection,
handleNewWorkspace,
handleOpenInFinder,
handleOpenSettings,
importableWorktrees,
isImportingWorktrees,
isRenaming,
renameSection,
renameValue,
setImportableWorktrees,
setRenameValue,
startRename,
submitRename,
Expand All @@ -59,10 +65,24 @@ export function DashboardSidebarProjectSection({

const totalWorkspaceCount = flattenedCollapsedWorkspaces.length;

// Rendered only while open so the checkbox state resets per invocation.
const importWorktreesDialog = importableWorktrees && (
<ImportWorktreesDialog
open
worktrees={importableWorktrees}
isImporting={isImportingWorktrees}
onOpenChange={(open) => {
if (!open) setImportableWorktrees(null);
}}
onConfirm={confirmImportWorktrees}
/>
);

if (isSidebarCollapsed) {
return (
<DashboardSidebarProjectContextMenu
onCreateSection={handleNewSection}
onImportWorktrees={handleImportWorktrees}
onOpenInFinder={handleOpenInFinder}
onOpenSettings={handleOpenSettings}
onRemoveFromSidebar={confirmRemoveFromSidebar}
Expand All @@ -80,6 +100,7 @@ export function DashboardSidebarProjectSection({
onWorkspaceHover={onWorkspaceHover}
onToggleCollapse={() => onToggleCollapse(project.id)}
/>
{importWorktreesDialog}
</div>
</DashboardSidebarProjectContextMenu>
);
Expand All @@ -89,6 +110,7 @@ export function DashboardSidebarProjectSection({
<div className="mt-1 first:mt-0">
<DashboardSidebarProjectContextMenu
onCreateSection={handleNewSection}
onImportWorktrees={handleImportWorktrees}
onOpenInFinder={handleOpenInFinder}
onOpenSettings={handleOpenSettings}
onRemoveFromSidebar={confirmRemoveFromSidebar}
Expand Down Expand Up @@ -133,6 +155,7 @@ export function DashboardSidebarProjectSection({
</motion.div>
)}
</AnimatePresence>
{importWorktreesDialog}
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ContextMenuTrigger,
} from "@superset/ui/context-menu";
import {
LuFolderInput,
LuFolderOpen,
LuFolderPlus,
LuPencil,
Expand All @@ -15,6 +16,7 @@ import {

interface DashboardSidebarProjectContextMenuProps {
onCreateSection: () => void;
onImportWorktrees: () => void;
onOpenInFinder: () => void;
onOpenSettings: () => void;
onRemoveFromSidebar: () => void;
Expand All @@ -24,6 +26,7 @@ interface DashboardSidebarProjectContextMenuProps {

export function DashboardSidebarProjectContextMenu({
onCreateSection,
onImportWorktrees,
onOpenInFinder,
onOpenSettings,
onRemoveFromSidebar,
Expand Down Expand Up @@ -51,6 +54,10 @@ export function DashboardSidebarProjectContextMenu({
<LuFolderPlus className="size-4 mr-2" />
New group
</ContextMenuItem>
<ContextMenuItem onSelect={onImportWorktrees}>
<LuFolderInput className="size-4 mr-2" />
Import untracked worktrees
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem
onSelect={onRemoveFromSidebar}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Button } from "@superset/ui/button";
import { Checkbox } from "@superset/ui/checkbox";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@superset/ui/dialog";
import { Label } from "@superset/ui/label";
import { useState } from "react";
import { LuGitBranch } from "react-icons/lu";

export interface ImportableWorktree {
branch: string;
path: string;
}

interface ImportWorktreesDialogProps {
open: boolean;
worktrees: ImportableWorktree[];
isImporting: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: (options: { runSetup: boolean }) => void;
}

export function ImportWorktreesDialog({
open,
worktrees,
isImporting,
onOpenChange,
onConfirm,
}: ImportWorktreesDialogProps) {
const [runSetup, setRunSetup] = useState(false);
const count = worktrees.length;

return (
<Dialog
open={open}
onOpenChange={isImporting ? undefined : onOpenChange}
modal
>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Import untracked worktrees</DialogTitle>
<DialogDescription>
{count === 1
? "1 worktree in this repository has no workspace. It will be imported as a workspace."
: `${count} worktrees in this repository have no workspace. They will be imported as workspaces.`}
</DialogDescription>
</DialogHeader>
<ul className="-mx-2.5 max-h-64 overflow-x-hidden overflow-y-auto overscroll-contain">
{worktrees.map((worktree) => (
<li
key={worktree.path}
className="grid min-w-0 grid-cols-[1rem_minmax(0,1fr)] items-center gap-x-3 rounded-md px-2.5 py-2"
>
<LuGitBranch
className="size-4 shrink-0 text-muted-foreground"
strokeWidth={2}
/>
<div className="flex min-w-0 flex-col">
<span
className="truncate text-[13px] font-medium leading-4 text-foreground"
title={worktree.branch}
>
{worktree.branch}
</span>
<span
className="mt-0.5 truncate font-mono text-[11px] leading-4 text-muted-foreground"
title={worktree.path}
>
{worktree.path}
</span>
</div>
</li>
))}
</ul>
<div className="flex items-center gap-2">
<Checkbox
id="import-worktrees-run-setup"
checked={runSetup}
onCheckedChange={(checked) => setRunSetup(checked === true)}
disabled={isImporting}
/>
<Label
htmlFor="import-worktrees-run-setup"
className="cursor-pointer text-[13px] font-normal text-muted-foreground"
>
Run setup script in each imported workspace
</Label>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isImporting}
>
Cancel
</Button>
<Button
onClick={() => onConfirm({ runSetup })}
disabled={isImporting}
>
{isImporting
? "Importing…"
: count === 1
? "Import 1 worktree"
: `Import ${count} worktrees`}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { ImportableWorktree } from "./ImportWorktreesDialog";
export { ImportWorktreesDialog } from "./ImportWorktreesDialog";
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { alert } from "@superset/ui/atoms/Alert";
import { toast } from "@superset/ui/sonner";
import { useNavigate } from "@tanstack/react-router";
import { useMemo, useState } from "react";
import { useMemo, useRef, useState } from "react";
import { useHostProjects } from "renderer/hooks/host-projects/useHostProjects";
import { useHostUrl } from "renderer/hooks/host-service/useHostTargetUrl";
import { getHostServiceClientByUrl } from "renderer/lib/host-service-client";
Expand All @@ -10,7 +10,9 @@ import { useDashboardSidebarSectionRename } from "renderer/routes/_authenticated
import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState";
import { useLocalHostService } from "renderer/routes/_authenticated/providers/LocalHostServiceProvider";
import { useOpenNewWorkspaceModal } from "renderer/stores/new-workspace-modal";
import { useWorkspaceCreates } from "renderer/stores/workspace-creates";
import type { DashboardSidebarProject } from "../../../../types";
import type { ImportableWorktree } from "../../components/ImportWorktreesDialog";

interface UseDashboardSidebarProjectSectionActionsOptions {
project: DashboardSidebarProject;
Expand All @@ -36,6 +38,13 @@ export function useDashboardSidebarProjectSectionActions({
// undefined (not null) when no host serves it — null would resolve to
// the local host and rename the wrong replica.
const servingHostUrl = useHostUrl(servingHostId ?? undefined);
const { submit } = useWorkspaceCreates();
const importingWorktreesRef = useRef(false);
// Non-null while the import confirmation dialog is open.
const [importableWorktrees, setImportableWorktrees] = useState<
ImportableWorktree[] | null
>(null);
const [isImportingWorktrees, setIsImportingWorktrees] = useState(false);
const { requestSectionRename } = useDashboardSidebarSectionRename();
const {
createSection,
Expand Down Expand Up @@ -124,6 +133,94 @@ export function useDashboardSidebarProjectSectionActions({
openModal(project.id);
};

// Menu action: list the worktrees git knows about that have no workspace
// row yet and open the confirmation dialog.
const handleImportWorktrees = async () => {
if (importingWorktreesRef.current) return;
if (!servingHostUrl) {
toast.error(
"Project's host is unreachable — cannot import worktrees right now",
);
return;
}
try {
const { worktrees } = await getHostServiceClientByUrl(
servingHostUrl,
).workspaceCreation.listProjectWorktrees.query({
projectId: project.id,
});
// `=== false` also skips hosts too old to report tracked-ness.
const untracked = worktrees.filter(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: On hosts that predate the new hasWorkspace/isMainWorktree fields, worktree.hasWorkspace === false is false because the field is undefined, so the list is treated as fully tracked and users get the 'All of this project's worktrees are already tracked' toast. That message is misleading because the real cause is host-version incompatibility, not zero untracked worktrees. Consider detecting the old-host case (e.g., no worktree carries the new fields) and showing a dedicated 'update your host' message instead of a false 'all tracked' success toast.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.ts, line 153:

<comment>On hosts that predate the new `hasWorkspace`/`isMainWorktree` fields, `worktree.hasWorkspace === false` is `false` because the field is `undefined`, so the list is treated as fully tracked and users get the 'All of this project's worktrees are already tracked' toast. That message is misleading because the real cause is host-version incompatibility, not zero untracked worktrees. Consider detecting the old-host case (e.g., no worktree carries the new fields) and showing a dedicated 'update your host' message instead of a false 'all tracked' success toast.</comment>

<file context>
@@ -124,6 +133,94 @@ export function useDashboardSidebarProjectSectionActions({
+				projectId: project.id,
+			});
+			// `=== false` also skips hosts too old to report tracked-ness.
+			const untracked = worktrees.filter(
+				(worktree) =>
+					worktree.hasWorkspace === false && worktree.isMainWorktree === false,
</file context>

(worktree) =>
worktree.hasWorkspace === false && worktree.isMainWorktree === false,
);
if (untracked.length === 0) {
toast.info("All of this project's worktrees are already tracked");
return;
}
setImportableWorktrees(untracked);
} catch (error) {
toast.error(
`Failed to list worktrees: ${error instanceof Error ? error.message : String(error)}`,
);
}
};

// Dialog confirm: adopt each worktree via the `worktreePath` branch of
// `workspaces.create` (no `git worktree add`; setup only when opted in).
const confirmImportWorktrees = async ({
runSetup,
}: {
runSetup: boolean;
}) => {
const untracked = importableWorktrees;
if (importingWorktreesRef.current || !untracked) return;
if (!servingHostId) {
toast.error(
"Project's host is unreachable — cannot import worktrees right now",
);
return;
}
importingWorktreesRef.current = true;
setIsImportingWorktrees(true);
try {
const outcomes = await Promise.all(
untracked.map(
(worktree) =>
submit({
hostId: servingHostId,
snapshot: {
id: crypto.randomUUID(),
projectId: project.id,
branch: worktree.branch,
worktreePath: worktree.path,
runSetup,
},
}).completed,
),
);
const errors = outcomes.flatMap((outcome) =>
outcome.ok ? [] : [outcome.error],
);
const imported = outcomes.length - errors.length;
if (errors.length > 0) {
toast.error(
`Imported ${imported} of ${untracked.length} worktrees: ${errors[0]}`,
);
} else {
toast.success(
imported === 1
? "Imported 1 worktree as a workspace"
: `Imported ${imported} worktrees as workspaces`,
);
}
Comment on lines +202 to +216

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Surface more than the first error on partial import failure.

When several worktrees fail for different reasons, errors[0] hides every failure except the first. Users cannot tell whether the other failures share the same cause.

Include the failure count or join distinct error messages instead of showing only the first one.

💬 Proposed fix to surface more failure detail
 			if (errors.length > 0) {
+				const uniqueErrors = [...new Set(errors)];
 				toast.error(
-					`Imported ${imported} of ${untracked.length} worktrees: ${errors[0]}`,
+					`Imported ${imported} of ${untracked.length} worktrees: ${uniqueErrors.join("; ")}`,
 				);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const errors = outcomes.flatMap((outcome) =>
outcome.ok ? [] : [outcome.error],
);
const imported = outcomes.length - errors.length;
if (errors.length > 0) {
toast.error(
`Imported ${imported} of ${untracked.length} worktrees: ${errors[0]}`,
);
} else {
toast.success(
imported === 1
? "Imported 1 worktree as a workspace"
: `Imported ${imported} worktrees as workspaces`,
);
}
const errors = outcomes.flatMap((outcome) =>
outcome.ok ? [] : [outcome.error],
);
const imported = outcomes.length - errors.length;
if (errors.length > 0) {
const uniqueErrors = [...new Set(errors)];
toast.error(
`Imported ${imported} of ${untracked.length} worktrees: ${uniqueErrors.join("; ")}`,
);
} else {
toast.success(
imported === 1
? "Imported 1 worktree as a workspace"
: `Imported ${imported} worktrees as workspaces`,
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarProjectSection/hooks/useDashboardSidebarProjectSectionActions/useDashboardSidebarProjectSectionActions.ts`
around lines 202 - 216, Update the partial-failure toast in the worktree import
action around the outcomes/error aggregation to surface more than errors[0].
Include the failure count or combine distinct error messages so users can see
details for all failed imports while preserving the existing success behavior.

setImportableWorktrees(null);
} finally {
importingWorktreesRef.current = false;
setIsImportingWorktrees(false);
}
};

const handleNewSection = () => {
const sectionId = createSection(project.id);
requestSectionRename(sectionId);
Expand All @@ -134,15 +231,20 @@ export function useDashboardSidebarProjectSectionActions({

return {
cancelRename,
confirmImportWorktrees,
confirmRemoveFromSidebar,
deleteSection,
handleImportWorktrees,
handleNewSection,
handleNewWorkspace,
handleOpenInFinder,
handleOpenSettings,
importableWorktrees,
isImportingWorktrees,
isRenaming,
renameSection,
renameValue,
setImportableWorktrees,
setRenameValue,
startRename,
submitRename,
Expand Down
Loading
Loading