diff --git a/CHANGELOG.md b/CHANGELOG.md
index caaa129..d6af74a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -150,6 +150,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(default **10 seconds**; **0** closes immediately), and selecting **Never** turns auto-close
off entirely.
+### Fixed
+
+- **Opening a second tool on a freshly placed branch no longer fails.** Opening a **new worktree**
+ placement card created the worktree and launched into it — but the card still read *new worktree*,
+ so a second click (open a **Console**, then open **Rider**) tried to `git worktree add` the same
+ branch again and failed with *git worktree add failed*, since the branch was now checked out in the
+ worktree the first click made. Placing a branch now **converts the card in place** to the real
+ checkout it became — a created worktree becomes a deletable **worktree**, a switched clone becomes a
+ **main clone** — with its solution chips re-scanned from the tree that now exists. Any further tool
+ clicks open that folder directly, no rescan required.
+
### Changed
- **MRU suggestions no longer drop down on focus.** The Branch and Solution boxes used to open
diff --git a/src/ViewModels/MainWindowViewModel.cs b/src/ViewModels/MainWindowViewModel.cs
index a619b0f..3be3ba7 100644
--- a/src/ViewModels/MainWindowViewModel.cs
+++ b/src/ViewModels/MainWindowViewModel.cs
@@ -394,6 +394,32 @@ public void RemoveTarget(TargetCard card)
Phase = DiscoveryPhase.NotFound;
}
+ ///
+ /// Swaps a just-placed card for the real checkout it became: a
+ /// whose worktree was created turns into a , a
+ /// whose main tree was switched turns into a
+ /// . Opening the card again then launches the folder that now
+ /// exists instead of trying to place the branch a second time — which git rejects, because the
+ /// branch is already checked out. Selection follows the swapped card so the just-opened target stays
+ /// current (and, for a new worktree, becomes deletable without waiting for a rescan).
+ ///
+ public void ReplaceTarget(TargetCard existing, DiscoveredTarget materialised)
+ {
+ var index = Targets.IndexOf(existing);
+ if (index < 0) return;
+
+ var wasSelected = ReferenceEquals(_selectedTarget, existing);
+ var card = new TargetCard(materialised);
+ Targets[index] = card;
+
+ // Replacing the selected item can bounce the list's SelectedItem through null; set it back
+ // explicitly so selection — and everything gated on it — lands on the materialised card.
+ if (wasSelected) SelectedTarget = card;
+
+ OnPropertyChanged(nameof(HasMultipleTargets));
+ OnPropertyChanged(nameof(MultiTargetHelperText));
+ }
+
// --- Auto-close countdown ---------------------------------------------------------
private bool _isClosingCountdown;
diff --git a/src/Views/MainWindow.axaml.cs b/src/Views/MainWindow.axaml.cs
index 4abe624..4ef24be 100644
--- a/src/Views/MainWindow.axaml.cs
+++ b/src/Views/MainWindow.axaml.cs
@@ -287,6 +287,10 @@ internal async Task OpenWithAsync(Editor editor, bool fromCommandLine = false)
_vm.AppendLog($"▸ Creating a worktree for '{branch}' in {card.Target.RepoName}…");
var ctx = await _opener.BuildMainContextAsync(repo, branch, _config);
folder = await _opener.CreateWorktreeAsync(repo, branch, ctx);
+ // The worktree now exists on disk: convert the card to a real one so a second tool
+ // click opens it instead of trying to add the same worktree again (git would refuse —
+ // the branch is now checked out here).
+ await MaterialisePlacementAsync(card, folder, TargetKind.Worktree);
}
else if (card.Target.Kind == TargetKind.SwitchMainClone)
{
@@ -296,6 +300,9 @@ internal async Task OpenWithAsync(Editor editor, bool fromCommandLine = false)
_vm.AppendLog($"▸ Switching {card.Target.RepoName}'s main tree to '{branch}'…");
var ctx = await _opener.BuildMainContextAsync(repo, branch, _config);
folder = await _opener.CheckoutInMainAsync(repo, branch, ctx);
+ // The main tree is now on the branch: convert the card to a plain main-clone target so
+ // a second click just reopens it rather than re-running the switch.
+ await MaterialisePlacementAsync(card, folder, TargetKind.MainClone);
}
else
{
@@ -338,6 +345,31 @@ internal async Task OpenWithAsync(Editor editor, bool fromCommandLine = false)
}
}
+ ///
+ /// Replaces a placement card whose branch was just put on disk (worktree created, or clone
+ /// switched) with the real checkout at . Done as
+ /// soon as the placement succeeds — before the launch itself, which may fail to locate the editor —
+ /// so a follow-up click on the same card opens the existing folder instead of asking git to place
+ /// the branch again. The solutions are re-globbed from the tree that now exists (the placement card
+ /// only previewed the clone's), keeping the chip row honest for the next open.
+ ///
+ private async Task MaterialisePlacementAsync(TargetCard card, string folder, TargetKind kind)
+ {
+ DateTime? updated = null;
+ try { updated = Directory.GetLastWriteTimeUtc(folder); }
+ catch { /* advisory meta only — an unreadable timestamp shouldn't block the swap */ }
+
+ var solutions = await Task.Run(() => _opener.FindSolutionsInFolder(folder, _config));
+ var materialised = card.Target with
+ {
+ Path = folder,
+ Kind = kind,
+ Solutions = solutions,
+ UpdatedUtc = updated,
+ };
+ _vm.ReplaceTarget(card, materialised);
+ }
+
// --- Delete (inline two-step confirm) -------------------------------------------------
/// First click: build a fresh deletion plan (dirty files / orphaned commits included)
diff --git a/tests/Fido.Tests/E2E/NewBranchRepoTests.cs b/tests/Fido.Tests/E2E/NewBranchRepoTests.cs
index beb50d7..bd9afec 100644
--- a/tests/Fido.Tests/E2E/NewBranchRepoTests.cs
+++ b/tests/Fido.Tests/E2E/NewBranchRepoTests.cs
@@ -193,6 +193,61 @@ await Harness.WithWindow(services, async window =>
});
}
+ ///
+ /// Regression: opening a placement card creates the worktree once and then converts the
+ /// card to the real checkout, so a second tool click (the user's "open a console, then open Rider")
+ /// launches the existing worktree instead of asking git to add it again — which git rejects because
+ /// the branch is already checked out there.
+ ///
+ [Test]
+ public async Task Opening_a_second_tool_reuses_the_created_worktree_instead_of_re_adding_it()
+ {
+ using var world = new TestRepoWorld();
+ var origin = world.CreateOrigin("Foo", "Foo");
+ var root = world.SearchRoot("root");
+ var clone = world.Clone(origin, root, "Foo");
+ world.PublishBranchToOrigin(origin, "feature/x"); // never fetched by the clone
+
+ var launcher = new FakeEditorLauncher();
+ var services = world.BuildServices([root], launcher, new FakeDialogService());
+
+ await Harness.WithWindow(services, async window =>
+ {
+ var vm = window.Vm();
+ await window.Discover("feature/x");
+ await AssertSingleCandidate(window, "feature/x", remoteOnly: true);
+
+ // First click opens a console on the (freshly created) worktree.
+ await window.OpenWithAsync(new Editor { Name = "Console", Kind = EditorKind.Console });
+ var worktree = vm.Targets[0].Path;
+ await Assert.That(Directory.Exists(worktree)).IsTrue();
+
+ // The card has become a real, deletable worktree — no rescan needed — and stays selected.
+ await Assert.That(vm.Targets[0].IsNewWorktree).IsFalse();
+ await Assert.That(vm.Targets[0].IsWorktree).IsTrue();
+ await Assert.That(vm.SelectedTarget!.IsWorktree).IsTrue();
+ await Assert.That(vm.CanDelete).IsTrue();
+
+ // Second click — Rider this time — must open the existing worktree, not fail on a re-add.
+ await window.OpenWithAsync(new Editor { Name = "Rider", Kind = EditorKind.Rider });
+
+ await Assert.That(window.LogText().Contains("git worktree add failed")).IsFalse();
+ await Assert.That(launcher.Launches.Count).IsEqualTo(2);
+
+ // Both launches landed inside the one worktree; no second, "-2" tree was created.
+ await Assert.That(Paths.StartsWith(launcher.Launches[0].Target, worktree)).IsTrue();
+ var riderTarget = launcher.Launches[1].Target;
+ await Assert.That(Paths.StartsWith(riderTarget, worktree)).IsTrue();
+ await Assert.That(riderTarget).EndsWith("Foo.sln");
+ await Assert.That(Directory.Exists(worktree + "-2")).IsFalse();
+
+ // Discovery still reports exactly one worktree for the branch.
+ await window.RunDiscoveryAsync();
+ await Assert.That(vm.Targets.Count).IsEqualTo(1);
+ await Assert.That(vm.Targets[0].IsWorktree).IsTrue();
+ });
+ }
+
[Test]
public async Task Opening_the_switch_card_moves_the_main_tree_onto_the_branch()
{