Merge branch 'claude/host-connection-top-bar-25d04e'
ci / build and test (push) Successful in 1m23s
ci / android head (push) Failing after 5s
ci / api image (push) Successful in 23s

This commit is contained in:
2026-08-03 14:33:52 +02:00
9 changed files with 591 additions and 172 deletions
@@ -795,8 +795,15 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
/// and a child window composites above everything its parent paints — so whatever Avalonia draws in the
/// same rectangle is drawn underneath it and its buttons cannot be clicked. Anything that covers the
/// terminal's area has to collapse the terminal instead, and that is every one of the conditions here: a
/// locked vault (the unlock card), the page area (every screen uses the full width), and the
/// quick-connect palette.
/// locked vault (the unlock card), the page area (every screen uses the full width), the quick-connect
/// palette, and the phone's connect sheet.
/// </para>
/// <para>
/// <b>The sheet is here rather than in <see cref="IsTerminalSurface"/>, and the palette is not.</b> The
/// palette replaces the whole surface, so collapsing everything the terminal half draws is right. The
/// sheet is raised from the terminal's own top bar and that bar has to stay on screen behind it —
/// dropping the surface would take the bar, the tabs and the phone's whole chrome with it and leave the
/// sheet floating over the page underneath. So only the renderer's rectangle is given up.
/// </para>
/// <para>
/// <b>The terminal and the pages are exclusive, and that is the whole of the rule.</b> They share one
@@ -825,7 +832,8 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
/// safe — that detaches it and destroys the whole WebView2 process tree.
/// </para>
/// </remarks>
internal bool IsTerminalShowing => IsTerminalSurface && SelectedTab is { HasSession: true };
internal bool IsTerminalShowing =>
IsTerminalSurface && !IsConnectSheetOpen && SelectedTab is { HasSession: true };
/// <summary>
/// Whether the terminal half of the window is the half being shown, pane or no pane.
@@ -889,6 +897,52 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
[RelayCommand]
private void ShowTerminal() => Surface = ShellSurface.Terminal;
/// <summary>
/// Whether the phone's connect menu is open over the terminal.
/// </summary>
/// <remarks>
/// <para>
/// Drawn by the Android head alone, and shell state rather than something that view could hold on its
/// own for the reason <see cref="IsSearching"/> is: it has to collapse the renderer while it is up. See
/// <see cref="IsTerminalShowing"/>.
/// </para>
/// <para>
/// It exists because the phone gives a terminal the whole screen. The bottom bar and the vault header
/// are gone while a shell is showing, so the three things that bar was the way to — a host, a host's
/// files, a bucket — need a way back that is not "leave the terminal first and remember what you were
/// doing". The menu is that, and every entry on it is one of the two navigation commands above.
/// </para>
/// </remarks>
[ObservableProperty]
private bool isConnectSheetOpen;
/// <summary>Raises the connect menu over the terminal.</summary>
/// <remarks>
/// Gated on the terminal surface rather than merely trusting its only button to be off screen otherwise.
/// The flag collapses the renderer, so one set while a page was showing would be a sheet nobody can see
/// holding a terminal hidden that nothing would put back.
/// </remarks>
[RelayCommand]
private void OpenConnectSheet()
{
if (!IsTerminalSurface)
{
return;
}
IsConnectSheetOpen = true;
}
/// <summary>Lowers the connect menu, leaving the terminal where it was.</summary>
/// <remarks>
/// The scrim, the CANCEL row and the system back gesture all come here. Choosing an entry does not, and
/// does not need to: every entry navigates, and leaving the terminal surface lowers the sheet on its own
/// — see <see cref="OnSurfaceChanged"/>, which is what makes "the sheet is only ever up over a terminal"
/// true of routes nobody wrote it for.
/// </remarks>
[RelayCommand]
private void CloseConnectSheet() => IsConnectSheetOpen = false;
// ---- Open terminals ----
/// <summary>
@@ -2533,7 +2587,22 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
}
/// <inheritdoc cref="OnScreenChanged" />
partial void OnSurfaceChanged(ShellSurface value) => RaiseSurfaceState();
/// <remarks>
/// <b>The one place the connect sheet is lowered by something other than a tap.</b> Every way out of a
/// terminal ends here — a rail or bottom-bar destination, the files screen, the palette connecting to a
/// host, closing the last tab, a lock — and each of them would otherwise leave the flag set on a shell
/// showing a page. That is not merely untidy: the flag collapses the renderer, so the next return to the
/// terminal would draw the sheet again over a rectangle held blank by it.
/// </remarks>
partial void OnSurfaceChanged(ShellSurface value)
{
if (value is not ShellSurface.Terminal)
{
IsConnectSheetOpen = false;
}
RaiseSurfaceState();
}
/// <remarks>
/// Both changes raise the same set, and they have to: <see cref="IsHostsShowing"/> and its four siblings
@@ -2596,6 +2665,9 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
partial void OnIsSearchingChanged(bool value) => RaiseTerminalState();
/// <inheritdoc cref="OnIsSearchingChanged" />
partial void OnIsConnectSheetOpenChanged(bool value) => RaiseTerminalState();
/// <remarks>
/// The unlock card and the confirmation swap, so arming one has to hide the other — see
/// <see cref="IsAskingForThePassphrase"/>.