14 KiB
Design: панель без iTerm2 + фон на клиенте
Issue: #4. Ветка: feat/4-headless-panel.
Намерение
Пользователь смотрит панель с iPad: Termius держит SSH local forward 8766 → 127.0.0.1:8766 на Mac,
страница открыта в Safari рядом с Termius (Split View). Все сессии Claude Code — в обычном tmux,
который работает сам по себе. Сейчас страница пропадает, если на Mac не запущен iTerm2, потому что
сервер поднимает скрипт AutoLaunch iTerm2. Переключение сессий с iPad уже работает — через tmux.
Нужно: страница доступна всегда, пока Mac включён и пользователь вошёл; фон меняется прямо на
странице, потому что тема по умолчанию в Safari раздражает.
Успех: iTerm2 закрыт → в Safari на iPad видно дерево сессий, лимиты и баланс; клики переключают tmux; фон выставлен под тему Termius и держится между перезагрузками.
Цели
- Фоновый сервер панели, независимый от iTerm2, автозапуск при входе, перезапуск при падении.
- Один владелец порта 8766; iTerm2-скрипт только регистрирует web view в Toolbelt.
- Шестерёнка ⚙ внизу панели (вариант A из мокапов): образцы фона, «свой цвет», «сброс»; выбор хранится на устройстве; текст подстраивается под яркость фона.
install.sh/uninstall.shуправляют службой; обновление старой установки без ручных шагов.
Не цели
- Досыл названий вкладок iTerm2 и цветов профиля в фоновый сервер (решено пользователем: в tmux имена и так из tmux; цвет — шестерёнкой, в Toolbelt тоже).
- Доступ к странице из сети напрямую (только через туннель).
- Мод Claude Code / TUI-версия панели (обсуждались, отложены).
- Запуск службы без входа пользователя в систему на самом Mac (см. «Открытые вопросы»).
Анти-цели (провал, даже если «работает»)
- Два сервера одновременно или удвоенный опрос лимитов (эндпоинт отвечает 429).
- Порт открыт наружу: в панели видны тексты сообщений сессий.
- Мелькание темы по умолчанию при каждой автоперезагрузке страницы (раз в минуту) после выбора фона.
- Установщик ломает чужие хуки/настройки или требует ручных шагов при обновлении.
Ограничения
- Python 3.8+, только стандартная библиотека; модуль
iterm2— только в режиме регистратора. - macOS,
launchctl; LaunchAgent в сессии пользователя (Aqua). - В LaunchAgent минимальный
PATH: путь кpython3фиксируется установщиком абсолютным; поискtmux/psуже идёт черезEXTRA_PATHв коде. - Токен лимитов — из Keychain через
security; служба должна работать в GUI-сессии пользователя, где Keychain разблокирован. - Сервер слушает только
127.0.0.1.
Решения, принятые с пользователем
- Вариант 0 (SSH-туннель + Safari) — основной сценарий, мод/TUI не делаем.
- Фоновая служба через LaunchAgent; один сервер на порту; iTerm2-скрипт — регистратор.
- Цвет профиля iTerm2 не досылать и не читать (вариант 3): в Toolbelt фон тоже ставится шестерёнкой.
- Место настройки фона — шестерёнка в нижней строке (вариант A).
- Перезагрузка Mac без GUI-входа — не решаем сейчас, запоминаем на будущее.
install.sh/uninstall.shзапускаются только за самим Mac (не по SSH). При запуске из SSH-сессии (SSH_CONNECTION/SSH_TTYзаданы) установщик предупреждает и завершается, ничего не меняя; в README — строка об этом.
Как устроено (то, что решено)
Режимы claude_agents.py
Один исходный файл, два режима:
- Фоновый сервер (
--headless, запускает launchd системнымpython3):asyncio.start_serverна127.0.0.1:PORT,refresher(None),usage_poller,balance_poller; безiterm2, безtheme_poller, без регистрации web view.live=None→build_treeуже судит о сессиях по PID и tmux. Блок__main__запускает этот режим при флаге независимо от наличияiterm2. - Регистратор (запуск iTerm2 из AutoLaunch, без флага): проверяет, отвечает ли порт; если нет —
launchctl kickstart gui/<uid>/<label>; затемasync_register_web_view_toolнаhttp://127.0.0.1:PORT/и остаётся жить (run_forever), ничего больше не делая. theme_poller,live_sessions, использование_appвactivate/open_worktree,USE_PROFILE_THEME,THEME_VARIANTи их ключи вCONFIG_KEYSудаляются. Старые ключи в чужомconfig.jsonпросто игнорируются (load_configберёт только известные ключи).activate/open_worktreeдля целей не из tmux становятся no-op.
LaunchAgent
- Метка
com.andrey.claude-agents(совпадает с идентификатором web view). ProgramArguments: абсолютный путь кpython3, путь к установленномуclaude_agents.py,--headless.RunAtLoad=true,KeepAlive=true,ThrottleIntervalпо умолчанию.- stdout/stderr — в
~/Library/Logs/claude-agents/panel.log.
install.sh / uninstall.sh
- install: как раньше хук и
settings.json; копируетclaude_agents.pyв AutoLaunch (регистратор); пишет plist в~/Library/LaunchAgents/; останавливает всё, что слушает порт (прежний сервер внутри iTerm2);launchctl bootout(если была) +bootstrap gui/<uid>+kickstart -k; ждёт, пока порт ответит, и сообщает результат; если iTerm2 запущен с включённым API — перезапускает скрипт AutoLaunch (как сейчас черезosascript). - uninstall:
launchctl bootout, удаляет plist, остальное как сейчас;--purgeудаляет и лог службы.
Фон на клиенте
- В
render_page: в<head>до отрисовки маленький скрипт читает выбор изlocalStorage(вtry/catch) и выставляет CSS-переменные--bg,--fg,--dimна:root— без мелькания. - Производные цвета: по относительной яркости фона выбирается светлый или тёмный текст;
--dim— смесь текста с фоном (как_mixна сервере, коэффициент 0.6). Линии дерева и разделители уже считаются от--fgчерезcolor-mix, отдельно не трогаются. - ⚙ — в строке
.footблока лимитов. Блок лимитов перерисовывается сервером раз в 15 с через/limits, поэтому ⚙ и строка образцов живут вне перерисовываемого HTML (отдельный элемент рядом с#limitsили восстанавливаются после вставки), чтобы раскрытая строка не схлопывалась. - Образцы: текущий по умолчанию (
#002b36), нейтральный тёмный (#1e1e1e), чёрный (#000000), тёмно-фиолетово-серый (#2b2b3a), светлый (#fdf6e3). «Свой цвет» —<input type="color">(на iPad открывает системную палитру). «Сброс» удаляет ключ и возвращает тему сервера. - Тема сервера по умолчанию —
DEFAULT_THEME, выровненная по нынешнему профилю iTerm2Chat(снято с работающей панели 2026-10-18):bg #002b36,fg #aacace,dim #668a91,green #859900,yellow #b58900,red #dc322f, шрифт'MesloLGLNF', 'MesloLGLNF-Regular', ui-monospace, monospace, размер 13. Значения зашиты в код, профиль не читается. - Если web view Toolbelt не сохраняет
localStorageмежду перезапусками iTerm2 — принимаем это: после перезапуска в Toolbelt тема по умолчанию (она и есть вид профиляChat). Серверного хранения выбора нет. Проверить поведение в начале реализации и записать результат сюда.
Оставлено исполнителю
- Точное расположение установленного
claude_agents.pyдля службы (та же копия в AutoLaunch или отдельная) — при условии одного исходного файла в репозитории. - Размер и вид ⚙ и образцов в пределах требований спеки (пальцем на iPad, ничего лишнего в свёрнутом виде).
- Тесты: pytest по чистым функциям (
settings_hooks, генерация plist, расчёт производных цветов, если он на сервере) и ручная проверка сценариев спеки, которые завязаны на launchd/iPad.
Риски
- Сохраняется ли
localStorageв web view Toolbelt iTerm2 между перезапусками iTerm2 — не проверено. Решено с пользователем: если нет, фон в Toolbelt сбрасывается к теме по умолчанию (вид профиляChat). - Доступ
securityк элементу KeychainClaude Code-credentialsиз процесса launchd может потребовать подтверждения пользователя один раз. /usr/bin/python3без установленных Command Line Tools — заглушка; установщик уже проверяет, чтоpython3реально работает, и фиксирует его путь.
Открытые вопросы
- После перезагрузки Mac без входа на нём (только SSH с iPad) LaunchAgent не стартует, а Keychain заблокирован. Пользователь такого не делал, но не исключает; решение отложено.
Decisions made without discussion
- 2026-10-18 — Сервер остаётся только на
127.0.0.1. Обоснование: в панели тексты сообщений сессий; с iPad и так заходят через туннель. Цена ошибки: если понадобится LAN — добавить ключbindв конфиг. - 2026-10-18 — Возможности iTerm2 для сессий вне tmux (имена вкладок, переход во вкладку,
lfв split iTerm2,tmux -CC) убираются целиком, а не только в фоновом режиме. Обоснование: следствие «один сервер на порту» + «не досылать данные из iTerm2»; пользователь держит всё в tmux. Цена ошибки: для сессий вне tmux теряется переход по клику; вернуть можно досылом из регистратора. - 2026-10-18 — Подстраивается только фон, а производные (текст, приглушённый, линии) считаются автоматически; цвета статусов не меняются. Обоснование: пользователь говорил о фоне. Цена ошибки: добавить выбор цвета текста.
- 2026-10-18 — Регистратор при неотвечающем порте делает
launchctl kickstart, а не поднимает свой сервер. Обоснование: один код сервера, нет гонки за порт. Цена ошибки: если launchd-службы нет (не переустановлено), в Toolbelt пустая страница доinstall.sh. - 2026-10-18 — Метка LaunchAgent
com.andrey.claude-agents, лог в~/Library/Logs/claude-agents/. Цена ошибки: переименование при следующей установке.