Keep drawing the terminal after Android takes the GPU context away
ci / build and test (push) Failing after 33s
ci / desktop nightly (push) Skipped
ci / api image (push) Skipped
ci / android head (push) Failing after 4m12s

The phone's terminal was blank whenever it was connected. Not slow, not
mis-sized, not disconnected: a live session accepting keystrokes, acknowledging
output and drawing nothing at all.

◆ THE WEBGL ADDON DOES NOT RECOVER FROM A LOST CONTEXT AND DOES NOT FAIL LOUDLY.
It stays loaded over a dead context and renders an empty rectangle, which is
xterm's documented behaviour and the reason its guidance is to subscribe to
onContextLoss and dispose. This page never did, and until there was a phone
there was no reason to notice.

Losing the context is ordinary on Android and nearly unheard of on Windows,
which is what made this a one-head bug in shared code. Collapsing the renderer
sets the native view to GONE — Avalonia's AndroidNativeControlHostImpl.HideWithSize,
read out of the assembly rather than guessed at — and a WebView with no surface
has no GL context. The shell collapses it every time a tab starts connecting,
every time the connect sheet opens and every time the app is backgrounded. Worse,
the ordering guarantees it for the first session on every launch: OpenSessionAsync
sends SESSION_OPENED before the tab reports a session, so IsTerminalShowing is
still false and the pane, the terminal and its GL context are all built inside a
collapsed WebView. WebView2 hides a child HWND and keeps rendering throughout,
which docs/platform-flags.md measured at length.

The addon is not reloaded after a loss. A pane that lost the context once is on a
surface that will do it again, and thrashing between renderers is worse than being
slow — the DOM renderer is what the existing fallback comment already argues for,
because a blank pane is not usable and a slow one is.

The comment above MINIMUM_FITTABLE_PIXELS was wrong for this head and is corrected
with it. It asserted that collapsing the WebView leaves this page's viewport alone,
so no observer fires and the guard protects nothing — true of a hidden child HWND,
false of a GONE view, which its parent's layout skips outright. On the phone the
guard is the only thing standing between a lock, a connect sheet or a trip to the
background and a remote pty reflowed to 2x1.

Also on the way past: the renderer-timeout message told phone users to install the
Microsoft Edge WebView2 runtime. That is the other blank-terminal failure mode's
message, and naming a runtime that cannot exist on the device is worse than saying
nothing at the one moment somebody is trying to work out what went wrong. It now
names Android's own WebView on that head, as a runtime check for the reason
MainWindowViewModel.GestureWait records beside its own.

Not verified on a device — there is no handset or emulator here, and no test
covers this page. The diagnosis is the decompiled hide path plus xterm's own
requirement, not an observation. 523 tests over the shell and the terminal pass,
and both heads build.
This commit is contained in:
2026-08-11 22:58:11 +02:00
parent 766fe6aebe
commit 53ff15ba86
2 changed files with 59 additions and 9 deletions
@@ -11247,10 +11247,7 @@ internal sealed partial class VaultViewModel(
}
catch (TimeoutException)
{
Abandon(
attempt,
"The terminal did not start, so nothing was connected. The Microsoft Edge WebView2 "
+ "runtime is probably missing or blocked; install it and try again.");
Abandon(attempt, RendererNeverStarted);
}
catch (SshHostKeyUnknownException exception)
{
@@ -11275,6 +11272,29 @@ internal sealed partial class VaultViewModel(
}
}
/// <summary>What a renderer that never attached is reported as.</summary>
/// <remarks>
/// <para>
/// The wait is translated rather than reported for the reason <see cref="OpenSessionAsync"/> gives —
/// <see cref="TimeoutException"/> says only "The operation has timed out" — and the whole value of the
/// translation is naming where to look. Which is why it cannot be one sentence: the desktop's answer is
/// a runtime this application does not install, and the phone has no such runtime and no such answer.
/// Telling somebody on a handset to install Microsoft Edge WebView2 is worse than saying nothing, at the
/// one moment they are trying to work out what went wrong.
/// </para>
/// <para>
/// A runtime check rather than a constructor parameter, for the reason
/// <c>MainWindowViewModel.GestureWait</c> records at length: which renderer is behind the terminal is a
/// fact about the platform this assembly is running on, not about one installation of it.
/// </para>
/// </remarks>
private static string RendererNeverStarted =>
OperatingSystem.IsAndroid()
? "The terminal did not start, so nothing was connected. Android's WebView is probably "
+ "disabled or updating; check it in Settings and try again."
: "The terminal did not start, so nothing was connected. The Microsoft Edge WebView2 "
+ "runtime is probably missing or blocked; install it and try again.";
/// <summary>Says, in one place, that an attempt ended without a session and why.</summary>
/// <remarks>
/// The reason goes to two places on purpose. The status line is where somebody watching this screen is