Give the terminal back the width and the keyboard the session shell took
ci / build and test (pull_request) Failing after 2m33s
ci / desktop nightly (pull_request) Skipped
ci / api image (pull_request) Skipped
ci / android head (pull_request) Successful in 3m34s

Seven things reported from a day's use of v5b's session shell, and they are one
commit because five of them are the same complaint from different angles: the
window spends too much of itself on chrome describing the session, and the parts
that are not chrome do not behave.

◆ THE HOST HEADER IS GONE, and that is a deliberate departure from the design.

Terminal.dc.html and SFTP.dc.html both draw a 60-pixel row above the pane: the
address on the left, a cross-surface button on the right. Both facts are worth
having and the strip they sat in was not — the tab already names the host, and a
full-width bar repeating it was the cheapest 60 pixels in the layout to give
back. The address is now the first line of the sidebar and the button is
stretched across the column under it, so nothing is lost and the pane is taller.

Which word that button carries and which command it runs used to be handed in
from the two usage sites in MainWindow.axaml, because the control was drawn
twice. One sidebar cannot do that, so SessionCrossSurfaceLabel and
OpenOtherSurfaceCommand resolve it in the shell — the same place SessionAddress
already decides which surface's fact to read. The two directions underneath are
untouched: SelectFilesHostAsync for a tab's host, OpenTerminalForFilesHostAsync
for a fresh terminal at whatever SFTP has open.

SessionHeader.axaml is deleted rather than left unused, and LayoutHarness stops
subtracting its 60 pixels from every session screen's budget — the same
treatment the retired window-wide tab strip got, and for the same reason: a
constant for chrome nobody draws is a suite quietly measuring the wrong
rectangle.

◆ AND THE SIDEBAR CLOSES, which the design has no state for at all.

300 pixels of an 1081-pixel minimum is a great deal to spend on a list that is
often two rows long. The column now folds to a 34-pixel rail carrying the
chevron that brings it back — a rail rather than nothing, because a panel that
vanishes leaving no trace is one people report as lost rather than as closed.
Both states live in the one control and swap on IsSessionSidebarOpen, so
MainWindow's own "Auto" column takes whichever width is showing without knowing
the state exists.

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.

---- THE FOUR SMALLER ONES ----

A SNIP LANDED IN A TERMINAL NOBODY COULD TYPE AT, and looked selected when it
got there. Two causes with nothing in common. The click moved Win32 focus onto
the sidebar row, and term.focus() in the page cannot take it back — only the
host can, so the shell raises TerminalFocusRequested and the window answers with
the same posted focus every other path here uses. The highlight was bash: xterm
wraps a paste in bracketed-paste markers, readline marks what arrives inside
them as an active region, and it stays in reverse video until the next
keystroke. Right for a clipboard paste, wrong for a snippet picked off a
sidebar. Single-line snips are typed rather than pasted now, which needs no
markers; multi-line still pastes, because "runs three commands unasked" is the
worse of the two failures and the markers are the whole of what prevents it.

A BLACK BAR UNDER THE TERMINAL, on Windows. xterm.css paints its scrolling
viewport #000 — its own comment explains why, and it is a macOS scrollbar
concern. Everywhere else that black is covered by the rows, except along the
bottom: the fit addon floors the row count, so the remainder below the last
whole row is bare viewport, up to a line tall, against this page's #171a26. The
light square at its right-hand end is where WebView2's classic scrollbar corner
lands. The viewport is repainted in the page's own background, and the scrollbar
with it — thin and in these colours rather than a grey Windows channel down the
side of a near-black terminal, and kept rather than hidden, because a surface
that scrolls with no sign that it does is worse than a quiet bar.

THE PINS ROW DREW A TOFU BOX. U+E946 is not in the embedded Material Icons face
at all — that file is the 2019 build and its cmap skips E944 and E946 — so the
rail's Pins row and the hosts screen's own pin badge have both been drawing a
missing-glyph rectangle since v5b picked the codepoint. push_pin in that vintage
is U+F10D, verified against the file rather than against a codepoints table for
a later release of the font. Every other icon codepoint in the repository was
audited the same way; this was the only miss.

