Skip to content

feat(desktop): add configurable worktree location#1626

Merged
Kitenite merged 2 commits into
mainfrom
kitenite/configurable-worktree-location
Feb 20, 2026
Merged

feat(desktop): add configurable worktree location#1626
Kitenite merged 2 commits into
mainfrom
kitenite/configurable-worktree-location

Conversation

@Kitenite
Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite commented Feb 20, 2026

Summary

  • Allow users to configure the base directory for new worktrees, both globally (Behavior settings) and per-project (Project settings)
  • Per-project setting overrides the global default; resetting falls back to the global path
  • Adds worktree_base_dir column to both projects and settings tables with migration 0030

Changes

Schema & Migration

  • Add worktree_base_dir text column to projects and settings tables in packages/local-db
  • Generate migration 0030_shallow_the_leader.sql

Backend (tRPC)

  • Add getWorktreeBaseDir / setWorktreeBaseDir procedures to the settings router
  • Add worktreeBaseDir to the project update patch
  • Add resolveWorktreePath() utility that resolves the effective worktree path (project override > global setting > default)
  • Use resolved path in workspace creation and selectDirectory window procedure

Frontend

  • Add WorktreeLocationPicker reusable component with browse/reset support
  • Add global worktree location setting in Behavior settings
  • Add per-project worktree location override in Project settings
  • Register new settings in search index for discoverability

Test Plan

  • Open Behavior settings, verify "Worktree location" picker appears and can browse/select a directory
  • Reset to default and confirm it falls back to the default path
  • Open a Project settings page, verify "Worktree Location" override section appears
  • Set a per-project override, create a worktree workspace, confirm it lands in the overridden directory
  • Clear the per-project override, confirm it falls back to the global setting
  • Search "worktree" in settings and confirm both global and project entries appear

Summary by CodeRabbit

  • New Features

    • Global and per-project worktree location settings with get/set operations
    • Directory picker UI for choosing custom worktree locations
    • Worktree location controls added to Behavior and Project settings pages
    • Settings search updated to include worktree location entries
  • Refactor

    • Centralized worktree path resolution for consistent path derivation
  • Chores

    • Local database schema updated to store worktree base directory

Allow users to customize where worktrees are created on disk, both
globally (in Behavior settings) and per-project (in Project settings).
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

This PR adds configurable worktree base directories (global and per-project), database columns to persist them, TRPC endpoints and a directory picker UI, a WorktreeLocationPicker component, and a centralized resolveWorktreePath utility used when creating workspaces.

Changes

