Arquitetura
Como o navegador, o servidor Node.js, o tmux e o CLI Claude se encaixam.
O purplemux são três camadas costuradas: um front-end no navegador, um servidor Node.js em :8022 e tmux + CLI Claude no host. Tudo entre elas é WebSocket binário ou um pequeno POST HTTP.
As três camadas
Browser Node.js server (:8022) Host
───────── ──────────────────────── ──────────────
xterm.js ◀──ws /api/terminal──▶ terminal-server.ts ──node-pty──▶ tmux (purple socket)
Timeline ◀──ws /api/timeline──▶ timeline-server.ts │
Status ◀──ws /api/status────▶ status-server.ts └─▶ shell ─▶ claude
Sync ◀──ws /api/sync──────▶ sync-server.ts
status-manager.ts ◀──POST /api/status/hook── status-hook.sh
rate-limits-watcher.ts ◀──POST /api/status/statusline── statusline.sh
JSONL watcher ──reads── ~/.claude/projects/**/*.jsonl
Cada WebSocket tem um propósito único; não multiplexam. A autenticação é um cookie JWT do NextAuth verificado no upgrade do WS.
Navegador
O front-end é um app Next.js (Pages Router). As peças que falam com o servidor:
| Componente | Biblioteca | Propósito |
|---|---|---|
| Painel de terminal | xterm.js |
Renderiza bytes de /api/terminal. Emite teclas, eventos de resize, mudanças de título (onTitleChange). |
| Timeline da sessão | React + useTimeline |
Renderiza turnos do Claude vindos de /api/timeline. Sem derivação de cliState — isso é tudo no servidor. |
| Indicadores de status | Zustand useTabStore |
Badges de aba, pontos da barra lateral, contagens de notificação dirigidos por mensagens de /api/status. |
| Sync multi-dispositivo | useSyncClient |
Observa edições de workspace/layout feitas em outro dispositivo via /api/sync. |
Títulos de aba e o processo em primeiro plano vêm do evento onTitleChange do xterm.js — o tmux é configurado (src/config/tmux.conf) para emitir #{pane_current_command}|#{pane_current_path} a cada dois segundos, e lib/tab-title.ts faz o parsing.
Servidor Node.js
server.ts é um servidor HTTP customizado que hospeda o Next.js mais quatro instâncias WebSocketServer do ws na mesma porta.
Endpoints WebSocket
| Caminho | Handler | Direção | Uso |
|---|---|---|---|
/api/terminal |
terminal-server.ts |
bidirecional, binário | I/O de terminal via node-pty conectado a uma sessão tmux |
/api/timeline |
timeline-server.ts |
servidor → cliente | Stream de entradas de sessão Claude parseadas do JSONL |
/api/status |
status-server.ts |
bidirecional, JSON | status:sync / status:update / status:hook-event do servidor, status:tab-dismissed / status:ack-notification / status:request-sync do cliente |
/api/sync |
sync-server.ts |
bidirecional, JSON | Estado de workspace cross-device |
Mais /api/install para o instalador de primeira execução (sem auth).
Protocolo binário do terminal
/api/terminal usa um protocolo binário pequeno definido em src/lib/terminal-protocol.ts:
| Código | Nome | Direção | Payload |
|---|---|---|---|
0x00 |
MSG_STDIN |
cliente → servidor | bytes de tecla |
0x01 |
MSG_STDOUT |
servidor → cliente | output do terminal |
0x02 |
MSG_RESIZE |
cliente → servidor | cols: u16, rows: u16 |
0x03 |
MSG_HEARTBEAT |
ambos | intervalo 30s, timeout 90s |
0x04 |
MSG_KILL_SESSION |
cliente → servidor | encerra a sessão tmux subjacente |
0x05 |
MSG_WEB_STDIN |
cliente → servidor | texto da barra de input web (entregue após sair do copy-mode) |
Backpressure: pty.pause quando WS bufferedAmount > 1 MB, retoma abaixo de 256 KB. No máximo 32 conexões concorrentes por servidor; as mais antigas são cortadas além disso.
Status manager
src/lib/status-manager.ts é a fonte única da verdade para cliState. Eventos de hook fluem por /api/status/hook (POST autenticado por token), são sequenciados (eventSeq por aba) e reduzidos para idle / busy / needs-input / ready-for-review / unknown pela deriveStateFromEvent. O watcher do JSONL atualiza só metadata, exceto por um único evento sintético interrupt.
Para a máquina de estados completa, veja Status da sessão (STATUS.md).
Camada tmux
O purplemux roda um tmux isolado em um socket dedicado — -L purple — usando sua própria config em src/config/tmux.conf. Seu ~/.tmux.conf nunca é lido.
Sessões são nomeadas pt-{workspaceId}-{paneId}-{tabId}. Um painel de terminal no navegador mapeia para uma sessão tmux, conectada via node-pty.
tmux socket: purple
├── pt-ws-MMKl07-pa-1-tb-1 ← aba 1 do navegador
├── pt-ws-MMKl07-pa-1-tb-2 ← aba 2 do navegador
└── pt-ws-MMKl07-pa-2-tb-1 ← split pane, aba 1
prefix está desabilitado, a status bar está off (o xterm.js desenha o chrome), set-titles está on e mouse on coloca a roda em copy-mode. O tmux é o motivo de as sessões sobreviverem a um navegador fechado, queda de Wi-Fi ou restart do servidor.
Para o setup completo do tmux, wrapper de comando e detalhes de detecção de processo, veja tmux & detecção de processo (TMUX.md).
Integração com o CLI Claude
O purplemux não dá fork nem embrulha o Claude — o binário claude é simplesmente o que você tem instalado. Duas coisas são adicionadas:
- Configurações de hook — Na inicialização,
ensureHookSettings()escreve~/.purplemux/hooks.json,status-hook.shestatusline.sh. Cada aba Claude inicia com--settings ~/.purplemux/hooks.json, entãoSessionStart,UserPromptSubmit,Notification,Stop,PreCompact,PostCompacttodos POSTam de volta ao servidor. - Leituras de JSONL —
~/.claude/projects/**/*.jsonlé parseado portimeline-server.tspara a visualização de conversa ao vivo, e observado porsession-detection.tspara detectar um processo Claude em execução, via os arquivos PID em~/.claude/sessions/.
Os scripts de hook leem ~/.purplemux/port e ~/.purplemux/cli-token e POSTam com x-pmux-token. Eles falham silenciosamente se o servidor está fora, então fechar o purplemux enquanto o Claude está rodando não quebra nada.
Sequência de inicialização
server.ts:start() percorre estes passos em ordem:
acquireLock(port)— guarda de instância única via~/.purplemux/pmux.lockinitConfigStore()+initShellPath()(resolve oPATHdo shell de login do usuário)initAuthCredentials()— carrega senha hashada com scrypt e secret HMAC no envscanSessions()+applyConfig()— limpa sessões tmux mortas, aplicatmux.confinitWorkspaceStore()— carregaworkspaces.jsonelayout.jsonpor workspaceautoResumeOnStartup()— relança shells nos diretórios salvos, tenta resume do ClaudegetStatusManager().init()— inicia o polling de metadataapp.prepare()(Next.js dev) ourequire('.next/standalone/server.js')(prod)listenWithFallback()embindPlan.host:port(0.0.0.0ou127.0.0.1conforme política de acesso)ensureHookSettings(result.port)— escreve ou atualiza scripts de hook com a porta realgetCliToken()— lê ou gera~/.purplemux/cli-tokenwriteAllClaudePromptFiles()— atualiza oclaude-prompt.mdde cada workspace
A janela entre a resolução da porta e o passo 10 é o motivo de os scripts de hook serem regerados a cada inicialização: precisam da porta real embutida.
Servidor customizado vs. grafo de módulos do Next.js
Onde ler mais
docs/TMUX.md— config tmux, wrapper de comando, tree walking, protocolo binário do terminal.docs/STATUS.md— máquina de estados do CLI Claude, fluxo de hook, evento sintético de interrupt, watcher JSONL.docs/DATA-DIR.md— todos os arquivos que o purplemux escreve.
Próximos passos
- Diretório de dados — todos os arquivos que esta arquitetura toca.
- Referência da CLI — falando com o servidor por fora do navegador.
- Solução de problemas — diagnosticando quando algo aqui se comporta mal.