diff --git a/packages/cli/src/commands/__tests__/settings.test.ts b/packages/cli/src/commands/__tests__/settings.test.ts index db4268b0c..086496bf4 100644 --- a/packages/cli/src/commands/__tests__/settings.test.ts +++ b/packages/cli/src/commands/__tests__/settings.test.ts @@ -62,6 +62,7 @@ describe("settings commands", () => { expect(parseValue("ntfyEnabled", "yes")).toBe(true); expect(parseValue("maxConcurrent", "4")).toBe(4); expect(parseValue("worktreeNaming", "task-id")).toBe("task-id"); + expect(parseValue("worktreesDir", "~/.fn-worktrees/{repo}")).toBe("~/.fn-worktrees/{repo}"); expect(parseValue("defaultNodeId", "node-abc-123")).toBe("node-abc-123"); expect(parseValue("unavailableNodePolicy", "block")).toBe("block"); expect(parseValue("unavailableNodePolicy", "fallback-local")).toBe("fallback-local"); @@ -164,8 +165,23 @@ describe("settings commands", () => { expect(updateSettings).toHaveBeenCalledWith({ runStepsInNewSessions: true }); }); - it("runSettingsSet with project updates maxParallelSteps", async () => { - const updateSettings = vi.fn().mockResolvedValue(makeSettings({ maxParallelSteps: 3 })); + it("runSettingsSet with project updates worktreesDir", async () => { + const updateSettings = vi.fn().mockResolvedValue(makeSettings({ worktreesDir: "~/.fn-worktrees/{repo}" })); + const getSettings = vi.fn().mockResolvedValue(makeSettings({ worktreesDir: "~/.fn-worktrees/{repo}" })); + vi.mocked(resolveProject).mockResolvedValue({ + projectId: "proj-1", + projectName: "demo-project", + projectPath: "/projects/demo", + isRegistered: true, + store: { updateSettings, getSettings } as any, + }); + + await runSettingsSet("worktreesDir", "~/.fn-worktrees/{repo}", "demo-project"); + + expect(updateSettings).toHaveBeenCalledWith({ worktreesDir: "~/.fn-worktrees/{repo}" }); + }); + + it("runSettingsSet with project updates maxParallelSteps", async () => { const updateSettings = vi.fn().mockResolvedValue(makeSettings({ maxParallelSteps: 3 })); const getSettings = vi.fn().mockResolvedValue(makeSettings({ maxParallelSteps: 3 })); vi.mocked(resolveProject).mockResolvedValue({ projectId: "proj-1", diff --git a/packages/cli/src/commands/settings.ts b/packages/cli/src/commands/settings.ts index eab2b96d8..178d7d24f 100644 --- a/packages/cli/src/commands/settings.ts +++ b/packages/cli/src/commands/settings.ts @@ -6,6 +6,7 @@ export const VALID_SETTINGS = [ "maxConcurrent", "maxWorktrees", "worktreeNaming", + "worktreesDir", "taskPrefix", "ntfyTopic", "autoResolveConflicts", @@ -24,6 +25,7 @@ const PROJECT_ONLY_SETTINGS = [ "maxConcurrent", "maxWorktrees", "worktreeNaming", + "worktreesDir", "taskPrefix", "autoResolveConflicts", "smartConflictResolution", @@ -52,7 +54,7 @@ const ENUM_SETTINGS: Record = { unavailableNodePolicy: ["block", "fallback-local"], }; -const STRING_SETTINGS: readonly string[] = ["taskPrefix", "ntfyTopic", "defaultModel", "defaultNodeId"]; +const STRING_SETTINGS: readonly string[] = ["taskPrefix", "ntfyTopic", "defaultModel", "defaultNodeId", "worktreesDir"]; // Validation ranges for numeric settings const NUMBER_RANGES: Record = { @@ -202,7 +204,7 @@ export async function runSettingsShow(projectName?: string): Promise { }, { title: "Worktrees", - keys: ["worktreeNaming", "recycleWorktrees"], + keys: ["worktreeNaming", "worktreesDir", "recycleWorktrees"], }, { title: "Tasks", diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx index 78b34c614..d68b82f57 100644 --- a/packages/dashboard/app/components/SettingsModal.tsx +++ b/packages/dashboard/app/components/SettingsModal.tsx @@ -410,6 +410,7 @@ export function SettingsModal({ recycleWorktrees: false, executorAllowSiblingBranchRename: false, worktreeNaming: "random", + worktreesDir: "", includeTaskIdInCommit: true, worktreeInitCommand: "", ntfyEnabled: false, @@ -1670,6 +1671,7 @@ export function SettingsModal({ const payload = { ...form, worktreeInitCommand: form.worktreeInitCommand?.trim() || undefined, + worktreesDir: form.worktreesDir?.trim() || undefined, taskPrefix: form.taskPrefix?.trim() || undefined, githubTrackingDefaultRepo: form.githubTrackingDefaultRepo?.trim() || undefined, githubAuthToken: form.githubAuthToken?.trim() || undefined, @@ -3733,6 +3735,19 @@ export function SettingsModal({ : "How to name fresh worktree directories. Only applies when recycling is off."} +
+ + + setForm((f) => ({ ...f, worktreesDir: e.target.value })) + } + /> + Optional. Supports ~ and {"{repo}"}. Defaults to <projectRoot>/.worktrees when unset and only affects newly-created worktrees. +