5.6 KiB
jcode iOS App
Status: v2 rebuild. Pure Swift. This replaces the earlier prototype and the Rust-mobile-core/simulator direction, both removed in the
ios-app-restartbranch history.
Product definition
A native iOS remote control for jcode servers running on your own machines. The phone renders conversations and drives sessions; all heavy lifting (LLM calls, tools, git, files, MCP) stays on the server. Reachability is assumed to be Tailscale (or LAN); the app never talks to LLM providers directly.
Design identity: dark, calm, terminal-native without being retro. Mint accent
(#4DD9A6) for live/connected state. Dense information in touchable cards.
Architecture decision: pure Swift
The app is a thin client over an existing, server-owned protocol. Behavior worth sharing lives server-side already. A shared Rust core would duplicate the server protocol a third time and require an FFI bridge, custom renderer work, and a parallel simulator to stay honest. Instead:
- Swift owns the client. SwiftUI for UI, Swift 6 concurrency,
@Observable. - The server protocol is the single source of truth. The Swift codec is validated by fixture tests against real wire JSON; drift fails tests.
- Testability without devices comes from layering, not a simulator:
everything below the view layer builds and tests on macOS via
swift test.
Layering
ios/
Package.swift SPM package, builds on macOS + iOS
Sources/JCodeKit/ platform-free client core (no UIKit)
Gateway.swift endpoints: /health, /pair, /ws
Pairing.swift pairing code -> auth token
Wire.swift Request/ServerEvent codecs (NDJSON over WS)
Transport.swift WebSocketTransport protocol + URLSession impl
Connection.swift actor: connect/auth/reconnect, AsyncStream<ServerEvent>
SessionReducer.swift pure state machine: events -> transcript/app state
CredentialStore.swift Keychain-backed server credentials (protocol + impl)
Sources/JCodeMobile/ SwiftUI app shell (iOS only)
JCodeMobileApp.swift entry
AppModel.swift @Observable glue: Connection + Reducer -> views
Views/ Pairing, Chat, Transcript, Sessions, Settings
QRScannerView.swift camera pairing
Theme.swift colors/typography tokens
Tests/JCodeKitTests/ swift test on macOS: codec fixtures, reducer, pairing
project.yml XcodeGen spec for the app target
Rules:
JCodeKitnever imports UIKit/SwiftUI. It must compile for macOS so the whole behavior layer is testable headlessly by agents on this machine.- Views contain no protocol or state-transition logic.
AppModelonly forwards actions and publishes reducer output. SessionReduceris a pure function(State, ServerEvent) -> State(plus local user intents). All streaming/tool/session edge cases are unit tested there, replacing the old Rust simulator's role.
Protocol
Server side (already shipped, unchanged):
jcode pairCLI generates a 6-digit code (5 min TTL) and QR withjcode://pair?host=H&port=P&code=C.POST http://host:7643/pairwith{code, device_id, device_name}returns{token, server_name, server_version}. Token is stored hashed server-side.GET /healthfor reachability checks.ws://host:7643/ws?token=...upgrades to a WebSocket carrying the same newline-delimited JSON protocol as Unix-socket TUI clients (crates/jcode-protocol/src/wire.rs,#[serde(tag = "type")]).
Client v1 requests: subscribe, message, cancel, soft_interrupt,
ping, get_history, resume_session, set_model, rename_session,
clear.
Client v1 events consumed: ack, text_delta, reasoning_delta,
reasoning_done, text_replace, tool_start, tool_input, tool_exec,
tool_done, message_end, done, error, pong, state, session,
session_renamed, history, model_changed, available_models_updated,
tokens, interrupted, status_detail, notification, compaction.
Unknown event types are ignored by design (forward compatibility).
Feature scope
v1 (this rebuild):
- Pair via QR scan or manual host/port/code; multiple saved servers in Keychain
- Connect/disconnect lifecycle with automatic reconnect + backoff
- Chat: send, streamed assistant text, markdown rendering, reasoning indicator
- Tool calls rendered as collapsible cards with live status
- Interrupt (cancel) and soft-interrupt (queue a message mid-run)
- Session list (from history payload), switch via
resume_session, rename - Model picker from
available_models - Token usage + connection status surfaces
- Error and disconnect banners that never silently drop user input
Later (explicitly out of v1): push notifications (APNs), Live Activities, voice input, image attachments, tool approval UX, ambient/swarm dashboards, widgets, Mac Catalyst polish.
Testing strategy
swift test(macOS, no Xcode project needed):- codec round-trips against fixture JSON captured from the real server
SessionReducerstreaming scenarios (deltas, tool lifecycle, history replay, interrupts, errors, reconnect resubscribe)- pairing client against a stubbed URLProtocol
- connection actor against a fake
WebSocketTransport
xcodebuild buildof the app target (XcodeGen) keeps the UI compiling.- Manual/automated device pass via
xcrun simctlonce a simulator runtime is installed; not a CI gate.
CI
A macOS job runs swift test in ios/ plus xcodegen + xcodebuild build
for the app target. TestFlight delivery can be re-added later (the previous
Codemagic pipeline was removed with the prototype).