From eacb823a37b71c669314320bdcdf71a37791b160 Mon Sep 17 00:00:00 2001 From: Fusion Date: Fri, 15 May 2026 08:43:25 -0700 Subject: [PATCH] =?UTF-8?q?feat(FN-4606):=20complete=20Step=206=20?= =?UTF-8?q?=E2=80=94=20CLI=20and=20dashboard=20settings=20surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref: Runfusion/Fusion#321 Fusion-Task-Id: FN-4606 Fusion-Task-Lineage: 94b23f37-944e-4bcd-bfa1-67d0a8c0a267 --- .../src/commands/__tests__/settings.test.ts | 20 +++++++++++++++++-- packages/cli/src/commands/settings.ts | 6 ++++-- .../app/components/SettingsModal.tsx | 15 ++++++++++++++ .../__tests__/SettingsModal.test.tsx | 17 +++++++++++++++- 4 files changed, 53 insertions(+), 5 deletions(-) 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. +