Merge main into the phone connections branch
ci / android head (push) Canceled after 0s
ci / api image (push) Canceled after 0s
ci / build and test (push) Canceled after 46s

Main had already taken this branch's first two commits, so what merged is the
Connections work against three things that landed beside it. Four of the six
conflicts were prose about arrangements both sides changed; two were real.

**The phone hub gained a Teams row while this branch was moving the keychain
onto it.** Both are additions to `IsMoreSurface` and both belong: teams because
the desktop reaches them from its rail and the phone through the hub, the
keychain because a bottom bar is for the places a session moves between. The
membership test, the back gesture's first case and the hub's own arithmetic all
take the union. The distinction is now written down rather than implied — teams
is the design's count plus one, and the keychain is the only rearrangement of
it: the bar lost a slot to gain that row.

**`ConnectAndAnnounceAsync` was the real one.** Main gave it
`RememberTypedPasswordAsync`, which binds the password that just worked to the
host it worked on; this branch had replaced the `HostRowViewModel` that method
needs with a four-field `ConnectionTarget`. Keeping both meant deciding what a
manual connection does with a password that succeeded, and the answer was
already written on the screen it is typed into: nothing. There is no item to
bind a credential to and none to bind it on, and that path saves nothing by
design.

So `ConnectionTarget` carries the row again — as a nullable, in place of the
host id it had, with `HostId` derived from it. Two things read it and both are
things that can only be done to a keychain item rather than to an address:
naming the log entry, and keeping the password. Null is not missing data there;
it is the whole of what makes the manual path different, and having one field
rather than two keeps "was this a keychain host" a question with one answer.

The desktop's rail lost SFTP and S3 to the tab strip on main, so the README's
"a rail with nine slots has room" was true when it was written this afternoon
and is not now. It says the room rather than the number.

Phase 11's four new device checks and main's Phase 12 on teams were the same
conflict twice — two appends to the end of one file — and both are kept.

Verified after resolving: the solution builds, the Android head builds clean,
and 837 tests pass across the seven client suites, including main's own additions
(233 shell, 79 layout, 240 domain, 118 sync, 54 session, 74 terminal, 39
storage).
This commit is contained in:
jaap-jan committed 2026-08-03 15:35:49 +02:00
commit 562fb444a8
88 files changed
+9866 -1707

No files matched your search

@@ -80,9 +80,10 @@
</RadioButton>
<!--
Tags. The one category holding nothing secret — a tag is a name — and here because renaming one is
one write instead of twenty, and a rename needs somewhere to happen. Making one usually happens in
a host's editor instead; this is where they are renamed and deleted.
Tags. The one category holding nothing secret — a tag is a name — and here because a host's editor
is the only place on this head that makes one, which leaves nowhere else to see the whole set or
what wears it. Deleting one happens here, with every other item kind; renaming is desktop work,
because nothing on this screen opens an editor.
-->
<RadioButton GroupName="section" Classes="chip" IsChecked="{Binding ShowsTags, Mode=OneWay}"
Command="{Binding ShowSectionCommand}"
@@ -112,11 +113,46 @@
-->
<TextBlock Grid.Row="2" Classes="detail" Margin="18,6,18,2" Text="{Binding SectionSummary}" />
<!-- ============ what to do with the chosen item ============ -->
<!--
DELETE and nothing beside it. The desktop's detail pane offers EDIT as well, and there is nothing here
for it to open — so this is the one action this head has for a keychain item, and until it existed the
confirmation below could not be reached at all.
It shares row 2 with that confirmation rather than being a bottom bar like the one on HOSTS or FILES,
because the question is what this button turns into: ShowsItemActions is false while a deletion is
armed, so the two swap in place and DELETE cannot be pressed again underneath its own question. A
button at the foot of the screen and its question at the top would be one exchange drawn twice.
And it names the item, because a keychain row is a card with a background of its own — see the selected
style below, which had nothing to colour until now. DELETE is the last button that should be pressed on
a guess about which row it means.
-->
<Border Grid.Row="2" IsVisible="{Binding ShowsItemActions}" Margin="12,4"
Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
BorderThickness="1" CornerRadius="12" Padding="14,10">
<Grid ColumnDefinitions="*,10,Auto">
<StackPanel Grid.Column="0" Spacing="3" VerticalAlignment="Center">
<TextBlock Classes="label" Text="SELECTED" />
<StackPanel Orientation="Horizontal" Spacing="7">
<TextBlock Classes="mono" FontSize="13" FontWeight="SemiBold"
Text="{Binding SelectedVaultItem.Name}" TextTrimming="CharacterEllipsis" />
<Border Classes="tag outline">
<TextBlock Text="{Binding SelectedVaultItem.Type}" />
</Border>
</StackPanel>
</StackPanel>
<Button Grid.Column="2" Classes="danger" Height="44" Width="104" Content="DELETE"
Command="{Binding DeleteSelectedItemCommand}" />
</Grid>
</Border>
<!-- ============ ◆ the counted confirmation ============ -->
<!--
In place, above the list it refers to. The text comes from the view model, which counts what breaks —
"three hosts authenticate with this key and will refuse to connect" — and the count is the whole
difference between a sentence somebody reads and one they click past.
In place, above the list it refers to and over the button that asked it. The text comes from the view
model, which counts what breaks — "three hosts authenticate with this key and will refuse to connect" —
and the count is the whole difference between a sentence somebody reads and one they click past.
-->
<Border Grid.Row="3" IsVisible="{Binding IsConfirmingDeletion}" Margin="12,4"
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
@@ -152,6 +188,16 @@
<Setter Property="Padding" Value="0" />
<Setter Property="MinHeight" Value="0" />
</Style>
<!--
The fill goes on the card and not on the ContentPresenter behind it, which is where HOSTS and
FILES put theirs. Those rows are bare Borders; a keychain row is Border.card, which paints an
opaque Sidebar over anything drawn underneath — so the selector has to reach the card itself or
the chosen row looks exactly like the others. It has to look different now that a button above
the list acts on it.
-->
<Style Selector="ListBoxItem:selected Border.card">
<Setter Property="Background" Value="{StaticResource Active}" />
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:VaultItemRowViewModel">