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. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `taskPrefix` | `string` | `"FN"` | Prefix used for newly generated task IDs. |
|
||||||
| `includeTaskIdInCommit` | `boolean` | `true` | Include task ID as commit scope in generated commits. |
|
| `includeTaskIdInCommit` | `boolean` | `true` | Include task ID as commit scope in generated commits. |
|
||||||
| `commitAuthorEnabled` | `boolean` | `true` | Apply explicit `--author` attribution on Fusion 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 |
|
| `buildCommand` | Project | `GET/PUT /api/settings` | Project build command |
|
||||||
| `recycleWorktrees` | Project | `GET/PUT /api/settings` | Worktree pool toggle |
|
| `recycleWorktrees` | Project | `GET/PUT /api/settings` | Worktree pool toggle |
|
||||||
| `worktreeNaming` | Project | `GET/PUT /api/settings` | Worktree naming strategy |
|
| `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 |
|
| `taskPrefix` | Project | `GET/PUT /api/settings` | Task ID prefix |
|
||||||
| `includeTaskIdInCommit` | Project | `GET/PUT /api/settings` | Commit scope formatting |
|
| `includeTaskIdInCommit` | Project | `GET/PUT /api/settings` | Commit scope formatting |
|
||||||
| `defaultProviderOverride` | Project | `GET/PUT /api/settings` | Project-level override for base default provider |
|
| `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 [prefixError, setPrefixError] = useState<string | null>(null);
|
||||||
const [researchLimitError, setResearchLimitError] = useState<string | null>(null);
|
const [researchLimitError, setResearchLimitError] = useState<string | null>(null);
|
||||||
const [overlapPathPickerIndex, setOverlapPathPickerIndex] = useState<number | null>(null);
|
const [overlapPathPickerIndex, setOverlapPathPickerIndex] = useState<number | null>(null);
|
||||||
|
const [worktreesDirPickerOpen, setWorktreesDirPickerOpen] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
entries: overlapPathPickerEntries,
|
entries: overlapPathPickerEntries,
|
||||||
@@ -467,6 +468,15 @@ export function SettingsModal({
|
|||||||
refresh: refreshOverlapPathPicker,
|
refresh: refreshOverlapPathPicker,
|
||||||
} = useWorkspaceFileBrowser("project", overlapPathPickerIndex !== null, projectId);
|
} = 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 { nodes } = useNodes();
|
||||||
const experimentalFeatures = form.experimentalFeatures ?? {};
|
const experimentalFeatures = form.experimentalFeatures ?? {};
|
||||||
const remoteAccessEnabled = isExperimentalFeatureEnabled(experimentalFeatures, "remoteAccess");
|
const remoteAccessEnabled = isExperimentalFeatureEnabled(experimentalFeatures, "remoteAccess");
|
||||||
@@ -1607,6 +1617,35 @@ export function SettingsModal({
|
|||||||
}
|
}
|
||||||
}, [closeOverlapPathPicker]);
|
}, [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) => {
|
const handleOverlapIgnorePathChange = useCallback((index: number, value: string) => {
|
||||||
setForm((f) => {
|
setForm((f) => {
|
||||||
const currentPaths = f.overlapIgnorePaths && f.overlapIgnorePaths.length > 0
|
const currentPaths = f.overlapIgnorePaths && f.overlapIgnorePaths.length > 0
|
||||||
@@ -3737,15 +3776,25 @@ export function SettingsModal({
|
|||||||
</div>
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="worktreesDir">Worktrees Directory</label>
|
<label htmlFor="worktreesDir">Worktrees Directory</label>
|
||||||
<input
|
<div className="settings-overlap-ignore-path-controls">
|
||||||
id="worktreesDir"
|
<input
|
||||||
type="text"
|
id="worktreesDir"
|
||||||
placeholder="~/.fn-worktrees/{repo} or .worktrees"
|
type="text"
|
||||||
value={form.worktreesDir || ""}
|
placeholder="~/.fn-worktrees/{repo} or .worktrees"
|
||||||
onChange={(e) =>
|
value={form.worktreesDir || ""}
|
||||||
setForm((f) => ({ ...f, worktreesDir: e.target.value }))
|
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>
|
<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>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
@@ -6596,6 +6645,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 */}
|
{/* Import Confirmation Dialog */}
|
||||||
{importDialogOpen && importPreview && (
|
{importDialogOpen && importPreview && (
|
||||||
<div className="modal-overlay open" onClick={(e) => e.target === e.currentTarget && setImportDialogOpen(false)} role="dialog" aria-modal="true">
|
<div className="modal-overlay open" onClick={(e) => e.target === e.currentTarget && setImportDialogOpen(false)} role="dialog" aria-modal="true">
|
||||||
|
|||||||
Reference in New Issue
Block a user