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
+10 -3
View File
@@ -568,9 +568,16 @@ go at 360dp:
believing they typed an `l`.
**The surface has since taken the whole screen.** `PhoneShell` collapses the header, the session strip
and the bottom bar while a terminal is showing — one binding on `IsShowingPages` each — and the screen
draws a 35-pixel bar in their place: back, the session pills, and a `+` raising a sheet with the three
connections there are. That sheet is the head's first control that could be drawn over the renderer, so
and the bottom bar while a shell is showing, and the screen draws a 35-pixel bar in their place: back,
the session pills, and a `+` raising a sheet with the three connections there are.
*A shell rather than the surface, and the two parted company once that surface gained a connect page.*
With nothing running, Connections is a box, a CONNECT button and the machines connected to before — a
page in everything but which enum it is in — so `RefreshChrome` keeps the bar (and, wide, the rail) under
it and the Connections entry lights for the first time. It is the one screen reachable by closing your
last tab, and collapsing the nav there left the system back gesture as the only route to Hosts or
Settings. The header is not part of that: the surface draws its own bar, and a vault header above it is
the second row of chrome this head exists to avoid. That sheet is the head's first control that could be drawn over the renderer, so
it collapses it rather than covering it, exactly as the desktop's palette does; whether Android's
WebView actually composites above Avalonia content is still the unverified question recorded below, and
collapsing is correct under either answer.
+60 -3
View File
@@ -446,6 +446,31 @@ target lands. Anything still in the source vault is a partial move, which is sur
not happen with the network up: the groups are written top-down and the hosts last, so an interruption leaves
hosts behind and never a shelf with nothing on it.
### 3.3b Moving a key, and moving a host with its key · **needs a second vault**
In your personal vault: add an SSH key, then two hosts that both authenticate with it. On the Keychain
screen select the key and press **MOVE**, and read the panel before choosing the shared vault.
**Pass:** the panel says what uses the key — "Used by 2 hosts…" — before anything happens. Afterwards the key
carries the destination's badge under an id it did not have a moment ago, *and both hosts still say `key`
under their names* and still connect. The sentence names the vault and the two hosts that followed it.
Then the other direction: with a key back in your personal vault and a host bound to it, choose **Move to
another vault…** on the host and pick the shared vault. The tick box under the picker offers to bring the
key, unticked, with the count of what else uses it underneath. Leave it and press MOVE; then move the host
back, tick it, and press MOVE again.
**Pass:** unticked, the host lands in the shared vault and the status line says the key it authenticates with
is in another vault and will not resolve for anybody else there. Ticked, the key lands in the shared vault
too and the host still says `key`. Choosing a *different* vault in the picker re-asks the question, and the
box disappears when the destination is the vault the key is already in.
**Failure means:** a host that says `password` after either move is the re-aim not having happened — the item
takes a new id in the destination, so every host bound to it and every group lending it has to be rewritten
as it lands, and a host bound to something its vault no longer holds refuses to connect rather than falling
back to a typed password. A tick box that is ticked when the panel opens is worse than a bug: it moves a
private key into a shared vault on the strength of a decision nobody made.
### 3.4 A group deleted on another machine · **needs two machines**
Make a group on machine A, file a host into it, sync. On machine B, sync, then delete the group and sync
@@ -1114,6 +1139,29 @@ with both hosts and both ticks still there.
"delete prod-db?" is not a confirmation anybody reads. A question that cleared the ticks on KEEP is a
selection thrown away for declining to destroy it.
### 8.14a The key question is asked for one host and for a move · **needs two writable vaults**
With a host bound to a key that is in the vault it is leaving, tick that host alone and choose **Move to
vault**.
**Pass:** under the picker, an unticked box offering to bring the key, with the count of what else uses it
underneath. Choosing a different destination re-asks it, and it disappears when the destination is the vault
the key is already in — the same behaviour as the desktop's, which 3.3 measures.
Now choose **Copy to vault** on the same host.
**Pass:** no box at all. A copy that took the key away would leave the original — still sitting in the vault
it started in — unable to connect, which is the one thing "copy" promises not to do.
Tick a second host and open **Move to vault** again.
**Pass:** still no box. Which key to carry is a fact about one machine, and a selection of six has six
answers.
**Failure means:** a box under a copy is `ChosenBindingToBring` no longer reading
`ChosenHostsAreBeingCopied`, and it moves a private key out from under a host that is still using it. A box
that is ticked when the panel opens is worse than a bug for the reason 3.3 gives.
### 8.15 A tap connects, and a long press chooses · **the one worth the most care**
On a host bound to a key or a stored credential — one that needs nothing typed — tap the row once.
@@ -1320,12 +1368,13 @@ headless surface has a native view to answer with. `docs/android-port.md` still
Open a shell from HOSTS.
**Pass:** the vault header, the session strip and the four-entry bottom bar are all gone. What is left is
**Pass:** the vault header, the session strip and the three-entry bottom bar are all gone. What is left is
one bar — a back arrow, the session pills, a `+` — and then the terminal down to the accessory keys. Press
back: all three come back, the tab is still in the strip and its dot is still green.
**Failure means:** one of the three rows is not bound on `IsShowingPages`, or the terminal is being reached
by a route that leaves `Surface` on `Page`.
**Failure means:** the strip is not bound on `IsShowingPages`, `RefreshChrome` is not reading `HasTabs`, or
the terminal is being reached by a route that leaves `Surface` on `Page`. A bar still there **with a shell
showing** is the one that matters: that is the third of the screen this arrangement exists to give back.
### 11.2 The connect menu is not drawn over the renderer · **the important one**
@@ -1391,6 +1440,14 @@ Close every tab, then press Connections in the bottom bar.
keychain has ever connected to anything — a RECENT list underneath. The whole thing scrolls with the
keyboard up, and the password box shows dots with **no suggestion strip** above the keyboard.
**And the bottom bar is still there, with Connections lit.** Press Hosts: it goes straight there, without
the back gesture. Then open a shell — the bar goes, which is 11.1. Wide, the same holds for the rail.
**Failure means:** for the bar, `RefreshChrome` is back to collapsing the nav for the whole terminal
surface rather than for a shell — which strands anybody who reaches this screen by closing their last tab.
For an unlit Connections entry, `IsCurrent` lost its binding: on this screen it is the only true thing the
bar could say about where you are.
Type a machine you can reach and press CONNECT: a tab appears in the bar and the shell opens. Type
something malformed — no `user@`, or `:70000` — and the refusal appears under the boxes, in the warning
colour, with no tab and nothing dialled.