Mobidex is an iOS-only SwiftUI app for viewing and steering Codex sessions that are running on remote servers over SSH.
This is a native SwiftUI app, not Expo React Native. The hard part is native SSH, process transport, credential storage, and app-server streaming; Expo would still need a custom native module and a prebuilt dev client for that path.
- Manage SSH servers manually.
- Store passwords and private keys in Keychain, not in
UserDefaults. - Connect with password or RSA/Ed25519 OpenSSH private key authentication.
- Configure the remote Codex executable path per server; it defaults to
codex. - Discover projects and session counts from remote Codex data under
CODEX_HOMEor~/.codex. - Add a project manually by remote folder path.
- Start or reuse Codex's default unix-socket app-server daemon with
app-server --listen unix://, then attach over SSH withapp-server proxy. - List and read Codex threads through app-server JSON-RPC.
- Include all app-server thread source kinds when listing sessions so CLI, VS Code, exec, app-server, and subagent sessions can appear.
- Render user/assistant messages, reasoning, plans, commands, file changes, tools, agent events, web searches, media placeholders, reviews, compaction, and unknown item types.
- Stream live assistant, reasoning, plan, turn-plan, command-output, terminal-input, file-change, turn-diff, MCP-progress, and turn events into the conversation view.
- Start, steer, and interrupt turns.
- Start a first thread from a selected project even when the project has no listed sessions yet; project/session taps promote the conversation detail on compact layouts and reset to automatic column visibility on regular-width screens.
- Respond to supported approval requests, including current command/file-change approvals and legacy
execCommandApproval/applyPatchApprovalrequests.
- SSH reachable from the iOS device or simulator.
codexavailable on the remotePATH, or a server-specific Codex path such as/home/ubuntu/.bun/bin/codexor~/.bun/bin/codex.- If that Codex path is a Node/Bun wrapper and the SSH server's non-login PATH cannot find the runtime, point Mobidex at a small remote wrapper script that exports the needed PATH before execing
codex. python3available for project discovery.- Codex data in
CODEX_HOMEor~/.codex.
Project discovery infers projects and session counts from:
config.tomlproject entries.sessions/**/rollout-*.jsonl.archived_sessions/**/rollout-*.jsonl.
Manual project paths are still supported when discovery cannot infer a project.
Session listing and hydration come from thread/list and thread/read after the app-server connection is established.
Server metadata is stored under mobidex.servers.v3. This intentionally ignores older mobidex.servers.v1 and mobidex.servers.v2 metadata written before the current project/session-count and Codex-path shape; re-add servers on installs that still have older local metadata.
Generate the Xcode project:
xcodegen generateThe normal Xcode scheme is the intended build/test path. After the Mobidex rename and project regeneration, Scripts/verify-official-scheme.sh succeeds on this machine and writes /tmp/mobidex-official-scheme.log.
The generated project pins Apple Developer team JX3932QCN8 and bundle ID com.getresq.mobidex for the Mobidex target. Simulator builds use local simulator signing while still providing the Keychain entitlement needed to save server credentials. For physical-device builds, use that team and let Xcode create or refresh a development profile for com.getresq.mobidex.
The helper-built MobidexTests target produces a hosted simulator .xctest bundle under Mobidex.app/PlugIns. Scripts/verify-simulator-tests.sh runs that hosted bundle with xcodebuild test-without-building and a generated .xctestrun file, which gives a deterministic simulator XCTest execution gate.
The helper-built MobidexUITests target produces MobidexUITests-Runner.app. Scripts/verify-tap-ui-smoke.sh starts the deterministic fake SSH/app-server used by seed mode, writes a generated UI .xctestrun, and uses XCUITest to tap through Connect, project selection, composer send, approval, steer, and stop-turn controls.
Useful verification helpers:
Scripts/verify-discovery.sh
Scripts/verify-app-server-schema.sh
Scripts/verify-ios-build.sh Mobidex
Scripts/verify-ios-build.sh MobidexTests
Scripts/verify-ios-build.sh MobidexUITests
SDK=iphoneos Scripts/verify-ios-build.sh Mobidex
SDK=iphoneos Scripts/verify-ios-build.sh MobidexTests
Scripts/verify-simulator-tests.sh
Scripts/verify-simulator-launch.sh
Scripts/verify-inapp-ssh-smoke.sh
Scripts/verify-tap-ui-smoke.shScripts/verify-official-scheme.sh is the canonical recheck for the normal Xcode scheme gate. It records Xcode SDKs, available runtimes, runtime disk images, simulator devices, scheme destinations, whether the matching simulator runtime is present, and whether any simulator runtime images are unusable. It currently succeeds on this machine and writes /tmp/mobidex-official-scheme.log.
By default the official-scheme helper runs build-for-testing against generic/platform=iOS Simulator. If you set MOBIDEX_SCHEME_ACTION=test, also set a concrete MOBIDEX_DESTINATION such as platform=iOS Simulator,id=<simulator-udid>.
verify-discovery.sh extracts the same discovery Python that the app sends over SSH, wraps it with the app's heredoc command shape, simulates Citadel's appended ;exit, checks zsh-safe exit-status propagation, and runs it against a synthetic .codex tree.
verify-app-server-schema.sh checks the Mobidex protocol surface against the generated app-server schema in ~/Code/codex; set CODEX_SOURCE_DIR if the Codex checkout lives elsewhere. ChatGPT auth-token refresh requests are detected but not answered by this MVP.
The helper reproduces the local SwiftPM include/module-map/resource-bundle workarounds needed for Citadel, Swift Crypto, SwiftNIO, and Wellz26 swift-nio-ssh under Xcode 26.4.1. It uses a deterministic Swift package clone root at ${TMPDIR:-/tmp}/mobidex-source-packages so package checkouts and build products stay aligned. It defaults to SDK=iphonesimulator; pass SDK=iphoneos for a device-SDK compile check. Simulator logs are written to /tmp/mobidex-<target>-verify.log; device-SDK logs are written to /tmp/mobidex-<target>-iphoneos-verify.log.
Run the verify-ios-build.sh invocations one at a time because they share generated module-map and build output directories.
verify-simulator-tests.sh builds MobidexTests, verifies the hosted .xctest bundle has a processed Info.plist, writes build/MobidexGenerated.xctestrun with the app-hosted XCTest injector environment, selects an available iOS simulator unless MOBIDEX_SIMULATOR_ID or MOBIDEX_DESTINATION is provided, and runs xcodebuild test-without-building. Custom MOBIDEX_APP_PATH and MOBIDEX_TEST_BUNDLE_PATH values are written into the generated .xctestrun as absolute paths. The default log is /tmp/mobidex-simulator-tests.log; set MOBIDEX_TEST_TIMEOUT_SECONDS to change the hard timeout.
verify-tap-ui-smoke.sh builds MobidexUITests, starts verify-inapp-ssh-smoke.sh in setup-only seed mode, writes a temporary generated UI .xctestrun, and runs xcodebuild test-without-building against a concrete simulator destination. The UI test launches the app with smoke environment values, then taps the visible controls for Connect, project row, composer, Send, Approve, Send, and Stop Turn. The default log is /tmp/mobidex-tap-ui-smoke.log; set MOBIDEX_UI_SMOKE_TIMEOUT or MOBIDEX_UI_TEST_TIMEOUT_SECONDS to tune timeouts. Set MOBIDEX_UI_XCTESTRUN_PATH only when you intentionally want to keep the generated run file for debugging, because it contains smoke environment values.
verify-simulator-launch.sh uses the helper to force a Debug iphonesimulator app build, selects an available iOS simulator unless MOBIDEX_SIMULATOR_ID is provided, installs and launches com.getresq.mobidex, verifies the launched process is still running after a short settle delay, and writes a screenshot to /tmp/mobidex-simulator-launch.png unless MOBIDEX_SCREENSHOT_PATH is set. MOBIDEX_SKIP_BUILD=1 and MOBIDEX_APP_PATH are local debugging escape hatches; the default path is the build-install-launch smoke.
verify-inapp-ssh-smoke.sh builds the app, starts a disposable localhost SSH server, installs the app on a simulator, seeds a smoke server through launch environment, and connects through the real Citadel app path. By default, MOBIDEX_SMOKE_AUTH=private-key uses local sshd with a generated Ed25519 key, starts Codex app-server, sends a short prompt, waits for assistant output, and writes /tmp/mobidex-inapp-ssh-smoke.png. It creates a temporary Codex wrapper with the current PATH baked in so local ~/.bun/bin/codex wrappers can find Node under macOS sshd. This default smoke can spend model/API resources; override MOBIDEX_SMOKE_PROMPT, MOBIDEX_SMOKE_EXPECTED_TEXT, MOBIDEX_SMOKE_TIMEOUT, or MOBIDEX_SMOKE_CODEX_PATH when needed. MOBIDEX_SMOKE_AUTH=password defaults to connection mode and uses a disposable AsyncSSH password server to validate the in-app password-auth command path without requiring a system account password or PAM. Add MOBIDEX_SMOKE_MODE=turn to run the full password-auth app-server turn smoke; this can spend model/API resources.
MOBIDEX_SMOKE_MODE=control uses a deterministic fake app-server over the same in-app SSH transport to exercise start, approval response, steer, live assistant delta, and interrupt without spending model/API resources. It also promotes the compact UI to the conversation detail before screenshot capture.
MOBIDEX_SMOKE_MODE=approval uses the same fake app-server but stops at the pending approval state, promotes the compact UI to the conversation detail, asserts the thread/approval/active-turn state, and captures a screenshot that should show the active conversation and approval card. This is a non-tap visible UI checkpoint; use verify-tap-ui-smoke.sh for interaction validation.
verify-visible-ui-smokes.sh runs the non-tap approval and control visible UI smokes back to back and writes screenshots to /tmp/mobidex-visible-ui-smokes unless MOBIDEX_VISIBLE_SCREENSHOT_DIR is set.
verify-inapp-ssh-smoke.sh also supports MOBIDEX_SMOKE_SETUP_ONLY=1 with MOBIDEX_SMOKE_ENV_PATH=/path/to/env.sh; this starts the disposable server, writes app launch environment values, and stays alive until stopped. verify-tap-ui-smoke.sh uses that setup path internally.
MOBIDEX_SMOKE_MODE=seed MOBIDEX_STAY_ALIVE_ON_SUCCESS=1 seeds a fake SSH server into the launched app and keeps the disposable server alive for manual Simulator UI probing. Stop the script to clean it up.
verify-manual-ui-seed.sh is a convenience wrapper around seed mode. It defaults to password auth, keeps the Simulator and disposable SSH server alive, captures /tmp/mobidex-manual-ui-seed.png, and prints the manual tap checklist. For noninteractive validation of the wrapper without leaving the server running, use:
MOBIDEX_STAY_ALIVE_ON_SUCCESS=0 MOBIDEX_KEEP_SIMULATOR=0 Scripts/verify-manual-ui-seed.shTo use a custom package clone root, pass:
MOBIDEX_SOURCE_PACKAGES_DIR=/path/to/source-packages Scripts/verify-ios-build.sh MobidexWhen a reachable SSH host is available, this script checks the remote prerequisites, runs the app's heredoc-shaped project discovery command with merged stderr/stdout, verifies the configured Codex executable responds to app-server initialize, calls thread/list for up to five discovered project paths plus an unfiltered fallback when needed, and calls thread/read when the live host returns a thread:
MOBIDEX_SSH_HOST=host.example.com \
MOBIDEX_SSH_USER=mazdak \
MOBIDEX_SSH_IDENTITY_FILE=~/.ssh/id_ed25519 \
MOBIDEX_CODEX_PATH='~/.bun/bin/codex' \
Scripts/verify-live-host.shOptional variables:
MOBIDEX_SSH_PORTdefaults to22.MOBIDEX_CODEX_HOMEoverrides remoteCODEX_HOMEfor both discovery and app-server.MOBIDEX_CODEX_PATHdefaults tocodex.MOBIDEX_LIVE_CREATE_THREAD=1creates a temporary ephemeral read-only no-turn thread, reads its metadata withthread/readandincludeTurns=false, then lets the app-server drop it when the verifier process exits. This does not validate hydrated turns, archival, streaming, or steering.MOBIDEX_LIVE_CREATE_TURN=1is a stronger opt-in smoke that starts a real temporary turn. It creates a non-ephemeral temporary thread in the live cwd, sends one prompt, accepts either a completedturn/startresponse,turn/completed, or a completed turn found bythread/read, then archives the temporary thread. This can spend model/API resources and should only be used on a host intended for live validation.MOBIDEX_LIVE_TURN_PROMPToverrides the prompt for that opt-in turn; it defaults toReply exactly: mobidex live verification..MOBIDEX_LIVE_TURN_TIMEOUToverrides the materialized-turn completion wait, in seconds; it defaults to180.MOBIDEX_LIVE_CWDsets the remote cwd for that temporary thread; otherwise the script creates and removes a unique~/.mobidex-live-verify.*directory and its generated Codex project-trust stanza.
The script uses non-interactive ssh with BatchMode=yes, so it is best for SSH config, agent, or identity-file validation. Password authentication can be smoke-tested in app with MOBIDEX_SMOKE_AUTH=password Scripts/verify-inapp-ssh-smoke.sh, or through a full app-server turn with MOBIDEX_SMOKE_AUTH=password MOBIDEX_SMOKE_MODE=turn MOBIDEX_SMOKE_CODEX_PATH='~/.bun/bin/codex' Scripts/verify-inapp-ssh-smoke.sh.
Verified locally:
xcodegen generate.Scripts/verify-discovery.sh.Scripts/verify-app-server-schema.sh.Scripts/verify-ios-build.sh Mobidex.Scripts/verify-ios-build.sh MobidexTests.Scripts/verify-ios-build.sh MobidexUITests.SDK=iphoneos Scripts/verify-ios-build.sh Mobidex.SDK=iphoneos Scripts/verify-ios-build.sh MobidexTests.Scripts/verify-simulator-tests.sh; this validated app-hostedMobidexTestson an iOS simulator withxcodebuild test-without-building, executing 29 tests with 0 failures.MOBIDEX_UI_SMOKE_TIMEOUT=120 Scripts/verify-tap-ui-smoke.sh; this validated tap-level UI control on an iOS simulator through a generated UI.xctestrun, executing 1 XCUITest with 0 failures. The log shows synthesized taps for Connect, project row, composer, Send, Approve, Send, and Stop Turn against the deterministic fake SSH/app-server.Scripts/verify-simulator-launch.sh; this validated installing and launching the helper-built simulator app on an available iOS simulator, process survival after launch, and screenshot capture of the rendered initialServers/No ServersUI.Scripts/verify-inapp-ssh-smoke.sh; this validated in-app private-key authentication through Citadel to a disposable localhost SSH server, app-server startup through the configured Codex path, aturn/startfrom the app path, active-turn hydration, assistant output detection, and screenshot capture. The latest passing run usedMOBIDEX_SMOKE_CODEX_PATH='~/.bun/bin/codex'and reportedassistantSectionCount: 1,conversationSectionCount: 2, andexpectedTextFound: true.MOBIDEX_SMOKE_AUTH=password MOBIDEX_SMOKE_MODE=connection MOBIDEX_SMOKE_TIMEOUT=120 Scripts/verify-inapp-ssh-smoke.sh; this validated in-app password authentication through Citadel against a disposable AsyncSSH password server and a remote command executed from the app path.MOBIDEX_SMOKE_AUTH=password MOBIDEX_SMOKE_MODE=turn MOBIDEX_SMOKE_TIMEOUT=120 MOBIDEX_SMOKE_CODEX_PATH='~/.bun/bin/codex' Scripts/verify-inapp-ssh-smoke.sh; this validated in-app password authentication through Citadel, app-server startup,.codexdiscovery through a shell session,turn/start, active-turn hydration, assistant output detection, and screenshot capture against a disposable AsyncSSH password server.MOBIDEX_SMOKE_AUTH=password MOBIDEX_SMOKE_MODE=control MOBIDEX_SMOKE_TIMEOUT=120 MOBIDEX_SCREENSHOT_PATH=/tmp/mobidex-control-ui-smoke.png Scripts/verify-inapp-ssh-smoke.sh; this validated the launched app's control path over SSH against a deterministic fake app-server, includingthread/start,turn/start, approval response,turn/steer, live assistant delta rendering,turn/interrupt, and a compact conversation-detail screenshot with the steered assistant response.MOBIDEX_SMOKE_AUTH=password MOBIDEX_SMOKE_MODE=approval MOBIDEX_SMOKE_TIMEOUT=120 MOBIDEX_SCREENSHOT_PATH=/tmp/mobidex-approval-ui-smoke.png Scripts/verify-inapp-ssh-smoke.sh; this validated the launched app reaches a visible compact conversation detail with an active turn, stop button, command approval card, and user message.MOBIDEX_VISIBLE_SCREENSHOT_DIR=/tmp/mobidex-visible-ui-smokes Scripts/verify-visible-ui-smokes.sh; this reruns both visible UI smokes and writesapproval.pngandcontrol.png.MOBIDEX_SMOKE_AUTH=password MOBIDEX_SMOKE_MODE=seed MOBIDEX_STAY_ALIVE_ON_SUCCESS=1 Scripts/verify-inapp-ssh-smoke.sh; this seeds the launched app with a fake SSH server for manual Simulator UI probing and keeps the disposable server alive until the script is stopped.MOBIDEX_STAY_ALIVE_ON_SUCCESS=0 MOBIDEX_KEEP_SIMULATOR=0 Scripts/verify-manual-ui-seed.sh; this validates the manual UI seed wrapper without leaving the disposable server running. RunningScripts/verify-manual-ui-seed.shwithout those overrides is the interactive handoff path and waits for the user to stop it.Scripts/verify-live-host.shsyntax and missing-configuration failure path.Scripts/verify-live-host.shagainst a reachable key/agent SSH devbox withMOBIDEX_CODEX_PATH='~/.bun/bin/codex'; this validated SSH command execution, remotepython3, the configured Codex executable, heredoc-shaped.codexdiscovery filtered to existing remote directories, app-server stdioinitialize, and livethread/list.MOBIDEX_LIVE_CREATE_THREAD=1 Scripts/verify-live-host.shagainst the same devbox; this validated ephemeral no-turnthread/startand metadatathread/readwithincludeTurns=false.MOBIDEX_LIVE_CREATE_TURN=1 Scripts/verify-live-host.shagainst the same devbox; this validated a temporary materialized turn,thread/read includeTurns=true, archive cleanup, and fallback completion detection when the server records a completed turn without emittingturn/completedto this stdio verifier.- A localhost
sshdlive-verifier smoke with a generated identity file; this validatedIdentitiesOnly=yes, zsh-safe discovery exit handling, a temporary Codex wrapper for non-login PATH, app-server initialize,thread/list,thread/read, and ephemeral no-turnthread/start. - Focused tests compile into
MobidexTestsand execute throughScripts/verify-simulator-tests.sh. - App-view-model tests cover discovered
.codexsession counts, stale discovered-count clearing, completedturn/startresponses without completion notifications, live plan deltas, turn-plan updates, file-change patch updates, legacy file-change output deltas, turn-diff updates, terminal interaction output, MCP progress, current/legacy command approval responses, legacy patch approval responses, and server-request resolution cleanup. - App-view-model tests cover the no-selected-thread composer path that creates a thread before starting a turn, including a
thread/startednotification arriving before thethread/startresponse. REVIEW_NOTES.mdrecords the subagent review checkpoints for async state handling, app-server EOF behavior, discovery shell wrapping, project generation, and verification helpers.
Still needs environment/input:
- Optional live-host UI validation against a real Codex session on a suitable host. The deterministic fake-server UI path is covered by
Scripts/verify-tap-ui-smoke.sh; live model/API validation remains intentionally opt-in.
Local raw input channels are still unavailable (simctl has no tap/type operation and CoreGraphics event posting is denied), but the generated UI .xctestrun path gives a working XCUITest automation channel.
Mobidex uses trust-on-first-use SSH host-key pinning. If a pinned host key changes, the connection is rejected until the server is re-added or its pin is cleared by editing the endpoint.