Skip to content

Remote work mode, headless host, iOS/Android and browser clients - #329

Merged
Tryanks merged 47 commits into
mainfrom
remote-and-mobile
Sep 5, 2026
Merged

Remote work mode, headless host, iOS/Android and browser clients#329
Tryanks merged 47 commits into
mainfrom
remote-and-mobile

Conversation

@Tryanks

@Tryanks Tryanks commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Draft. gpui-base is pinned to a git revision in [patch.crates-io]: the crates.io 0.6.0 release declares an unused dependency that does not compile for iOS/Android, and the fix is merged upstream but not yet released. Switch back to the registry once a release includes it, then merge.

What this adds

One tcode is the host; other desktops, phones and browser tabs are screens for it, over LAN or an overlay network. No relay service.

  • Protocol seam. The client endpoint moves into tcode-client (HostLink), the host emits one ordered NDJSON stream, and tcode-ui builds without the runtime behind feature flags. Protocol v2: session commands carry session_id; selection is client-local; a multi-client mux forwards events only to subscribers; reconnects fetch only the session tail.
  • Remote transport (tcode-remote). TLS with a per-host self-signed certificate pinned at pairing (fingerprint in codes, QR and invite links; trust-on-first-use for hand-typed codes), single-use six-digit pairing codes, device tokens with revocation, _tcode._tcp mDNS discovery, reconnecting native and browser clients.
  • tcode-headless. A server binary: serve, pair, and --features web to serve the browser client over HTTPS.
  • Desktop. Settings → Remote (host toggle, pairing code + QR, paired devices, paired/nearby hosts), --connect <host_id>, terminal over the link, preview reverse RPC, remote attachment rendering.
  • iOS and Android clients (tcode-mobile + crates/ios, crates/android): UI-only by design; they host the desktop SessionsSidebar, ChatView and Composer in a compact mode, with phone-only hosts/pairing/settings screens, camera QR pairing, Bonjour/mDNS discovery, keyboard insets. Own gpui platform backends (crates/platform/gpui-ios, gpui-android).
  • Browser client (tcode-web): same screens compiled to wasm, served by the headless host.
  • CI/release. iOS/Android/wasm checks; artifacts for tcode-headless (with web bundle), a debug Android APK and an unsigned iOS IPA.
  • Docs. README "Remote work mode", docs/remote.md operator guide, docs/mobile-design.md phone spec, docs/plans/remote-and-mobile.md decisions and per-phase notes.

Verification

Every phase was gated on cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace, plus cargo check of tcode-ui/tcode-mobile/tcode-web for aarch64-apple-ios-sim, aarch64-linux-android and wasm32-unknown-unknown. End-to-end runs on this machine: desktop --connect against tcode-headless; iPhone 17 simulator and Pixel 6 AVD pairing, real provider turns, approvals from the phone, reconnect; Chrome over HTTPS. Screenshots: docs/images/remote/, docs/images/mobile/.

Known limits (recorded in the plan)

Terminal replay is the last 256 KiB per terminal; preview URLs are rewritten to the host address (dev servers must listen on all interfaces; no TCP tunnel); mobile packages are debug/unsigned; camera QR decoding was verified up to the permission prompt on simulators only.

Tryanks and others added 30 commits September 5, 2026 15:43
…put stream; tcode-ui gates the runtime behind local-host

P0 of docs/plans/remote-and-mobile.md: the client endpoint moves out of the
runtime into tcode-client, the host no longer routes acks through a shared
pending map, and tcode-ui compiles without tcode-runtime.
…covery, and the tcode-headless host

P1a of docs/plans/remote-and-mobile.md: plaintext WS carrying the existing
NDJSON contract, bearer device tokens minted by a single-use 6-digit code,
loopback-only admin pairing endpoint, reconnecting native client that replays
subscriptions, and a headless host binary that prints a pairing QR.
…placeholder with iOS and Android hosts

P2a of docs/plans/remote-and-mobile.md. gpui-pre-platform gains iOS/Android
arms, gpui-pre-wgpu carries only the mobile hunks (scale-factor hook, Metal on
iOS, Android HAL labels, font fallbacks, color emoji), gpui-pre-web disables
the nightly worker default. Desktop still resolves the registry macOS chain.
Placeholder app verified on the iPhone 17 simulator and the Pixel 6 AVD.
…ct, --pair, reconnect banner

P1b of docs/plans/remote-and-mobile.md. The local UI is now a mux client of
its own host; hosting can be toggled at runtime with a pairing code and QR,
paired devices can be revoked (enforced on the next keepalive), and
--connect <host_id> runs the whole window against a remote host with a
reconnect banner and no local host process.
…nt into type-only and process halves

P2b of docs/plans/remote-and-mobile.md. tcode-ui gains terminal, desktop,
and remote-client features; tcode-remote gains server/client features; agent
gains a default process feature so core/protocol/client build on wasm32 with
no ACP, ureq, or smol. Store seeding no longer blocks outside local-host.
Adds docs/mobile-design.md, the authoritative phone UI spec.
…-client

Paired-host records and tcode://pair invite parsing now live in the wasm-safe
tcode-client crate (re-exported by tcode-remote); tcode-remote reuses
tcode_client::ConnectionState instead of a duplicate enum. tcode-mobile gets
the MobileHost trait (hosts, pairing, transport, camera, safe area) with a
native implementation, so the phone screens and the browser shell share one
interface.
P3a of docs/plans/remote-and-mobile.md. tcode-web is a wasm-bindgen entry
around tcode-mobile with a WebHost: same-origin pairing over fetch,
localStorage host records, and a reconnecting web-sys WebSocket transport
that replays subscriptions and wakes on online/visibilitychange. The bundle
is embedded only behind the headless web feature; the desktop binary never
carries it. Static serving now answers HEAD correctly.
…and web patches

gpui-pre-platform is patched only with a fallback arm so gpui-kit's
unconditional dependency compiles on iOS/Android (upstream fixes pending);
our own gpui-ios/gpui-android backends now build against the published
gpui-pre-wgpu, and gpui-pre-web needs no patch once its default feature is
off in the shim manifest. Android color emoji render as tofu until a Noto
Color Emoji font is bundled. Also: mobile spec now mandates reusing the
desktop sidebar, ChatView, and Composer (docs/mobile-design.md §0.1).
…mposer in compact mode

P2c of docs/plans/remote-and-mobile.md. Hosts and pairing are phone-only
screens; the thread list, thread view and composer are the desktop entities
with an opt-in compact layout in tcode-ui (full-width sidebar, 44 pt touch
targets, bottom sheets for pickers, desktop-only affordances hidden). DM Sans
and the tcode theme are registered on the phone. Verified end to end on the
macOS phone preview against tcode-headless: pairing, a real Codex turn,
approval from the phone, queueing, reconnect and offline caching. Desktop
zh-CN now says 执行端 for the tcode host machine.
…ebug exports behind a feature

P3b of docs/plans/remote-and-mobile.md. tcode-ui uses web-time on wasm for
Instant/SystemTime, embeds the compact flow's SVG icons synchronously on
wasm32, and installs the shared asset source in the browser application.
Verified in Chrome against tcode-headless: code-only pairing (fixed origin),
thread list, a real streamed Claude turn, reconnect without reload; console
error capture stayed empty. Desktop binary still carries no bundle.
…sets, camera QR, device names

P2d of docs/plans/remote-and-mobile.md. Both hosts install the shared asset
source and fonts; MobileHost exposes full WindowInsets so the composer rides
above the IME; AVFoundation and CameraX/ML Kit scanners feed scan_qr; device
names come from UIDevice/Build.MODEL; header controls use the sidebar's asset
icons. Verified end to end on the iPhone 17 simulator and a fresh Pixel 6 AVD
against tcode-headless: pairing, streamed Claude turns, approvals from the
phone, Android back navigation, reconnect and recovery.
Deny/Allow share the first row; Always allow and Cancel turn take full rows,
so the English label no longer runs under the Allow button on phones.
… gpui-pre-platform

tcode-web depends on gpui-pre-web with default features off and builds the
Application with gpui::Application::with_platform, so gpui-pre-platform (and
its nightly-only multithreaded wasm default) leaves the browser graph.
…ly the gpui-kit dependency fix goes upstream
…bundle), debug Android APK, unsigned iOS IPA

Mobile packages ship unsigned/debug for now by product decision; release
notes say so. The finalize job checksums every package.
…ltered mux; incremental session snapshots

P4a of docs/plans/remote-and-mobile.md, protocol v2. The host keeps a live
session map instead of one global active session; every session command
carries session_id; clients select by (un)subscribing SessionStatus,
SessionEvents and per-session GitStatus. The mux forwards events only to
subscribed connections and unicasts subscription snapshots to the requester.
SessionEvents subscriptions carry the client's cursor so reconnects fetch
only the tail. StartDraft/ForkThread return the new session id.
…S discovery on desktop, Android and iOS; Noto Color Emoji bundled on Android

P4c of docs/plans/remote-and-mobile.md. The host serves TLS only with an
rcgen identity stored beside remote.json; fingerprints travel in pairing
codes, invite URLs and QR payloads; native clients pin them (trust on first
use for manual code pairing) and surface a certificate change as a distinct
offline reason. Discovery moves from UDP broadcast to _tcode._tcp DNS-SD
(mdns-sd on desktop/Android with the multicast lock, NetServiceBrowser on
iOS). Android bundles the CBDT Noto Color Emoji font; the system COLRv1 face
still wins font fallback, fixed separately in the platform loader. Also the
audited mobile copy in both locales.
…a fixture

With client-local selection, subscribing adopts the session and spawns a real
git probe whose late result overwrote the injected status, making the replica
consistency test flaky.
…achment rendering

P4b of docs/plans/remote-and-mobile.md. Topic::Terminal streams base64 PTY
output with a 256 KiB replay ring per terminal; TerminalInput/ResizeTerminal
drive the host PTY; a remote desktop client runs a client-side GridEmulator
in the drawer. Preview MCP requests travel as PreviewRequest events on
Topic::Preview to the client showing the session and return via PreviewReply
(first responder wins, 60 s timeout); loopback preview URLs are rewritten to
the paired host address. Attachment thumbnails, timeline images and the
lightbox load host paths through ReadFileBytes. The headless host now serves
the preview MCP explicitly; the runtime does not start MCP servers itself.
…te.md)

Hosting from the desktop or tcode-headless, pairing and fingerprints, phone and
browser clients, networking (ports, mDNS, overlays, preview URL rewrite),
security model (TLS pinning, TOFU, tokens, revocation), troubleshooting, limits.
… chrome

The phone's list is now its own render path instead of the desktop sidebar with
`if compact` sprinkled through it: a pill search field, project group headers
with a count and collapse chevron, and 56pt rows with a status glyph and a
"status word · time" subtitle. No app row, no sort/layout/add-project header
(the second "+" the phone must not have), no footer and no persistent selection
highlight — every one of those compact branches is deleted from the desktop
functions rather than left dead.

Compact chat sits on T1 paper and separates its timeline from the composer with
a faded hairline plus 8pt of air; a fresh draft gets the spec'd empty state. The
compact composer collapses the running row to one 40pt circular control (send,
red stop, or send with a "Queue" caption) instead of spinner + send + stop.
Bottom sheets get a grabber, hairline, T3 shadow and a 180ms presentation.

Idle threads show only their relative time (copy rule 6), and the audited
`mobile.*` strings land in both locales.
… and a real sheet scrim

The phone's three remaining shared pieces, all behind `compact`:

