Android: the redesign — a Vaults tab with hosts, keychain and snippets, card lists, a keychain that can add, and files that act on what is chosen
ci / build and test (push) Successful in 3m49s
ci / android head (push) Successful in 3m55s
ci / api image (push) Successful in 6s
ci / desktop nightly (push) Successful in 1m22s

The bottom bar is Vaults · Connections · Settings, drawn as Material 3 pills with icons from the embedded
Material font and a count of open sessions on Connections, which replaces the strip of shells above the
bar. Vaults is the open vault: its header carries the name as a switch to the vaults screen, the sync
light and a lock icon, and a Hosts · Keychain · Snippets segmented control sits under it. The keychain
and snippets move there from Settings (IsVaultSurface lights the tab; IsMoreSurface no longer lists
them), and back from either goes to the hosts. Settings becomes an account card and grouped sections:
security, vaults and history, files, this app.

Host rows are cards with a terminal tile and the live dot on its corner. The connect sheet names the
host and its address, labels its box, focuses it as it opens, reveals the password on request, and puts
CANCEL left and CONNECT right; every two-button row on the head now confirms on the right. While a
hosts-screen sheet is up the header and bar are dimmed too and a tap on them lowers it, and the bar
stands down while the software keyboard is up.

The keychain gains a floating + and a sheet of the desktop's five adds (generate a key, paste a key, a
password, a bucket, a tag) over the editors VaultViewModel already had, drawn as a full-screen page with
labels above the boxes; a chosen item gets EDIT and COPY PUBLIC KEY beside MOVE and DELETE. Snippets get
a floating + and cards that say RUNS or INSERTS. Files trade the seven-button block for a selection bar
(save, and a ⋮ sheet with rename, move, permissions and a red delete), folder and file tiles, an upload
button, and header actions for a terminal on the same host, refresh and close; a terminal's + sheet
opens SFTP on that terminal's own host.

