Muxy

Architecture

The processes, the crates, and how data moves between them.

Processes

flowchart LR
    DESKTOP["Desktop app"] <-->|"Unix socket"| LOCAL
    TUI["muxy terminal UI"] <-->|"Unix socket"| LOCAL
    DESKTOP <-->|"SSH · muxy stdio<br/>from another computer"| LOCAL
    TUI <-->|"SSH · muxy stdio<br/>from another computer"| LOCAL
    PHONE["Phone app"] <-->|"TLS · after pairing"| NETWORK
    PHONE <-->|"SSH · muxy stdio"| LOCAL
    subgraph SERVER["muxy-server · one per profile"]
        LOCAL["Local listener"]
        NETWORK["Network listener · opt-in"]
        CONNECTION["Client connections"]
        CATALOG["Projects"]
        S1["Session thread"]
        S2["Session thread"]
        LOCAL --> CONNECTION
        NETWORK --> CONNECTION
        CONNECTION <--> CATALOG
        CONNECTION <--> S1
        CONNECTION <--> S2
    end
  • The desktop app ships the muxy and muxy-server executables, which are also released on their own. Either app starts the server if it isn't running.
  • The server keeps projects and which sessions belong to them. Apps keep tabs, panes, order, and workspaces.
  • The desktop app keeps a connection per server: this computer's, and one per remote device. A second connection to each carries extension work and remote file transfers, so they never wait behind terminal traffic.
  • Apps never include server code. They only speak the protocol.

Crates

flowchart TB
    APP["muxy-app"] --> UI["muxy-ui"]
    APP --> APPCORE["muxy-app-core"]
    APP --> CLIENT["muxy-client"]
    CLI["muxy-cli"] --> APPCORE
    CLI --> CLIENT
    MOBILE["muxy-mobile"] --> CLIENT
    CLIENT --> PROTOCOL["muxy-protocol"]
    APPCORE --> PROTOCOL
    SERVER["muxy-server"] --> TERMINAL["muxy-terminal"]
    SERVER --> PROTOCOL
    TERMINAL --> PROTOCOL
CrateRole
muxy-appThe desktop app.
muxy-uiReusable GPUI components and native web views. No app state.
muxy-app-coreThe app model without UI, shared by desktop and terminal UI: layouts, workspaces, settings, extensions.
muxy-cliThe muxy command: CLI and terminal UI.
muxy-clientConnects to a server, starting a local one if needed.
muxy-mobileThe client library for phones, with Swift and Kotlin bindings.
muxy-protocolMessages, encoding, screen types, and transports.
muxy-serverThe server: projects, sessions, history, Git, files, and mobile access.
muxy-terminalWraps the Ghostty terminal and the PTY.
muxy-coreShared basics, such as folders, locks, the shortcut catalog, and the app language.

Typing a key

sequenceDiagram
    participant App
    participant Connection as Server connection
    participant Session as Session thread
    participant Shell
    App->>Connection: Input
    Connection->>Session: Forward right away
    Session->>Shell: Write to the PTY
    Shell-->>Session: Output
    Note over Session: Next tick, about 16 ms later
    Session-->>Connection: Rows that changed
    Connection-->>App: Screen frame
    App->>Connection: Ack, ready for the next frame

Inside a session

flowchart LR
    PTY["PTY"] -->|"output"| READER["PTY reader thread"]
    READER --> OWNER["Session thread<br/>owns the Ghostty terminal"]
    REQUESTS["Input · resize · attach"] --> OWNER
    OWNER -->|"input"| PTY
    OWNER -->|"changed rows"| OUTBOX["Outbox of each attached app"]
    OWNER -->|"checkpoints"| DISK["Saved screen and history"]
  • One thread owns each terminal for its whole life. The Ghostty terminal isn't thread-safe, so everything else sends that thread messages.
  • A separate thread only reads the PTY and forwards bytes. Chatty programs produce many tiny reads.
  • Every tick, if anything changed, the session sends the changed rows and the cursor. If nothing changed, nothing is sent.
  • When output goes quiet after a burst, history is compressed.
  • A background writer saves the screen and history, so they survive the program and the server.

Inside a connection

flowchart LR
    READ["Reader"] -->|"input, acks, pings"| FAST["Handled right away"]
    READ -->|"everything else"| WORKER["Ordered background worker"]
    OUTBOX["Outbox<br/>control messages first<br/>one merged frame per session"] --> WRITE["Writer"]
  • Each app has its own connection and outbox, so a slow app never slows a fast one.
  • The reader never waits on disk or on starting processes.
  • Screen updates merge: a newer frame folds into the one not yet sent, which goes out once the app acknowledges the previous one.
  • Replies and other control messages never wait behind screen data.

Drawing in the app

  • For each visible terminal, the app keeps the screen rows as styled runs, plus any history it has fetched.
  • It repaints a pane only when a frame arrives or the view changes: one text line per row and one rectangle per color run.
  • Hidden terminal panes detach but keep their last screen and scroll position in memory, and the server still counts them as showing their session. They attach again when shown, displaying cached content first, even while offline.
  • The UI is built from muxy-ui components. Every app shortcut comes from one catalog, resolved through the keymap. Terminal preferences and key bindings live in terminal.toml, and the Quick Terminal's global shortcut is its own setting.

Extensions and web views

  • Web views are native WebKit views and stay alive while hidden. They have no terminal or server state.
  • The app loads extensions, enforces their permissions, and asks before sensitive actions. Their scripts run in JavaScriptCore on their own threads.
  • File, Git, and process work goes through the server. Extension web requests run in the app and can't reach the local machine or private networks.
  • Marketplace packages are verified before they are installed.
  • Language packs are resource-only bundles. The app reads their catalogs, checks that each translation keeps its key's placeholders, and runs nothing from them.

The browser

  • Browser panes embed polybrowser, which wraps the system WebKit view and keeps profiles and history.
  • A page sits in the same native host as extension web views, so it hides under app content and shares the keyboard with Muxy's shortcuts.
  • The server knows nothing about browsers. muxy browser reaches the desktop app over its own socket in the profile folder, app.sock, which only this user can open. One JSON request gets one JSON reply.

AI activity

  • Each session thread checks the running program, the title, and the screen against built-in rules to find agents and their state.
  • Apps get agent status from the server. They never read terminal output themselves.
  • One desktop app delivers system notifications, so alerts are never doubled.

Updates

  • If the new app speaks the running server's protocol version, the server keeps running and is replaced once no terminals are left.
  • Otherwise the update waits for every terminal to end, or ends them after the user confirms.
  • The old app bundle is kept until its server exits.
  • Linux user installs switch one symlink between complete version directories. Runtime leases keep old helpers available while apps, CLIs or servers use them; one previous version is also retained for rollback. Download checksums, package manifests and all three executable build identities are verified before activation. Package-manager paths are never replaced by this updater.