`material` becomes a public module — the phone shell paints with the same
tiers the spec names (`content_surface`, `group`/`grouped`, `faded_hairline`)
and was reaching for them through a `pub(crate)` door. Alongside them it gains
`segmented_track`/`segment` (§3.5's 40pt, radius-10, T2 control) and `scrim`.

`scrim` replaces `foreground.opacity(0.3)` behind both bottom sheets. In dark
mode the ink foreground is near-white, so that expression *lightened* the page
behind the sheet; the scrim follows the `overlay` token instead (#1F232852 /
#80), and the page recedes in both themes.

The compact model sheet grows the spec'd footer: "Effort" — only when the
active model describes a `reasoningEffort` select — over "Approval mode", both
as segmented controls that apply on tap and leave the sheet open. Its rows go
to 52pt. The Effort control carries the audited `mobile.effort` label rather
than the provider's own English descriptor, and the track scrolls rather than
ellipsizing "Extra High" into "Extra…" when a provider offers six levels.

`ChatView`'s empty state stops leaking the desktop launcher into compact mode:
no "Add project" button and no Ctrl+K hint (§3.0 bans shortcut copy outright).
…s screens

The phone stops being three inline `render_*` calls swapped by a field. Each
level is a `PageView` on a `gpui_base::NavStack`, so `set_page` pushes or pops
by depth and the stack animates a 200ms lateral slide with the incoming page
fading in (`NavMotion::Animated`). Launching straight into a remembered host
restores the stack immediately instead of animating from nothing, and Android's
back key keeps working because it still routes through `back()`.

One `nav_bar` replaces the two hand-built heading rows: 52pt on T1 paper, a
faded hairline underneath, a title/subtitle column centred on the bar rather
than between the buttons, a chevron + parent-title back control, and at most
two 44x44 icon buttons. Back and icon buttons are `foreground`, not the
saturated blue the review called out; blue is left to the primary button and
live state. The whole window now paints T1 paper, up into the status bar.

Bottom sheets slide up over 180ms with the scrim fading in step and stay
mounted through their exit so dismissal animates too.

Screens: the host list gains the wordmark + large title + one-line intro,
T2 host cards (name, "address · last connected", a "Last used" chip, no
chevron) and the spec'd empty state; pairing drops the "where to find the
code" instructions (§3.2 allows the expiry alone) and gets T2 fields plus a
marked slot for the nearby-hosts section another change is building; the thread
page shows the connection pill only when it is not connected; settings moves to
grouped cards, segmented controls, an About line that is the `tcode` wordmark
plus version and the real `PROTOCOL_VERSION`, and a danger "Disconnect".
Every screen in both themes against a local `tcode-headless`, plus three frames
pulled out of a 120fps recording of a push and a pop so the navigation motion
is checkable and not just asserted.
The same build on an iPhone 17 simulator (iOS 26.5) against the local
`tcode-headless`: the host list, the live thread list, an open thread with the
composer above the home indicator, and the pairing sheet with the scan button
the iOS host supplies.
…ate in the new shell

Carries P4c's behaviour into the rebuilt phone shell instead of its layout:
the host card grows the third 12pt mono line with the first four groups of the
pinned fingerprint (§3.1), the pairing sheet gets "Nearby hosts" — up to three
DNS-SD rows with a discovery spinner, each prefilling the endpoint and its
advertised fingerprint before focusing the code field (§3.2) — and the
post-pair step shows the pinned fingerprint to compare against the host's own
Settings → Remote before connecting. A changed certificate now outranks every
other connection state (§4): the status turns danger, becomes tappable, and
the sheet behind it explains and offers "Pair again" with the endpoint filled.

Copy: the hosts one-liner no longer wraps two characters onto a second line,
and `certificate_changed` is the §4 status word with its explanation split
into `certificate_changed_help`.
Captured from the desktop phone preview against a TLS `tcode-headless`
(`merge-host`) with the Mac's own `_tcode._tcp` beacon in range: nearby-host
discovery in both appearances, the host card's fingerprint line, the post-pair
comparison step, and §4's certificate-changed sheet.

The one defect the review turned up: "Nearby hosts" and its rows sat 16 apart
like unrelated form fields, so they now form one 8pt group.
…istic terminal replay test; copy

P4d follow-ups. mDNS results prefer an address on the /24 of the interface
that received the answer and skip virtual bridges; the phone runs each browse
on its own thread. The terminal replay test waits for octal-escaped markers on
the real TerminalOutput stream instead of racing the shell prompt. English
certificate-changed status shortened; spec §3.1 matches the shipped hosts copy.
…pui-pre-platform shim

gpui-kit main no longer declares gpui-pre-platform from gpui-base, so the
workspace needs no local copy of any upstream crate. The git pin is temporary
until a gpui-base release includes the change.
…dapt upstream tests to client-local selection

After merging main: the usage-limit resume tick used smol::Timer inside
tcode-ui, which does not build for the mobile/wasm feature sets (use the gpui
background executor); clippy on the CI toolchain flags chunks_exact with a
constant size; two new runtime tests set the removed host-global active
session and now go through the test client facade.
…fg for the preview URL rewrite

The Swift-provided extern symbols made the crate's test binary fail to link
on Windows; the rewrite helper's callers only exist off Linux.
@Tryanks
Tryanks marked this pull request as ready for review September 5, 2026 17:28
@Tryanks
Tryanks merged commit c5748d7 into main Sep 5, 2026
4 checks passed
@Tryanks
Tryanks deleted the remote-and-mobile branch September 5, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant