Give hosts and terminals their own screen, and the rest of the vault another
ci / build and test (ubuntu) (pull_request) Canceled after 0s
ci / build (windows) (pull_request) Canceled after 0s

Rebuilds the client's shell from an imported design: a titlebar and nav rail
it draws itself, real multi-session tabs over the one WebView, a Ctrl+K host
search, and a vault screen that merges keys, passwords and pinned host keys
into one table. Hosts left the vault column for their own screen beside the
terminal, which is what the design asks for and turned out to be the better
split anyway.

Two screens the design shows have nothing behind them yet — file transfer
and teams — and say so plainly rather than rendering invented data; every
other gap between the design and this build is recorded in
docs/design-import-gaps.md.
This commit is contained in:
2026-07-31 08:39:37 +02:00
parent d162271a45
commit 9a76eced14
37 changed files with 4672 additions and 1347 deletions
@@ -20,6 +20,8 @@
const SERVER_OUTPUT = 1;
const SERVER_SESSION_OPENED = 2;
const SERVER_SESSION_CLOSED = 3;
const SERVER_SESSION_ACTIVATED = 4;
const SERVER_SESSION_REMOVED = 5;
const CLIENT_INPUT = 1;
const CLIENT_ACKNOWLEDGE = 2;
@@ -229,6 +231,50 @@ function handleFrame(buffer) {
break;
}
case SERVER_SESSION_ACTIVATED: {
const session = sessions.get(sessionId);
// Ignored for a pane that does not exist. The host sends this when a tab is selected, and a tab
// whose session ended still has its pane — but one the host knows about and this page has not
// created yet cannot be shown, and inventing an empty terminal for it would be worse than waiting
// for the SessionOpened frame that is already on its way.
if (!session) {
break;
}
activate(sessionId);
// Refitted on activation, not only on resize. A hidden pane has no layout, so every resize while
// it was hidden was skipped by the guard in resize() — meaning it comes back holding whatever
// geometry it had when it was last visible, and the remote pty is still sized to match.
resize(session, sessionId);
break;
}
case SERVER_SESSION_REMOVED: {
const session = sessions.get(sessionId);
if (!session) {
break;
}
/*
The tab is gone, so the pane goes with it — and this is the only place that is true. A shell that
ended on its own keeps its pane, because the last thing the remote said is usually why it ended;
a tab the user closed has nothing left to read.
term.dispose() is what actually matters. It releases the WebGL context, and a browser hands out
about sixteen of those: without this, a day of opening and closing terminals ends with panes that
cannot get a renderer, and nothing outside this page would ever say why.
*/
session.term.dispose();
session.pane.remove();
sessions.delete(sessionId);
setStatus('');
break;
}
case SERVER_SESSION_CLOSED: {
const session = sessions.get(sessionId);
const reason = new TextDecoder().decode(payload);