Start port forwards on the phone, listed in a sheet off its terminal
ci / build and test (pull_request) Successful in 3m32s
ci / desktop nightly (pull_request) Skipped
ci / android head (pull_request) Successful in 5m17s
ci / api image (pull_request) Failing after 13s

The Android head skipped a host's forwards because it had nowhere to show
what was listening. Its terminal bar now draws a ⇄ for a tab that has
forwards, carrying the listening count in green and turning the danger
colour when one failed, and it raises a sheet over the same rows the
desktop sidebar lists: notation, route, state, failure reason and a tap
to switch each off or on. MainWindowViewModel.StartForwards no longer
returns early on Android.

The sheet is shell state like the connect sheet: it collapses the
renderer while up, is lowered by leaving the terminal, and the back
gesture closes it first.

Forwards ride the shell's keep-alive rather than holding the foreground
service themselves, since a forward cannot outlive its shell. They only
change the notification, which now counts ports forwarded beside shells
connected, fed by a new ForwardsChanged event and ListeningForwardCount.

The phone still has no editor for forwards; the More screen card, README
and design-import-gaps now say they run here and are edited on the
desktop.
This commit is contained in:
jaap-jan committed 2026-09-28 21:06:05 +02:00
1 parent f05deb4fbc
commit c3dfa42108
13 files changed
+415 -56

No files matched your search

@@ -157,6 +157,78 @@ public sealed partial class ShellFlowTests
ssh.Forwards.ShouldBeEmpty();
}
// ---- The phone's sheet ----
/// <remarks>
/// The phone lists a tab's forwards in a sheet over the terminal, so it obeys the connect sheet's rules:
/// it collapses the renderer while it is up, and leaving the terminal lowers it however it is left.
/// </remarks>
[Fact]
public async Task TheForwardsSheet_HidesTheRenderer_AndLeavingTheTerminalLowersIt()
{
await ConnectedHostWithForwardsAsync();
shell.SelectedTab.ShouldNotBeNull().HasForwards.ShouldBeTrue();
shell.OpenForwardsSheetCommand.Execute(null);
shell.IsForwardsSheetOpen.ShouldBeTrue();
shell.IsTerminalShowing.ShouldBeFalse("the sheet draws over the renderer's rectangle");
shell.IsTerminalSurface.ShouldBeTrue("the bar the sheet was raised from is on that surface");
shell.CloseForwardsSheetCommand.Execute(null);
shell.IsTerminalShowing.ShouldBeTrue();
shell.OpenForwardsSheetCommand.Execute(null);
shell.ShowScreenCommand.Execute(ShellScreen.Keychain);
shell.IsForwardsSheetOpen.ShouldBeFalse("a sheet left up over a page would hold the renderer blank");
shell.OpenForwardsSheetCommand.Execute(null);
shell.IsForwardsSheetOpen.ShouldBeFalse("it must not open over a page either");
}
/// <remarks>
/// What the phone's bar button and its keep-alive notification read: whether a tab has forwards, whether
/// one failed, and how many listen across every tab — with an event each time any of that moves.
/// </remarks>
[Fact]
public async Task TheCountsAPhoneShows_FollowEveryForwardsState()
{
ssh.ForwardFailure = spec => spec.BindPort == 5432 ? "Port 5432 on this machine is already in use." : null;
var changes = 0;
shell.ForwardsChanged += (_, _) => changes++;
await ConnectedHostWithForwardsAsync();
var tab = shell.Tabs.ShouldHaveSingleItem();
await EventuallyAsync(() => tab.Forwards.All(row => row.State is not PortForwardState.Starting), "both should settle");
tab.HasForwards.ShouldBeTrue();
tab.HasFailedForward.ShouldBeTrue();
tab.HasListeningForward.ShouldBeTrue();
shell.ListeningForwardCount.ShouldBe(1);
changes.ShouldBeGreaterThan(0);
var before = changes;
ssh.ForwardFailure = null;
await tab.Forwards[0].ToggleCommand.ExecuteAsync(null);
tab.HasFailedForward.ShouldBeFalse();
shell.ListeningForwardCount.ShouldBe(2);
changes.ShouldBeGreaterThan(before, "a retry that listens is a change the notification has to hear");
ssh.Shells.ShouldHaveSingleItem().Drop("An established connection was aborted by the server.");
await EventuallyAsync(() => !tab.IsLive, "the dropped shell should have ended the session");
shell.ListeningForwardCount.ShouldBe(0);
tab.HasListeningForward.ShouldBeFalse();
tab.HasForwards.ShouldBeTrue("the rows stay, saying they went with the session");
}
// ---- The editor ----
[Fact]