feat(FN-4606): complete Step 7 — docs and worktreesDir picker
Ref: Runfusion/Fusion#321 Fusion-Task-Id: FN-4606 Fusion-Task-Lineage: 94b23f37-944e-4bcd-bfa1-67d0a8c0a267
This commit is contained in:
5
.changeset/fn-4606-worktrees-dir.md
Normal file
5
.changeset/fn-4606-worktrees-dir.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
Add `worktreesDir` project setting to place task worktrees outside the project root. Supports absolute paths, paths relative to the project root, `~` expansion, and the `{repo}` token. Defaults to the existing `<projectRoot>/.worktrees` behavior when unset.
|
||||
@@ -228,6 +228,7 @@ Override precedence for direct merges is:
|
||||
| `recycleWorktrees` | `boolean` | `false` | Reuse worktrees from a pool for faster startup. |
|
||||
| `executorAllowSiblingBranchRename` | `boolean` | `false` | Opt back into the legacy executor behavior that silently allocates sibling branches (`fusion/<task-id>-2`, `-2-2`, …) when the canonical task branch is already checked out elsewhere. When disabled (default), branch conflicts fail loudly, leave the task in `todo` with `status: "failed"`, and expose stranded commits for explicit recovery via [`fn task branch-recovery`](./cli-reference.md#fn-task). See [Task Management → Branch conflict recovery](./task-management.md#branch-conflict-recovery). The dashboard Settings modal exposes the same toggle with warning copy because this legacy mode is discouraged. |
|
||||
| `worktreeNaming` | `"random" \| "task-id" \| "task-title"` | `"random"` | Naming mode for new worktree directories. |
|
||||
| `worktreesDir` | `string` | `undefined` | Optional container directory for task worktrees. Supports absolute paths, project-relative paths, `~` expansion, and `{repo}` token substitution (project root basename). Defaults to `<projectRoot>/.worktrees` when unset and applies to newly-created worktrees/pool scans. |
|
||||
| `taskPrefix` | `string` | `"FN"` | Prefix used for newly generated task IDs. |
|
||||
| `includeTaskIdInCommit` | `boolean` | `true` | Include task ID as commit scope in generated commits. |
|
||||
| `commitAuthorEnabled` | `boolean` | `true` | Apply explicit `--author` attribution on Fusion commits. |
|
||||
|
||||
@@ -199,6 +199,7 @@ API endpoints reviewed:
|
||||
| `buildCommand` | Project | `GET/PUT /api/settings` | Project build command |
|
||||
| `recycleWorktrees` | Project | `GET/PUT /api/settings` | Worktree pool toggle |
|
||||
| `worktreeNaming` | Project | `GET/PUT /api/settings` | Worktree naming strategy |
|
||||
| `worktreesDir` | Project | `GET/PUT /api/settings` | Optional worktree container directory (supports absolute/project-relative paths, `~`, `{repo}` token) |
|
||||
| `taskPrefix` | Project | `GET/PUT /api/settings` | Task ID prefix |
|
||||
| `includeTaskIdInCommit` | Project | `GET/PUT /api/settings` | Commit scope formatting |
|
||||
| `defaultProviderOverride` | Project | `GET/PUT /api/settings` | Project-level override for base default provider |
|
||||
|
||||
@@ -457,6 +457,7 @@ export function SettingsModal({
|
||||
const [prefixError, setPrefixError] = useState<string | null>(null);
|
||||
const [researchLimitError, setResearchLimitError] = useState<string | null>(null);
|
||||
const [overlapPathPickerIndex, setOverlapPathPickerIndex] = useState<number | null>(null);
|
||||
const [worktreesDirPickerOpen, setWorktreesDirPickerOpen] = useState(false);
|
||||
|
||||
const {
|
||||
entries: overlapPathPickerEntries,
|
||||
@@ -467,6 +468,15 @@ export function SettingsModal({
|
||||
refresh: refreshOverlapPathPicker,
|
||||
} = useWorkspaceFileBrowser("project", overlapPathPickerIndex !== null, projectId);
|
||||
|
||||
const {
|
||||
entries: worktreesDirPickerEntries,
|
||||
currentPath: worktreesDirPickerCurrentPath,
|
||||
setPath: setWorktreesDirPickerPath,
|
||||
loading: worktreesDirPickerLoading,
|
||||
error: worktreesDirPickerError,
|
||||
refresh: refreshWorktreesDirPicker,
|
||||
} = useWorkspaceFileBrowser("project", worktreesDirPickerOpen, projectId);
|
||||
|
||||
const { nodes } = useNodes();
|
||||
const experimentalFeatures = form.experimentalFeatures ?? {};
|
||||
const remoteAccessEnabled = isExperimentalFeatureEnabled(experimentalFeatures, "remoteAccess");
|
||||
@@ -1607,6 +1617,35 @@ export function SettingsModal({
|
||||
}
|
||||
}, [closeOverlapPathPicker]);
|
||||
|
||||
const openWorktreesDirPicker = useCallback(() => {
|
||||
setWorktreesDirPickerPath(".");
|
||||
setWorktreesDirPickerOpen(true);
|
||||
}, [setWorktreesDirPickerPath]);
|
||||
|
||||
const closeWorktreesDirPicker = useCallback(() => {
|
||||
setWorktreesDirPickerOpen(false);
|
||||
}, []);
|
||||
|
||||
const selectWorktreesDirFromPicker = useCallback((path: string) => {
|
||||
const normalizedPath = path.endsWith("/") ? path : `${path}/`;
|
||||
setForm((f) => ({ ...f, worktreesDir: normalizedPath }));
|
||||
closeWorktreesDirPicker();
|
||||
}, [closeWorktreesDirPicker]);
|
||||
|
||||
const selectCurrentWorktreesDir = useCallback(() => {
|
||||
const normalizedPath = worktreesDirPickerCurrentPath === "."
|
||||
? "./"
|
||||
: (worktreesDirPickerCurrentPath.endsWith("/") ? worktreesDirPickerCurrentPath : `${worktreesDirPickerCurrentPath}/`);
|
||||
setForm((f) => ({ ...f, worktreesDir: normalizedPath }));
|
||||
closeWorktreesDirPicker();
|
||||
}, [worktreesDirPickerCurrentPath, closeWorktreesDirPicker]);
|
||||
|
||||
const handleWorktreesDirPickerOverlayClick = useCallback((event: MouseEvent<HTMLDivElement>) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
closeWorktreesDirPicker();
|
||||
}
|
||||
}, [closeWorktreesDirPicker]);
|
||||
|
||||
const handleOverlapIgnorePathChange = useCallback((index: number, value: string) => {
|
||||
setForm((f) => {
|
||||
const currentPaths = f.overlapIgnorePaths && f.overlapIgnorePaths.length > 0
|
||||
@@ -3737,15 +3776,25 @@ export function SettingsModal({
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="worktreesDir">Worktrees Directory</label>
|
||||
<input
|
||||
id="worktreesDir"
|
||||
type="text"
|
||||
placeholder="~/.fn-worktrees/{repo} or .worktrees"
|
||||
value={form.worktreesDir || ""}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, worktreesDir: e.target.value }))
|
||||
}
|
||||
/>
|
||||
<div className="settings-overlap-ignore-path-controls">
|
||||
<input
|
||||
id="worktreesDir"
|
||||
type="text"
|
||||
placeholder="~/.fn-worktrees/{repo} or .worktrees"
|
||||
value={form.worktreesDir || ""}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, worktreesDir: e.target.value }))
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm"
|
||||
onClick={openWorktreesDirPicker}
|
||||
aria-label="Browse worktrees directory"
|
||||
>
|
||||
Browse
|
||||
</button>
|
||||
</div>
|
||||
<small>Optional. Supports <code>~</code> and <code>{"{repo}"}</code>. Defaults to <code><projectRoot>/.worktrees</code> when unset and only affects newly-created worktrees.</small>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
@@ -6595,6 +6644,56 @@ export function SettingsModal({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{worktreesDirPickerOpen && (
|
||||
<div
|
||||
className="modal-overlay open"
|
||||
onClick={handleWorktreesDirPickerOverlayClick}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Browse worktrees directory"
|
||||
>
|
||||
<div className="modal modal-lg settings-overlap-path-picker-modal" onClick={(event) => event.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<h3>Select worktrees directory</h3>
|
||||
<button className="modal-close" onClick={closeWorktreesDirPicker} aria-label="Close">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body settings-overlap-path-picker-body">
|
||||
<p className="settings-overlap-path-picker-note">
|
||||
Navigate to the folder where Fusion should create task worktrees, then select the current directory.
|
||||
</p>
|
||||
<FileBrowser
|
||||
entries={worktreesDirPickerEntries}
|
||||
currentPath={worktreesDirPickerCurrentPath}
|
||||
onSelectFile={selectWorktreesDirFromPicker}
|
||||
onNavigate={setWorktreesDirPickerPath}
|
||||
loading={worktreesDirPickerLoading}
|
||||
error={worktreesDirPickerError}
|
||||
onRetry={refreshWorktreesDirPicker}
|
||||
workspace="project"
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<div className="modal-actions-left">
|
||||
<small>
|
||||
Current directory: <code>{worktreesDirPickerCurrentPath === "." ? "(project root)" : worktreesDirPickerCurrentPath}</code>
|
||||
</small>
|
||||
</div>
|
||||
<div className="modal-actions-right">
|
||||
<button className="btn btn-sm" onClick={closeWorktreesDirPicker}>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="btn btn-primary btn-sm" onClick={selectCurrentWorktreesDir}>
|
||||
Select current directory
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Import Confirmation Dialog */}
|
||||
{importDialogOpen && importPreview && (
|
||||
|
||||
Reference in New Issue
Block a user