Three pages, one deck
Dense on purpose
The whole application is a StackLayout with three children —
page-computers, page-this-computer, page-settings — behind a single row of
navigation buttons. There is no dashboard, no onboarding wizard, no empty state that fills a screen with an
illustration. The first page you land on is the working surface, and it is full of readouts.
The design brief for this is written down. DESIGN.md asks for a console that is "technical, not
sterile; cinematic, not flashy; ambient, not animated for its own sake; dense in information, but easy to scan," and
specifies the palette down to individual hex values. Every colour in the drawing below is one of those values.
| Page | objectName | Nav id | Component |
|---|---|---|---|
| Computers | page-computers | nav-computers | CommandDeck.qml — 805 lines |
| This Computer | page-this-computer | nav-this-computer | CommandThisComputer.qml |
| Settings | page-settings | nav-settings | CommandSettings.qml — 557 lines |
Two breakpoints are handled inside the Computers page itself: compactSidebar below 1320 px and
stackedLayout below 920 px. The window will not shrink past 820 × 780, which is the point at which
the three-column deck stops being legible.
Palette
#050608 abyss background#081015 deep ambient#0d1214 base panel#2b312d default hairline#42e59a primary signal#6cd9ff telemetry#ffd58c restrained highlight#ff7c7c destructive / errorFrom DESIGN.md §8: don't "turn the app into a neon gimmick", don't "use heavy blur, aggressive glow,
or large continuous animations", don't "mix too many accent colors". The monospace font is scoped:
"Monospace is for telemetry, network addresses, and secondary technical labels only."
The Computers deck
Three columns: a host rail, a selected-host schematic, and a live protocol panel. Every label, button caption and placeholder string below is taken verbatim from the QML; automation ids are annotated in the margin because they are the same strings the CLI and MCP layers use.
enabled binding requires !device.clientTrusted — on a trusted host it greys out.
The CONNECT caption is conditional too: it reads >>> PREPARE & CONNECT <<< when
the device is neither trusted nor on the Tailscale route.
In-session controls
Once a stream is live the cockpit window is no longer the thing you are looking at. Connector puts a frameless,
always-on-top overlay window next to the stream — and, on capable Linux hosts, a second copy of the same controls
inside the remote desktop. The local overlay is only shown when the host overlay is not in use:
visible: (sessionActive || sessionConnecting) && !sessionUsesHostOverlay.
Window with Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint,
registered for automation on completion so its buttons are addressable by the same ui.activate path
as everything else. Activating it calls releaseSessionMouse() first — clicking the overlay while the
stream holds the pointer would otherwise do nothing.
This Computer
The hosting side of the app. Four status tiles across the top — TAILNET, HOST, SETUP, AUTO — then a setup ladder, then the trust controls.
Host readiness ladder
Steps are generated in localHostSetupSteps() with one of four states: complete,
current, warning, blocked. Each carries a title, an explanation, and optionally
an action id and a button caption.
| Condition | Title | State | Action |
|---|---|---|---|
| CLI not found | Install Tailscale | blocked | DOWNLOAD |
| not signed in | Sign Into Tailscale | current | OPEN TAILSCALE |
| signed in, offline | Bring Tailscale Online | warning | OPEN TAILSCALE |
| online | Tailscale Ready | complete | — |
The hosting rung follows the same shape: Host Runtime (blocked, platform unsupported), Install Host
Runtime, Turn On Hosting with a TURN ON action, Wait For Host Runtime. On Linux the
sign-in copy even branches on whether an AuthURL is present, suggesting either the login page or
sudo tailscale up.
Trust controls
- FALLBACK TRUST —
pair-client-name,pair-pin,approve-pairing - LOCAL HOST LOGIN —
sunshine-username,sunshine-password,save-sunshine-credentials,clear-sunshine-credentials - TRUSTED CLIENTS — one row per record, each with
revoke-client-<uuid>
Settings
Six stream controls and an automation-socket readout. Everything here maps directly onto the
settings.get / settings.set RPC pair, which is why the MCP tool
connector_set_settings takes exactly these six optional arguments.
| Control | automationId | Settings key |
|---|---|---|
| QUALITY | settings-quality-preset | quality_preset |
| WINDOW MODE | settings-display-mode | display_mode |
| LINUX DESKTOP MODE | settings-desktop-session-mode | desktop_session_mode |
| ABSOLUTE MOUSE | settings-absolute-mouse | absolute_mouse |
| MIC PASSTHROUGH | settings-microphone-passthrough | microphone_passthrough_enabled |
| PERFORMANCE OVERLAY | settings-performance-overlay | performance_overlay |
Three of these have real consequences elsewhere on this site. desktop_session_mode chooses between
physical and the dedicated-workspace modes described on the
host runtime page. absolute_mouse is the preference that the
mouse routing contract can override. And
microphone_passthrough_enabled is what puts --microphone-passthrough 1 on the
connectorstream command line and provisions the PipeWire graph on the host.
The Settings page shows the live control socket path and its running state, sourced from
ConnectorControlServer::statusChanged(running, socketName, message). It is the one place in the UI that
admits the socket exists.
Every control is addressable
QML components in this project accept an automationId property and mirror it into
objectName. That single convention is what makes the GUI scriptable, and it is applied consistently
enough that the UI tree doubles as an API reference.
$ build/connector-app/connectorctl ui tree | head -40 { "nodes": [ { "id": "connector-window", "type": "ApplicationWindow", "children": [ { "id": "nav-computers", "type": "SidebarButton", "text": "COMPUTERS" }, { "id": "nav-this-computer", "type": "SidebarButton", "text": "THIS COMPUTER" }, { "id": "nav-settings", "type": "SidebarButton", "text": "SETTINGS" }, { "id": "page-computers", "children": [ { "id": "computer-search", "type": "TextField" }, { "id": "available-only-toggle", "type": "CheckBox" }, { "id": "rail-filter-all", "type": "DeckButton" }, { "id": "rail-filter-ready", "type": "DeckButton" }, { "id": "connect-selected-device", "type": "DeckButton" }, { "id": "pair-selected-device", "type": "DeckButton" }, { "id": "selected-device-ssh-mode", "type": "HudComboBox" } ] } ] } ] } # the same tree is what an agent reads through connector://ui/tree $ build/connector-app/connectorctl ui activate rail-filter-ready { "activated": true, "id": "rail-filter-ready" }
An interface that a script can read is an interface that a test can assert on. The integration suite drives this application entirely through the same ids a human clicks.
Figures 06.1 and 06.2 are hand-drawn reconstructions from the QML source, not screenshots. Panel titles, button
captions, placeholder text, status strings, colours and layout proportions are taken from the code; the device names
and addresses shown are the resolver hints committed in scripts/connector-mcp.py.