Give the user menu one door into settings instead of three
ci / build and test (push) Successful in 2m39s
ci / desktop nightly (push) Failing after 34s
ci / api image (push) Failing after 10s
ci / android head (push) Successful in 4m5s

Settings, Vaults and Preferences all opened the same full-window settings mode
and differed only in which of its seven pages they landed on. SettingsNav, which
lists every one of those pages including Vaults and Preferences, is on screen the
whole time you are in there — so two of the three rows were deep links into a
place the third one opens. They saved one click and cost a menu that read as
though it offered three destinations.

It was three rows for a reason that had stopped applying. Until v5c, "Settings"
and "Preferences" were two doors onto the same bare Preferences screen, and
splitting them was the fix for something real; Vaults arrived beside them when
VaultsScreen became a settings page too. Folding those two back in is not a
return to that state. They are pages now, with a rail of their own, and what a
menu at the foot of the nav rail is for is getting into settings at all.

Nothing became unreachable — both are one click deeper, from the rail that was
always going to be the way to them.

◆ WHAT STAYS, AND WHY IT IS NOT THE SAME ARGUMENT.

Logout stays and still lands on the Account page rather than on General, because
its confirmation card is drawn inline on that one page while the vault is
unlocked; arming it from anywhere else would raise a card nobody could see. The
vault switches and New vault above the divider stay too — neither is a page of
the settings mode, and the switches have no other home.

◆ THE ROWS ARE REMOVED FROM THE MARKUP, NOT UNWIRED.

NavRailTests asserts their absence rather than only asserting that Settings still
works, because the failure that guards against is silent: a row left in the
markup with its handler deleted is a menu entry that does nothing when pressed,
which reads as a broken application rather than a tidied menu. The helper it
asserts through excludes the vault switches by their data context and not by
their label — a vault is free to be called "Vaults", and a test that a menu entry
is absent must not be defeatable by what somebody named a vault.

Two facts go, one comes back plus the absence check; the layout suite is 166.

Manual check 1.8 walked a reader through all three rows in turn and now walks
them through the one, then through SettingsNav's seven — which is where the two
went. design-import-gaps.md gains a v5c-5 section rather than an edit to the v5c
table: what that table records is what shipped in that pass, and it did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
jaap-janandClaude Opus 5 committed 2026-08-26 11:01:54 +02:00
1 parent c270f3fe39
commit ed936380b3
5 files changed
+94 -78

No files matched your search