THE KBD CHIP CUT THE CHORD IN HALF. 34 pixels is the design's width for a chip
reading ⌘K, and this build substitutes CTRL K — six characters and a space,
wider than 34 at 10.5 mono. MinWidth and padding instead, so the design's
footprint survives for the day this face has a ⌘ to draw.

---- AND THE POPOVER UNDER THE USER CHIP ----

Reported as not matching the design, and it was not: Button.poprow set a corner
radius and a padding and never touched the Background, so every row wore the
Fluent theme's own #33FFFFFF button fill. Six raised pills stacked in a menu the
design draws as six lines of text — and the hover rule underneath was already
correct and simply invisible against a fill that never went away. Set on the
ContentPresenter as well as on the Button, the same as Button.flat, because the
theme binds its brush there and a Background set only on the control loses to
it. The panel itself gets this window's own radius-12 card treatment through a
FlyoutPresenter class rather than by widening the shared context-menu rule, and
Vaults and Preferences stop being drawn one step dimmer than Settings and
Logout, which read as two disabled entries in a menu of five live ones.

---- WHAT PROVES IT ----

Three tests in the layout suite, two of them checked against the defect they
describe: the popover row's resting fill (fails with #33ffffff without the
style), and the kbd chip against the natural width of its own text, measured on
a detached copy because a TextBlock's DesiredSize is already clipped to what it
was given and reports 34 inside a 34-pixel chip either way. SessionSidebarTests
is new — the sidebar has never been laid out by a test, and it now holds a
string of unbounded length beside a button that has to stay clickable. In the
shell suite: the cross-surface row in both directions, the closed state
surviving to disk, and the focus request being made when a snip lands and not
made when it does not.
This commit is contained in:
2026-08-10 16:49:44 +02:00
parent 6fc1e3a7c5
commit ccaf7a8e72
22 changed files with 865 additions and 216 deletions
+124 -59
View File
@@ -19,70 +19,100 @@
Every row here is a command the shell already exposes for exactly this purpose — see
MainWindowViewModel.PinFolderFromSidebarCommand, AddSnippetFromSidebarCommand and InsertSnippetCommand —
so this control carries no logic of its own beyond the list it draws and the click it forwards.
── v5c-4: THE SESSION BLOCK AT THE HEAD, AND THE HEADER ROW THAT IS GONE ─────────────────────────────────
The 60-pixel host header that used to sit above the pane on both surfaces has been retired, and its two
contents moved up here: the address it printed, and the cross-surface button — "Open SFTP" from a
terminal, "Open terminal" from SFTP. Which of the two words it is and which command it runs are resolved
by the shell now rather than handed in from the two usage sites; see
MainWindowViewModel.SessionCrossSurfaceLabel and OpenOtherSurfaceCommand. The pane keeps that height.
The address is the fact the header row existed for, so it moves rather than disappears. It sits where the
QUICK ACCESS heading used to print the selected tab's short label — that label said less than the address
does and would be the same word twice beside it.
── AND THE COLUMN CLOSES ────────────────────────────────────────────────────────────────────────────────
300 pixels is a lot of a 1180-pixel window to give a list that is often two rows long, so the column
folds to a 34-pixel rail carrying the way back. A rail rather than nothing: a panel that vanishes without
trace is one people report as lost. Both halves live in this control and swap on
MainWindowViewModel.IsSessionSidebarOpen, so MainWindow.axaml's own "Auto" column takes whichever width
is showing without knowing anything about the state — and the pane beside it grows into what is freed.
-->
<Border Width="300" Background="{StaticResource Sidebar}"
BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="6" Margin="16,20">
<Panel>
<!-- ============ QUICK ACCESS ============ -->
<Grid ColumnDefinitions="*,Auto" Margin="8,0">
<TextBlock Grid.Column="0" Classes="label" Text="QUICK ACCESS" FontSize="10" />
<TextBlock Grid.Column="1" Classes="mono" FontSize="10"
Foreground="{StaticResource TextGhost}"
Text="{Binding SelectedTab.Label}" TextTrimming="CharacterEllipsis" MaxWidth="130" />
</Grid>
<!-- ============ THE RAIL, WHEN THE COLUMN IS CLOSED ============ -->
<!--
Painted and bordered like the open column so the closing reads as the same surface narrowing rather
than as one piece of furniture being swapped for another.
-->
<Border Width="34" Background="{StaticResource Sidebar}"
BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0"
IsVisible="{Binding !IsSessionSidebarOpen}">
<Button Classes="flat sidebargrip" VerticalAlignment="Top" Margin="0,20,0,0"
Command="{Binding ToggleSessionSidebarCommand}"
ToolTip.Tip="Show quick access, snips and the way across to the other surface">
<TextBlock Text="&#xE5CB;" FontFamily="{StaticResource IconFont}" FontSize="18"
Foreground="{StaticResource TextFaint}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Button>
</Border>
<ItemsControl ItemsSource="{Binding ActiveTabPinnedPaths}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="x:String">
<!--
A single level of $parent[ItemsControl] reaches the shell directly, the same way the old pin
strip's chips did: this ItemsControl's own DataContext is MainWindowViewModel, so one hop up
from the path's string DataContext lands on it.
-->
<Button Classes="sidebarrow"
Command="{Binding $parent[ItemsControl].((vm:MainWindowViewModel)DataContext).OpenPinnedPathCommand}"
CommandParameter="{Binding}"
ToolTip.Tip="{Binding}">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="&#xE2C7;" FontFamily="{StaticResource IconFont}" FontSize="15"
Foreground="{StaticResource AccentText}" VerticalAlignment="Center" />
<TextBlock FontFamily="{StaticResource MonoFont}" FontSize="12.5" Text="{Binding}"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- ============ THE COLUMN ============ -->
<Border Width="300" Background="{StaticResource Sidebar}"
BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0"
IsVisible="{Binding IsSessionSidebarOpen}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="6" Margin="16,20">
<Button Classes="sidebaradd" Command="{Binding PinFolderFromSidebarCommand}"
ToolTip.Tip="Opens the active tab's host for editing, at QUICK ACCESS.">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="&#xE145;" FontFamily="{StaticResource IconFont}" FontSize="14"
VerticalAlignment="Center" />
<TextBlock Text="Pin folder" VerticalAlignment="Center" />
</StackPanel>
</Button>
<!-- ============ THE SESSION ============ -->
<!--
The address, and the button that closes the column. Both on one row, and the address is the
trimming one: a long account@host:port is exactly the string that would otherwise push the
close button off the edge of a panel whose whole point is that it can be got rid of.
-->
<Grid ColumnDefinitions="*,Auto" Margin="8,0,0,0">
<TextBlock Grid.Column="0" FontFamily="{StaticResource MonoFont}" FontWeight="Bold"
FontSize="12.5" Foreground="{StaticResource AccentText}"
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
Text="{Binding SessionAddress}" ToolTip.Tip="{Binding SessionAddress}" />
<Button Grid.Column="1" Classes="flat sidebargrip"
Command="{Binding ToggleSessionSidebarCommand}"
ToolTip.Tip="Close this column. The terminal takes the width, and the rail it leaves behind brings it back.">
<TextBlock Text="&#xE5CC;" FontFamily="{StaticResource IconFont}" FontSize="18"
Foreground="{StaticResource TextFaint}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Button>
</Grid>
<!-- ============ SNIPS (the terminal surface only) ============ -->
<StackPanel Spacing="6" Margin="0,16,0,0" IsVisible="{Binding #Root.ShowsSnips}">
<!--
The cross-surface button, stretched across the column rather than sized to its own caption: it
is the one action in this panel that is not a list row, and a 90-pixel button floating at the
left of a 300-pixel column would read as unfinished.
-->
<Button Classes="headerghost" HorizontalAlignment="Stretch" Margin="0,4,0,10"
Content="{Binding SessionCrossSurfaceLabel}"
Command="{Binding OpenOtherSurfaceCommand}" />
<TextBlock Classes="label" Text="SNIPS" FontSize="10" Margin="8,0" />
<!-- ============ QUICK ACCESS ============ -->
<TextBlock Classes="label" Text="QUICK ACCESS" FontSize="10" Margin="8,0" />
<ItemsControl ItemsSource="{Binding SnippetsScreen.Visible}">
<ItemsControl ItemsSource="{Binding ActiveTabPinnedPaths}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:SnippetRowViewModel">
<DataTemplate x:DataType="x:String">
<!--
A single level of $parent[ItemsControl] reaches the shell directly, the same way the old pin
strip's chips did: this ItemsControl's own DataContext is MainWindowViewModel, so one hop up
from the path's string DataContext lands on it.
-->
<Button Classes="sidebarrow"
Command="{Binding $parent[ItemsControl].((vm:MainWindowViewModel)DataContext).InsertSnippetCommand}"
Command="{Binding $parent[ItemsControl].((vm:MainWindowViewModel)DataContext).OpenPinnedPathCommand}"
CommandParameter="{Binding}"
ToolTip.Tip="{Binding Snippet.Command}">
ToolTip.Tip="{Binding}">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="{}{ }" FontFamily="{StaticResource MonoFont}" FontWeight="Bold"
FontSize="11" Foreground="{StaticResource AccentText}"
VerticalAlignment="Center" />
<TextBlock FontFamily="{StaticResource MonoFont}" FontSize="12.5" Text="{Binding Label}"
<TextBlock Text="&#xE2C7;" FontFamily="{StaticResource IconFont}" FontSize="15"
Foreground="{StaticResource AccentText}" VerticalAlignment="Center" />
<TextBlock FontFamily="{StaticResource MonoFont}" FontSize="12.5" Text="{Binding}"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
</StackPanel>
</Button>
@@ -90,19 +120,54 @@
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Classes="sidebaradd" Command="{Binding AddSnippetFromSidebarCommand}"
ToolTip.Tip="Opens the snippet editor.">
<Button Classes="sidebaradd" Command="{Binding PinFolderFromSidebarCommand}"
ToolTip.Tip="Opens the active tab's host for editing, at QUICK ACCESS.">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="&#xE145;" FontFamily="{StaticResource IconFont}" FontSize="14"
VerticalAlignment="Center" />
<TextBlock Text="Add Snip" VerticalAlignment="Center" />
<TextBlock Text="Pin folder" VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
<!-- ============ SNIPS (the terminal surface only) ============ -->
<StackPanel Spacing="6" Margin="0,16,0,0" IsVisible="{Binding #Root.ShowsSnips}">
</StackPanel>
</ScrollViewer>
</Border>
<TextBlock Classes="label" Text="SNIPS" FontSize="10" Margin="8,0" />
<ItemsControl ItemsSource="{Binding SnippetsScreen.Visible}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:SnippetRowViewModel">
<Button Classes="sidebarrow"
Command="{Binding $parent[ItemsControl].((vm:MainWindowViewModel)DataContext).InsertSnippetCommand}"
CommandParameter="{Binding}"
ToolTip.Tip="{Binding Snippet.Command}">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="{}{ }" FontFamily="{StaticResource MonoFont}" FontWeight="Bold"
FontSize="11" Foreground="{StaticResource AccentText}"
VerticalAlignment="Center" />
<TextBlock FontFamily="{StaticResource MonoFont}" FontSize="12.5" Text="{Binding Label}"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Classes="sidebaradd" Command="{Binding AddSnippetFromSidebarCommand}"
ToolTip.Tip="Opens the snippet editor.">
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="&#xE145;" FontFamily="{StaticResource IconFont}" FontSize="14"
VerticalAlignment="Center" />
<TextBlock Text="Add Snip" VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Border>
</Panel>
</UserControl>