diff --git a/docs/manual-checks.md b/docs/manual-checks.md
index a33fdd8..3a53cc7 100644
--- a/docs/manual-checks.md
+++ b/docs/manual-checks.md
@@ -795,6 +795,16 @@ With host A selected, right-click host B and choose Delete.
wrong machine. `HostGridTests` covers both halves headlessly, so this is a confirmation that a real popup
behaves as the headless one did.
+**And the same on the group cards above.** Open a group, then right-click a card inside it and choose
+Delete.
+
+**Pass:** the question names the **card**, not the group that is open — and Open on that menu goes into the
+card, rather than back out to ALL HOSTS. Right-clicking the space around the group cards opens no menu.
+
+**Failure means:** the menu is reading `GroupTarget`'s fallback, which is the group whose contents are on
+screen. That fallback is right for the EDIT and DELETE buttons beside the heading and wrong for a menu that
+opened on a card.
+
### 7.10 Clicking a host in the palette connects
Ctrl+K, then click a result with the mouse rather than pressing Enter.
diff --git a/src/DodoSSH.Client.App/Views/HostsScreen.axaml b/src/DodoSSH.Client.App/Views/HostsScreen.axaml
index 77700f9..f57211e 100644
--- a/src/DodoSSH.Client.App/Views/HostsScreen.axaml
+++ b/src/DodoSSH.Client.App/Views/HostsScreen.axaml
@@ -282,6 +282,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DodoSSH.Client.App/Views/HostsScreen.axaml.cs b/src/DodoSSH.Client.App/Views/HostsScreen.axaml.cs
index e291c2e..35adc4f 100644
--- a/src/DodoSSH.Client.App/Views/HostsScreen.axaml.cs
+++ b/src/DodoSSH.Client.App/Views/HostsScreen.axaml.cs
@@ -96,6 +96,11 @@ internal sealed partial class HostsScreen : UserControl
HostGrid.AddHandler(PointerPressedEvent, OnPointerPressed, RoutingStrategies.Tunnel);
HostGrid.AddHandler(ContextRequestedEvent, OnContextRequested, RoutingStrategies.Tunnel);
+ // And the group cards answer a right click the same way, for the same reason: their menu's commands
+ // read the vault's group selection, and without this they would act on whichever card was selected
+ // before — or, with none, on the group the trail ends with, which is not on screen at all.
+ GroupGrid.AddHandler(ContextRequestedEvent, OnGroupContextRequested, RoutingStrategies.Tunnel);
+
HostGrid.PointerMoved += OnPointerMoved;
HostGrid.PointerReleased += OnPointerReleased;
HostGrid.PointerCaptureLost += OnPointerCaptureLost;
@@ -204,6 +209,33 @@ internal sealed partial class HostsScreen : UserControl
vault.SelectedSidebarRow = row;
}
+ ///
+ /// Points the group menu at whatever was right-clicked.
+ ///
+ ///
+ ///
+ /// The host grid's rule, applied to the cards above it — see . What is
+ /// different is what an unaimed menu would have done: GroupTarget falls back to the open group
+ /// when no card is selected, so Edit and Delete over a card would have been offered about the group whose
+ /// contents are showing rather than the one the pointer is on. That fallback is right for a pair of
+ /// buttons that sit beside the heading and wrong for a menu that opened on a card.
+ ///
+ ///
+ /// Cancelled outright over the space around the cards, as the host grid's is. That is not a group, and
+ /// the fallback is exactly what would make the menu look like it worked there.
+ ///
+ ///
+ private void OnGroupContextRequested(object? sender, ContextRequestedEventArgs e)
+ {
+ if (Vault is not { } vault || RowUnder(e.Source) is not HostGroupRowViewModel row)
+ {
+ e.Handled = true;
+ return;
+ }
+
+ vault.SelectedGroup = row;
+ }
+
///
/// Remembered rather than acted on. Whether this press is a click or the start of a drag is not known
/// until the pointer moves, so this is the point at which both are still possible.
diff --git a/tests/DodoSSH.Client.App.Layout.Tests/HostGridTests.cs b/tests/DodoSSH.Client.App.Layout.Tests/HostGridTests.cs
index c193c7a..773eb43 100644
--- a/tests/DodoSSH.Client.App.Layout.Tests/HostGridTests.cs
+++ b/tests/DodoSSH.Client.App.Layout.Tests/HostGridTests.cs
@@ -29,10 +29,11 @@ namespace DodoSSH.Client.App.Layout.Tests;
///
/// Separate from , which measures these controls rather than driving them.
/// What is here is the one gesture that cannot be expressed as a binding and cannot be checked by
-/// measuring: a right click has to move the selection before the menu opens, because all three of
-/// that menu's commands read the vault's host selection. A menu that quietly acted on whichever host
+/// measuring: a right click has to move the selection before the menu opens, because the commands
+/// on both of that screen's menus read the vault's selection. A menu that quietly acted on whichever host
/// happened to be selected would delete the wrong machine, which is the version of this mistake worth a
-/// suite.
+/// suite — and the group cards have the same menu with a fallback behind it that makes getting it wrong
+/// quieter still.
///
///
/// A real over a real unlocked vault, for the reason the other suites here use
@@ -159,6 +160,82 @@ public sealed class HostGridTests : IAsyncLifetime
});
}
+ ///
+ /// The same rule on the cards above, where getting it wrong is quieter and worse.
+ ///
+ ///
+ ///
+ /// The host grid's menu acts on nothing when it is not aimed; this one acts on the wrong group.
+ /// GroupTarget falls back to the group whose contents are on screen when no card is selected — the
+ /// right answer for the pair of buttons beside the heading, and the wrong one for a menu that opened on a
+ /// card, which would then offer to delete a group the pointer is nowhere near.
+ ///
+ ///
+ /// Open is the one entry that takes a parameter, because OpenGroupCommand's null is a real
+ /// argument — it is ALL HOSTS. That makes its CommandParameter binding the half most likely to
+ /// rot: a path that resolves to nothing compiles, draws, and quietly leaves the grid at the top level.
+ ///
+ ///
+ [Fact]
+ public async Task ARightClickSelectsTheGroupUnderThePointer()
+ {
+ await AddGroupAsync("staging");
+
+ await OnTheGridAsync((screen, window) =>
+ {
+ var first = GroupRow(vault, "production");
+ var other = GroupRow(vault, "staging");
+
+ vault.SelectedGroup = first;
+
+ RightClick(CardFor(screen, other), window);
+
+ vault.SelectedGroup.ShouldBeSameAs(other);
+
+ var menu = screen.GroupGrid.ContextMenu.ShouldNotBeNull();
+ menu.IsOpen.ShouldBeTrue();
+
+ var items = menu.Items.OfType