Choose more than one host card on the desktop, the way the phone already can

The chosen-hosts set has been in VaultViewModel since the phone's connect card became a contextual action
bar: a set of entity ids, a tick on the row, and seven things that can be done to it. Only one head could
fill it. The desktop's grid answered a press with one selection — the card the drawer, CONNECT and the
context menu are about — so filing eleven imported machines under a heading was eleven drags, and clearing
out a vault was eleven rounds of the deletion question.

So the pointer gets three ways into the same set. Ctrl-clicks a card to tick it, Shift-clicks to tick the run
between the anchor and the card, and drags a band out over the space between and below the cards to tick
everything it touches. Esc, CLEAR, a plain click on a card and a click on the empty space each drop it, and
Ctrl+A takes every card being drawn — VisibleHosts, so with something in the find box that is the ones on
screen and not the ones it is hiding, which is the version of that shortcut whose result can be checked
before Delete is pressed.

TWO SELECTIONS ON ONE SCREEN, AND KEEPING THEM FROM DISAGREEING IS MOST OF THE CHANGE. Ctrl and Shift are
answered on the tunnel and marked handled, so the ListBox never moves its own mark onto the card: a
Ctrl-click that also selected would light the card it had just unticked and open the drawer on a machine
somebody is removing from a set. A plain press drops the set unless it lands on a ticked card, and that case
is deferred to the release, because the press may be the start of a drag of all of it. After any ordinary
click exactly one card is in play, which is what makes every command on the screen unambiguous again.

The context menu is where the seven live, and it is one markup with two halves gated on IsChoosingHosts.
Connect, Browse files and Edit… are drawn only for a single ticked host, as the phone's sheet collapses them
and for the same reason; the other four read better for a count. A right click on a card outside the set
drops the set first, so a Delete… about the card under the pointer can never be offered while six sit ticked
behind the menu — the same rule OnContextRequested has always enforced for the selection, reached from the
other direction. No bar of buttons: the phone raises one because it has no other way to hold seven entries,
and a strip repeating a menu that already exists would be a second home for the wording that matters most.
What the desktop gains instead is a count beside the HOSTS heading, CLEAR, and a sentence saying where the
actions are.

A drag that starts on a ticked card carries every ticked card. The payload is a list rather than a row now,
and a drop of more than one goes through FileChosenHostsUnder, which makes the refusals once — an open
editor, and a group belonging to one keychain — and reports a count instead of forty status lines. Moving
whichever card the pointer happened to be holding and leaving the other five where they are is a gesture
that quietly does a fraction of what it looks like it does, and the five left behind look filed.

The three panels the set's actions raise had never been drawn in a window: the vault picker with its key
question, the group picker, and the deletion question. All three sit above the grid rather than over it,
which is the arrangement the GROUPS section and the phone's list already use and for the reason written
there — the ticked cards are the information the question exists to give, so the grid shortens instead.

A DEFECT FOUND BEHIND IT, AND IT WAS ALREADY LIVE ON THE PHONE. The deletion question names a count and the
run that answers it reads the set again, and nothing kept the two the same set: the panel is deliberately
above a live list, so one more tick between "Delete these 6 hosts?" and pressing DELETE deleted seven, with
the seventh named in nothing the user had read. It needed a deliberate act on a phone and a second's work
with a band, which is what turned it up. VaultViewModel now remembers which hosts the question was asked
about and drops the question when the set stops being them — the question rather than the set, because what
somebody has just chosen is what they meant. It also covers the case nobody performs: a colleague's deletion
arriving mid-question and shrinking the set under it.

VERIFIED. 354 tests in App.Tests and 111 in App.Layout.Tests, build clean, no new warnings. Six gesture tests
drive real pointer and key input through the headless window — the modifier click and what it must not do to
the selection, the run and its re-measurement from the anchor, the band and the click that drops the set,
Ctrl+A under a filter, and the menu's two halves — plus a DragOver carrying two hosts. Four layout tests
measure the strip and the three panels at the window's minimum; the vault panel binds a key to its host
first, or it would measure the short shape and certify the tall one. Two flow tests cover the multi-drop's
write and its refusal, and the deletion question dropping itself.

manual-checks gains 7.6a for dragging a set, which no test can see for the reason 7.6 gives, and 7.7a for the
gestures — the rectangle actually being painted and the tick and the fill being legible together are the two
things the harness cannot look at.
This commit is contained in:
2026-08-06 14:11:48 +02:00
parent 1e8a1f2e83
commit 507cd9ff88
8 changed files with 1418 additions and 64 deletions
+44
View File
@@ -882,6 +882,23 @@ the drop are covered by nothing. What *is* automated is the decision each drag e
`HostGridTests.TheGroupCardsAreWhatAcceptsADroppedHost` raises a real `DragOver` over both kinds of card —
and the write at the end, `ShellFlowTests.MovingAHostToAGroup_FilesItAndTakesItOffTheLevelItCameFrom`.
### 7.6a Dragging a whole set onto a group card · **also uncovered, and the same reason**
Tick three hosts (7.7a is how), then pick one of the three up and drag it onto a group card.
**Pass:** the card marks itself exactly as it does for one host, and the drop files **all three** — the
status line says how many, and all three leave the level. Ticking nothing and dragging a single card still
files that one card, which is what this gesture has always done.
**And a card that is not in the set** drags alone: press one of the unticked cards and the ticks come off
before the drag starts, so what lands is the one machine that was under the pointer.
**Failure means:** a drag that filed one of three is the payload having been built from the card rather than
from the set — the thing this gesture must never do quietly, since the other two stay behind looking filed.
The write is `ShellFlowTests.DroppingTheChosenHostsOnAGroupCard_FilesEveryOneOfThem` and the drag event's
answer is `HostGridTests.AGroupCardTakesAWholeSetOfDraggedHosts`; the platform's half of it is covered by
nothing, as 7.6 explains.
### 7.7 A click still selects, and a double click still connects
Click host cards; drag one a few pixels without releasing; double-click one. Then double-click the space
@@ -897,6 +914,33 @@ double-tap handler has lost its check that the pointer was over a card.
**And the same two gestures on the group cards above**, where they mean select and open rather than select
and connect — the space around *those* cards must open nothing, for the same reason. See 3.2.
### 7.7a Choosing several cards with Ctrl, Shift and a band · **the desktop's half of the phone's set**
With four or more hosts in the grid: Ctrl-click two cards, Shift-click a third, then drag a rectangle out
over the cards starting from the empty space below them. Finally press Esc, and try Ctrl+A.
**Pass:** each ticked card takes a ✓ beside its status dot and a filled surface, and the strip beside the
HOSTS heading counts them. A Ctrl-click does **not** move the selection — whichever card was lit before
stays lit, and the drawer, if it is open, goes on showing the same machine. Shift ticks the whole run
between the anchor and the card, in the order the cards are laid out. The band draws an accent rectangle
that follows the pointer and ticks every card it *touches*, unticking them again as it is pulled back off.
Ctrl+A ticks every card being drawn — with something in the find box, that is the ones on screen and not the
ones it is hiding. Esc, CLEAR, a plain click on a card and a click on the empty space each drop the set.
**And the menu is about the set while one is up:** right-click a ticked card and the entries are Connect,
Browse files and Edit… only while exactly one is ticked, then Change group…, Move to another vault…, Copy to
another vault…, Duplicate and Delete… — the last naming the count. Right-clicking a card **outside** the set
drops the set and gives the ordinary four back, aimed at that card.
**Also check the band does not start** on the scrollbar, on a group card, or inside a panel above the grid,
and that dragging the scrollbar still scrolls.
**Failure means:** a Ctrl-click that also selects is the press not being handled on the way down, and it
puts the drawer on a machine somebody is removing from a set. A menu offering both halves at once is the
`IsVisible` bindings in `HostsScreen.axaml` having come apart, which is how the wrong machine gets deleted.
`HostGridTests` drives all of these headlessly; what it cannot see is the rectangle actually being painted
and the two marks being legible together, which is what this check is for.
### 7.8 The highlight clears after a drag that goes nowhere
Drag a host over a group card and release outside the grid, or press Escape mid-drag.