Архитектура
Как браузер, сервер Node.js, tmux и CLI Claude складываются вместе.
purplemux — это три слоя, сшитых вместе: фронтенд в браузере, сервер Node.js на :8022 и tmux + Claude CLI на хосте. Между ними — либо бинарный WebSocket, либо небольшой HTTP POST.
Три слоя
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
У каждого WebSocket одна задача; они не мультиплексируют. Аутентификация — это NextAuth JWT cookie, проверяемая при WS-апгрейде.
Браузер
Фронтенд — Next.js (Pages Router). Части, которые общаются с сервером:
| Компонент | Библиотека | Назначение |
|---|---|---|
| Панель терминала | xterm.js |
Рендерит байты с /api/terminal. Эмитит keystroke, события resize и смены заголовка (onTitleChange). |
| Таймлайн сессии | React + useTimeline |
Рендерит ходы Claude из /api/timeline. Без вычисления cliState на клиенте — оно полностью на сервере. |
| Индикаторы статуса | Zustand useTabStore |
Бейджи вкладок, точки в боковой панели, счётчики уведомлений, управляемые сообщениями /api/status. |
| Multi-device sync | useSyncClient |
Наблюдает за правками рабочих пространств / раскладок с других устройств через /api/sync. |
Заголовки вкладок и переднеплановой процесс берутся из события onTitleChange xterm.js — tmux настроен (src/config/tmux.conf) эмитить #{pane_current_command}|#{pane_current_path} каждые две секунды, а lib/tab-title.ts это парсит.
Сервер Node.js
server.ts — это кастомный HTTP-сервер, в который вписан Next.js плюс четыре экземпляра ws WebSocketServer на том же порту.
WebSocket-эндпоинты
| Путь | Хендлер | Направление | Использование |
|---|---|---|---|
/api/terminal |
terminal-server.ts |
двунаправленный, бинарный | I/O терминала через node-pty, привязанный к tmux-сессии |
/api/timeline |
timeline-server.ts |
сервер → клиент | Стримит записи сессии Claude, распарсенные из JSONL |
/api/status |
status-server.ts |
двунаправленный, JSON | status:sync / status:update / status:hook-event от сервера, status:tab-dismissed / status:ack-notification / status:request-sync от клиента |
/api/sync |
sync-server.ts |
двунаправленный, JSON | Состояние рабочего пространства между устройствами |
Плюс /api/install для первого запуска инсталлера (без аутентификации).
Бинарный протокол терминала
/api/terminal использует крошечный бинарный протокол, описанный в src/lib/terminal-protocol.ts:
| Код | Имя | Направление | Полезная нагрузка |
|---|---|---|---|
0x00 |
MSG_STDIN |
клиент → сервер | Байты клавиш |
0x01 |
MSG_STDOUT |
сервер → клиент | Вывод терминала |
0x02 |
MSG_RESIZE |
клиент → сервер | cols: u16, rows: u16 |
0x03 |
MSG_HEARTBEAT |
оба | Интервал 30 с, таймаут 90 с |
0x04 |
MSG_KILL_SESSION |
клиент → сервер | Завершить нижележащую tmux-сессию |
0x05 |
MSG_WEB_STDIN |
клиент → сервер | Текст из веб-строки ввода (доставляется после выхода из copy-mode) |
Backpressure: pty.pause при bufferedAmount > 1 МБ у WS, возобновление ниже 256 КБ. Не более 32 одновременных подключений на сервер, самые старые сбрасываются сверх лимита.
Status manager
src/lib/status-manager.ts — единственный источник истины для cliState. События хуков идут через /api/status/hook (POST с токеном), секвенсятся (eventSeq на вкладку) и сводятся в idle / busy / needs-input / ready-for-review / unknown через deriveStateFromEvent. JSONL-watcher обновляет только метаданные, кроме одного синтетического события interrupt.
Полную машину состояний см. в Статусе сессии (STATUS.md).
Слой tmux
purplemux запускает изолированный tmux на отдельном сокете — -L purple — со своим конфигом в src/config/tmux.conf. Ваш ~/.tmux.conf никогда не читается.
Сессии именуются pt-{workspaceId}-{paneId}-{tabId}. Одна панель терминала в браузере соответствует одной tmux-сессии, привязанной через node-pty.
tmux socket: purple
├── pt-ws-MMKl07-pa-1-tb-1 ← вкладка браузера 1
├── pt-ws-MMKl07-pa-1-tb-2 ← вкладка браузера 2
└── pt-ws-MMKl07-pa-2-tb-1 ← разделённая панель, вкладка 1
prefix отключён, статус-бар выключен (хром рисует xterm.js), set-titles включён, а mouse on отправляет колесо в copy-mode. tmux — причина, по которой сессии переживают закрытие браузера, обрыв Wi-Fi или перезапуск сервера.
Полную настройку tmux, обёртку над командами и детали определения процесса см. в tmux и определении процессов (TMUX.md).
Интеграция с Claude CLI
purplemux не форкает и не оборачивает Claude — бинарь claude тот, что у вас установлен. Добавляются две вещи:
- Hook settings — на старте
ensureHookSettings()пишет~/.purplemux/hooks.json,status-hook.shиstatusline.sh. Каждая вкладка Claude запускается с--settings ~/.purplemux/hooks.json, поэтомуSessionStart,UserPromptSubmit,Notification,Stop,PreCompact,PostCompactвсе POST'ятся обратно на сервер. - Чтение JSONL —
~/.claude/projects/**/*.jsonlпарситсяtimeline-server.tsдля живого вида разговора и наблюдаетсяsession-detection.ts, чтобы определять запущенный процесс Claude через PID-файлы в~/.claude/sessions/.
Скрипты хуков читают ~/.purplemux/port и ~/.purplemux/cli-token и POST'ят с x-pmux-token. Они тихо проваливаются, если сервер выключен, поэтому закрытие purplemux при работающем Claude ничего не ломает.
Последовательность запуска
server.ts:start() идёт по этим шагам по порядку:
acquireLock(port)— защита единственного экземпляра через~/.purplemux/pmux.lockinitConfigStore()+initShellPath()(резолвPATHлогин-шелла пользователя)initAuthCredentials()— загрузка scrypt-хеша пароля и HMAC-секрета в envscanSessions()+applyConfig()— очистка мёртвых tmux-сессий, применениеtmux.confinitWorkspaceStore()— загрузкаworkspaces.jsonиlayout.jsonкаждого рабочего пространстваautoResumeOnStartup()— перезапуск шеллов в сохранённых каталогах, попытка resume ClaudegetStatusManager().init()— старт поллинга метаданныхapp.prepare()(Next.js dev) илиrequire('.next/standalone/server.js')(prod)listenWithFallback()наbindPlan.host:port(0.0.0.0или127.0.0.1в зависимости от политики доступа)ensureHookSettings(result.port)— запись или обновление скриптов хуков с реальным портомgetCliToken()— чтение или генерация~/.purplemux/cli-tokenwriteAllClaudePromptFiles()— обновлениеclaude-prompt.mdкаждого рабочего пространства
Окно между резолвом порта и шагом 10 — причина, по которой скрипты хуков регенерируются при каждом старте: им нужен актуальный порт.
Кастомный сервер vs граф модулей Next.js
Где почитать дальше
docs/TMUX.md— конфиг tmux, обёртка команд, обход дерева процессов, бинарный протокол терминала.docs/STATUS.md— машина состояний Claude CLI, поток хуков, синтетическое событие interrupt, JSONL-watcher.docs/DATA-DIR.md— каждый файл, который пишет purplemux.
Что дальше
- Каталог данных — каждый файл, которого касается архитектура выше.
- CLI reference — общение с сервером вне браузера.
- Поиск проблем — диагностика, когда что-то здесь капризничает.