diff --git a/README.md b/README.md index 7657904..66487f3 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,36 @@ Sessions survive disconnects: the shell (and whatever's running in it) keeps goi --- +## Clients + +The browser is the reference client; two native clients and a remote-access +service are also in the repo (all consume the same wire protocol — the server +stays a byte-shuttle). + +- **iOS / iPad app** ([`ios/`](ios/), branch `feat/ios-client`) — a native + SwiftUI + SwiftTerm **pure remote client** built for the walk-away loop: + native terminal with scrollback replay, QR/manual pairing (Keychain), the + remote **Approve / Reject** gate + three-way plan gate, **APNs push with + lock-screen Allow / Deny behind Face ID**, deep links, Projects, multi-session + switcher, timeline, diff, quick-reply, and an **adaptive iPhone/iPad split + layout**. Looks like the desktop (amber-gold, dark-first). P0 needs zero server + changes; P1 adds two declared additive touch-points. See + [`ios/README.md`](ios/README.md). *(Not yet merged.)* +- **Desktop app** ([`desktop/`](desktop/)) — a Mac/Windows **Electron shell that + embeds this Node server + node-pty** (all-in-one), for native notifications, + tray, deep links and launch-at-login. Design in + [`docs/DESKTOP_PLAN.md`](docs/DESKTOP_PLAN.md). *(Built; packaged & launch- + verified on Apple Silicon.)* +- **Rendezvous relay** (`term-relay/` · `agent/` · `relay-e2e/` · `relay-web/` · + `relay-auth/` · `relay-contracts/`) — an "ngrok-for-Claude-Code" with + **end-to-end encryption** to reach a self-hosted host from anywhere, without a + VPN or port-forwarding. Security-audited but **pre-production / not deployed**; + see [`docs/PLAN_RELAY_INDEX.md`](docs/PLAN_RELAY_INDEX.md) and + [`docs/DEPLOY_RELAY.md`](docs/DEPLOY_RELAY.md). For remote access **today**, use + **Tailscale** (§ Security & deployment below). + +--- + ## Quick start ### Prerequisites diff --git a/ios/README.md b/ios/README.md index 91ee9e3..30423da 100644 --- a/ios/README.md +++ b/ios/README.md @@ -1,20 +1,32 @@ # WebTerm iOS Client -Native iOS client for the web-terminal server in the repository root. It is a -**pure remote client**: it speaks the same WebSocket wire protocol and HTTP -endpoints as the web frontend and requires **zero server changes** in P0. +Native **iPhone + iPad** client for the web-terminal server in the repository +root. It is a **pure remote client**: it speaks the same WebSocket wire protocol +and HTTP endpoints as the web frontend. P0 needs **zero server changes**; P1 adds +exactly **two declared, additive server touch-points** (an APNs sender + token +endpoint, and an optional `lastOutputAt` field on `GET /live-sessions`) — nothing +else in `src/` / `public/` changes. + +The whole app is a **single adaptive codebase**: iPhone (and iPad Slide Over / +small splits) get the compact stack layout; iPad regular width gets a +**NavigationSplitView** sidebar + detail — no forked UI. Min deployment **iOS +17.0**; Swift 6 language mode. The look matches the **desktop web theme** +(amber-gold accent `#E3A64A` on warm-dark, dark-appearance-first). ## Build & Run Requirements: macOS 15, Xcode 16.3 (Swift 6.1), [XcodeGen](https://github.com/yonaskolb/XcodeGen), -iOS 18.4 simulator (iPhone 16). SwiftTerm 1.13 is resolved for the App target only. +an iOS 17+ simulator (CI tests on iPhone 16 **and** iPad Pro 11"). SwiftTerm 1.13 +is resolved for the App target only. ```bash cd ios xcodegen generate # project.yml → WebTerm.xcodeproj -open WebTerm.xcodeproj # or run the full test suite headless: +open WebTerm.xcodeproj # or run the full test suite headless (both idioms): xcodebuild -project WebTerm.xcodeproj -scheme WebTerm \ -destination 'platform=iOS Simulator,name=iPhone 16,arch=arm64' test +xcodebuild -project WebTerm.xcodeproj -scheme WebTerm \ + -destination 'platform=iOS Simulator,name=iPad Pro 11-inch (M4)' test ``` Layout: @@ -34,6 +46,108 @@ Per-package tests: `swift test --package-path Packages/`. The server runs from the repo root: `npm install && npm start` (see the root [README](../README.md)). +## Features + +The product loop is **vibe coding**: kick off Claude Code on your Mac, walk away, +and check / steer it from your phone — approve or reject permission gates +remotely, watch status, and reattach with full scrollback. + +### P0 — daily-usable pocket cockpit + +- **Native terminal** — [SwiftTerm](https://github.com/migueldeicaza/SwiftTerm), + not a web view. Full ANSI/true-color, native selection, IME and keyboard. On + attach it replays the host's whole scrollback ring, then resumes the live + stream. The canvas matches the desktop (warm near-black `#100F0D`, gold caret). +- **Pairing** — scan the web UI's connect-device **QR**, or type the host URL. + A **confirm step** shows the parsed `scheme://host:port` and makes **zero + network calls until you confirm** (a scanned code is untrusted input). A + two-step probe verifies reachability + the `Origin` handshake before saving the + host to the **Keychain**. Per-`§5.4` warning tiers (public host = blocking + warning, plaintext LAN = notice, Tailscale = fine). +- **Session list (chooser + dashboard in one)** — every running session with a + semantic **status badge** (working / waiting / idle / stuck — colour **and** + shape, never colour alone), live **telemetry chips** (cost / context / PR, SF + Mono, greyed when stale), a **preview thumbnail**, cols×rows, swipe-to-kill, + pull-to-refresh, and multi-host switching. +- **Remote approve / reject** — the core steering primitive. A tool gate shows a + prominent **Approve / Reject** card (≥44 pt); a plan gate shows the **three-way** + sheet (Approve+Auto → `acceptEdits` / Approve+Review → `default` / Keep + Planning → `reject`, matching the web client exactly). Haptic on arrival. +- **Away digest** — on reattach, a summary of what happened while you were gone + ("ran N tools · waiting · done"), expandable into the full timeline. +- **Sessions survive everything** — kill the app / background / lose the network + → foreground reattaches and replays. One live WS for the foreground session; + the rest are HTTP-polled. Reconnect uses 1s→30s backoff; a 25 s ping keeps the + socket honest. +- **Mobile key-bar** — Esc / Esc·Esc / ⇧Tab / arrows / Enter (`\r`) / ^C / ^R / + ^O / ^L / ^T / ^B / ^D / Tab / `/`, byte-for-byte from the web key-bar, sent + raw (no soft-keyboard pop). Hardware-keyboard chords via `UIKeyCommand`. +- **Privacy shade** — the terminal is covered whenever the app isn't active + (`scenePhase != .active`), so the multitasking snapshot never leaks terminal + bytes. +- **Notifications (P0)** — reuses the server's existing **ntfy bridge** (see + below), zero new code, default-off. + +### P1 — walk-away complete + +- **APNs push + lock-screen Allow / Deny** — the host notifies your phone on a + held gate; from the **lock screen** you Allow (behind **Face ID / passcode**, + `.authenticationRequired`) or Deny **without opening the app**. The decision is + a single-use capability token; it's validated then discarded, never persisted, + never logged. (Needs a paid Apple account + `.p8` on the server — see + [`PLAN_IOS_CLIENT.md`](../docs/PLAN_IOS_CLIENT.md) §T-iOS-20.) +- **Deep links** — `webterminal://open?host=…&join=…` (strict whitelist, UUID- + validated) opens straight to a gated session, cold or warm; push taps use the + same router. +- **Projects** — auto-discovered git repos grouped by namespace, favourites + + collapsed state synced through the server's `/prefs` (unknown keys preserved so + iOS never clobbers web-written prefs), dirty badge, detail page (sessions / + worktrees / CLAUDE.md), and **"open Claude in this repo"** (`attach(cwd)` + + inject `claude\r`). +- **Multi-session switcher** — unread dots (from `lastOutputAt`) and sanitized + **OSC titles** (bidi / zero-width stripped, length-capped — titles are + attacker-influenced). +- **Activity timeline** — the full `/events` drill-down, class → icon + colour. +- **Diff viewer** — read-only git diff, staged/unstaged, per-file hunks. +- **Quick-reply chips** — tap-to-send snippets + an editable palette (floats while + a gate is waiting). +- **Session thumbnails** — off-screen SwiftTerm renders, concurrency-capped and + cached by `(sessionId, lastOutputAt)`. + +### iPad + +- **Adaptive split view** — regular width shows a sidebar (session list + + projects, continue-last banner) beside the terminal detail; compact width (incl. + Slide Over) collapses to the iPhone stack. One code path (`LayoutPolicy`), the + privacy shade covers both. +- **Hardware keyboard** — the on-screen key-bar auto-hides when a hardware + keyboard is attached. +- **Pointer** — right-click / long-press context menu on the terminal (open in + cwd / kill / copy), routed through the same guarded endpoints. + +### Look & feel + +A single frozen **design system** (`App/WebTerm/DesignSystem/`): one amber-gold +accent token (`#E3A64A` dark / `#C9892F` light) matching the desktop `--accent`, +semantic status colours from the web palette, an 8-pt spacing scale, SF Mono +tabular numbers, reduce-motion-gated animation and haptics, and reusable +primitives (`StatusBadge` / `TelemetryChip` / `Card` / …). The app is +**dark-appearance-first** to match the desktop's dark theme (gold reads best on +warm-dark). Every view consumes tokens — no hardcoded colours. + +### Status & what's verified + +Built and green: **290 app tests** (iPhone 16 + iPad Pro 11"), **261 package +tests**, **10 integration tests** against a real Node server, plus one XCUITest +happy-path (pair → attach → type → approve). Coverage ≥ 80 % on the four logic +packages. **Not yet done / needs hardware:** on-device gesture / IME / camera-QR / +haptics / lock-screen-Allow walkthroughs are **DEFERRED to a real device**; APNs +end-to-end needs a paid Apple account. The client is on the `feat/ios-client` +branch (not yet merged). Design & task detail live in +[`PLAN_IOS_CLIENT.md`](../docs/PLAN_IOS_CLIENT.md) and +[`PLAN_IOS_IPAD.md`](../docs/PLAN_IOS_IPAD.md); progress in +[`PROGRESS_LOG.md`](../docs/PROGRESS_LOG.md). + ## ntfy notification bridge (P0 "host finds the phone") Until APNs push lands (P1, T-iOS-20/21), the phone is notified via the