Remote work mode, headless host, iOS/Android and browser clients - #329
Merged
Conversation
…add client/remote/headless crate stubs
…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
…ials, motion, per-screen fixes
…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.
…indows headless interrupt statics)
…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.
…connect test's temp dir
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
tcode-client(HostLink), the host emits one ordered NDJSON stream, andtcode-uibuilds without the runtime behind feature flags. Protocol v2: session commands carrysession_id; selection is client-local; a multi-client mux forwards events only to subscribers; reconnects fetch only the session tail.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._tcpmDNS discovery, reconnecting native and browser clients.tcode-headless. A server binary:serve,pair, and--features webto serve the browser client over HTTPS.--connect <host_id>, terminal over the link, preview reverse RPC, remote attachment rendering.tcode-mobile+crates/ios,crates/android): UI-only by design; they host the desktopSessionsSidebar,ChatViewandComposerin 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).tcode-web): same screens compiled to wasm, served by the headless host.tcode-headless(with web bundle), a debug Android APK and an unsigned iOS IPA.docs/remote.mdoperator guide,docs/mobile-design.mdphone spec,docs/plans/remote-and-mobile.mddecisions and per-phase notes.Verification
Every phase was gated on
cargo fmt --all --check,cargo clippy --workspace --all-targets -- -D warnings,cargo test --workspace, pluscargo checkoftcode-ui/tcode-mobile/tcode-webforaarch64-apple-ios-sim,aarch64-linux-androidandwasm32-unknown-unknown. End-to-end runs on this machine: desktop--connectagainsttcode-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.