Merge branch 'claude/gallant-brahmagupta-1f8244'
ci / build and test (ubuntu) (push) Has been cancelled
ci / build (windows) (push) Has been cancelled

Writes down that locking the vault leaves shells running, and shows the count on
the unlock screen rather than leaving it to be inferred.

Conflict resolution:

- ShellFlowTests' fixture keeps main's FakeSshConnectionFactory. The branch added
  an IdleSshConnectionFactory for exactly what main's fake already does — a shell
  that is open, silent and never closes on its own — so FakeSshConnections.cs is
  dropped rather than merged, leaving one fake SSH stack in the suite instead of
  two that would drift apart.
- MainWindowViewModel and TerminalWorkspace: both sides added their own members,
  so both are kept.
- TerminalWorkspaceTests was added by both branches, with the renderer gate on
  one side and session lifetime on the other. Merged into one class over one set
  of helpers; the gate tests now use FakeConnectionFactory rather than an
  NSubstitute stub, since the suite already has the fake. gallant's polling
  Timeout constant is PollTimeout, which no longer reads as the renderer's.
- platform-flags.md keeps main's measured focus section and drops the short
  "nothing hands the terminal keyboard focus" entry the branch still carried,
  which that section supersedes.

One genuine disagreement between the branches, left visible rather than
flattened: this branch measured that a collapsed WebView cannot be typed into and
attributed it to a hidden WS_CHILD window being ineligible for keyboard focus,
while main's focus work measured Win32 focus still held by that hidden window and
added a lock path that moves the keyboard off it. Both results stand; the
mechanism sentence now defers to the focus entry, which makes the input barrier
something the lock path maintains rather than something the platform guarantees.

Full suite green, including the container-backed SSH tests.
This commit is contained in:
2026-07-29 15:46:41 +02:00
10 changed files with 453 additions and 31 deletions
+47
View File
@@ -158,6 +158,53 @@ headless test renders and focuses correctly and would confirm the wrong belief.
the plumbing that drives it — that connecting asks for focus once per session, that a failed connect does
not, and that locking stops the forwarding.
**The lock/unlock cycle does not resize the pane at all, and the 40 px guard is not what makes it safe.**
Measured on Windows with a live shell, against a real `sshd` in a container, in a harness mirroring
`MainWindow.axaml`'s `340,*` grid: with the `NativeWebView` collapsed by `IsVisible=false`, the page still
reports `paneWidth: 840, paneHeight: 760`, unchanged `cols`/`rows`, and `visibilityState: "visible"`.
Hiding is `SetWindowPos(holder, …, SWP_HIDEWINDOW)`, which does not resize the holder, so no
`ResizeObserver` callback fires, no fit runs, and **no `window-change` reaches the remote** — before,
during or after the cycle. `stty size` on the remote answered `50 118` both before locking and after
unlocking, and the renderer's own buffer came back byte for byte, wrapped lines included.
The guard's irrelevance here was established rather than assumed: the same run with
`MINIMUM_FITTABLE_PIXELS` patched to `0` — the guard fully disabled — produced an identical clean result.
So the guard is still worth keeping for the paths it was written for, minimising and a splitter dragged to
the edge, but it is **not** on the lock path and must not be cited as the reason locking is safe. It was
described that way when it landed.
Two further results from the same harness, both about the deliberate decision that shells outlive a lock
(README, `MainWindowViewModel.LockAsync`):
- **A collapsed WebView is not typed into.** With the harness confirmed as the foreground window and all
twelve injected `SendInput` events accepted, not one character of the probe reached the remote pty, and a
`Ctrl-U` afterwards answered `BEL` — nothing was sitting in the remote's line editor either. So the lock
screen is a real input barrier even though the session behind it is live, and that is what makes
surviving the lock defensible rather than merely convenient. The *mechanism* is not what this run
concluded: it read the result as a hidden `WS_CHILD` window being ineligible for keyboard focus, but the
focus entry above measured Win32 focus still held by the hidden holder window, and the lock path now
moves the keyboard off it deliberately. Take the barrier as measured here and the reason from there —
which also means the barrier is something the lock path maintains, not something the platform guarantees.
- **The session survives the cycle in the real control, not only in tests.** `LiveSessionCount` was 1
before, during and after, and the shell accepted a command again immediately on unlock.
*Suspected, seen once, not reproduced:* on the first run — before the harness learned to wait for the
window's scale to settle — the window opened at 2558x1367 px and the page reported a 2202x1328 pane
(312x88 characters) for a window 1180 logical units wide, which looks like physical pixels arriving where
CSS pixels were expected. A later re-push to 1177x672 then reflowed the wrapped line and split it in two.
Both events straddled a DPI settle rather than the lock, and three later runs at `RenderScaling 1.00`
never showed it. If a user reports mangled scrollback after moving the window between displays of
different scale, start here.
**WebView2 will not initialise when the host executable sits under a very long path.**
`CreateCoreWebView2Environment` fails with `COMException 0x80080005 CO_E_SERVER_EXEC_FAILURE` ("Server
execution failed") and the terminal never appears. Hit while building the harness above: the same binary
that failed from a ~230-character directory ran first time from `%TEMP%\h`. The exact threshold was not
established and the mechanism is unconfirmed — the user data folder is created beside the executable by
default and the browser process is launched with paths derived from it, so `MAX_PATH` is the obvious
suspect. Relevant to packaging: an installer that lands under a deep per-user path would break the
terminal with an error that names nothing.
**The Windows app manifest must declare a `supportedOS` list.** Without it the process reports a
downlevel Windows version and Avalonia's native control host fails outright — *"Unable to create child
window for native control host"* — so the WebView, and therefore the terminal, does not start at all.