Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Logs
logs
*.log
**-lock.**
npm-debug.log*
yarn-debug.log*
yarn-error.log*
Expand Down
15 changes: 14 additions & 1 deletion src-tauri/src/shared/workspaces_core/worktree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,20 @@ where
return Err("Cannot create a worktree from another worktree.".to_string());
}

let worktree_root = data_dir.join("worktrees").join(&parent_entry.id);
// Determine worktree root: per-workspace setting > global setting > default
let worktree_root = if let Some(custom_folder) = &parent_entry.settings.worktrees_folder {
PathBuf::from(custom_folder)
Comment thread
Thlnking marked this conversation as resolved.
} else {
Comment thread
Thlnking marked this conversation as resolved.
let global_folder = {
let settings = app_settings.lock().await;
settings.global_worktrees_folder.clone()
};
if let Some(global_folder) = global_folder {
PathBuf::from(global_folder).join(&parent_entry.id)
} else {
data_dir.join("worktrees").join(&parent_entry.id)
}
};
std::fs::create_dir_all(&worktree_root)
.map_err(|err| format!("Failed to create worktree directory: {err}"))?;

Expand Down
5 changes: 5 additions & 0 deletions src-tauri/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,8 @@ pub(crate) struct WorkspaceSettings {
pub(crate) launch_scripts: Option<Vec<LaunchScriptEntry>>,
#[serde(default, rename = "worktreeSetupScript")]
pub(crate) worktree_setup_script: Option<String>,
#[serde(default, rename = "worktreesFolder")]
pub(crate) worktrees_folder: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
Expand Down Expand Up @@ -635,6 +637,8 @@ pub(crate) struct AppSettings {
pub(crate) composer_code_block_copy_use_modifier: bool,
#[serde(default = "default_workspace_groups", rename = "workspaceGroups")]
pub(crate) workspace_groups: Vec<WorkspaceGroup>,
#[serde(default, rename = "globalWorktreesFolder")]
pub(crate) global_worktrees_folder: Option<String>,
#[serde(default = "default_open_app_targets", rename = "openAppTargets")]
pub(crate) open_app_targets: Vec<OpenAppTarget>,
#[serde(default = "default_selected_open_app_id", rename = "selectedOpenAppId")]
Expand Down Expand Up @@ -1182,6 +1186,7 @@ impl Default for AppSettings {
composer_list_continuation: default_composer_list_continuation(),
composer_code_block_copy_use_modifier: default_composer_code_block_copy_use_modifier(),
workspace_groups: default_workspace_groups(),
global_worktrees_folder: None,
open_app_targets: default_open_app_targets(),
selected_open_app_id: default_selected_open_app_id(),
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ type SettingsEnvironmentsSectionProps = {
environmentDraftScript: string;
environmentSavedScript: string | null;
environmentDirty: boolean;
worktreesFolderDraft: string;
worktreesFolderSaved: string | null;
worktreesFolderDirty: boolean;
onSetEnvironmentWorkspaceId: Dispatch<SetStateAction<string | null>>;
onSetEnvironmentDraftScript: Dispatch<SetStateAction<string>>;
onSetWorktreesFolderDraft: Dispatch<SetStateAction<string>>;
onSaveEnvironmentSetup: () => Promise<void>;
};

Expand All @@ -24,14 +28,20 @@ export function SettingsEnvironmentsSection({
environmentDraftScript,
environmentSavedScript,
environmentDirty,
worktreesFolderDraft,
worktreesFolderSaved,
worktreesFolderDirty,
onSetEnvironmentWorkspaceId,
onSetEnvironmentDraftScript,
onSetWorktreesFolderDraft,
onSaveEnvironmentSetup,
}: SettingsEnvironmentsSectionProps) {
const hasAnyChanges = environmentDirty || worktreesFolderDirty;

return (
<SettingsSection
title="Environments"
subtitle="Configure per-project setup scripts that run after worktree creation."
subtitle="Configure per-project setup scripts and worktree locations."
>
{mainWorkspaces.length === 0 ? (
<div className="settings-empty">No projects yet.</div>
Expand Down Expand Up @@ -116,12 +126,57 @@ export function SettingsEnvironmentsSection({
onClick={() => {
void onSaveEnvironmentSetup();
}}
disabled={environmentSaving || !environmentDirty}
disabled={environmentSaving || !hasAnyChanges}
Comment thread
Thlnking marked this conversation as resolved.
>
{environmentSaving ? "Saving..." : "Save"}
</button>
</div>
</div>

<div className="settings-field">
<label className="settings-field-label" htmlFor="settings-worktrees-folder">
Worktrees folder
</label>
<div className="settings-help">
Custom location for worktrees. Leave empty to use the default location.
</div>
<div className="settings-field-row">
<input
id="settings-worktrees-folder"
type="text"
className="settings-input"
value={worktreesFolderDraft}
onChange={(event) => onSetWorktreesFolderDraft(event.target.value)}
placeholder="/path/to/worktrees"
disabled={environmentSaving}
/>
<button
type="button"
className="ghost settings-button-compact"
onClick={async () => {
try {
const { open } = await import("@tauri-apps/plugin-dialog");
const selected = await open({
directory: true,
multiple: false,
title: "Select worktrees folder",
});
if (selected && typeof selected === "string") {
onSetWorktreesFolderDraft(selected);
}
} catch (error) {
pushErrorToast({
title: "Failed to open folder picker",
message: error instanceof Error ? error.message : String(error),
});
}
}}
disabled={environmentSaving}
>
Browse
</button>
</div>
</div>
</>
)}
</SettingsSection>
Expand Down
61 changes: 37 additions & 24 deletions src/features/settings/hooks/useSettingsEnvironmentsSection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,37 +19,33 @@ export type SettingsEnvironmentsSectionProps = {
environmentDraftScript: string;
environmentSavedScript: string | null;
environmentDirty: boolean;
worktreesFolderDraft: string;
worktreesFolderSaved: string | null;
worktreesFolderDirty: boolean;
onSetEnvironmentWorkspaceId: Dispatch<SetStateAction<string | null>>;
onSetEnvironmentDraftScript: Dispatch<SetStateAction<string>>;
onSetWorktreesFolderDraft: Dispatch<SetStateAction<string>>;
onSaveEnvironmentSetup: () => Promise<void>;
};

export const useSettingsEnvironmentsSection = ({
mainWorkspaces,
onUpdateWorkspaceSettings,
}: UseSettingsEnvironmentsSectionArgs): SettingsEnvironmentsSectionProps => {
const [environmentWorkspaceId, setEnvironmentWorkspaceId] = useState<string | null>(
null,
);
const [environmentWorkspaceId, setEnvironmentWorkspaceId] = useState<string | null>(null);
const [environmentDraftScript, setEnvironmentDraftScript] = useState("");
const [environmentSavedScript, setEnvironmentSavedScript] = useState<string | null>(
null,
);
const [environmentLoadedWorkspaceId, setEnvironmentLoadedWorkspaceId] = useState<
string | null
>(null);
const [environmentSavedScript, setEnvironmentSavedScript] = useState<string | null>(null);
const [environmentLoadedWorkspaceId, setEnvironmentLoadedWorkspaceId] = useState<string | null>(null);
const [environmentError, setEnvironmentError] = useState<string | null>(null);
const [environmentSaving, setEnvironmentSaving] = useState(false);
const [worktreesFolderDraft, setWorktreesFolderDraft] = useState("");
const [worktreesFolderSaved, setWorktreesFolderSaved] = useState<string | null>(null);

const environmentWorkspace = useMemo(() => {
if (mainWorkspaces.length === 0) {
return null;
}
if (mainWorkspaces.length === 0) return null;
if (environmentWorkspaceId) {
const found = mainWorkspaces.find((workspace) => workspace.id === environmentWorkspaceId);
if (found) {
return found;
}
if (found) return found;
}
return mainWorkspaces[0] ?? null;
}, [environmentWorkspaceId, mainWorkspaces]);
Expand All @@ -58,11 +54,16 @@ export const useSettingsEnvironmentsSection = ({
return normalizeWorktreeSetupScript(environmentWorkspace?.settings.worktreeSetupScript);
}, [environmentWorkspace?.settings.worktreeSetupScript]);

const worktreesFolderFromWorkspace = useMemo(() => {
return environmentWorkspace?.settings.worktreesFolder ?? null;
}, [environmentWorkspace?.settings.worktreesFolder]);

const environmentDraftNormalized = useMemo(() => {
return normalizeWorktreeSetupScript(environmentDraftScript);
}, [environmentDraftScript]);

const environmentDirty = environmentDraftNormalized !== environmentSavedScript;
const worktreesFolderDirty = (worktreesFolderDraft.trim() || null) !== worktreesFolderSaved;

useEffect(() => {
if (!environmentWorkspace) {
Expand All @@ -72,53 +73,61 @@ export const useSettingsEnvironmentsSection = ({
setEnvironmentDraftScript("");
setEnvironmentError(null);
setEnvironmentSaving(false);
setWorktreesFolderDraft("");
setWorktreesFolderSaved(null);
return;
}

if (environmentWorkspaceId !== environmentWorkspace.id) {
setEnvironmentWorkspaceId(environmentWorkspace.id);
}
}, [environmentWorkspace, environmentWorkspaceId]);

useEffect(() => {
if (!environmentWorkspace) {
return;
}

if (!environmentWorkspace) return;
if (environmentLoadedWorkspaceId !== environmentWorkspace.id) {
setEnvironmentLoadedWorkspaceId(environmentWorkspace.id);
setEnvironmentSavedScript(environmentSavedScriptFromWorkspace);
setEnvironmentDraftScript(environmentSavedScriptFromWorkspace ?? "");
setWorktreesFolderSaved(worktreesFolderFromWorkspace);
setWorktreesFolderDraft(worktreesFolderFromWorkspace ?? "");
setEnvironmentError(null);
return;
}

if (!environmentDirty && environmentSavedScript !== environmentSavedScriptFromWorkspace) {
setEnvironmentSavedScript(environmentSavedScriptFromWorkspace);
setEnvironmentDraftScript(environmentSavedScriptFromWorkspace ?? "");
setEnvironmentError(null);
}
if (!worktreesFolderDirty && worktreesFolderSaved !== worktreesFolderFromWorkspace) {
setWorktreesFolderSaved(worktreesFolderFromWorkspace);
setWorktreesFolderDraft(worktreesFolderFromWorkspace ?? "");
}
}, [
environmentDirty,
environmentLoadedWorkspaceId,
environmentSavedScript,
environmentSavedScriptFromWorkspace,
environmentWorkspace,
worktreesFolderDirty,
worktreesFolderFromWorkspace,
worktreesFolderSaved,
]);

const handleSaveEnvironmentSetup = async () => {
if (!environmentWorkspace || environmentSaving) {
return;
}
if (!environmentWorkspace || environmentSaving) return;
const nextScript = environmentDraftNormalized;
const nextFolder = worktreesFolderDraft.trim() || null;
setEnvironmentSaving(true);
setEnvironmentError(null);
try {
await onUpdateWorkspaceSettings(environmentWorkspace.id, {
worktreeSetupScript: nextScript,
worktreesFolder: nextFolder,
});
setEnvironmentSavedScript(nextScript);
setEnvironmentDraftScript(nextScript ?? "");
setWorktreesFolderSaved(nextFolder);
setWorktreesFolderDraft(nextFolder ?? "");
} catch (error) {
setEnvironmentError(error instanceof Error ? error.message : String(error));
} finally {
Expand All @@ -134,8 +143,12 @@ export const useSettingsEnvironmentsSection = ({
environmentDraftScript,
environmentSavedScript,
environmentDirty,
worktreesFolderDraft,
worktreesFolderSaved,
worktreesFolderDirty,
onSetEnvironmentWorkspaceId: setEnvironmentWorkspaceId,
onSetEnvironmentDraftScript: setEnvironmentDraftScript,
onSetWorktreesFolderDraft: setWorktreesFolderDraft,
onSaveEnvironmentSetup: handleSaveEnvironmentSetup,
};
};
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type WorkspaceSettings = {
launchScript?: string | null;
launchScripts?: LaunchScriptEntry[] | null;
worktreeSetupScript?: string | null;
worktreesFolder?: string | null;
};

export type LaunchScriptIconId =
Expand Down Expand Up @@ -303,6 +304,7 @@ export type AppSettings = {
composerListContinuation: boolean;
composerCodeBlockCopyUseModifier: boolean;
workspaceGroups: WorkspaceGroup[];
globalWorktreesFolder: string | null;
openAppTargets: OpenAppTarget[];
selectedOpenAppId: string;
};
Expand Down