Skip to content

Install the cockpit as a PWA

The cockpit is served by agentctl top --web --gateway and is an installable progressive web app, which is what makes it a workspace on a phone as well as on a laptop.

Installing changes how the cockpit is launched and framed. It does not add capability: the service worker caches nothing, so there is no offline mode, and there is no web push, so a closed app never notifies. Both are covered below.

Before you install

  • HTTPS. A browser only installs from a secure origin. The cockpit binds locally (:8731) and webview-login proxies it on :8736; the HTTPS proxy you put in front is what makes the origin installable. See The webview gateway.
  • The root of a hostname. The manifest declares start_url and scope as /, and the service worker is served from the origin root with Service-Worker-Allowed: /. A cockpit mounted under a path prefix does not install.
  • An identity that resolves to an agent. The manifest link, the service-worker registration and the iOS meta tags are injected only into the scoped single-agent cockpit. The operator grid — what a request whose identity maps to no agent gets — has none of them and is not installable.

The manifest, the service worker and the icons are exempt from the login proxy and reach the cockpit unauthenticated, because iOS Safari fetches them itself, without a session, to decide whether to offer the install. Behind an OIDC driver these fetches used to be redirected to the identity provider, and Safari read a login page where it expected JSON and silently declined to offer "Add to Home Screen".

Install it

iOS, iPadOS Safari only. Share → Add to Home Screen. No other iOS browser can install.
Android Chrome menu → Install app / Add to Home screen.
Desktop Chrome, Edge The install control in the address bar, or the browser menu's Install entry. The app opens in its own window with no address bar.

What the service worker does

/sw.js is eleven lines. It calls skipWaiting() on install, clients.claim() on activate, and registers an empty fetch handler. Every request goes to the network.

The empty handler exists only because a browser will not treat a site as installable without one. Nothing is precached deliberately: the cockpit UI ships inside the agentctl binary and changes on every upgrade, and the document is served Cache-Control: no-store, so a precached shell would mean serving a stale UI after an upgrade.

Two consequences:

  • With the host unreachable, the installed app shows the browser's network error page. There is no offline shell and no queued-message store.
  • After an agentctl upgrade the next launch is already the new UI. There is no update prompt, no "reload to update" banner and no version skew to manage.

Name, icon and colours

The shipped binary carries no branding: the defaults are a neutral agentctl icon and the background #0b0e14. A host overrides them with the webview_icon, webview_bg and webview_app_name scalars (agentctl host set <key> <value>). webview_bg becomes both the manifest theme_color and its background_color, so the splash and the status bar match the cockpit.

Two things about naming are worth knowing before you look at an installed icon and wonder:

  • On iOS the home-screen label comes from apple-mobile-web-app-title, which is rendered into the authenticated document and is therefore the agent's name.
  • The manifest's name — what Android and desktop label the installed app with — is produced for an unauthenticated request, because that route is exempt from the login proxy and has its identity header stripped. It is therefore the generic agentctl, and webview_app_name, which is appended to the agent name, does not reach it either. The colours and the icon are not affected; they are read from host config, not from the request.

The icon URLs carry a ?v=<hash> of the icon bytes so a changed icon gets a fresh URL. That is not always enough: some installs cache the home-screen icon against the URL path and never re-check it when only the query changes. If you change webview_icon on a host whose users have already installed, expect the old icon to persist until they reinstall.

Notifications

The cockpit asks for notification permission when an agent's cockpit opens, and again on the first message you send. When permission is granted and the cockpit is loaded but not focused, an agent reply raises a page notification and vibrates on Android.

There is no Push API subscription and no badging. A notification requires the document to be open with its stream connected, so an installed app that is closed produces nothing. The unread count is written into the document title, which is visible on a browser tab and not on an installed app.

After the phone sleeps

iOS freezes a backgrounded PWA, and the live event stream can die with it without the page noticing. The cockpit re-opens the stream on visibilitychange and on a back/forward-cache restore, and the re-seed recomputes the in-progress-turn state from the transcript, so returning to the app reconciles itself instead of needing a reload. Nothing to configure.

The transport ceilings this sits on — HTTP/2 at the proxy, and why response compression must be off on the stream routes — are the proxy's business, not the PWA's. See The webview gateway.

Staying signed in

The session cookie lasts 30 days by default, so an installed app does not ask for a login every day. Override with WEBVIEW_SESSION_TTL (any Go duration, e.g. 168h).

With the basic auth driver, sign in through the HTML form rather than an Authorization header. A standalone PWA suppresses the browser's native 401 prompt, and per-request Basic replay through a service worker is unreliable, so the form's cookie session is the only thing that works once installed. Scripts and API clients can still send Basic preemptively.