fix(dashboard): hand off cleanly from setup wizard to model onboarding

On a fresh install useAuthOnboarding's effect ran at mount before the
setup wizard's 500ms auto-open timer fired. The one-shot ref locked,
and the resolved fetch could either stack model onboarding on top of
the wizard or never re-trigger after the wizard closed.

- Gate the trigger on projectId being set so the wizard owns the
  bootstrap phase; the auth check only fires once a project exists.
- Re-check setupWizardOpen via a ref when the auth fetch resolves to
  avoid stacking onboarding on top of a wizard opened mid-fetch.
- Release the one-shot in that suppressed branch so the effect retries
  when the wizard closes.

Adds two regression tests: fresh-install handoff and mid-fetch wizard
suppression.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Fusion
2026-04-28 14:30:38 -07:00
committed by gsxdsm
parent 5e5020b78f
commit 5e6dbbb6f3
7 changed files with 300 additions and 109 deletions

View File

@@ -2770,6 +2770,22 @@ export function SettingsModal({
</small>
</div>
)}
<div className="form-group">
<label htmlFor="worktreeRebaseLocalBase" className="checkbox-label">
<input
id="worktreeRebaseLocalBase"
type="checkbox"
checked={form.worktreeRebaseLocalBase !== false}
onChange={(e) =>
setForm((f) => ({ ...f, worktreeRebaseLocalBase: e.target.checked }))
}
/>
Also rebase onto local default-branch HEAD
</label>
<small>
In addition to the remote rebase above, also rebase the task branch onto the local default-branch HEAD (rootDir). This catches sibling tasks that merged locally but haven't been pushed yet — without it, two concurrent tasks where one deletes code can have the other silently re-introduce it via the fallback strategy. Enabled by default; only disable if it causes issues with your workflow.
</small>
</div>
</>
);
case "commands":
@@ -2937,6 +2953,24 @@ export function SettingsModal({
</label>
<small>When enabled, lock files (package-lock.json, pnpm-lock.yaml, etc.) are resolved using 'ours' strategy, generated files (dist/*, *.gen.ts) using 'theirs' strategy, and trivial whitespace conflicts are auto-resolved without spawning an AI agent. Complex code conflicts still require AI review.</small>
</div>
<div className="form-group">
<label htmlFor="mergeConflictStrategy">Conflict Fallback Strategy</label>
<select
id="mergeConflictStrategy"
value={form.mergeConflictStrategy ?? "smart"}
onChange={(e) =>
setForm((f) => ({ ...f, mergeConflictStrategy: e.target.value as "smart" | "ai-only" | "prefer-main" | "abort" }))
}
>
<option value="smart">Smart — AI, then auto-resolve, then prefer task branch (default)</option>
<option value="ai-only">AI only — never silently pick a side</option>
<option value="prefer-main">Prefer main — fall back to keeping main's version</option>
<option value="abort">Abort — fail merge if AI can't resolve (manual review)</option>
</select>
<small>
What to do on the final merge attempt when AI and auto-resolve haven't fully resolved the conflict. <strong>Smart</strong> matches the historical behavior — the task branch wins on hard conflicts, which is fast but can resurrect code an earlier sibling task deleted. <strong>Prefer main</strong> sides with main instead, protecting just-merged sibling work. <strong>AI only</strong> retries the AI agent rather than silently choosing a side. <strong>Abort</strong> requires manual resolution for any conflict the AI can't handle.
</small>
</div>
<div className="form-group">
<label htmlFor="pushAfterMerge" className="checkbox-label">
<input

View File

@@ -1702,31 +1702,6 @@ export function TaskDetailModal({
</div>
)}
<MergeDetails task={task} />
<div className="detail-section">
<h4>Node Routing</h4>
<dl className="detail-source-grid">
<div>
<dt>Task Override</dt>
<dd>{task.nodeId ?? <span className="detail-source-empty">(none)</span>}</dd>
</div>
<div>
<dt>Effective Node</dt>
<dd>{(task as Task & { effectiveNodeId?: string }).effectiveNodeId ?? "local execution"}</dd>
</div>
<div>
<dt>Routing Source</dt>
<dd>{(task as Task & { effectiveNodeSource?: string }).effectiveNodeSource ?? "local"}</dd>
</div>
<div>
<dt>Unavailable Node Policy</dt>
<dd>{(settings as Settings & { unavailableNodePolicy?: string } | undefined)?.unavailableNodePolicy ?? "block"}</dd>
</div>
<div>
<dt>Blocking Reason</dt>
<dd>{((task as Task & { blockedReason?: string; statusReason?: string }).blockedReason || (task as Task & { statusReason?: string }).statusReason) ?? <span className="detail-source-empty">(not blocked)</span>}</dd>
</div>
</dl>
</div>
{task.sourceIssue && (
<div className="detail-section detail-source-section">
<h4>Source Issue</h4>