Merge branch 'main'
ci / build and test (push) Successful in 2m3s
ci / android head (push) Successful in 3m21s
ci / desktop nightly (push) Successful in 45s
ci / api image (push) Successful in 33s

Two of main's changes land in files this branch rewrote, and both needed carrying
across by hand rather than by the merge.

The phone's nav staying up on Connections with nothing running is a fourth input
to RefreshChrome, which this branch had already given two more — whether hosts are
ticked and whether the host editor is filling the screen. They compose: the rail
and the bottom bar now ask (pages || connectPage) && !editing, so a page-shaped
terminal surface keeps its way off the screen and the editor still takes the whole
display.

The key question under the host's move panel is the harder one, because this
branch deleted the panel it was added to. The connect card is gone and the phone's
only route to a move is the action bar, so leaving the merge to take this side
would have removed a capability main had just shipped — silently, since nothing
would fail to build. It is asked in the action bar's own picker instead, in two
shapes fewer than the desktop's: one host, because which key to carry is a fact
about one machine and a selection of six has six answers, and a move rather than a
copy, because taking the key out from under an original that is staying put would
leave that original unable to connect. BindingOfTheMovingHost splits into
MovableBindingOf so both heads answer it the same way from different panels.

Main also fixed a real trap in the same commit — a host that only inherited its
key from its group arrived in the destination naming nothing at all, because the
group stays behind — and the batch move had the same bug for the same reason. It
goes through Detached now, which is where that fix lives.

The carried host is written as the carry left it rather than being detached again,
which is the one thing worth measuring: the key takes a new id over there, so a
run that rebuilt the payload from the row would send the machine across naming a
tombstone. Both directions are pinned, along with the rule about which shapes the
question is asked in at all.
This commit is contained in:
2026-08-06 09:30:00 +02:00
20 changed files with 2530 additions and 113 deletions
@@ -25,8 +25,9 @@
── a terminal gets the screen ─────────────────────────────────────────────────────────────────────────
Three of the four rows below stand down while a shell is showing: the header, the shells strip and the
bottom bar itself. All three are bound on IsShowingPages, which is the same question asked once — the
surface is either a page or a terminal, and these are the chrome a page has.
bottom bar itself. The strip asks IsShowingPages directly; the other two go through flags the control
computes, and the bar's is the one that differs — it stays up on Connections with nothing running, which
is the terminal surface drawing a page rather than a shell. See PhoneShell.RefreshChrome.
The arithmetic is why. Header 56, strip 46, bar 64, and the terminal's own two rows on top of that: at
360dp the shell was framed by about a third of the display, all of it about somewhere the user was not.
@@ -316,10 +317,15 @@
<!-- ============ navigation ============ -->
<!--
Gone while a terminal is showing, which is the whole of that surface's arrangement: the bar's three
Gone while a shell is showing, which is the whole of that surface's arrangement: the bar's three
destinations are replaced by a back arrow and a + that leads to all of them, both in the terminal's
own bar. See TerminalScreen.axaml.
◆ A shell rather than the surface, which are not the same question — Connections with nothing running
is that surface drawing a page, and the bar stays under it. The screen it stands down for is the one
that pays for the room; a box asking which machine to connect to does not, and taking the nav away
from somebody who has just closed their last tab left them with only the back gesture.
◆ It is no longer a single question — a wide surface takes the rail instead — so it reads one flag
the control computes rather than a condition here. See PhoneShell.ShowsBottomBar.
@@ -348,12 +354,15 @@
rather than the door. The enum member stays ShellSurface.Terminal, for the reason the keychain's
tab is not called Vault: the surface is a terminal, and the word a user reads is the product's.
The only entry here that never lights, and deliberately no longer tries: this bar is collapsed
while the terminal is showing, so IsCurrent could only ever be read as false. Binding it anyway
would be a rule about a state this control cannot be in. What marks it as current is the surface
filling the screen.
◆ It lights again, and IsTerminalSurface is the right question rather than a near miss. This bar
is drawn in exactly two situations — a page, and Connections with nothing running — and that flag
separates them: false for every page, true for the one screen where this is where you are. With a
shell up the bar is gone and nothing here is read at all. It was left unbound while the bar was
collapsed for the whole of the terminal surface, which made a lit state unreachable; a screen the
bar can now sit under and not point at is the entry looking broken instead.
-->
<views:NavButton Grid.Column="1" Label="Connections" Glyph="⌗"
IsCurrent="{Binding IsTerminalSurface}"
Command="{Binding ShowTerminalCommand}" />
<!--