A native macOS SwiftUI application for managing smithers workflows, agent sessions, terminals, and tickets.
Requires macOS 14 (Sonoma) or later, Apple Silicon (arm64).
Status: early / unstable. SmithersGUI is in active development and is not expected to be stable until ~mid-March 2027. Expect crashes, broken flows, and breaking changes between releases. If you hit a bug, please send logs (see Reporting bugs below).
Download SmithersGUI.dmg
· .sha256
· .sig
Unsigned by Apple. On first launch macOS will refuse to open it ("can't be opened because Apple cannot check it for malicious software"). To allow it: open System Settings → Privacy & Security, scroll to the message about SmithersGUI being blocked, and click Open Anyway (you'll be prompted for your password). macOS will remember this for future launches. Verifying the eth signature below is the way to confirm the binary is the one we built.
Releases are signed with a secp256k1 key (an Ethereum wallet) so you can prove the DMG you downloaded is the same bits we built — no Apple Developer ID required.
Signer address: 0xA1aaEC6B60547BE8677247f9Eb2d9fCc975496fb
The signature is over the SHA-256 of the DMG. Verify with foundry's cast:
# 1. download the artifacts
curl -LO https://download.smithers.sh/SmithersGUI.dmg
curl -LO https://download.smithers.sh/SmithersGUI.dmg.sig
# 2. hash the DMG and verify the signature recovers the signer address
HASH=0x$(shasum -a 256 SmithersGUI.dmg | awk '{print $1}')
SIG=$(cat SmithersGUI.dmg.sig)
cast wallet verify --address 0xA1aaEC6B60547BE8677247f9Eb2d9fCc975496fb "$HASH" "$SIG"
# → prints "Validation succeeded." on a good signatureIf cast wallet verify succeeds, the DMG matches what was signed by the holder of the private key (stored in the maintainer's macOS Keychain — see scripts/init-signing-key.ts and scripts/sign-dmg.ts).
The app writes a structured JSON log to ~/Library/Logs/SmithersGUI/app.log, and macOS drops native crash reports into ~/Library/Logs/DiagnosticReports/SmithersGUI-*.ips. To send everything in one go, paste this into Terminal:
zip -j ~/Desktop/smithers-logs.zip \
~/Library/Logs/SmithersGUI/app.log \
~/Library/Logs/DiagnosticReports/SmithersGUI-*.ips 2>/dev/null
open -R ~/Desktop/smithers-logs.zipThat produces ~/Desktop/smithers-logs.zip and reveals it in Finder. Attach the zip to your bug report along with:
- a short description of what you were doing
- the workspace folder you'd opened (path is fine — we don't need its contents)
- whether you were on the splash/welcome screen or inside a workspace when it broke
The app degrades gracefully without these — specific features will be unavailable:
| Dependency | What breaks without it | Install |
|---|---|---|
| tmux | Terminal multiplexing (split panes, named sessions) is disabled. Falls back to a single direct Ghostty shell per terminal surface. Searched at /opt/homebrew/bin/tmux, /usr/local/bin/tmux, /usr/bin/tmux, and PATH. |
brew install tmux |
| git | Session fork / timeline-replay fails. Error: SessionForkError.gitUnavailable. |
xcode-select --install |
| nvim (Neovim) | "Open in Neovim" option for tickets is hidden. The built-in editor still works. Searched at /opt/homebrew/bin/nvim, /usr/local/bin/nvim, /usr/bin/nvim, and PATH. |
brew install neovim |
Agent CLIs (claude, codex, gemini, kimi, amp, forge) |
Only agents whose CLI binary is found on PATH appear in the agent picker. Each requires its own API key (e.g. ANTHROPIC_API_KEY, OPENAI_API_KEY). |
Install per agent's docs |
The terminal is being migrated from a macOS-only AppKit path to a
cross-platform SwiftUI surface fed by libghostty's pipes backend via
libsmithers-core:
TerminalSurface.swift— shared SwiftUI entry point. Compiles on macOS + iOS. Driven by byte streams from aTerminalPTYTransport.TerminalView+macOS.swift— macOS bridge that delegates to the existing apprt-backedTerminalSurfaceRepresentable(inTerminalView.swift, now guarded#if os(macOS)).ios/Sources/SmithersiOS/Terminal/TerminalIOSRenderer.swift— iOS UITextView renderer over the shared model. libghostty VT-level rendering (viaghostty-vt.xcframeworkfrom the 0092 PoC) replaces this body in a follow-up.
Compatibility note: RuntimePTYTransport is wired through 0120's
SmithersRuntime wrapper but the 0120 runtime still ships a fake
transport with no real byte stream. macOS therefore continues to use
the legacy smithers-session-daemon path inside
TerminalSurfaceRepresentable during migration. Once the 0094
WebSocket PTY lands and 0120's transport graduates, flip shared
callers off TerminalView (macOS-only) onto TerminalSurface and
delete the daemon fallback.
Live-run inspector tabs keep a per-run stream cursor (afterSeq) and
continue reconnecting from the last acknowledged sequence.
- Reconnect behavior: short blips (<2s) keep the current tree visible and avoid disruptive banners; longer interruptions show a stale-state banner ("stale since …") while preserving the last-known tree.
- Background tab behavior: run tabs keep their devtools/log subscriptions alive while backgrounded, so returning to a tab does not reset the stream cursor.
- Gap resync behavior:
GapResyncevents force snapshot-first recovery (deltas are ignored until a follow-up snapshot arrives). - Ghost retention: unmounted task nodes are retained in a ghost map so inspector selection/history stays resolvable.
- Memory envelope:
N × (active tree + buffered live events + ghost map)forNconcurrently open run tabs. Ghost retention is capped and oldest entries are evicted. - Ghost cap:
set
SMITHERS_DEVTOOLS_GHOST_CAP=<positive int>to override the default cap (256ghost task entries per run store).
| Dependency | Version | Purpose |
|---|---|---|
| macOS | 14.0+ (Sonoma), Apple Silicon | Target platform — no Intel/Linux/Windows support |
| Xcode / Swift | 15+ / Swift 5.9+ | Compiler toolchain (xcode-select --install for CLI tools only is not enough — needs full Xcode for xcodebuild) |
| Rust | stable (1.80+) | Builds libcodex_ffi.a from codex-ffi/ |
| Zig | exactly 0.15.2 (pinned in .zigversion) |
Build driver (build.zig) + required for rebuilding the Ghostty xcframework |
| xcodegen | 2.43+ | Regenerates SmithersGUI.xcodeproj from project.yml |
| ViewInspector (Swift package) | auto | Test-only dependency, fetched automatically |
Zig has no official LTS release. We pin 0.15.2 (matching ghostty's
minimum_zig_version) via.zigversion.zvmpicks this up automatically when youcdinto the repo.build.zigwill hard-fail at compile time if the Zig version doesn't match.
git clone --recursive https://github.com/smithersai/gui.git
cd gui
zvm use # picks up .zigversion (0.15.2)
zig build ghostty # one-time, slow (~3-10 min). Builds GhosttyKit.xcframework.
zig build # builds codex-ffi + SmithersGUIIf you already cloned without --recursive:
git submodule update --init --recursiveWhy the separate ghostty step? The Ghostty xcframework is a ~200 MB build artifact that isn't shipped in the ghostty submodule, so every fresh clone has to build it once. After that, it lives in
ghostty/macos/GhosttyKit.xcframework/and day-to-day builds just reuse it. Rerunzig build ghosttyonly when you bump the ghostty submodule.
The project uses build.zig as a Makefile-style entrypoint. All common tasks go through zig build <step>:
| Command | What it does |
|---|---|
zig build |
Build everything needed to run the app (codex-ffi + swift build). Default step. |
zig build run |
Build then launch .build/debug/SmithersGUI. |
zig build codex-ffi |
Just the Rust FFI staticlib (codex-ffi/target/release/libcodex_ffi.a). |
zig build swift |
Just the Swift app via swift build. |
zig build xcode |
Build via xcodebuild (Debug). Pass -Drelease=true for Release. |
zig build xcodegen |
Regenerate SmithersGUI.xcodeproj from project.yml. Run this after editing project.yml. |
zig build test |
Run cargo test + swift test. |
zig build ghostty |
Rebuild ghostty/macos/GhosttyKit.xcframework from source. Slow; only needed if the vendored xcframework is missing or you want to update it. |
zig build clean |
cargo clean + swift package clean. |
Pass -Drelease=true to any build step for release-mode compilation.
Physical iPhones cannot reach the Mac's localhost:4000. For preview
testing, build SmithersiOS with a reachable Plue base URL baked into
Info.plist.
Internet-reachable review with ngrok:
# Terminal 1: keep this running
./ios/scripts/start-preview-tunnel.sh
# Terminal 2: build/install with the generated tunnel URL
source build/preview-tunnel/plue-preview.env
DEVICE_ID=<device-identifier> INSTALL_ON_DEVICE=1 ./ios/scripts/build-for-device.shstart-preview-tunnel.sh starts the local Plue Docker stack if
http://localhost:4000/api/health is not already healthy, starts
ngrok http 4000, captures the HTTPS URL as PLUE_PREVIEW_URL, and
writes PLUE_BASE_URL exports to build/preview-tunnel/plue-preview.env.
Zero-egress LAN testing:
./ios/scripts/build-for-device.shWith no PLUE_PREVIEW_URL or PLUE_BASE_URL, the build script detects the
Mac's LAN IP with ipconfig getifaddr en0, bakes
http://<LAN-IP>:4000 into SmithersPlueBaseURL, and generates a temporary
Info.plist with a narrow ATS exception for that IP only. Override with
PLUE_LAN_IP=... or PLUE_DEVICE_BASE_URL=... when needed.
The OAuth2 redirect remains smithers://oauth2/callback; the app accepts
the smithers callback scheme and derives the backend base URL from the
baked PLUE_BASE_URL / PLUE_PREVIEW_URL value. No production Plue deploy
is involved.
Validation sequence:
./ios/scripts/start-preview-tunnel.sh
source build/preview-tunnel/plue-preview.env
DEVICE_ID=<device-identifier> INSTALL_ON_DEVICE=1 ./ios/scripts/build-for-device.sh
# Launch on device, tap Sign In, complete OAuth2, and return via smithers://.
LAN_URL="http://$(ipconfig getifaddr en0):4000"
curl -fsS "$LAN_URL/api/health"codex-ffi/— a standalone Rust crate in this repo that wrapscodex-corevia C ABI. Produceslibcodex_ffi.a(~115 MB static archive) which Swift links against. Depends on codex crates via path into thecodex/submodule.ghostty/macos/GhosttyKit.xcframework/— a prebuilt xcframework checked into the ghostty submodule. You normally don't need to rebuild this;zig build ghosttyis only for toolchain updates.- SmithersGUI — the Swift app itself, linking both of the above.
error: This project requires Zig 0.15.2. You have ...— runzvm use(reads.zigversion).ld: library 'codex_ffi' not found— you skipped the codex-ffi step. Runzig build codex-ffi(or justzig build).ld: library 'ghostty-fat' not found— the ghostty submodule wasn't initialized. Rungit submodule update --init --recursive. If the.xcframeworkdirectory exists but is empty, you need to rebuild it withzig build ghostty(requires a working Zig toolchain with macOS SDK support).- Submodule clone fails or is empty — make sure you cloned with
--recursive, or rungit submodule update --init --recursiveafter the fact.