feat(dashboard): expose mergeAdvanceAutoSync in the project settings modal

Adds the missing form control for the auto-sync mode introduced by the
merger hook. Lives next to Direct merge commit routing / Integration
worktree inside the merge-strategy panel and only renders when
mergeStrategy === "direct". Three options with descriptions cribbed from
docs/settings-reference.md:

  - Stash + fast-forward (default): preserve local edits across the snap
  - Fast-forward only: skip dirty worktrees, surface the banner instead
  - Off: legacy behavior; project root stays stale until manual pull

Value is normalized through normalizeMergeAdvanceAutoSyncMode on both the
merged-settings and scoped-settings load paths so a missing/invalid
stored value cleanly falls back to the default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-05-23 15:50:03 -07:00
parent a2ac5cb7a3
commit d8493f9467
2 changed files with 53 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
---
"@fusion/dashboard": patch
---
feat(dashboard): expose `mergeAdvanceAutoSync` in the project settings modal
Adds the missing form control for the auto-sync mode introduced by the merger hook. Lives next to the existing Direct merge commit routing / Integration worktree controls inside the merge-strategy panel and only renders when `mergeStrategy === "direct"`. Three options with the same labels and descriptions as `docs/settings-reference.md`:
- **Stash + fast-forward (default)** — preserve local edits across the auto-snap
- **Fast-forward only** — skip dirty worktrees, surface the banner instead
- **Off** — legacy behavior, project root stays stale until manual pull
Value is normalized through `normalizeMergeAdvanceAutoSyncMode` on both the merged-settings and scoped-settings load paths so a missing/invalid stored value cleanly falls back to the default without spamming validation errors.

View File

@@ -11,6 +11,7 @@ import {
resolveProjectDefaultModel, resolveProjectDefaultModel,
resolveTitleSummarizerSettingsModel, resolveTitleSummarizerSettingsModel,
normalizeMergeIntegrationWorktreeMode, normalizeMergeIntegrationWorktreeMode,
normalizeMergeAdvanceAutoSyncMode,
} from "@fusion/core"; } from "@fusion/core";
import type { AgentPermissionPolicyRules, Settings, GlobalSettings, ThemeMode, ColorTheme, ModelPreset, NtfyNotificationEvent, AgentPromptsConfig, ThinkingLevel } from "@fusion/core"; import type { AgentPermissionPolicyRules, Settings, GlobalSettings, ThemeMode, ColorTheme, ModelPreset, NtfyNotificationEvent, AgentPromptsConfig, ThinkingLevel } from "@fusion/core";
import { fetchSettings, fetchSettingsByScope, updateSettings, updateGlobalSettings, fetchAuthStatus, loginProvider, logoutProvider, cancelProviderLogin, saveApiKey, clearApiKey, fetchModels, testNotification, fetchBackups, createBackup, exportSettings, importSettings, fetchMemoryFile, fetchMemoryFiles, saveMemoryFile, compactMemory, fetchGlobalConcurrency, updateGlobalConcurrency, installQmd, testMemoryRetrieval, triggerMemoryDreams, fetchGitRemotes, fetchGitRemotesDetailed, fetchProjects, fetchDashboardHealth, checkForUpdates, fetchRemoteSettings, updateRemoteSettings, fetchRemoteStatus, installCloudflared, startRemoteTunnel, stopRemoteTunnel, killExternalTunnel, regenerateRemotePersistentToken, generateShortLivedRemoteToken, fetchRemoteQr, fetchRemoteUrl, submitProviderManualCode } from "../api"; import { fetchSettings, fetchSettingsByScope, updateSettings, updateGlobalSettings, fetchAuthStatus, loginProvider, logoutProvider, cancelProviderLogin, saveApiKey, clearApiKey, fetchModels, testNotification, fetchBackups, createBackup, exportSettings, importSettings, fetchMemoryFile, fetchMemoryFiles, saveMemoryFile, compactMemory, fetchGlobalConcurrency, updateGlobalConcurrency, installQmd, testMemoryRetrieval, triggerMemoryDreams, fetchGitRemotes, fetchGitRemotesDetailed, fetchProjects, fetchDashboardHealth, checkForUpdates, fetchRemoteSettings, updateRemoteSettings, fetchRemoteStatus, installCloudflared, startRemoteTunnel, stopRemoteTunnel, killExternalTunnel, regenerateRemotePersistentToken, generateShortLivedRemoteToken, fetchRemoteQr, fetchRemoteUrl, submitProviderManualCode } from "../api";
@@ -443,6 +444,7 @@ export function SettingsModal({
autoMerge: true, autoMerge: true,
mergeStrategy: "direct", mergeStrategy: "direct",
mergeIntegrationWorktree: "reuse-task-worktree", mergeIntegrationWorktree: "reuse-task-worktree",
mergeAdvanceAutoSync: "stash-and-ff",
recycleWorktrees: false, recycleWorktrees: false,
executorAllowSiblingBranchRename: false, executorAllowSiblingBranchRename: false,
worktreeNaming: "random", worktreeNaming: "random",
@@ -688,6 +690,7 @@ export function SettingsModal({
const normalizedSettings = { const normalizedSettings = {
...s, ...s,
mergeIntegrationWorktree: normalizeMergeIntegrationWorktreeMode(s.mergeIntegrationWorktree), mergeIntegrationWorktree: normalizeMergeIntegrationWorktreeMode(s.mergeIntegrationWorktree),
mergeAdvanceAutoSync: normalizeMergeAdvanceAutoSyncMode(s.mergeAdvanceAutoSync),
}; };
setForm(normalizedSettings); setForm(normalizedSettings);
setInitialValues(normalizedSettings); // Store initial values to detect explicit clears setInitialValues(normalizedSettings); // Store initial values to detect explicit clears
@@ -699,6 +702,9 @@ export function SettingsModal({
mergeIntegrationWorktree: scoped.project.mergeIntegrationWorktree === undefined mergeIntegrationWorktree: scoped.project.mergeIntegrationWorktree === undefined
? undefined ? undefined
: normalizeMergeIntegrationWorktreeMode(scoped.project.mergeIntegrationWorktree), : normalizeMergeIntegrationWorktreeMode(scoped.project.mergeIntegrationWorktree),
mergeAdvanceAutoSync: scoped.project.mergeAdvanceAutoSync === undefined
? undefined
: normalizeMergeAdvanceAutoSyncMode(scoped.project.mergeAdvanceAutoSync),
}, },
}); // Store initial scoped values for null-as-delete }); // Store initial scoped values for null-as-delete
setLoading(false); setLoading(false);
@@ -4673,6 +4679,40 @@ export function SettingsModal({
</div> </div>
)} )}
</div> </div>
<div className="form-group">
<label htmlFor="mergeAdvanceAutoSync">Auto-sync project checkout after merge</label>
<select
id="mergeAdvanceAutoSync"
className="select"
value={form.mergeAdvanceAutoSync ?? "stash-and-ff"}
onChange={(e) =>
setForm((f) => ({
...f,
mergeAdvanceAutoSync: e.target.value as "off" | "ff-only" | "stash-and-ff",
}))
}
data-testid="merge-advance-auto-sync-select"
>
<option value="stash-and-ff">Stash + fast-forward (default) — preserve local edits</option>
<option value="ff-only">Fast-forward only — skip dirty worktrees</option>
<option value="off">Off — leave the project root stale (legacy behavior)</option>
</select>
<details className="settings-option-details">
<summary>More details</summary>
<small>
After Fusion advances the integration branch ref, the merger can auto-sync other
worktrees still checked out on that branch (typically your project-root
checkout). <code>Stash + fast-forward</code> snapshots real local edits as a patch
against the previous tip, snaps the worktree to the new tip, then reapplies the
patch — untracked files that collide with newly-tracked paths are left in a temp
dir for manual recovery. <code>Fast-forward only</code> snaps cleanly when the
worktree has no edits and skips otherwise. <code>Off</code> is the legacy
behavior: <code>git status</code> in your project root will show the new commits
inverted as &quot;staged changes&quot; until you pull manually. Only applies to direct
merges.
</small>
</details>
</div>
</> </>
)} )}
{form.mergeStrategy === "pull-request" && ( {form.mergeStrategy === "pull-request" && (