Skip to content

Spike: replace per-session embedded Ghostty with single Ghostty + tmux backend #198

@dhilgaertner

Description

@dhilgaertner

Summary

Investigate replacing the current architecture — one embedded Ghostty window per session — with a single embedded Ghostty instance backed by tmux, where each Crow session/tab maps to a tmux session (or window) that we switch into when the user selects the tab.

This is a research/design spike. Output is a design doc + go/no-go recommendation, not production code.

Motivation

Current pain points

  1. Resource overhead — every session spawns its own embedded Ghostty/PTY. With 5–10 sessions open, that's 5–10 terminal processes plus duplicated rendering and memory overhead.
  2. Tab-switch cost — switching tabs swaps which embedded terminal view is visible; under load this is noticeably heavier than it needs to be.
  3. No standardized "prompt ready" signal — today we rely on sleep-and-hope heuristics in launcher code to know when the embedded shell is ready to receive send-keys. This is fragile and a recurring source of bugs.
  4. No session persistence — if Crow restarts, embedded terminal state is lost. Users have to rebuild context.

Proposed model

  • One embedded Ghostty instance (via libghostty) for the whole app.
  • A tmux server runs in the background. Each Crow session is either:
    • Option A: a tmux window inside one shared tmux session (e.g. crow-cockpit), or
    • Option B: a separate tmux session per Crow session, switched via switch-client.
  • Clicking a tab in Crow's UI fires tmux select-window (or switch-client) — the embedded Ghostty instantly reflects the new context. No new process, no view swap.
  • A bundled shell wrapper sources the user's normal shell config and emits a CLAUDE-READY marker (OSC 9 or OSC 133) so we can detect prompt-ready without sleeps.

Wins if this works

  • 1 Ghostty process instead of N
  • Faster, lighter tab switching (tmux window switch is ~ms)
  • Reliable prompt-ready detection via tmux capture-pane polling or tmux -CC control mode events
  • Session persistence across Crow restarts — reattach the same tmux session and pick up where the user left off
  • Cleaner API surface for send-keys and capture-pane than driving N independent PTYs

Investigation scope

A. Architecture options

  • Compare Option A (one tmux session, N windows) vs Option B (N tmux sessions, one window each)
    • Trade-offs: window-targeting ergonomics, status-line behavior, attach/detach semantics, multi-pane support per Crow session
  • How does this interact with the current Packages/CrowGhostty / embedded Ghostty integration? Does libghostty support a single long-lived embedded view that we re-target, or do we need to manage the surface differently?
  • Where does ClaudeLauncher (Packages/CrowClaude/) fit? Today it spawns Claude in a fresh embedded terminal. New flow: launch Claude inside a tmux window we created.
  • Interaction with the agent abstraction work in Agent abstraction Phase A: introduce CodingAgent protocol (behavior-preserving) #166 / Spec: Support OpenAI Codex as an alternative to Claude Code #150ClaudeCodeAgent and any future CodexAgent would still launch via tmux. Does the abstraction need a TerminalBackend concept too?

B. Prompt-readiness signal

  • Spike a bundled shell wrapper script that:
    • Sources the user's full shell config (.zshenv.zprofile.zshrc, or bash equivalents) so aliases / oh-my-zsh / asdf / nvm / conda all keep working
    • Adds an OSC 9 (\033]9;CROW-READY\007) and/or OSC 133 (\033]133;A...) marker on every precmd / PROMPT_COMMAND
    • Preserves any existing user-defined precmd / PROMPT_COMMAND
    • Hands off via exec "$SHELL" -i
  • Decide between two detection strategies and prototype both:
    1. Pollingtmux capture-pane -p -t <target> every 20–50 ms, scan for marker. Simple, no parsing infra.
    2. Control mode — run tmux as tmux -CC, parse the %output event stream live. Zero polling, harder to implement, much closer to "real" event-driven.
  • Test with the shells we care about: zsh (default on macOS), bash, fish (best-effort).

C. Distribution / first-run UX

  • tmux availability — detect it (which tmux); options if missing:
    • Prompt to brew install tmux (most users already have brew)
    • Bundle a static tmux binary inside the .app (heavier, fully self-contained)
  • Code-signing/notarization implications of bundled binaries.
  • First-run onboarding copy: explain "Crow runs your sessions inside an isolated tmux environment that loads your normal shell config — no changes to your dotfiles."
  • Storage of session→tmux-window mapping in AppState / persistence layer so tabs survive restart and can rebind to existing tmux sessions.

D. Risks / unknowns

  • libghostty embedding: can a single surface be cleanly re-attached to different PTYs / tmux clients, or do we need one Ghostty surface per tmux client connection? Might force Option B (one tmux session per Crow session) so each gets its own tmux attach client.
  • Scroll-back, copy-mode, and search behavior change when tmux owns the buffer instead of Ghostty. UX regression risk.
  • Mouse / clipboard integration via tmux — currently Ghostty handles this directly.
  • User shells that don't cleanly support our marker (custom prompt frameworks like starship — does it preserve OSC 133?).
  • Migration path: existing sessions in the field would need to be re-bound to tmux. What does upgrade look like?

Deliverables

  1. Design doc in docs/ (proposed: docs/tmux-backend-spec.md) covering:
    • Recommended option (A vs B) with rationale
    • Architecture diagram: Crow UI ↔ libghostty ↔ tmux ↔ shell wrapper ↔ Claude/Codex
    • Prompt-readiness mechanism (chosen approach + fallback)
    • Distribution plan for tmux + shell wrapper
    • Migration plan for existing users
  2. Working prototype (throwaway branch is fine) demonstrating:
    • One embedded Ghostty, two Crow tabs, tab switching via tmux select-window or switch-client
    • waitForPrompt(session: ..., timeout: ...) Swift helper using OSC marker detection
  3. Recommendation: ship / ship-with-conditions / don't ship, with concrete next-phase tickets if green-lit.

Non-goals

Source

Captured from a brainstorming conversation exploring whether a tmux-backed cockpit pattern (used by several Claude-heavy power-user tools) would be a better fit for Crow than the current per-session Ghostty embedding.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions