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
4 changes: 4 additions & 0 deletions apps/desktop/src/lib/trpc/routers/projects/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,7 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {
branchPrefixMode: z.enum(BRANCH_PREFIX_MODES).nullable().optional(),
branchPrefixCustom: z.string().nullable().optional(),
workspaceBaseBranch: z.string().nullable().optional(),
worktreeBaseDir: z.string().nullable().optional(),
hideImage: z.boolean().optional(),
defaultApp: z.enum(EXTERNAL_APPS).nullable().optional(),
}),
Expand Down Expand Up @@ -844,6 +845,9 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {
...(input.patch.workspaceBaseBranch !== undefined && {
workspaceBaseBranch: input.patch.workspaceBaseBranch,
}),
...(input.patch.worktreeBaseDir !== undefined && {
worktreeBaseDir: input.patch.worktreeBaseDir,
}),
...(input.patch.hideImage !== undefined && {
hideImage: input.patch.hideImage,
}),
Expand Down
20 changes: 20 additions & 0 deletions apps/desktop/src/lib/trpc/routers/settings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,26 @@ export const createSettingsRouter = () => {
return { success: true };
}),

getWorktreeBaseDir: publicProcedure.query(() => {
const row = getSettings();
return row.worktreeBaseDir ?? null;
}),

setWorktreeBaseDir: publicProcedure
.input(z.object({ path: z.string().nullable() }))
.mutation(({ input }) => {
localDb
.insert(settings)
.values({ id: 1, worktreeBaseDir: input.path })
.onConflictDoUpdate({
target: settings.id,
set: { worktreeBaseDir: input.path },
})
.run();

return { success: true };
}),

// TODO: remove telemetry procedures once telemetry_enabled column is dropped
getTelemetryEnabled: publicProcedure.query(() => {
return true;
Expand Down
36 changes: 29 additions & 7 deletions apps/desktop/src/lib/trpc/routers/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,9 @@ import { homedir } from "node:os";
import path from "node:path";
import type { BrowserWindow } from "electron";
import { dialog } from "electron";
import { z } from "zod";
import { publicProcedure, router } from "..";

/**
* Window router for window controls
* Handles minimize, maximize, close, and platform detection
*
* Uses a getter function to always access the current window,
* allowing window recreation on macOS without stale references.
*/
export const createWindowRouter = (getWindow: () => BrowserWindow | null) => {
return router({
minimize: publicProcedure.mutation(() => {
Expand Down Expand Up @@ -53,6 +47,34 @@ export const createWindowRouter = (getWindow: () => BrowserWindow | null) => {
return homedir();
}),

selectDirectory: publicProcedure
.input(
z
.object({
title: z.string().optional(),
defaultPath: z.string().optional(),
})
.optional(),
)
.mutation(async ({ input }) => {
const window = getWindow();
if (!window) {
return { canceled: true, path: null };
}

const result = await dialog.showOpenDialog(window, {
properties: ["openDirectory", "createDirectory"],
Comment on lines +65 to +66
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.

⚠️ Potential issue | 🟡 Minor

Add "promptToCreate" for Windows — "createDirectory" is macOS-only

Per Electron's documentation, "createDirectory" only applies on macOS to allow creating new directories from the dialog, while "promptToCreate" is the Windows equivalent that prompts for creation if the entered path doesn't exist. Without "promptToCreate", Windows users must create the target directory externally before they can select it.

🛠️ Proposed fix for cross-platform directory creation
 const result = await dialog.showOpenDialog(window, {
-  properties: ["openDirectory", "createDirectory"],
+  properties: [
+    "openDirectory",
+    "createDirectory",  // macOS: allow creating a new directory in-dialog
+    "promptToCreate",   // Windows: prompt to create if path doesn't exist
+  ],
   title: input?.title ?? "Select Directory",
   defaultPath: input?.defaultPath ?? undefined,
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/window.ts` around lines 65 - 66, The
open-directory dialog only passes the macOS-specific "createDirectory" option,
so on Windows it won't prompt to create non-existent folders; update the
dialog.showOpenDialog call (the invocation that assigns to result) to include
the cross-platform option "promptToCreate" in the properties array alongside
"openDirectory" and "createDirectory" so Windows users are prompted to create
the directory. Ensure the properties array passed to
dialog.showOpenDialog(window, { properties: [...] }) contains both
"createDirectory" and "promptToCreate".

title: input?.title ?? "Select Directory",
defaultPath: input?.defaultPath ?? undefined,
});
Comment on lines +65 to +69
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and view the relevant lines
fd window.ts apps/desktop/src/lib/trpc/routers/

Repository: superset-sh/superset

Length of output: 107


🌐 Web query:

Electron 40.2.1 dialog.showOpenDialog properties createDirectory promptToCreate macOS Windows

💡 Result:

In Electron v40.2.1, both createDirectory and promptToCreate are values you put in the options.properties: string[] array passed to dialog.showOpenDialog(...) / dialog.showOpenDialogSync(...):

  • createDirectory (macOS only): enables creating new folders directly from the open dialog UI. [1]
  • promptToCreate (Windows only): if the user types a path that doesn’t exist, Windows will prompt about creating it; Electron may then return a non-existent path, but Electron does not create it—your app must create the file/folder itself if you want it created. [1]

Example:

const { dialog } = require('electron');

const result = await dialog.showOpenDialog({
  properties: ['openDirectory', 'createDirectory'] // macOS-only behavior
});

// Windows-only behavior would be: properties: ['openFile', 'promptToCreate']

Source: Electron dialog API docs (v40.2.1 / latest docs page). [1]


🌐 Web query:

Electron dialog.showOpenDialog cross-platform properties documentation

💡 Result:

Electron’s official docs for dialog.showOpenDialog([window,] options) list these cross‑platform options fields and properties flags:

Common options fields (all platforms)

  • title, defaultPath, buttonLabel, filters, properties (electronjs.org)

properties (cross‑platform unless noted)

Platform-specific properties / options

Important cross-platform behavior notes

  • Windows + Linux: you can’t have both a file selector and directory selector at once; if you set ['openFile','openDirectory'], Electron will show a directory selector. (electronjs.org)
  • Linux: defaultPath may be ignored when using portal file choosers unless the portal backend is v4+. (electronjs.org)

Citations:


🏁 Script executed:

# View the actual file around lines 50-76 to verify the code snippet
cat -n apps/desktop/src/lib/trpc/routers/window.ts | sed -n '50,76p'

Repository: superset-sh/superset

Length of output: 963


🏁 Script executed:

# Also check the full context of imports and zod usage
cat -n apps/desktop/src/lib/trpc/routers/window.ts | head -20

Repository: superset-sh/superset

Length of output: 869


🏁 Script executed:

# Check if there's any directory creation logic elsewhere in the file
rg "mkdir|createDirectory|writeFile" apps/desktop/src/lib/trpc/routers/window.ts

Repository: superset-sh/superset

Length of output: 118


"createDirectory" is a macOS-only dialog property; Windows users cannot create directories from this picker

Electron's dialog.showOpenDialog properties accepts "createDirectory" only on macOS and is silently ignored on Windows and Linux. Windows has "promptToCreate", which prompts the user about non-existent paths, but does not automatically create the directory—the app must implement the directory creation logic (e.g., fs.mkdir()) after the user confirms.

To improve Windows/Linux UX, add "promptToCreate" and implement directory creation logic:

Proposed fix for cross-platform directory handling

Add "promptToCreate" to the properties array and implement directory creation when the selected path doesn't exist:

 const result = await dialog.showOpenDialog(window, {
-  properties: ["openDirectory", "createDirectory"],
+  properties: ["openDirectory", "createDirectory", "promptToCreate"],
   title: input?.title ?? "Select Directory",
   defaultPath: input?.defaultPath ?? undefined,
 });

 if (result.canceled || result.filePaths.length === 0) {
   return { canceled: true, path: null };
 }

+const selectedPath = result.filePaths[0];
+// Ensure directory exists (Windows users may need to create it after promptToCreate)
+try {
+  await fs.mkdir(selectedPath, { recursive: true });
+} catch (err) {
+  // Handle error if directory creation fails
+}
-return { canceled: false, path: result.filePaths[0] };
+return { canceled: false, path: selectedPath };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/window.ts` around lines 65 - 69, The
dialog.openDirectory call in the window router uses the macOS-only
"createDirectory" property and so on Windows/Linux users can't create folders;
update the properties array passed to dialog.showOpenDialog (the call assigning
to result) to include "promptToCreate" in addition to "openDirectory" and keep
"createDirectory" for macOS, then after the dialog returns (check
result.canceled and result.filePaths[0]) verify the selected path exists and, if
not, create it programmatically (use fs/promises.mkdir with { recursive: true }
or equivalent) before returning/using the path; ensure you handle and propagate
errors (or return a sensible failure) and reference the same variables used in
this scope (result, window, input) so callers receive the created directory
path.


if (result.canceled || result.filePaths.length === 0) {
return { canceled: true, path: null };
}

return { canceled: false, path: result.filePaths[0] };
}),

selectImageFile: publicProcedure.mutation(async () => {
const window = getWindow();
if (!window) {
Expand Down
20 changes: 3 additions & 17 deletions apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import { homedir } from "node:os";
import { join } from "node:path";
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";
import { workspaceInitManager } from "main/lib/workspace-init-manager";
import { SUPERSET_DIR_NAME, WORKTREES_DIR_NAME } from "shared/constants";
import { z } from "zod";
import { publicProcedure, router } from "../../..";
import { resolveWorkspaceBaseBranch } from "../utils/base-branch";
Expand Down Expand Up @@ -39,6 +36,7 @@ import {
sanitizeBranchName,
worktreeExists,
} from "../utils/git";
import { resolveWorktreePath } from "../utils/resolve-worktree-path";
import { copySupersetConfigToWorktree, loadSetupConfig } from "../utils/setup";
import { initializeWorkspaceWorktree } from "../utils/workspace-init";

Expand Down Expand Up @@ -207,13 +205,7 @@ async function handleNewWorktree({
prInfo,
});

const worktreePath = join(
homedir(),
SUPERSET_DIR_NAME,
WORKTREES_DIR_NAME,
project.name,
localBranchName,
);
const worktreePath = resolveWorktreePath(project, localBranchName);

await createWorktreeFromPr({
mainRepoPath: project.mainRepoPath,
Expand Down Expand Up @@ -437,13 +429,7 @@ export const createCreateProcedures = () => {
}
}

const worktreePath = join(
homedir(),
SUPERSET_DIR_NAME,
WORKTREES_DIR_NAME,
project.name,
branch,
);
const worktreePath = resolveWorktreePath(project, branch);

const targetBranch = resolveWorkspaceBaseBranch({
explicitBaseBranch: input.baseBranch,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { homedir } from "node:os";
import { join } from "node:path";
import { type SelectProject, settings } from "@superset/local-db";
import { localDb } from "main/lib/local-db";
import { SUPERSET_DIR_NAME, WORKTREES_DIR_NAME } from "shared/constants";

/** Resolves base dir: project override > global setting > default (~/.superset/worktrees) */
export function resolveWorktreePath(
project: Pick<SelectProject, "name" | "worktreeBaseDir">,
branch: string,
): string {
if (project.worktreeBaseDir) {
return join(project.worktreeBaseDir, project.name, branch);
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find calls to resolveWorktreePath with context
rg -n "resolveWorktreePath" --type ts -A 5 -B 5

Repository: superset-sh/superset

Length of output: 4671


🏁 Script executed:

#!/bin/bash
# Find the resolve-worktree-path.ts file to see the implementation
fd "resolve-worktree-path.ts" -type f

Repository: superset-sh/superset

Length of output: 234


🏁 Script executed:

#!/bin/bash
# Search for branch sanitization patterns in the codebase
rg "branch.*sanitiz|sanitiz.*branch|replace.*\/" --type ts -i

Repository: superset-sh/superset

Length of output: 214


🏁 Script executed:

#!/bin/bash
# View the context around line 208 in create.ts
sed -n '180,220p' apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts

Repository: superset-sh/superset

Length of output: 1068


🏁 Script executed:

#!/bin/bash
# View the context around line 432 in create.ts
sed -n '410,450p' apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts

Repository: superset-sh/superset

Length of output: 1168


🏁 Script executed:

#!/bin/bash
# Find the sanitizeBranchName function
rg -n "function sanitizeBranchName|const sanitizeBranchName|export.*sanitizeBranchName" --type ts -A 10

Repository: superset-sh/superset

Length of output: 752


🏁 Script executed:

#!/bin/bash
# Also look at the git utils file to find sanitizeBranchName
fd "git.ts" --type f | head -5

Repository: superset-sh/superset

Length of output: 121


🏁 Script executed:

#!/bin/bash
# View the sanitizeSegment function and more context in branch.ts
cat apps/desktop/src/shared/utils/branch.ts

Repository: superset-sh/superset

Length of output: 1288


🏁 Script executed:

#!/bin/bash
# Look at more context around localBranchName on line 208
sed -n '190,210p' apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts

Repository: superset-sh/superset

Length of output: 603


🏁 Script executed:

#!/bin/bash
# Look at more context around branch on line 432 - trace back further
sed -n '370,435p' apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts

Repository: superset-sh/superset

Length of output: 1982


🏁 Script executed:

#!/bin/bash
# Find where handleNewWorktree is called
rg -n "handleNewWorktree" --type ts -B 5 -A 2

Repository: superset-sh/superset

Length of output: 1389


🏁 Script executed:

#!/bin/bash
# Look for HandleNewWorktreeParams definition to understand the interface
rg -n "interface HandleNewWorktreeParams" --type ts -A 10

Repository: superset-sh/superset

Length of output: 1086


🏁 Script executed:

#!/bin/bash
# Get more context around line 949 where handleNewWorktree is called
sed -n '920,960p' apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts

Repository: superset-sh/superset

Length of output: 952


🏁 Script executed:

#!/bin/bash
# Also check what the actual branch value is and trace back where it comes from
sed -n '850,950p' apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts | head -80

Repository: superset-sh/superset

Length of output: 2018


🏁 Script executed:

#!/bin/bash
# Find getPrLocalBranchName function definition
rg -n "function getPrLocalBranchName|const getPrLocalBranchName|export.*getPrLocalBranchName" --type ts -A 5

Repository: superset-sh/superset

Length of output: 692


🏁 Script executed:

#!/bin/bash
# Also look at the full create.ts around line 378 to see what happens with branch variable before line 432
sed -n '350,400p' apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts

Repository: superset-sh/superset

Length of output: 1613


Branch names containing / create unexpected nested directory structures in worktrees.

When resolveWorktreePath receives branch names with slashes (e.g., from getPrLocalBranchName returning "forkowner/feature" or from sanitizeBranchName preserving slashes like "feature/my-branch"), node:path's join treats them as path separators instead of literal characters. This results in nested directories like baseDir/project/forkowner/feature instead of a flat layout. Multiple branches sharing a namespace prefix create intermediate directories that become orphaned when worktrees are removed.

The issue occurs in both code paths:

  • PR imports via handleNewWorktree use getPrLocalBranchName which returns unsanitized branch names containing /
  • Manual branch creation sanitizes via sanitizeBranchName, which intentionally preserves / structure by splitting, sanitizing segments, and rejoining

Branch names should be sanitized to replace / with a safe character (like -) before being passed to resolveWorktreePath.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/resolve-worktree-path.ts`
at line 13, resolveWorktreePath is joining raw branch names that may contain '/'
so node:path interprets them as path separators, causing nested directories;
ensure the branch is sanitized to replace '/' with a safe character (e.g. '-')
before joining. Update resolveWorktreePath to normalize the branch parameter
(sanitize slashes -> '-') prior to calling join(project.worktreeBaseDir,
project.name, branch) so callers like getPrLocalBranchName and
sanitizeBranchName don't need to change, and keep the unique identifier logic
intact (use replace-all on the branch string or equivalent).

}

const row = localDb.select().from(settings).get();
const baseDir =
row?.worktreeBaseDir ??
join(homedir(), SUPERSET_DIR_NAME, WORKTREES_DIR_NAME);

return join(baseDir, project.name, branch);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import { Switch } from "@superset/ui/switch";
import { useEffect, useState } from "react";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { resolveBranchPrefix, sanitizeSegment } from "shared/utils/branch";
import {
useDefaultWorktreePath,
WorktreeLocationPicker,
} from "../../../components/WorktreeLocationPicker";
import { BRANCH_PREFIX_MODE_LABELS } from "../../../utils/branch-prefix";
import {
isItemVisible,
Expand Down Expand Up @@ -48,6 +52,10 @@ export function BehaviorSettings({ visibleItems }: BehaviorSettingsProps) {
SETTING_ITEM_ID.BEHAVIOR_RESOURCE_MONITOR,
visibleItems,
);
const showWorktreeLocation = isItemVisible(
SETTING_ITEM_ID.BEHAVIOR_WORKTREE_LOCATION,
visibleItems,
);

const utils = electronTrpc.useUtils();

Expand Down Expand Up @@ -210,6 +218,30 @@ export function BehaviorSettings({ visibleItems }: BehaviorSettingsProps) {
},
});

const { data: worktreeBaseDir, isLoading: isWorktreeBaseDirLoading } =
electronTrpc.settings.getWorktreeBaseDir.useQuery();
const setWorktreeBaseDir =
electronTrpc.settings.setWorktreeBaseDir.useMutation({
onMutate: async ({ path }) => {
await utils.settings.getWorktreeBaseDir.cancel();
const previous = utils.settings.getWorktreeBaseDir.getData();
utils.settings.getWorktreeBaseDir.setData(undefined, path);
return { previous };
},
onError: (_err, _vars, context) => {
if (context?.previous !== undefined) {
utils.settings.getWorktreeBaseDir.setData(
undefined,
context.previous,
);
}
},
onSettled: () => {
utils.settings.getWorktreeBaseDir.invalidate();
},
});
const defaultWorktreePath = useDefaultWorktreePath();

const previewPrefix =
resolveBranchPrefix({
mode: branchPrefix?.mode ?? "none",
Expand Down Expand Up @@ -375,6 +407,25 @@ export function BehaviorSettings({ visibleItems }: BehaviorSettingsProps) {
</div>
)}

{showWorktreeLocation && (
<div className="space-y-0.5">
<Label className="text-sm font-medium">Worktree location</Label>
<p className="text-xs text-muted-foreground">
Base directory for new worktrees
</p>
<WorktreeLocationPicker
currentPath={worktreeBaseDir}
defaultPathLabel={`Default (${defaultWorktreePath})`}
defaultBrowsePath={worktreeBaseDir}
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.

⚠️ Potential issue | 🟡 Minor

Browse dialog doesn't pre-navigate to the app default when no global path is saved

defaultBrowsePath={worktreeBaseDir} passes null/undefined when the global setting is unset, so the OS dialog opens at the OS default (e.g., ~/), not at ~/.superset/worktrees. The label correctly shows the default, but the dialog starting directory is inconsistent with it.

🔧 Suggested fix
-						defaultBrowsePath={worktreeBaseDir}
+						defaultBrowsePath={worktreeBaseDir ?? defaultWorktreePath}
📝 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
defaultBrowsePath={worktreeBaseDir}
defaultBrowsePath={worktreeBaseDir ?? defaultWorktreePath}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx`
at line 419, The file's BehaviorSettings component passes
defaultBrowsePath={worktreeBaseDir} which can be null/undefined so the OS dialog
opens at the OS default instead of the app default; update BehaviorSettings to
provide a fallback when worktreeBaseDir is unset (e.g., compute a
resolvedBrowsePath = worktreeBaseDir ?? APP_DEFAULT_WORKTREE_DIR or call a
helper like getDefaultWorktreeDir()) and pass
defaultBrowsePath={resolvedBrowsePath} so the browse dialog always pre-navigates
to the app default; reference the defaultBrowsePath prop and the worktreeBaseDir
value in your change.

disabled={
isWorktreeBaseDirLoading || setWorktreeBaseDir.isPending
}
onSelect={(path) => setWorktreeBaseDir.mutate({ path })}
onReset={() => setWorktreeBaseDir.mutate({ path: null })}
/>
</div>
)}

{false && showTelemetry && (
<div className="flex items-center justify-between">
<div className="space-y-0.5">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Button } from "@superset/ui/button";
import { Label } from "@superset/ui/label";
import { electronTrpc } from "renderer/lib/electron-trpc";

interface WorktreeLocationPickerProps {
currentPath: string | null | undefined;
defaultPathLabel: string;
dialogTitle?: string;
defaultBrowsePath?: string | null;
disabled?: boolean;
onSelect: (path: string) => void;
onReset: () => void;
}

export function useDefaultWorktreePath() {
const { data: homeDir } = electronTrpc.window.getHomeDir.useQuery();
return homeDir ? `${homeDir}/.superset/worktrees` : "~/.superset/worktrees";
}

export function WorktreeLocationPicker({
currentPath,
defaultPathLabel,
dialogTitle = "Select worktree location",
defaultBrowsePath,
disabled,
onSelect,
onReset,
}: WorktreeLocationPickerProps) {
const selectDirectory = electronTrpc.window.selectDirectory.useMutation();

const handleBrowse = async () => {
const result = await selectDirectory.mutateAsync({
title: dialogTitle,
defaultPath: defaultBrowsePath ?? undefined,
});
if (!result.canceled && result.path) {
onSelect(result.path);
}
Comment on lines +29 to +38
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.

⚠️ Potential issue | 🟠 Major

Unhandled promise rejection: mutateAsync without try-catch

mutateAsync returns a promise that can throw; if no try-catch (or .catch) is present, a failed IPC call produces an unhandled promise rejection. Unhandled promise rejections occur when the browser sees a failed Promise without a .catch anywhere — using .mutate instead lets react-query catch it automatically.

If the Electron IPC channel errors (e.g., window is destroyed mid-flight), the rejection surfaces as a fatal unhandled error in the renderer process.

Two valid fixes:

Option A — wrap with try-catch (keeps mutateAsync):

🛡️ Proposed fix (try-catch)
-	const handleBrowse = async () => {
-		const result = await selectDirectory.mutateAsync({
-			title: dialogTitle,
-			defaultPath: defaultBrowsePath ?? undefined,
-		});
-		if (!result.canceled && result.path) {
-			onSelect(result.path);
-		}
-	};
+	const handleBrowse = async () => {
+		try {
+			const result = await selectDirectory.mutateAsync({
+				title: dialogTitle,
+				defaultPath: defaultBrowsePath ?? undefined,
+			});
+			if (!result.canceled && result.path) {
+				onSelect(result.path);
+			}
+		} catch {
+			// IPC errors are surfaced via selectDirectory.error
+		}
+	};

Option B — switch to .mutate() with onSuccess callback:

🛡️ Proposed fix (mutate + onSuccess)
-	const selectDirectory = electronTrpc.window.selectDirectory.useMutation();
+	const selectDirectory = electronTrpc.window.selectDirectory.useMutation({
+		onSuccess: (result) => {
+			if (!result.canceled && result.path) {
+				onSelect(result.path);
+			}
+		},
+	});

-	const handleBrowse = async () => {
-		const result = await selectDirectory.mutateAsync({
-			title: dialogTitle,
-			defaultPath: defaultBrowsePath ?? undefined,
-		});
-		if (!result.canceled && result.path) {
-			onSelect(result.path);
-		}
-	};
+	const handleBrowse = () => {
+		selectDirectory.mutate({
+			title: dialogTitle,
+			defaultPath: defaultBrowsePath ?? undefined,
+		});
+	};
📝 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 selectDirectory = electronTrpc.window.selectDirectory.useMutation();
const handleBrowse = async () => {
const result = await selectDirectory.mutateAsync({
title: dialogTitle,
defaultPath: defaultBrowsePath ?? undefined,
});
if (!result.canceled && result.path) {
onSelect(result.path);
}
const selectDirectory = electronTrpc.window.selectDirectory.useMutation();
const handleBrowse = async () => {
try {
const result = await selectDirectory.mutateAsync({
title: dialogTitle,
defaultPath: defaultBrowsePath ?? undefined,
});
if (!result.canceled && result.path) {
onSelect(result.path);
}
} catch {
// IPC errors are surfaced via selectDirectory.error
}
};
Suggested change
const selectDirectory = electronTrpc.window.selectDirectory.useMutation();
const handleBrowse = async () => {
const result = await selectDirectory.mutateAsync({
title: dialogTitle,
defaultPath: defaultBrowsePath ?? undefined,
});
if (!result.canceled && result.path) {
onSelect(result.path);
}
const selectDirectory = electronTrpc.window.selectDirectory.useMutation({
onSuccess: (result) => {
if (!result.canceled && result.path) {
onSelect(result.path);
}
},
});
const handleBrowse = () => {
selectDirectory.mutate({
title: dialogTitle,
defaultPath: defaultBrowsePath ?? undefined,
});
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/components/WorktreeLocationPicker/WorktreeLocationPicker.tsx`
around lines 29 - 38, The call to selectDirectory.mutateAsync in handleBrowse
can reject and cause an unhandled promise—wrap the await in a try/catch (or
replace mutateAsync with mutate + onSuccess) so rejections are handled;
specifically, update handleBrowse (which calls selectDirectory.mutateAsync) to
catch errors and either log/handle them and only call onSelect(result.path) when
there is no error, or switch to selectDirectory.mutate({ input }, { onSuccess:
res => { if (!res.canceled && res.path) onSelect(res.path) }, onError: err => {
/* handle/log */ } }) to let react-query handle errors.

};

return (
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label className="text-sm font-medium">Directory</Label>
<code className="text-xs bg-muted px-1.5 py-0.5 rounded text-foreground block mt-1">
{currentPath ?? defaultPathLabel}
</code>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={handleBrowse}
disabled={disabled || selectDirectory.isPending}
>
Browse...
</Button>
{currentPath && (
<Button
variant="outline"
size="sm"
onClick={onReset}
disabled={disabled}
>
Reset
</Button>
)}
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export {
useDefaultWorktreePath,
WorktreeLocationPicker,
} from "./WorktreeLocationPicker";
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ import { Switch } from "@superset/ui/switch";
import { cn } from "@superset/ui/utils";
import type { ReactNode } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { HiOutlineCog6Tooth, HiOutlinePaintBrush } from "react-icons/hi2";
import {
HiOutlineCog6Tooth,
HiOutlineFolderOpen,
HiOutlinePaintBrush,
} from "react-icons/hi2";
import { LuImagePlus, LuTrash2 } from "react-icons/lu";
import { electronTrpc } from "renderer/lib/electron-trpc";
import {
Expand All @@ -21,6 +25,10 @@ import {
} from "shared/constants/project-colors";
import { resolveBranchPrefix, sanitizeSegment } from "shared/utils/branch";
import { ClickablePath } from "../../../../components/ClickablePath";
import {
useDefaultWorktreePath,
WorktreeLocationPicker,
} from "../../../../components/WorktreeLocationPicker";
import { BRANCH_PREFIX_MODE_LABELS_WITH_DEFAULT } from "../../../../utils/branch-prefix";
import { ScriptsEditor } from "./components/ScriptsEditor";

Expand Down Expand Up @@ -171,6 +179,11 @@ export function ProjectSettings({ projectId }: ProjectSettingsProps) {
});
};

const { data: globalWorktreeBaseDir } =
electronTrpc.settings.getWorktreeBaseDir.useQuery();
const defaultWorktreePath = useDefaultWorktreePath();
const globalPath = globalWorktreeBaseDir ?? defaultWorktreePath;

const getPreviewPrefix = (
mode: BranchPrefixMode | "default",
): string | null => {
Expand Down Expand Up @@ -321,6 +334,32 @@ export function ProjectSettings({ projectId }: ProjectSettingsProps) {
)}
</SettingsSection>

<SettingsSection
icon={<HiOutlineFolderOpen className="h-4 w-4" />}
title="Worktree Location"
description="Override the global worktree directory for this project."
>
<WorktreeLocationPicker
currentPath={project.worktreeBaseDir}
defaultPathLabel={`Using global default: ${globalPath}`}
dialogTitle="Select worktree location for this project"
defaultBrowsePath={project.worktreeBaseDir ?? globalWorktreeBaseDir}
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.

⚠️ Potential issue | 🟡 Minor

Browse dialog doesn't fall back to the app default path when neither project nor global path is set

defaultBrowsePath={project.worktreeBaseDir ?? globalWorktreeBaseDir} resolves to undefined when both are unset, opening the OS dialog at ~/ instead of ~/.superset/worktrees — inconsistent with the defaultPathLabel that already shows the computed globalPath.

defaultWorktreePath is already in scope from line 184.

🔧 Suggested fix
-					defaultBrowsePath={project.worktreeBaseDir ?? globalWorktreeBaseDir}
+					defaultBrowsePath={project.worktreeBaseDir ?? globalWorktreeBaseDir ?? defaultWorktreePath}
📝 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
defaultBrowsePath={project.worktreeBaseDir ?? globalWorktreeBaseDir}
defaultBrowsePath={project.worktreeBaseDir ?? globalWorktreeBaseDir ?? defaultWorktreePath}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/project/`$projectId/components/ProjectSettings/ProjectSettings.tsx
at line 346, The defaultBrowsePath prop currently uses project.worktreeBaseDir
?? globalWorktreeBaseDir which can be undefined; change it to use the
precomputed defaultWorktreePath (which already incorporates project/global
fallbacks) so the OS dialog opens at the intended app default when both are
unset; update the JSX prop defaultBrowsePath={defaultWorktreePath} to reference
defaultWorktreePath instead of project.worktreeBaseDir/globalWorktreeBaseDir.

disabled={updateProject.isPending}
onSelect={(path) =>
updateProject.mutate({
id: projectId,
patch: { worktreeBaseDir: path },
})
}
onReset={() =>
updateProject.mutate({
id: projectId,
patch: { worktreeBaseDir: null },
})
}
/>
</SettingsSection>

<div className="pt-3 border-t">
<ScriptsEditor projectId={project.id} />
</div>
Expand Down
Loading
Loading