You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Rebuild Pear as a native macOS app (Swift + SwiftUI + AppKit interop, libghostty for the terminal, existing Rust broker), drop Electron, and explicitly drop Windows/Linux. Goal: close the structural typing-latency gap with native terminals like Ghostty / iTerm, plus the secondary wins (memory, battery, bundle size, native UX).
This issue is the parent for the rewrite. Sub-issues will track each phase.
Assumed dependency: the agent-relay team will publish an official Swift SDK on a timeline compatible with Phase 1. If that slips, see the "Fallback if Swift SDK slips" section below.
Why
We've spent the last few PRs (#29, #30, #31, #32) systematically removing every avoidable cost on the typing hot path:
The Electron IPC and the browser compositor frame are structural — they exist as long as we ship Electron. Predictive/local echo (mosh-style) is the only way to hide them without restructuring, and it has its own correctness gotchas (raw mode, TUIs, password prompts).
Going native eliminates both costs and reaches the same floor as Ghostty (which can be ~5–10ms perceived for a shell prompt). That's the goal of this epic.
Goals
Typing latency: p50 keystroke-to-paint ≤ 20ms for a shell prompt; within 5ms of Ghostty on the same hardware.
Memory: ≤ 100MB at idle with one agent (vs. ~400MB today).
Bundle size: ≤ 50MB (vs. ~150MB today).
Battery: ≤ 0.5%/hr idle on Apple Silicon (current Electron is closer to 2–4%/hr).
Feature parity with current Electron app within ~6 months.
Non-goals
Cross-platform. Windows and Linux are dropped, explicitly. (If we wanted to keep them, we'd be doing predictive echo + sidecar work instead of this rewrite.)
Reuse of React UI code. Plan for a clean rewrite.
Migrating the broker. The existing Rust broker stays unchanged.
Mac App Store. Sandbox restrictions break broker spawning and project-directory writes. Direct distribution (notarized DMG, Sparkle auto-update) from day one.
Tech stack
Area
Choice
Notes
Language
Swift 6
Strict concurrency, async/await
UI
SwiftUI + AppKit interop
SwiftUI for most views; NSViewRepresentable for terminal + complex split panes
Terminal renderer
libghostty (embed via C bindings)
MIT-licensed, GPU-accelerated. SwiftTerm is the fallback if embedding turns out to be impractical.
Broker comms
Official Swift SDK from agent-relay team
Assumed available. Fallback: hybrid Swift + Node-sidecar (see below)
State management
@Observable (Swift 5.9+)
Don't over-engineer. Reach for TCA only if explicit need.
Persistence
JSON files, compat with Electron app's user-data dir
Easy import path
Diff viewer
swift-diff + Splash/Sourceful (or port shiki via a JS bridge for syntax highlighting parity)
Replaces react-diff-view + shiki
Graph view
Custom NSView + Core Animation, or SpriteKit if layout gets gnarly
Key point: the broker doesn't change. Only the UI process is rewritten.
Phased plan
Each phase becomes its own sub-issue (links to be added as they're filed).
Phase 0 — Foundations & spikes (2 weeks)
Goal: prove the assumptions and pick the renderer.
Xcode project scaffold, SPM dependency setup, CI building a stub Pear.app on a Mac runner with notarization wired
Spike 1 (gating): Embed libghostty in an NSView. Get one PTY's output painting. Measure keystroke-to-paint latency vs Ghostty on the same hardware. Decision: ship with libghostty or fall back to SwiftTerm?
Spike 2 (gating): Confirm the agent-relay Swift SDK timeline with that team. Get the SDK building against our existing broker. If timeline slips → activate the Node-sidecar fallback plan
Spike 3: State pattern with @Observable, mirroring the current zustand split (agent / project / ui / typing / pty-buffer stores)
Spike 4: Migration path from Electron user-data dir — read existing projects.json etc. from ~/Library/Application Support/pear-by-agent-relay/
Decision doc: terminal renderer choice (libghostty vs SwiftTerm vs custom), state pattern, persistence layer, broker comms (direct SDK vs sidecar)
Exit criteria:
A spike build types into a real broker-spawned agent end-to-end
Latency measurement in hand: ≤ 20ms p50, within 5ms of Ghostty
All Phase 0 decisions made and documented
Phase 1 — Vertical slice MVP (4 weeks)
Goal: one usable terminal pane that proves the architecture; team dogfood.
Single window, single project, single terminal pane
Deprecation notice on Electron build with 6-month timeline
Migration guide for users
Beta → GA
Exit criteria:
GA build shipped to all users
Electron build marked deprecated, EOL date published
Total estimate
Team size
Estimated duration
1 senior Swift-comfortable dev
9–12 months
2 devs
~6 months
3 devs
~4 months
Recommend 2 devs + 1 designer (the surface area is large enough that design parity matters).
Risks
Ranked by severity:
agent-relay Swift SDK slips. If the SDK isn't ready by the end of Phase 0, the whole rewrite blocks. Fallback: hybrid Swift app + Node sidecar running the existing TS SDK over a Unix-domain JSON-RPC socket. Adds an extra hop for control-plane RPCs (fine — only PTY chunks need to be fast, and those go direct to broker WebSocket). See the architecture diagram in Dedicated IPC channel for PTY chunks + opt-in typing trace #32's discussion for the hybrid layout.
libghostty embedding doesn't pan out. Either the C bindings are insufficient or the API stability isn't there yet. Fallback: SwiftTerm. Risk: we don't close the typing-latency gap as much as planned.
Team velocity in Swift. If the current team is React-shaped, expect a ramp. Mitigate by hiring one experienced Swift dev for foundation work and pairing.
User data migration bugs. Years of project config, channels, integrations. Mitigate with verbose-logging importer, keep Electron app installable for one release after GA.
Cross-platform user churn. Drop is permanent. Mitigate (or accept): 6-month deprecation notice, optionally open-source the Electron build as a community fork.
Cloud agents UX drift. That surface is in active flux; rebuilding it as it changes is wasted work. Mitigate by feature-freezing it during the rewrite, or scheduling it last (Phase 4) when it's stabilized.
Apple Silicon vs Intel. Easy in principle (Universal binary for both broker and app), but needs verification before GA.
Fallback if Swift SDK slips
If by the end of Phase 0 the agent-relay team doesn't have a credible timeline for a Swift SDK that lands before Phase 1 completes:
Activate the Node-sidecar hybrid:
Swift app talks directly to the broker's WebSocket for PTY chunks (the latency-critical path). Define and document the worker_stream message format as a stable contract.
A bundled Node sidecar runs @agent-relay/sdk and exposes a local JSON-RPC server over a Unix-domain socket. Swift calls it for spawnAgent, listAgents, attachTerminal, persona resolution, broker logs, burn hooks.
Sidecar managed as a child process of the app (start on launch, shutdown on quit, restart on crash).
Adds ~30MB to the bundle (single-executable Node) and the operational overhead of one extra process. Worth it to unblock.
When/if a Swift SDK lands, retire the sidecar in a single PR.
Open questions (need answers by end of Phase 0)
libghostty: which Ghostty version do we target, and how do we handle their breaking changes? Do we vendor or pin?
Broker process lifecycle: launchd agent (survives app close) or app child process?
Universal binary or Apple Silicon only on day one?
Migration UX: prompt on first launch, or pure auto-discovery?
AI conversation history (ai-hist): port to Swift, run via the Node sidecar, or skip in Phase 1?
Mac App Store: confirm it's permanently off the table, or revisit later with a sandboxed variant?
macOS minimum version: 13 (Ventura), 14 (Sonoma), or 15 (Sequoia)? SwiftUI features and @Observable argue for 14+.
Success criteria (GA)
p50 keystroke-to-paint ≤ 20ms for shell prompt, within 5ms of Ghostty
Idle memory ≤ 100MB with one agent attached
App bundle ≤ 50MB
Idle battery ≤ 0.5%/hr on M-series
No daily-flow regressions in 2-week pre-GA dogfood
All P0 features from Phases 1–4 shipped
Migration from Electron app works for 100% of internal dogfood data
Auto-update path verified end-to-end on a real user laptop
Summary
Rebuild Pear as a native macOS app (Swift + SwiftUI + AppKit interop, libghostty for the terminal, existing Rust broker), drop Electron, and explicitly drop Windows/Linux. Goal: close the structural typing-latency gap with native terminals like Ghostty / iTerm, plus the secondary wins (memory, battery, bundle size, native UX).
This issue is the parent for the rewrite. Sub-issues will track each phase.
Assumed dependency: the agent-relay team will publish an official Swift SDK on a timeline compatible with Phase 1. If that slips, see the "Fallback if Swift SDK slips" section below.
Why
We've spent the last few PRs (#29, #30, #31, #32) systematically removing every avoidable cost on the typing hot path:
sendInput(Move PTY buffer out of zustand; drop input queueing #31)broker:pty-chunkIPC channel that bypassesbroker:event's payload spread, structured-clone, and history push; added opt-in__pearTypingStats()trace (Dedicated IPC channel for PTY chunks + opt-in typing trace #32)Typing is meaningfully better but still distinguishable from a native terminal. The remaining gap is architectural:
The Electron IPC and the browser compositor frame are structural — they exist as long as we ship Electron. Predictive/local echo (mosh-style) is the only way to hide them without restructuring, and it has its own correctness gotchas (raw mode, TUIs, password prompts).
Going native eliminates both costs and reaches the same floor as Ghostty (which can be ~5–10ms perceived for a shell prompt). That's the goal of this epic.
Goals
Non-goals
Tech stack
NSViewRepresentablefor terminal + complex split panes@Observable(Swift 5.9+)react-diff-view+shikiNSView+ Core Animation, or SpriteKit if layout gets gnarly@xyflow/reactNSSplitViewControllerallotmentnotarytoolArchitecture
Key point: the broker doesn't change. Only the UI process is rewritten.
Phased plan
Each phase becomes its own sub-issue (links to be added as they're filed).
Phase 0 — Foundations & spikes (2 weeks)
Goal: prove the assumptions and pick the renderer.
Pear.appon a Mac runner with notarization wiredNSView. Get one PTY's output painting. Measure keystroke-to-paint latency vs Ghostty on the same hardware. Decision: ship with libghostty or fall back to SwiftTerm?@Observable, mirroring the current zustand split (agent / project / ui / typing / pty-buffer stores)projects.jsonetc. from~/Library/Application Support/pear-by-agent-relay/Exit criteria:
Phase 1 — Vertical slice MVP (4 weeks)
Goal: one usable terminal pane that proves the architecture; team dogfood.
Exit criteria:
Phase 2 — Core UX parity (8 weeks)
Goal: usable for ~80% of daily flows; beta gate.
Terminals:
Sidebar:
Chat / relay messages:
@mentionautocompleteDialogs:
Settings:
Theme:
Exit criteria:
Phase 3 — Secondary surfaces (5 weeks)
ai-histintegration; may need a Swift port or sidecar)Exit criteria:
Phase 4 — Cloud agents + integrations (4 weeks)
Exit criteria:
Phase 5 — Polish, migration, ship (3 weeks)
~/Library/Application Support/pear-by-agent-relay/to native app's data dir, with verbose logging and rollbackExit criteria:
Total estimate
Recommend 2 devs + 1 designer (the surface area is large enough that design parity matters).
Risks
Ranked by severity:
Fallback if Swift SDK slips
If by the end of Phase 0 the agent-relay team doesn't have a credible timeline for a Swift SDK that lands before Phase 1 completes:
Activate the Node-sidecar hybrid:
@agent-relay/sdkand exposes a local JSON-RPC server over a Unix-domain socket. Swift calls it forspawnAgent,listAgents,attachTerminal, persona resolution, broker logs, burn hooks.Open questions (need answers by end of Phase 0)
ai-hist): port to Swift, run via the Node sidecar, or skip in Phase 1?@Observableargue for 14+.Success criteria (GA)
References