Mobile Access (run and triage your sessions from a phone)
Reach your Omniscio install from a phone: what you see when you open it, how you get in, and the first things to know before you rely on it. The two parts below cover staying connected when the network misbehaves, and what to do when something does not work.
What it is
What it is
Omniscio can expose a mobile-optimized web UI so you can triage your agent inbox from your phone when you're away from your desktop. It's designed for the "agent needs a decision" moments: approve a plan, answer a question, acknowledge a rate-limit, or send a one-line reply. The backend is an HTTP + WebSocket server running in the main Electron process; the frontend is the same React app served in a mobile-tuned layout. Omniscio does not ship its own tunnel — for access from outside your LAN you use Tailscale, which Omniscio auto-detects and starts for you once it is installed and signed in. By default the connection is private: Omniscio registers its address with tailscale serve, so only devices signed in to your own Tailscale account can open it, and your phone needs the Tailscale app. Reaching it from a device that cannot run Tailscale is possible through Tailscale Funnel, but only after you turn on Allow access from the public internet in Settings → Remote Access → Advanced (off by default, desktop only, confirmed with a warning). Installs that already used the public tunnel before this change keep public access until their owner switches it. Contract: tunnel-reconciler-tailnet-only-contract.md. Without Tailscale you can still use it on the same Wi-Fi as your desktop — but only after switching the server to bind all interfaces (it binds loopback-only by default; see step 2 below). All access is token-gated and mobile bootstrap is optimized for speed (a ~3–5 KB first payload instead of the full desktop state).
Where to find it
On your phone, at the address Omniscio gives you when Mobile Access is on — and on the desktop, in the mobile-access settings where that link and its token come from.
Two names for one pane — the sidebar row is "Remote Access". Hold this page's steps against what is on screen: the sidebar row you click is
Remote Access, while the heading inside that pane reads Mobile Access, and Settings search answers to both ("mobile access" and "remote access" are both indexed). So wherever this page writes Settings → Remote Access, the pane that opens is the one headed "Mobile Access". The feature itself is called Mobile Access throughout Omniscio; only the nav row differs.
How it behaves
How to use it
- Turn it on. Open Settings → Remote Access (the pane headed "Mobile Access"). Flip it on. Pick a port if you care (default
9876, range1024–65535) and copy the auto-generated UUID token. - Choose how you'll reach it. Two options:
- Same Wi-Fi only: Omniscio binds the server to loopback (
127.0.0.1) by default, so first switch its bind host to all interfaces (0.0.0.0) in Settings → Remote Access — otherwise the192.168.x.xURL below is unreachable from your phone. Then use the local URL Omniscio shows you (e.g.http://192.168.x.x:9876/?token=...). Unencrypted, but fine inside your house — and it now loads on iPhone Safari too. Plainhttp://to a LAN/Tailnet IP is an insecure context where Safari hidescrypto.randomUUID; Omniscio routes every renderer id throughsafeRandomUUID()(which falls back tocrypto.getRandomValues, not secure-context-gated), so the page no longer blank-screens on iOS over plain http. You don't need HTTPS or Tailscale just to get it to open on your phone — only to reach it from outside your LAN. See renderer-safe-random-uuid-contract.md. - Over Tailscale by tailnet IP (manual): when Tailscale is installed, signed in and online, the Pair Your Phone card and its QR advertise your tailnet address (
100.x.y.z) instead of the192.168.x.xone. That is deliberate: a phone on the tailnet has no route to the LAN address, and under the default loopback bind host nothing is listening on the LAN address at all — so advertising it handed Tailscale users a QR that could never connect. You still have to make the port reachable on the tailnet: either set the bind host to0.0.0.0, or keep it on loopback and forward withtailscale serve --bg --tcp 9876 tcp://127.0.0.1:9876(raw TCP, so the client's ownHostheader survives the DNS-rebinding guard — a plaintailscale serveHTTPS proxy rewritesHostto the MagicDNS name and gets a421). The Advanced pane's "For devices on the same Wi-Fi (no Tailscale needed)" field keeps showing the LAN address, because that is exactly what it is for. Selection lives inresolvePairHost()and is shared by all three pairing surfaces (Settings card, titlebar popover, onboarding step). - Anywhere, privately (the default): install Tailscale on the desktop and on your phone, sign both in to the same Tailscale account, and turn Mobile Access on. Omniscio detects Tailscale's install + login + online status and, if all green, runs
tailscale serve --bg <port>and verifies the registration from Tailscale's own record. You get anhttps://<machine>.<tailnet>.ts.net/address that only your signed-in devices can open; the pairing QR says so, and a Private line appears under "Connected via Tailscale". Your Tailscale network needs HTTPS certificates turned on (Tailscale admin → DNS); if they are off, Mobile Access says so with a one-click link to fix it and stops retrying every minute. - Anywhere, publicly (off by default): for a device that cannot run Tailscale, turn on Allow access from the public internet in Settings → Remote Access → Advanced (desktop only; a warning asks you to confirm). Omniscio then uses Tailscale Funnel instead: the same address, reachable from the public internet, still protected by the access token, Google sign-in and device approval, with a Public line under "Connected via Tailscale" and a monthly reminder that offers the private route. While the switch is off, any request Tailscale marks as coming from the public internet is refused on every entry point. The Tailscale address (private or public) outranks both addresses above whenever it is running.
- Easiest — copy the login link: whichever way you connect, the Pair Your Phone card in the same pane shows a copyable Login link — the full URL that signs you in automatically (the same value the QR code encodes). Copy it and open it on your phone instead of typing the address and token separately. It embeds your access token, so treat it like a password and keep it private.
- Same Wi-Fi only: Omniscio binds the server to loopback (
- Install as a home-screen app (recommended on Android). Omniscio ships a PWA manifest, so Chrome on Android offers a real Install app option — open the URL once (with
?token=...), then Chrome menu → Install app (or "Add to Home screen" on older Chrome). Tapping the resulting icon opens Omniscio in its own standalone window with no Chrome tab UI, and successive taps return to the same window instead of spawning a new tab each time. The token is saved tolocalStorageon first load, so the standalone launcher usesstart_url: "/"and the login page auto-redirects you back in. On iOS Safari, the equivalent is "Add to Home Screen" — Apple meta tags (apple-mobile-web-app-capable,apple-touch-icon) handle the standalone behavior there. Plain bookmarks still work but each tap spawns a new Chrome tab — install via the menu to get the standalone window. - Triage. Tap a session that needs you to see its pending action (colored chip: question, plan approval, rate-limit, auth error, user-stopped). Answer questions or approve plans inline. Type a short reply and send. The inbox updates live via WebSocket.
- Revoke if you ever need to. Regenerate the token in Settings → Remote Access → Regenerate. Any open mobile sessions disconnect on the next WebSocket reconnect.
All five inbox approval kinds — cron jobs, automation rules, CLI Pending mutations, recipe runs, and recipe-authoring outputs — are reachable on mobile via swipe navigation between inbox items just like sessions, SMS threads, and the daily digest. Their footer buttons (Approve / Reject / etc.) meet the 44px Apple HIG touch-target floor so taps register on the first try. Resolving the last attention item from inside a session-detail panel slides you back to the inbox list automatically (no more "All clear" dead-ends on mobile).
A polished step-by-step Tailscale setup walkthrough with screenshots lives at /docs/intro-sandbox/setup-tailscale.html (also published at https://jlstradingco.github.io/Agent-Orchestrator/setup-tailscale.html) — link users there for first-time anywhere-access setup. For the plain-English install-as-a-home-screen-app steps (Android + iPhone), the user-facing guide is the help site's Install on Your Phone page (docs/help-site/public/index.html).
Boot sequence & landing screen (2026-08-08 overhaul)
Opening the mobile app cold now follows a fixed, fast sequence — locked by the mobile-parallel-bootstrap contract:
- Lands on the unified Inbox. The phone opens on the Inbox tab (everything that needs you, across all projects) instead of the Projects list; Projects stays one tap / one back-swipe away. Phones only — desktop-browser Web Access and the desktop app keep their own landing.
- Boot data races the tunnel. The client fetches its boot payload over a plain
authenticated HTTP request (
GET /bootstrap) in parallel with the WebSocket dial — whichever arrives first hydrates (first-wins seal), so first content no longer waits the 1–5 s tunnel handshake. Any HTTP-path failure silently falls back to the WebSocket path;AMC_DISABLE_HTTP_BOOTSTRAP=1on the desktop reverts all clients to the old path. - No sign-in flash. A signed-in phone whose auth cookie lapsed used to flash the "Mobile Access" sign-in card while silently re-authenticating; that reconnect state is now visually identical to the app's own splash, and the card appears only when a manual sign-in is genuinely required.
- Translated from first paint. The app waits (bounded 750 ms) for the active language catalog before mounting, so labels never flash raw-then-translated.
- A phone-shaped skeleton. The pre-JS placeholder shows a top tab bar + inbox list rows matching the real landing screen (the desktop-shaped skeleton is gone on ≤640 px).
- Tappable immediately. The session-panel code chunk and the top needs-you conversations warm within a bounded window (500 ms / 300 ms) after the app is visible — they can no longer be starved for seconds — and the warm wave itself waits for first paint so it never slows the boot it serves.
Boot telemetry records which path won (bootstrapSource: http | ws) plus HTTP fetch
timings. Investigation + measurements: the
boot-sequence overhaul postmortem.
Lock to portrait on mobile
Settings → Sessions → "Lock to portrait on mobile" (mobilePortraitLockEnabled, default off) keeps the mobile web app in portrait instead of auto-rotating. It layers three mechanisms: a best-effort runtime fullscreen + screen.orientation.lock('portrait') (the live lock), a declarative CSS landscape-block overlay (the guarantee), and the served web manifest orientation (install-time default only).
Why fullscreen + an overlay: the real device proved screen.orientation.lock() is rejected even in the installed app with "the page needs to be fullscreen in order to call screen.orientation.lock()" — so the runtime lever now enters fullscreen first (its observed precondition) and then locks. Both need transient activation, so it can't run on load — it arms on the first tap. But fullscreen isn't a guaranteed fix (a WebAPK minted free-rotating can still refuse the lock; iOS has no lock()), so the CSS overlay is the backstop that can't silently fail: its visibility is keyed to the actual screen orientation, so when the lock holds the screen never reaches landscape and the overlay stays dormant; when the lock is refused, landscape shows a "rotate to portrait" screen. The served orientation: portrait manifest only re-freezes the Android WebAPK on Chrome's slow background update check — it cannot lock an already-running app — so it is the install-time default only (this is why a manifest-only feature left the phone rotating across ~1000 open/close cycles).
- Runtime lever —
applyPortraitLock(mobile-portrait-lock.ts), driven by useMobilePortraitLock at the app root. Gated on a touch device on the web client (never desktop Electron, never a hover-pointer desktop browser); touch, not viewport width, because width flips in landscape exactly when the gate must NOT drop. Order: lock supported? →requestFullscreen()(synchronously, inside the gesture) →lock('portrait'); if the lock is still refused after it entered fullscreen, it exits fullscreen again (never a chrome-less, still-rotating state). It ships its outcome +displayMode+requestedFullscreento the PC log ([ClientLog:orientation-lock]viashipClientLog) so an on-device result is one-glance confirmable, never silent. The hook re-arms onfullscreenchangeexit so the next tap re-locks after a swipe-out. - CSS overlay — PortraitLockOverlay at the app root (gated render: setting on + touch + web client), shown purely by
.portrait-lock-overlay+@media (orientation: landscape) and (hover: none)in globals.css. No JS coordinates it with the lock — the media query IS the "did the lock hold?" detector, so it never changes the experience of a working lock. - Manifest lever — the on-disk manifest.webmanifest stays
"orientation": "any"; the web-access server rewrites it to"portrait"per request when the setting is on (web-access-http-manifest.ts, wired pre-auth in web-access-http-routes.ts), servedapplication/manifest+json+no-cache, with a safe static fallback on any read error.
Platform notes: Android (installed Chrome/Edge PWA) — first tap → fullscreen + true portrait lock; if it's refused, the overlay keeps it upright. iOS Safari — no lock(), so the overlay is what keeps the app in portrait (the rotate-to-portrait screen). Desktop / hover-pointer browser — unaffected (gated out). Trade-off: fullscreen hides the Android status/address bar; swiping out of fullscreen releases the lock and the next tap re-locks.
Behaviour is unit-locked by tests/unit/lib/mobile-portrait-lock.test.ts (fullscreen-before-lock, exit-on-failure rollback, outcome beacon, disabled path stays silent), tests/unit/hooks/useMobilePortraitLock.test.ts (arm-on-gesture + re-arm on fullscreen exit), tests/unit/components/PortraitLockOverlay.test.tsx (gated render), and tests/unit/services/web/web-access-http-manifest.test.ts (served orientation reflects the setting; safe static fallback). Full contract: mobile-portrait-lock-contract.md.
Related
- Mobile Access part 2 — staying connected: the offline and reconnecting indicators, reconnect speed, the watchdog, signing in on the phone, and the settings behind them.
- Mobile Access part 3 — what to do when something does not work.
- mobile-device-tracking.md — which device is connecting, and approving or blocking one.
Related
- cli-control.md — a different localhost-only server for scripts and hotkeys
- set-up-sms-integration.md — SMS is an even lighter "reply from phone" path when you don't want a browser
Last verified 2026-09-28