+24
View File
@@ -741,3 +741,27 @@ to a 34-pixel rail carrying the chevron that brings it back — a rail rather th
that vanishes without trace is one people report as lost. The choice is written through to
`ClientSettings.SessionSidebarOpen` rather than held for the session: it is a decision about how much of the
window a terminal gets, and one that had to be made again on every launch would not really be on offer.
## v5c-5 — one door to settings, not three
**The user-chip popover keeps its Settings row and has lost Vaults and Preferences.** v5c gave it three, and
the table above records why: until that pass "Settings" and "Preferences" had been two doors onto the same
bare screen, so splitting them was a real fix, and Vaults arrived beside them when `VaultsScreen` became a
settings page too.
Living with it made the shape of the mistake clear. All three opened the same full-window mode and differed
only in which of its seven pages they landed on — and `SettingsNav`, which lists every one of those pages
including Vaults and Preferences, is on screen the entire time you are in there. So two of the three rows
were deep links into a place the third one opens, saving one click and costing a menu that read as though it
offered three destinations. A menu at the foot of the rail is for getting into settings at all; which page
you want is a question that has a rail of its own once you are there.
Nothing became unreachable: both pages are one click deeper, from the rail that was always going to be the
way to them. `NavRailTests` asserts the rows are gone from the markup rather than merely unwired, because a
row left behind with no handler is a menu entry that does nothing when pressed — which reads as a broken
application rather than a tidied menu.
Logout stays, and still lands on a page rather than on General: its confirmation card is drawn inline on the
Account page and nowhere else, so arming it from anywhere else would be a card raised where nobody could see
it. The vault switches and **New vault** above the divider stay too — they are what the popover is for
besides settings, and neither is a page of the settings mode.
+12 -9
View File
@@ -118,15 +118,17 @@ view models — this check is the version with a real connection on the end of i
### 1.8 Entering and leaving settings mode
From HOSTS, press the user chip at the foot of the nav rail and choose **Settings**. Then repeat from a
fresh open, choosing **Vaults**, and again choosing **Preferences**.
From HOSTS, press the user chip at the foot of the nav rail and choose **Settings** — which is the menu's
only way in now; the Vaults and Preferences rows beside it were folded back into it, because all three
opened this same mode and differed only in which page they landed on.
**Pass:** each of the three swaps the whole window's chrome — the titlebar becomes the 53px "Back to
application" bar, the ordinary nav rail is replaced by the 340px SettingsNav, and the status bar and the
update banner (if either was showing) are both gone. **Settings** lands on General; **Vaults** and
**Preferences** land on their own pages, each lit in SettingsNav's own list — General/Vaults/Account under
SETTINGS, Security/Preferences/Groups/Tags under CUSTOMIZE, Logout pinned below both. Click every one of the
seven rows in turn: each swaps the content column without leaving settings mode or touching the titlebar.
**Pass:** it swaps the whole window's chrome — the titlebar becomes the 53px "Back to application" bar, the
ordinary nav rail is replaced by the 340px SettingsNav, and the status bar and the update banner (if either
was showing) are both gone. It lands on **General**, with SettingsNav's own list beside it —
General/Vaults/Account under SETTINGS, Security/Preferences/Groups/Tags under CUSTOMIZE, Logout pinned below
both. Click every one of the seven rows in turn: each swaps the content column without leaving settings mode
or touching the titlebar, and Vaults and Preferences are among them — which is the whole reason the popover
no longer carries its own rows for those two.
Now open a terminal, leave it showing, and open Settings from the user chip again. Press **Back to
application** — or, without touching anything else, press Esc.
@@ -134,7 +136,8 @@ application** — or, without touching anything else, press Esc.
**Pass:** both return to exactly the screen that was showing before Settings was opened — the terminal,
still running, rather than HOSTS or anywhere else. Switching between settings pages first (General → Vaults
→ Preferences) does not change what "back" goes back to; only the screen open the moment Settings was
**first** entered does.
**first** entered does. The other doors into this mode — the palette, and the Preferences page's own two
importer rows — behave the same way, so the popover having one row rather than three changes nothing here.
**Failure means:** a return to HOSTS regardless of where Settings was opened from is
`MainWindowViewModel.settingsReturnScreen` being recaptured on every `EnterSettings` call rather than only on
+17 -30
View File
@@ -252,13 +252,23 @@
<Border Height="1" Margin="11,4" Background="{StaticResource BorderMid}" />
<!--
v5c: Settings, Vaults and Preferences now each land on their own page of the settings mode —
see MainWindowViewModel.EnterSettings and SettingsView.axaml. Settings opens on General, the
mode's own default landing page; Vaults and Preferences open directly on the page they name,
which is also what anything that used to navigate to ShellScreen.Vaults or
ShellScreen.Preferences now does — see ShowScreen. Three handlers rather than the two rows
sharing one before this wave: the mock's Settings area was a family of screens that did not
exist yet, and now that it does, "Settings" and "Preferences" are no longer the same click.
◆ ONE ROW, WHERE v5c PUT THREE.
Settings, Vaults and Preferences all opened the same full-window settings mode — see
MainWindowViewModel.EnterSettings and SettingsView.axaml — differing only in which of its
seven pages they landed on. Two of the three were therefore deep links into a place the third
one opens, and SettingsNav is already on screen the whole time listing every page including
those two, so the shortcut saved one click and cost a menu that read as though it offered
three destinations.
It was three rows because settings mode had only just been built out of what used to be two
bare ShellScreens: "Settings" and "Preferences" were the same click before that wave, and
splitting them was the fix for a real thing. Folding the other two back in is not a return to
that state — Vaults and Preferences are pages now, reached from the rail inside settings, and
what this menu is for is getting there at all.
What still lands on a page rather than on General is Logout below, and it has to: its
confirmation card is drawn inline on the Account page and nowhere else.
-->
<Button Classes="poprow" Click="OnPopoverSettingsPressed">
<StackPanel Orientation="Horizontal" Spacing="10">
@@ -268,29 +278,6 @@
</StackPanel>
</Button>
<!--
◆ THE SAME TREATMENT AS SETTINGS ABOVE AND LOGOUT BELOW, which these two did not have: their
label was TextGhost where the other two rows' was Text, so a menu of five equally live
destinations drew two of them in the colour this window uses for something switched off. The
icons stay one step quieter than the words — the idiom the nav rail's own rows already follow
— but "quieter than the word beside it" and "dimmed" are not the same statement.
-->
<Button Classes="poprow" Click="OnPopoverVaultsPressed">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock FontFamily="{StaticResource IconFont}" Text="&#xE84F;" FontSize="12"
Foreground="{StaticResource TextGhost}" />
<TextBlock Text="Vaults" FontSize="10" Foreground="{StaticResource Text}" />
</StackPanel>
</Button>
<Button Classes="poprow" Click="OnPopoverPreferencesPressed">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock FontFamily="{StaticResource IconFont}" Text="&#xE429;" FontSize="12"
Foreground="{StaticResource TextGhost}" />
<TextBlock Text="Preferences" FontSize="10" Foreground="{StaticResource Text}" />
</StackPanel>
</Button>
<Border Height="1" Margin="11,4" Background="{StaticResource BorderMid}" />
<!--
@@ -59,28 +59,6 @@ internal sealed partial class NavRail : UserControl
}
}
/// <summary>Opens settings mode on its Preferences page.</summary>
private void OnPopoverPreferencesPressed(object? sender, RoutedEventArgs e)
{
ClosePopover();
if (DataContext is MainWindowViewModel shell)
{
shell.EnterSettingsCommand.Execute(SettingsPage.Preferences);
}
}
/// <summary>Opens settings mode on its Vaults page.</summary>
private void OnPopoverVaultsPressed(object? sender, RoutedEventArgs e)
{
ClosePopover();
if (DataContext is MainWindowViewModel shell)
{
shell.EnterSettingsCommand.Execute(SettingsPage.Vaults);
}
}
/// <summary>
/// Starts a sign-out, through the Account settings page so the confirmation card has somewhere to be
/// seen — see <see cref="MainWindowViewModel.SignOutFromPopover"/>.
@@ -210,32 +210,38 @@ public sealed class NavRailTests : IAsyncLifetime
}
/// <summary>
/// Settings, Vaults and Preferences each enter settings mode on the page they promise, and shut the
/// popover behind them.
/// Settings enters settings mode on its landing page, and shuts the popover behind it.
/// </summary>
/// <remarks>
/// <para>
/// v5c: these three used to land on a bare <c>ShellScreen</c> — Settings and Preferences on the very
/// same one, since the mock's own Settings area did not exist yet. Now that it does, each opens the
/// settings mode on its own page — see <see cref="MainWindowViewModel.EnterSettings"/> — and "Settings"
/// and "Preferences" are no longer the same click.
/// </para>
/// <para>
/// Three <see cref="Fact"/>s over one private body rather than a <see cref="Theory"/>: <c>SettingsPage</c>
/// is <c>internal</c>, and a public theory method may not carry an internal type in its signature.
/// </para>
/// v5c gave this menu three navigation rows — Settings, Vaults and Preferences — because settings mode
/// had just been built out of what were two bare <c>ShellScreen</c>s, and "Settings" and "Preferences"
/// had been the same click until then. All three opened the same mode and differed only in which page
/// they landed on, which made two of them deep links into a place the third one opens, past a
/// <c>SettingsNav</c> that lists every page anyway. One row now; see <c>NavRail.axaml</c>'s own remark.
/// </remarks>
[Fact]
public Task ThePopoversSettingsRow_EntersSettingsOnGeneralAndClosesThePopover() =>
APopoverRowLandsOnAsync("Settings", SettingsPage.General);
/// <summary>
/// The two rows that were folded into it are gone from the menu rather than merely unwired.
/// </summary>
/// <remarks>
/// Worth a test of its own because the failure it guards is silent: a row left in the markup and
/// stripped of its handler is a menu entry that does nothing when pressed, which reads as a broken
/// application rather than as a tidied menu. Both destinations are still reachable — one click deeper,
/// from <c>SettingsNav</c> — which is the whole argument for removing them from here.
/// </remarks>
[Fact]
public Task ThePopoversVaultsRow_EntersSettingsOnVaultsAndClosesThePopover() =>
APopoverRowLandsOnAsync("Vaults", SettingsPage.Vaults);
public Task ThePopoverNoLongerOffersVaultsOrPreferencesOfItsOwn() =>
OnTheRailAsync((rail, window) =>
{
Click(UserChip(rail), window);
[Fact]
public Task ThePopoversPreferencesRow_EntersSettingsOnPreferencesAndClosesThePopover() =>
APopoverRowLandsOnAsync("Preferences", SettingsPage.Preferences);
PopoverRows(window).ShouldNotContain("Vaults");
PopoverRows(window).ShouldNotContain("Preferences");
PopoverRows(window).ShouldContain("Settings");
});
private Task APopoverRowLandsOnAsync(string label, SettingsPage target) =>
OnTheRailAsync((rail, window) =>
@@ -369,6 +375,24 @@ public sealed class NavRailTests : IAsyncLifetime
.OfType<TextBlock>()
.Any(text => string.Equals(text.Text, label, StringComparison.Ordinal)));
/// <summary>
/// Every word the open popover offers as a place to go.
/// </summary>
/// <remarks>
/// The vault switches are excluded by their data context rather than by their label, because a vault is
/// free to be called anything — including "Vaults" — and a test asserting that a menu entry is absent
/// must not be satisfiable or defeatable by what somebody named a vault.
/// </remarks>
private static IReadOnlyList<string> PopoverRows(Visual window) =>
[.. window.GetVisualDescendants()
.OfType<Button>()
.Where(button => button.Classes.Contains("poprow")
&& button.DataContext is not VaultToggleViewModel)
.SelectMany(button => button.GetVisualDescendants().OfType<TextBlock>())
.Select(text => text.Text)
.Where(text => !string.IsNullOrEmpty(text))
.Select(text => text!)];
/// <summary>Whether a vault row's magenta check square is drawn, for whether it is currently shown.</summary>
private static bool HasVisibleCheck(Button row) =>
row.GetVisualDescendants().OfType<Border>().Any(