Architecture
13x is a local-first desktop app built on Tauri 2: a Rust backend that owns real PowerShell PTY sessions and on-disk state, paired with a React 18 + TypeScript frontend in the system WebView. The two halves talk only over Tauri's in-process IPC — there is no HTTP server, no socket, and no cloud.
The shape of it
A Rust core spawns and owns real shells; a React grid renders them; nothing crosses a network in between. The frontend invokes Rust commands one way, Rust emits events the other way, both over Tauri's in-process IPC. tauri.conf.json declares no remote allowlist, the only outbound endpoint is the updater (a placeholder URL), and there is no telemetry in the codebase.
Process model
main.rs builds a single Tauri app. It first repairs PATH (services::path_env::ensure_user_path), then registers managed state and plugins (dialog, updater, process). A setup hook reconciles and prunes run + snapshot history and installs Claude Code hooks. All ~60 Tauri commands are registered in one generate_handler! block.
- Managed services: TerminalManager, RunRecorder, RelayRunner, the ts-resolver SidecarManager, BlastCache, MapWatcher, AgentStateWatcher, ClassifyCache
- The Rust binary is named thirteenx; the product is 13x (identifier com.thirteenx.desktop)
Frontend to backend (commands)
The React app invokes Rust via @tauri-apps/api. Commands live in src-tauri/src/commands/* (terminals, rigs, resume, runs, codemap, context_packets, advisor, relay, worktrees, session, project) and are thin wrappers that delegate to a service. Models are serialized camelCase via serde so the wire shape matches the TypeScript types (e.g. TerminalSession).
Backend to frontend (events)
The Rust side pushes asynchronous updates with app.emit on named channels defined in models/events.rs. The frontend subscribes once at startup (initTerminalEvents, initRelayEvents in App.tsx) and starts a Pulse idle timer.
- terminal://output — streamed PTY bytes
- terminal://status, terminal://exit, terminal://error — lifecycle and failures
- terminal://agent-state — inferred agent turn state
- relay://run | step | activity — the separate relay channel
Terminal core
TerminalManager owns every live shell via portable-pty (ConPTY on Windows). launch opens a pty, resolves a shell (shell_resolver candidates: pwsh, then Windows PowerShell or a POSIX login-shell fallback), spawns the first that starts, emits a banner, then spawns a per-session reader thread (read_loop) that streams output and tees raw bytes into an active RunRecorder. Auto-start commands and agent map-context prompts are typed in after fixed delays via generation-pinned threads.
Generation-pinned session safety
Restart reuses a session id but bumps a generation counter. The reader, auto-start, replay, and run-recording threads all check the generation under the sessions Mutex, so a stale thread can never corrupt the relaunched terminal. The xterm view persists across restarts because the id is reused.
Frontend shell
App.tsx is the top-level component: TopBar, Sidebar, and a single <main> whose body switches on a Zustand view-mode (grid / graph / map / worktrees / relay) or focus mode. State is split into per-feature Zustand stores (terminalStore, rigStore, resumeStore, codeMapStore, graphStore, and others). Global keyboard chords are bound in a capture-phase keydown handler so app shortcuts win over xterm.
TypeScript-analysis sidecar
Code-map symbol resolution that needs ts-morph runs out-of-process in the bundled ts-resolver binary (Tauri externalBin from src-tauri/binaries). SidecarManager keeps one resident process keyed by scanned root, talks length-prefixed JSON-RPC (write_frame/read_frame) over stdio, applies per-method timeouts with a PID-killing watchdog, and respawns-and-retries once on a crash. Because it is a separate process, a slow analysis can never block the UI or the Rust manager Mutex. In dev it falls back to node --import tsx/esm server.ts.
GUI PATH repair
path_env::ensure_user_path queries the login shell once at startup and merges Homebrew / Microsoft / snap dirs before any subprocess spawns, fixing the macOS works-in-terminal-not-in-app bug for pwsh / claude / codex. The probe runs with a 3s timeout, so a very slow shell profile or exotic setup could still hide a CLI — see Limitations.
Build pipeline
Vite builds the React app to dist on a fixed port 1420 (vite.config.ts). tauri.conf.json's beforeBuildCommand runs npm run build && npm run build:resolver (bun compiles the sidecar). The bundled ts-resolver[.exe] is placed next to the app executable at bundle time and run directly in production.
Keyboard shortcuts
- Ctrl + L — launch grid (from idle only)
- Ctrl + K — kill all (opens confirm)
- Ctrl + S — save rig (prompts if unsaved)
- Ctrl + B — open broadcast modal (grid running)
- Ctrl + Enter — jump to broadcast bar (grid running)
- Ctrl + Space — jump to next agent that needs you
- Ctrl + G — toggle grid / symbol-graph view
- Ctrl + 1..9 — focus terminal 1..9
- Ctrl + Alt + Arrows — move selection across the overview grid
- Ctrl + Alt + Enter — open the selected terminal in focus mode
- Esc — close editor / panel / modal, or leave focus mode
Where your data lives
- <app config dir>/com.thirteenx.desktop/rigs.json — saved rigs (path, shell count, layout, slots, roles, startup commands, autostart); corrupt files are moved aside to rigs.corrupt.<timestamp>.json
- <project>/.13x/sessions/<slot>/ — per-slot session dir; holds agent-events.log (Claude Code hook tokens tailed by AgentStateWatcher), deleted on each fresh PTY launch so stale state is never replayed
- <project>/.13x/bin/bootstrap.ps1 — per-project PowerShell bootstrap script loaded via -File with THIRTEENX_SLOT / THIRTEENX_SLOT_DIR env vars for best-effort agent tracking
- <project>/.13x/map/codegraph.json — local code-map graph (files, symbols, imports) consumed by context packets and the sidecar
- <project>/.13x/snapshots/ — rig checkpoints, latest.json, and audit.jsonl resume audit log; written and pruned at startup
- <project>/.13x/context-packets/ — generated context packets (<timestamp>-<task>.md and .json)
- runs.json (app data) — run history; reconciled (active to interrupted) and pruned at startup by run_store
- ~/.claude/settings.json — env-guarded agent-state hooks installed/refreshed at startup by claude_hooks::ensure_installed (best-effort; never blocks startup)
- src-tauri/binaries/ts-resolver[.exe] — bundled sidecar binary (Tauri externalBin), run directly in production
Privacy and safety
- Fully local: the UI talks to Rust over in-process Tauri IPC, not a network socket — no HTTP server, no accounts, no telemetry in the codebase
- The only declared outbound endpoint is the updater, and it is a placeholder (https://REPLACE_ME/13x/latest.json) — the app ships no live update server
- All working state stays on disk under the project's .13x/ dir or the OS app-config/app-data dir; nothing is uploaded
- Subprocesses are deliberate and visible: the PowerShell PTY shell, the ts-resolver sidecar, the login-shell PATH probe, and (on stop) a process-tree kill
- The agent-state watcher only tails local agent-events.log files, seeding offsets to current file lengths on first watch so a previous run's events are never replayed
- Output decoding is local lossy UTF-8; bytes are streamed to the WebView and, only when capture is enabled, appended to a local run log
Limitations
Several parts of the architecture are intentionally heuristic or unfinished — documented here honestly.
- PowerShell-first: every slot launches pwsh (or Windows powershell.exe), with a POSIX login-shell only as a last-resort fallback on macOS/Linux. There is no per-slot shell picker yet.
- macOS/Linux GUI PATH repair queries the login shell with a 3s timeout; a very slow or exotic profile could still hide a CLI, requiring a launch from a terminal or a standard rc-file entry.
- The sidecar requires Node in dev (node --import tsx/esm) or the bundled bun binary in production; if neither is present, TypeScript symbol analysis fails with an actionable error.
- Auto-start and agent map-context commands are typed after fixed delays (~700ms / 2600ms) rather than on a true ready signal, since a persistent PowerShell never emits a per-command ready event.
- The CSP is null (security.csp in tauri.conf.json), relying on the local-only design rather than a content-security policy.
- DONE / agent state is best-effort: a persistent interactive PTY does not exit per command, so completion is inferred heuristically and agent turn state depends on Claude Code hook logs being present.