Cohort / File(s) Summary
Database Schema
packages/local-db/drizzle/0030_shallow_the_leader.sql, packages/local-db/drizzle/meta/0030_snapshot.json, packages/local-db/drizzle/meta/_journal.json, packages/local-db/src/schema/schema.ts
Adds worktree_base_dir (worktreeBaseDir) text column to projects and settings; adds migration 0030 and updated DB snapshot/journal.
Settings Router
apps/desktop/src/lib/trpc/routers/settings/index.ts
Adds getWorktreeBaseDir query and `setWorktreeBaseDir(path: string
Projects Router
apps/desktop/src/lib/trpc/routers/projects/projects.ts
Adds worktreeBaseDir: z.string().nullable().optional() to patch/update input schemas for per-project setting.
Window Router
apps/desktop/src/lib/trpc/routers/window.ts
Adds selectDirectory mutation to open native directory picker (options: title, defaultPath) and return `{ canceled: boolean; path: string
Worktree Path Resolution
apps/desktop/src/lib/trpc/routers/workspaces/procedures/create.ts, apps/desktop/src/lib/trpc/routers/workspaces/utils/resolve-worktree-path.ts
Introduces resolveWorktreePath() utility and replaces inline homedir/constant-based path assembly with resolver that prefers project, then global setting, then default.
Worktree Location Picker Component
apps/desktop/src/renderer/routes/_authenticated/settings/components/WorktreeLocationPicker/WorktreeLocationPicker.tsx, .../index.ts
Adds WorktreeLocationPicker component and useDefaultWorktreePath() hook; integrates selectDirectory mutation and exposes browse/reset UI.
Settings UI Integration
apps/desktop/src/renderer/routes/_authenticated/settings/behavior/.../BehaviorSettings.tsx, apps/desktop/src/renderer/routes/_authenticated/settings/project/$projectId/.../ProjectSettings.tsx
Integrates picker into global behavior and per-project settings, wiring TRPC queries/mutations, optimistic updates, and disabled states.
Settings Search
apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts
Adds BEHAVIOR_WORKTREE_LOCATION and PROJECT_WORKTREE_LOCATION setting IDs and corresponding searchable items.
Config
biome.jsonc
Schema $schema reference changed (version update).

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as WorktreeLocationPicker
    participant TRPC as TRPC Routers<br/>(window/settings/projects)
    participant DB as LocalDB<br/>(settings/projects)
    participant Resolver as resolveWorktreePath

    rect rgba(100,150,255,0.5)
    User->>UI: Click "Browse..."
    UI->>TRPC: selectDirectory(title, defaultPath)
    TRPC->>User: Open native directory picker
    User->>TRPC: Select directory
    TRPC->>UI: Return { canceled: false, path }
    UI->>TRPC: setWorktreeBaseDir(path) or updateProject({ worktreeBaseDir: path })
    TRPC->>DB: Upsert worktree_base_dir
    DB->>TRPC: Confirm write
    TRPC->>UI: Return success
    UI->>User: Show updated path
    end

    rect rgba(150,200,100,0.5)
    Resolver->>DB: Read project.worktree_base_dir
    alt project has value
        DB->>Resolver: Return project.worktree_base_dir
    else
        Resolver->>DB: Read settings.worktree_base_dir
        alt settings has value
            DB->>Resolver: Return settings.worktree_base_dir
        else
            Resolver->>Resolver: Use default (~/.superset/worktrees)
        end
    end
    Resolver->>Resolver: Return baseDir/projectName/branch
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped where paths were set in code,
A burrow for each branch to go,
Now projects rest where I bestow,
With pickers, settings, DB in tow,
Hooray — my worktrees freely grow! 🌱

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(desktop): add configurable worktree location' clearly and concisely describes the main feature being added—allowing users to configure worktree locations globally and per-project.
Description check ✅ Passed The PR description comprehensively covers the objectives, changes across schema/migration/backend/frontend layers, and includes a detailed test plan matching the repository template structure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch kitenite/configurable-worktree-location

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 20, 2026

🚀 Preview Deployment

🔗 Preview Links

Service Status Link
Neon Database (Neon) View Branch
Fly.io Electric (Fly.io) View App
Vercel API (Vercel) Open Preview
Vercel Web (Vercel) Open Preview
Vercel Marketing (Vercel) Open Preview
Vercel Admin (Vercel) Open Preview
Vercel Docs (Vercel) Open Preview

Preview updates automatically with new commits

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts (1)

450-465: Consider adding "workspace" to both new items' keyword lists for better search discoverability.

Other worktree-adjacent behavior entries (BEHAVIOR_DELETE_LOCAL_BRANCH, BEHAVIOR_BRANCH_PREFIX) include "workspace" as a keyword since that's the user-facing term for a worktree in this app. A user typing "workspace location" or "workspace directory" won't surface either of these new settings.

✨ Suggested keyword additions
  keywords: [
    "worktree",
    "location",
    "directory",
    "path",
    "folder",
    "storage",
    "base",
    "default",
+   "workspace",
  ],
  keywords: [
    "project",
    "worktree",
    "location",
    "directory",
    "path",
    "folder",
    "storage",
    "override",
+   "workspace",
  ],

Also applies to: 726-741

🤖 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/utils/settings-search/settings-search.ts`
around lines 450 - 465, The settings entries for the new worktree-related items
need "workspace" added to their keywords for better search discoverability;
update the keywords array for SETTING_ITEM_ID.BEHAVIOR_WORKTREE_LOCATION and the
other newly added worktree-related setting (the second new item in the same
changeset) to include the string "workspace" so searches like "workspace
location" or "workspace directory" will match.
apps/desktop/src/lib/trpc/routers/settings/index.ts (1)

629-642: Consider validating path accessibility before persisting.

setWorktreeBaseDir stores an arbitrary string without checking that the path exists or is a writable directory. The error would surface only at workspace creation time (via the failing git worktree add), which can be confusing for users.

Since the primary flow goes through the OS directory picker (selectDirectory), this is largely self-mitigating, but defensive validation would improve the experience if the API is called by other means.

🛡️ Optional: add fs-level validation
+import { statSync } from "node:fs";
+
 setWorktreeBaseDir: publicProcedure
     .input(z.object({ path: z.string().nullable() }))
     .mutation(({ input }) => {
+        if (input.path !== null) {
+            try {
+                const stat = statSync(input.path);
+                if (!stat.isDirectory()) {
+                    throw new TRPCError({ code: "BAD_REQUEST", message: "Path must be a directory" });
+                }
+            } catch (err) {
+                if (err instanceof TRPCError) throw err;
+                throw new TRPCError({ code: "BAD_REQUEST", message: `Directory does not exist: ${input.path}` });
+            }
+        }
         localDb
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/trpc/routers/settings/index.ts` around lines 629 - 642,
The setWorktreeBaseDir mutation currently persists whatever string is passed
without checking that the path exists and is a writable directory; change the
mutation to be async, validate input.path using Node fs (e.g., fs.promises.stat
and fs.access with fs.constants.W_OK) to ensure the path exists and is a
directory and is writable, and if validation fails throw a TRPC error (e.g.,
BAD_REQUEST) instead of persisting; only after successful validation perform the
localDb.insert(...).onConflictDoUpdate(...).run() call to save the
worktreeBaseDir in the settings table.
apps/desktop/src/lib/trpc/routers/workspaces/utils/resolve-worktree-path.ts (1)

34-34: Use project.id to ensure unique worktree paths for same-named projects.

project.name is not unique in the schema — multiple projects can have the same name. If they share the same baseDir and branch name, resolveWorktreePath returns identical paths, causing the second git worktree add to fail.

Include project.id to guarantee uniqueness:

♻️ Proposed fix: use a unique path component
-	return join(baseDir, project.name, branch);
+	// Include project id prefix to avoid collisions between same-named projects
+	return join(baseDir, `${project.name}-${project.id.slice(0, 8)}`, branch);

Update the Pick constraint to include id:

 export function resolveWorktreePath(
-	project: Pick<SelectProject, "name" | "worktreeBaseDir">,
+	project: Pick<SelectProject, "id" | "name" | "worktreeBaseDir">,
 	branch: string,
 ): string {
🤖 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 34, resolveWorktreePath currently builds worktree paths using
project.name which is not unique; change resolveWorktreePath to include
project.id (e.g., join(baseDir, project.id, project.name, branch) or
join(baseDir, `${project.id}-${project.name}`, branch)) so paths are unique per
project id, and update the Pick type/param that supplies project to include id
(in addition to name) so callers still type-check.
apps/desktop/src/lib/trpc/routers/window.ts (1)

75-75: Redundant ?? undefinedinput?.defaultPath is already string | undefined

z.string().optional() produces string | undefined (never null), so input?.defaultPath ?? undefined is identical to input?.defaultPath. The null-coalescing fallback is a no-op here.

🔧 Suggested cleanup
-				defaultPath: input?.defaultPath ?? undefined,
+				defaultPath: input?.defaultPath,
🤖 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` at line 75, The property
assignment uses a redundant null-coalescing fallback: replace the expression
"input?.defaultPath ?? undefined" with the simpler "input?.defaultPath" because
the schema (z.string().optional()) already yields string | undefined; update the
code where the object property defaultPath is set (look for the assignment using
input?.defaultPath) to remove the "?? undefined" no-op.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/settings/behavior/components/BehaviorSettings/BehaviorSettings.tsx`:
- 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.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/components/WorktreeLocationPicker/WorktreeLocationPicker.tsx`:
- Around line 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.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/project/`$projectId/components/ProjectSettings/ProjectSettings.tsx:
- 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.

---

Nitpick comments:
In `@apps/desktop/src/lib/trpc/routers/settings/index.ts`:
- Around line 629-642: The setWorktreeBaseDir mutation currently persists
whatever string is passed without checking that the path exists and is a
writable directory; change the mutation to be async, validate input.path using
Node fs (e.g., fs.promises.stat and fs.access with fs.constants.W_OK) to ensure
the path exists and is a directory and is writable, and if validation fails
throw a TRPC error (e.g., BAD_REQUEST) instead of persisting; only after
successful validation perform the
localDb.insert(...).onConflictDoUpdate(...).run() call to save the
worktreeBaseDir in the settings table.

In `@apps/desktop/src/lib/trpc/routers/window.ts`:
- Line 75: The property assignment uses a redundant null-coalescing fallback:
replace the expression "input?.defaultPath ?? undefined" with the simpler
"input?.defaultPath" because the schema (z.string().optional()) already yields
string | undefined; update the code where the object property defaultPath is set
(look for the assignment using input?.defaultPath) to remove the "?? undefined"
no-op.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/resolve-worktree-path.ts`:
- Line 34: resolveWorktreePath currently builds worktree paths using
project.name which is not unique; change resolveWorktreePath to include
project.id (e.g., join(baseDir, project.id, project.name, branch) or
join(baseDir, `${project.id}-${project.name}`, branch)) so paths are unique per
project id, and update the Pick type/param that supplies project to include id
(in addition to name) so callers still type-check.

In
`@apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts`:
- Around line 450-465: The settings entries for the new worktree-related items
need "workspace" added to their keywords for better search discoverability;
update the keywords array for SETTING_ITEM_ID.BEHAVIOR_WORKTREE_LOCATION and the
other newly added worktree-related setting (the second new item in the same
changeset) to include the string "workspace" so searches like "workspace
location" or "workspace directory" will match.

<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.

Comment on lines +29 to +38
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);
}
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.

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.

@Kitenite Kitenite merged commit b4f4965 into main Feb 20, 2026
12 of 13 checks passed
@Kitenite Kitenite deleted the kitenite/configurable-worktree-location branch February 20, 2026 06:53
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
apps/desktop/src/lib/trpc/routers/workspaces/utils/resolve-worktree-path.ts (1)

16-16: Narrow the select to only the needed column.

localDb.select().from(settings).get() fetches every column from the settings row when only worktreeBaseDir is required. Drizzle supports column-level selection.

♻️ Proposed refactor
-	const row = localDb.select().from(settings).get();
-	const baseDir =
-		row?.worktreeBaseDir ??
-		join(homedir(), SUPERSET_DIR_NAME, WORKTREES_DIR_NAME);
+	const row = localDb
+		.select({ worktreeBaseDir: settings.worktreeBaseDir })
+		.from(settings)
+		.get();
+	const baseDir =
+		row?.worktreeBaseDir ??
+		join(homedir(), SUPERSET_DIR_NAME, WORKTREES_DIR_NAME);
🤖 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 16, The code currently queries the entire settings row via
localDb.select().from(settings).get(); narrow the query to only the needed
column by selecting settings.worktreeBaseDir explicitly (e.g. localDb.select({
worktreeBaseDir: settings.worktreeBaseDir }).from(settings).get()), update uses
of the resulting variable (const row) to access row.worktreeBaseDir, and keep
existing null/undefined checks in resolve-worktree-path.ts to handle a missing
settings row.
apps/desktop/src/lib/trpc/routers/window.ts (2)

68-68: Redundant ?? undefinedinput?.defaultPath already resolves to undefined when absent

input?.defaultPath evaluates to undefined when input is undefined or defaultPath is not set. The ?? undefined fallback is a no-op (compare with line 67 where ?? "Select Directory" provides a meaningful default).

♻️ Proposed cleanup
-  defaultPath: input?.defaultPath ?? undefined,
+  defaultPath: input?.defaultPath,
🤖 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` at line 68, Remove the redundant
"?? undefined" fallback for defaultPath: the expression input?.defaultPath
already yields undefined when absent, so update the code where defaultPath is
assigned (the property defaultPath that currently uses input?.defaultPath ??
undefined) to simply use input?.defaultPath; this keeps behavior identical while
cleaning up the unnecessary nullish coalescing.

68-68: Redundant ?? undefinedinput?.defaultPath already resolves to undefined when absent

input?.defaultPath evaluates to undefined when input is undefined or the property is absent. The ?? undefined fallback is a no-op.

♻️ Proposed cleanup
-  defaultPath: input?.defaultPath ?? undefined,
+  defaultPath: input?.defaultPath,
🤖 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` at line 68, The expression
input?.defaultPath ?? undefined is redundant; change the assignment to use
input?.defaultPath directly (remove the "?? undefined" fallback) wherever
defaultPath is set (the line using defaultPath: input?.defaultPath ?? undefined)
so the value will already be undefined when missing; update any similar
occurrences of "?? undefined" on the same property to keep behavior consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/lib/trpc/routers/window.ts`:
- Around line 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".
- Around line 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.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/resolve-worktree-path.ts`:
- 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).

---

Nitpick comments:
In `@apps/desktop/src/lib/trpc/routers/window.ts`:
- Line 68: Remove the redundant "?? undefined" fallback for defaultPath: the
expression input?.defaultPath already yields undefined when absent, so update
the code where defaultPath is assigned (the property defaultPath that currently
uses input?.defaultPath ?? undefined) to simply use input?.defaultPath; this
keeps behavior identical while cleaning up the unnecessary nullish coalescing.
- Line 68: The expression input?.defaultPath ?? undefined is redundant; change
the assignment to use input?.defaultPath directly (remove the "?? undefined"
fallback) wherever defaultPath is set (the line using defaultPath:
input?.defaultPath ?? undefined) so the value will already be undefined when
missing; update any similar occurrences of "?? undefined" on the same property
to keep behavior consistent.

In `@apps/desktop/src/lib/trpc/routers/workspaces/utils/resolve-worktree-path.ts`:
- Line 16: The code currently queries the entire settings row via
localDb.select().from(settings).get(); narrow the query to only the needed
column by selecting settings.worktreeBaseDir explicitly (e.g. localDb.select({
worktreeBaseDir: settings.worktreeBaseDir }).from(settings).get()), update uses
of the resulting variable (const row) to access row.worktreeBaseDir, and keep
existing null/undefined checks in resolve-worktree-path.ts to handle a missing
settings row.

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

Comment on lines +65 to +69
const result = await dialog.showOpenDialog(window, {
properties: ["openDirectory", "createDirectory"],
title: input?.title ?? "Select Directory",
defaultPath: input?.defaultPath ?? undefined,
});
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.

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant