-
Notifications
You must be signed in to change notification settings - Fork 962
feat(desktop): add configurable worktree location #1626
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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(() => { | ||
|
|
@@ -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"], | ||
| title: input?.title ?? "Select Directory", | ||
| defaultPath: input?.defaultPath ?? undefined, | ||
| }); | ||
|
Comment on lines
+65
to
+69
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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:
💡 Result: In Electron v40.2.1, both
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 🌐 Web query:
💡 Result: Electron’s official docs for Common
Platform-specific
Important cross-platform behavior notes
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 -20Repository: 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.tsRepository: superset-sh/superset Length of output: 118
Electron's To improve Windows/Linux UX, add Proposed fix for cross-platform directory handlingAdd 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 |
||
|
|
||
| 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) { | ||
|
|
||
| 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Find calls to resolveWorktreePath with context
rg -n "resolveWorktreePath" --type ts -A 5 -B 5Repository: 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 fRepository: 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 -iRepository: 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.tsRepository: 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.tsRepository: 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 10Repository: 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 -5Repository: 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.tsRepository: 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.tsRepository: 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.tsRepository: superset-sh/superset Length of output: 1982 🏁 Script executed: #!/bin/bash
# Find where handleNewWorktree is called
rg -n "handleNewWorktree" --type ts -B 5 -A 2Repository: 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 10Repository: 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.tsRepository: 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 -80Repository: 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 5Repository: 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.tsRepository: superset-sh/superset Length of output: 1613 Branch names containing When The issue occurs in both code paths:
Branch names should be sanitized to replace 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| 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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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, | ||||||
|
|
@@ -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(); | ||||||
|
|
||||||
|
|
@@ -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", | ||||||
|
|
@@ -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} | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Browse dialog doesn't pre-navigate to the app default when no global path is saved
🔧 Suggested fix- defaultBrowsePath={worktreeBaseDir}
+ defaultBrowsePath={worktreeBaseDir ?? defaultWorktreePath}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| 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"> | ||||||
|
|
||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unhandled promise rejection:
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 🛡️ 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 🛡️ 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
Suggested change
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 { | ||||||
|
|
@@ -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"; | ||||||
|
|
||||||
|
|
@@ -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 => { | ||||||
|
|
@@ -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} | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Browse dialog doesn't fall back to the app default path when neither project nor global path is set
🔧 Suggested fix- defaultBrowsePath={project.worktreeBaseDir ?? globalWorktreeBaseDir}
+ defaultBrowsePath={project.worktreeBaseDir ?? globalWorktreeBaseDir ?? defaultWorktreePath}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| 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> | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add
"promptToCreate"for Windows —"createDirectory"is macOS-onlyPer 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