The theme takes the audit's visual findings: Fluent's ClipToBounds is cleared on glowing buttons so the
glow is no longer cut to a square (the desktop's Button.accent too), prose is Montserrat with mono kept
for addresses and code, disabled secondary buttons are quieter than enabled ones, Fluent's dropdown,
checkbox, switch, focus and selection brushes come from the palette, touch no longer leaves hover fills
behind, and lists, nav entries and icon buttons carry names for TalkBack.

Builds clean; checked on the emulator end to end: unlock, the three vault views, adding and deleting a
password, connecting through the new sheet, files on the terminal's host with the selection bar and
its sheet. App tests 727 of 733 pass with 5 skipped; the one failure, TheCountsAPhoneShows_
FollowEveryForwardsState, passes three runs of three alone and is untouched here. Layout tests pass.
This commit is contained in:
jaap-jan committed 2026-10-04 15:50:09 +02:00
1 parent 6806e542f9
commit 5b32c76c95
24 files changed
+2022 -654

No files matched your search

+123 -101
View File
@@ -24,10 +24,11 @@
are the pair a session moves between; on the desktop the terminal is not a rail entry at all.
── 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. 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 rows around the screen stand down while a shell is showing: the header and the bottom bar. Both 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. (A third row, the strip of open shells, was removed by the 2026-10-04 redesign;
the count on Connections took its place — see the remark where it was.)
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.
@@ -78,7 +79,7 @@
<views:PhoneRail Grid.Column="0"
IsVisible="{Binding $parent[views:PhoneShell].ShowsRail}" />
<Grid Grid.Column="1" RowDefinitions="Auto,*,Auto,Auto">
<Grid Grid.Column="1" RowDefinitions="Auto,*,Auto">
<!--
The header: which vault, and whether it is synced.
@@ -106,34 +107,53 @@
header in HostsScreen, which the header stands down for, both move with it for the same reason;
see the remark on each.
-->
<StackPanel>
<Border Background="{StaticResource DeepChrome}" BorderBrush="{StaticResource Border}"
BorderThickness="0,0,0,1" Padding="14,0" Height="56">
BorderThickness="0,0,0,1" Padding="10,0,6,0" Height="60">
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
<!--
Filled rather than outlined since v2. The mark is the one thing on this header that is not a
fact about the vault, and the design gives it the accent as a solid tile — which is also what
the launcher icon draws, so the two agree.
-->
<Border Grid.Column="0" Width="26" Height="26" CornerRadius="8"
Background="{StaticResource Accent}" VerticalAlignment="Center">
<TextBlock Text="&gt;_" Foreground="{StaticResource AccentInk}"
FontFamily="{StaticResource MonoFont}" FontSize="10" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
◆ The vault's name is a button now, and it opens the vaults.
<TextBlock Grid.Column="1" Classes="heading" Margin="10,0,8,0" FontSize="16"
Text="{Binding Vault.VaultName}" TextTrimming="CharacterEllipsis" />
The 2026-10-04 redesign draws the name with a chevron, as the switch it is on every phone app
that holds more than one of something. What it leads to is VAULTS, which is where the vaults
this phone shows are chosen — so the chevron is a promise the screen behind it keeps, rather
than a dropdown this head has not got.
The mark stays filled, and takes the gradient Button.primary has: it is the one thing on this
header that is not a fact about the vault, and the launcher icon draws it the same way.
-->
<Button Grid.Column="0" Classes="row" MinHeight="48" Padding="4,0,8,0" CornerRadius="12"
HorizontalAlignment="Left"
AutomationProperties.Name="{Binding Vault.VaultName, StringFormat='Vault {0}. Choose vaults'}"
Command="{Binding ShowScreenCommand}"
CommandParameter="{x:Static vm:ShellScreen.Vaults}">
<StackPanel Orientation="Horizontal" Spacing="10">
<Border Width="32" Height="32" CornerRadius="10" Background="{StaticResource AccentGradient}"
VerticalAlignment="Center">
<TextBlock Text="&gt;_" Foreground="{StaticResource AccentInk}"
FontFamily="{StaticResource MonoFont}" FontSize="12" FontWeight="Bold"
HorizontalAlignment="Center" VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw" />
</Border>
<TextBlock Classes="heading" FontSize="18" FontWeight="Bold" MaxWidth="170"
Text="{Binding Vault.VaultName}" TextTrimming="CharacterEllipsis" />
<TextBlock Classes="icon" Text="&#xE5CF;" FontSize="20" Foreground="{StaticResource TextFaint}"
VerticalAlignment="Center" />
</StackPanel>
</Button>
<!--
The sync light, and it is green only when it has earned it — see SyncLabel. The design draws
a permanently green "Synced" here, which is the one claim on that mock-up this application
will not make.
-->
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<Ellipse Classes="dot" Classes.live="{Binding IsFullySynced}" Width="6" Height="6"
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6" VerticalAlignment="Center"
Margin="0,0,6,0">
<Ellipse Classes="dot" Classes.live="{Binding IsFullySynced}" Width="8" Height="8"
VerticalAlignment="Center" />
<TextBlock Classes="label" FontSize="9" Text="{Binding SyncLabel}" />
<TextBlock FontSize="12" FontWeight="SemiBold" Foreground="{StaticResource TextDim}"
Text="{Binding SyncLabel}" VerticalAlignment="Center" />
</StackPanel>
<!--
@@ -145,16 +165,49 @@
the bar it raises takes this header's place — so the pencil is in that bar, beside the count of
what it would edit. See HostActionBar.axaml, which is drawn in the Panel below this one.
What is left in this row is the vault's name, the sync light and LOCK: three facts about the
keychain, and none of them about a row.
What is left in this row is the vault's name, the sync light and the lock: three facts about
the keychain, and none of them about a row. The lock is an icon since the redesign, with its
name on it for a screen reader rather than the word LOCK in nine-pixel capitals.
-->
<Button Grid.Column="3" Classes="icon" Margin="4,0,0,0" Command="{Binding LockCommand}"
ToolTip.Tip="Lock the keychain">
<TextBlock Text="LOCK" Classes="label" FontSize="8.5"
Foreground="{StaticResource TextDim}" />
<Button Grid.Column="3" Classes="icon" Command="{Binding LockCommand}"
AutomationProperties.Name="Lock the keychain" ToolTip.Tip="Lock the keychain">
<TextBlock Classes="icon" Text="&#xE897;" FontSize="21" />
</Button>
</Grid>
</Border>
<!--
◆ HOSTS · KEYCHAIN · SNIPPETS: the vault's three kinds of content, as one switch.
The redesign's answer to the audit's M8, that SETTINGS held the product. The keychain and the
snippets were rows on the hub, a screen away from the hosts they are used with; they are the open
vault's contents as much as the hosts are, so they now sit beside them under the VAULTS tab, and the
hub keeps what is about this phone rather than about a vault.
Narrow surfaces only. A wide one has the rail, which lists all three already — see
PhoneShell.ShowsVaultTabs. The chips are RadioButtons for the reason the keychain's own are: the
three are exclusive and a screen reader should say so, and Mode=OneWay so the switch moves when the
screen does and never ahead of it.
-->
<Border IsVisible="{Binding $parent[views:PhoneShell].ShowsVaultTabs}" Padding="16,12,16,4">
<Border Classes="segmented">
<Grid ColumnDefinitions="*,*,*">
<RadioButton Grid.Column="0" Classes="segment" GroupName="vaulttab" Content="Hosts"
IsChecked="{Binding IsHostsShowing, Mode=OneWay}"
Command="{Binding ShowScreenCommand}"
CommandParameter="{x:Static vm:ShellScreen.Hosts}" />
<RadioButton Grid.Column="1" Classes="segment" GroupName="vaulttab" Content="Keychain"
IsChecked="{Binding IsKeychainShowing, Mode=OneWay}"
Command="{Binding ShowScreenCommand}"
CommandParameter="{x:Static vm:ShellScreen.Keychain}" />
<RadioButton Grid.Column="2" Classes="segment" GroupName="vaulttab" Content="Snippets"
IsChecked="{Binding IsSnippetsShowing, Mode=OneWay}"
Command="{Binding ShowScreenCommand}"
CommandParameter="{x:Static vm:ShellScreen.Snippets}" />
</Grid>
</Border>
</Border>
</StackPanel>
</Panel>
<!-- ============ ◆ the contextual action bar ============ -->
@@ -277,67 +330,14 @@
</Panel>
<!--
The shells strip. The design puts it above the nav on every screen, and that is the single most
desktop-shaped idea in the product — it is what makes a terminal a surface you switch to rather than
a screen you navigate away from. Kept, because losing it would make an open shell invisible from the
host list, which is where you are when you open the next one.
◆ THE SHELLS STRIP IS GONE, AND THE BADGE ON CONNECTIONS IS WHAT REPLACED IT.
v2 draws the sessions as pills rather than as a labelled row, and drops the word SHELLS: with a
rounded chip carrying a live dot and a name, the label was spending nine characters of a 360dp row
saying what the row already looks like.
On every screen except the one it names. The terminal draws these same sessions in its own bar, and
two rows of the same pills — one of them 46 pixels of it — is the arrangement this surface exists to
stop. Wrapped rather than given a second condition, because the strip's own visibility is about
whether there are any tabs and this one is about which surface is up.
◆ And it stands down under the host editor, which is a page rather than a card since the pencil moved
into the action bar. A strip of open shells above a form is the same two-rows-of-chrome problem the
terminal has, on a screen where the form is the whole point of being there. See
PhoneShell.ShowsShellStrip, which is where that "and" is made, Avalonia's bindings having none.
It sat above the bar on every page — the single most desktop-shaped idea in the product, kept so that
an open shell was never invisible from the host list. The 2026-10-04 redesign keeps that promise with
less: Connections carries the number of open sessions in a green badge, and one tap on it is the
sessions themselves, in the terminal's own bar. That is 46 pixels of every page given back to the list,
and one row of the same pills where there were two on the way into a shell.
-->
<Panel Grid.Row="2" IsVisible="{Binding $parent[views:PhoneShell].ShowsShellStrip}">
<!-- DeepChrome rather than Sidebar, since v5, joining the header and the bar above and below it:
the strip is chrome the same way they are — a frame around the screen rather than a pane of
it — and Sidebar is what a card sits on, which this row is not. -->
<Border IsVisible="{Binding HasTabs}" Background="{StaticResource DeepChrome}"
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0" Height="46">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled">
<ItemsControl ItemsSource="{Binding Tabs}" Margin="12,0" VerticalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="6" /></ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:TerminalTabViewModel">
<Button Classes="row" MinHeight="34" Padding="13,0" CornerRadius="10"
Background="{StaticResource Panel}" BorderBrush="{StaticResource BorderMid}"
BorderThickness="1"
Command="{Binding $parent[views:PhoneShell].((vm:MainWindowViewModel)DataContext).SelectTabCommand}"
CommandParameter="{Binding}">
<StackPanel Orientation="Horizontal" Spacing="7" VerticalAlignment="Center">
<!--
Green only while there is a shell behind the tab. It used to be lit unconditionally,
which was true when a tab could not exist without a session; one can now —
connecting opens the tab first — and a dot that was green before anything had
answered would be the one thing on this strip claiming something untrue.
Amber while it is being made, which is the other half of that correction. Not being
green stopped the dot lying, but it left a tab still dialling drawn exactly like a
tab whose shell has exited — the two states on this strip with the least in common,
one worth waiting for and one over. See Phone.axaml.
-->
<Ellipse Classes="dot" Classes.live="{Binding IsLive}"
Classes.connecting="{Binding IsConnecting}" Width="6" Height="6"
VerticalAlignment="Center" />
<TextBlock Classes="mono" FontSize="11" Text="{Binding Label}" />
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</Panel>
<!-- ============ navigation ============ -->
<!--
@@ -353,20 +353,26 @@
◆ 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.
── three rather than four ─────────────────────────────────────────────────────────────────────────
The keychain moved behind SETTINGS. A bottom bar is for the places a session moves between, and keys,
credentials and tags are not that — they are managed occasionally and then left alone, which is the
shape of everything else already behind the hub. What is left is the two halves of using this
application, and the drawer holding the rest.
── VAULTS · CONNECTIONS · SETTINGS ────────────────────────────────────────────────────────────────
Three entries still, and the first is the open vault rather than its host list: hosts, the keychain
and snippets are a switch at the top of it. The keychain had moved behind SETTINGS on the argument that
a bottom bar is for the places a session moves between; the redesign keeps the bar at three and puts the
vault's own contents back one tap from anywhere, which is what the audit's M8 asked for. 72 tall, for
the pill and a 12-pixel label under it — see NavButton.
-->
<!-- DeepChrome rather than Chrome, since v5 — see the remark on the vault header, above, which this
bar is the foot of the same frame the header is the top of. -->
<Border Grid.Row="3" IsVisible="{Binding $parent[views:PhoneShell].ShowsBottomBar}"
<Border Grid.Row="2" IsVisible="{Binding $parent[views:PhoneShell].ShowsBottomBar}"
Background="{StaticResource DeepChrome}" BorderBrush="{StaticResource Border}"
BorderThickness="0,1,0,0" Height="64">
BorderThickness="0,1,0,0" Height="72">
<Grid ColumnDefinitions="*,*,*">
<views:NavButton Grid.Column="0" Label="Hosts" Glyph="▤" IsCurrent="{Binding IsHostsShowing}"
<!--
IsVaultSurface rather than IsHostsShowing, for the reason SETTINGS reads IsMoreSurface: the tab
stands for three screens, and it stays lit on whichever of them is up. A tap goes to the hosts,
which is where the application opens and what the vault's switch starts on.
-->
<views:NavButton Grid.Column="0" Label="Vaults" Glyph="&#xE1A1;" IsCurrent="{Binding IsVaultSurface}"
Command="{Binding ShowScreenCommand}"
CommandParameter="{x:Static vm:ShellScreen.Hosts}" />
@@ -386,9 +392,11 @@
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.
◆ The count is the open tabs, and it is what the shells strip used to say. See the remark above.
-->
<views:NavButton Grid.Column="1" Label="Connections" Glyph="⌗"
IsCurrent="{Binding IsTerminalSurface}"
<views:NavButton Grid.Column="1" Label="Connections" Glyph="&#xEB8E;"
IsCurrent="{Binding IsTerminalSurface}" Count="{Binding Tabs.Count}"
Command="{Binding ShowTerminalCommand}" />
<!--
@@ -396,18 +404,32 @@
and a bar that went dark the moment you opened one of them would only ever light two of its
three entries.
"Settings" and a gear, where it was "More" and a hamburger. The hub held a handful of things
that did not fit and could not be called anything better than More; with the keychain, teams and
preferences all on it, it is the place everything that is not connecting to something lives —
which is what a gear has meant on a phone since before this application existed.
"Settings" and a gear, where it was "More" and a hamburger. With the vault's contents moved under
VAULTS, what is behind it is about this phone and this account — security, the terminal's
behaviour, which vaults are shown, the logs — which is what a gear has meant on a phone since
before this application existed.
-->
<views:NavButton Grid.Column="2" Label="Settings" Glyph="⚙" IsCurrent="{Binding IsMoreSurface}"
<views:NavButton Grid.Column="2" Label="Settings" Glyph="&#xE8B8;" IsCurrent="{Binding IsMoreSurface}"
Command="{Binding ShowScreenCommand}"
CommandParameter="{x:Static vm:ShellScreen.More}" />
</Grid>
</Border>
<!--
◆ THE REST OF THE SCRIM, over the two rows of chrome a sheet on the hosts screen cannot reach.
The hosts screen's sheets are drawn inside the screen's own row, so their scrim dimmed the list and
left the header — with the lock on it — and the bottom bar bright and live above and below every
sheet (audit M12). These two finish the job in the same colour, declared last so they draw over
the header and the bar, and a tap on either does what a tap on the sheet's own scrim does: lowers
the sheet. See PhoneShell.ShowsSheetScrim and OnChromeScrimTapped.
-->
<Button Grid.Row="0" Classes="scrim" IsVisible="{Binding $parent[views:PhoneShell].ShowsSheetScrim}"
Click="OnChromeScrimTapped" AutomationProperties.Name="Close the sheet" />
<Button Grid.Row="2" Classes="scrim" IsVisible="{Binding $parent[views:PhoneShell].ShowsSheetScrim}"
Click="OnChromeScrimTapped" AutomationProperties.Name="Close the sheet" />
</Grid>
</Grid>