Teach the page and the shell to put a reattached view back together

The page's socket now retries itself forever with backoff — a dropped
socket is an ordinary event on a phone, not the end of the terminal's
life — and createSession is idempotent, so a replay landing on a pane
that survived changes nothing. A replay creating a pane that did not
survive writes one dim line saying the earlier output stayed on the
host, because that is the truth about a reloaded page's scrollback.

The shell answers RendererReattached with the two things only it owns:
the font size, and which tab is active.
This commit is contained in:
2026-08-09 10:54:38 +02:00
parent 4d1f07f253
commit aaff81272a
2 changed files with 121 additions and 11 deletions
@@ -525,6 +525,7 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
// list. Detached in DisposeAsync, which is the only point either of them ends.
this.workspace.SessionEnded += OnWorkspaceSessionEnded;
this.workspace.FontSizeStepRequested += OnFontSizeStepRequested;
this.workspace.RendererReattached += OnRendererReattached;
settings = new ClientSettingsStore(paths);
@@ -635,6 +636,31 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
private void OnFontSizeStepRequested(object? sender, TerminalFontSizeStepEventArgs e) =>
Dispatcher.UIThread.Post(() => StepTerminalFontSize(e.Step));
/// <remarks>
/// <para>
/// Marshalled for the same reason as <see cref="OnFontSizeStepRequested"/>: this arrives on the data
/// plane's socket-accept thread, and both properties it reads here — <see cref="TerminalFontSize"/> and
/// <see cref="SelectedTab"/> — are bound to by the interface.
/// </para>
/// <para>
/// <see cref="TerminalWorkspace.RendererReattached"/> fires once the workspace has replayed what it
/// owns — the live sessions. Font size and the choice of active tab are not the workspace's to know;
/// they live here, so this is the other half of putting a reattached page back the way it was. The size
/// is sent exactly as <see cref="TellRendererTheFontSizeAsync"/> sends it at startup, because nothing
/// has changed — the page has merely forgotten, and this is only a reminder.
/// </para>
/// </remarks>
private void OnRendererReattached(object? sender, EventArgs e) =>
Dispatcher.UIThread.Post(() =>
{
_ = workspace.SetFontSizeAsync(TerminalFontSize, CancellationToken.None).AsTask();
if (SelectedTab is { } tab)
{
_ = workspace.ActivateSessionAsync(tab.SessionId, CancellationToken.None).AsTask();
}
});
[ObservableProperty]
private ShellState state = ShellState.Starting;
@@ -3051,6 +3077,7 @@ internal sealed partial class MainWindowViewModel : ObservableObject, IAsyncDisp
workspace.SessionEnded -= OnWorkspaceSessionEnded;
workspace.FontSizeStepRequested -= OnFontSizeStepRequested;
workspace.RendererReattached -= OnRendererReattached;
transfers.PropertyChanged -= OnTransfersPropertyChanged;
// Stopped here rather than left to the process exiting with it: the loop holds no vault key and