Andrey Epifancev 521182d4a4 spec: archive headless-panel change (#4)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:59:24 +04:00

24 KiB
Raw Blame History

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, выровненная по нынешнему профилю iTerm2 Chat (снято с работающей панели 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 к элементу Keychain Claude 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/. Цена ошибки: переименование при следующей установке.
  • 2026-10-18 (план) — Тесты на unittest из stdlib, не pytest. Обоснование: pytest не установлен, pip install запрещён хуком на main и противоречит «только stdlib»; unittest-тесты pytest тоже запускает. Цена ошибки: переписать на pytest-стиль при желании.
  • 2026-10-18 (план) — Копия скрипта для службы — ~/.local/share/claude-agents/claude_agents.py, отдельно от копии в AutoLaunch. Обоснование: служба не зависит от каталога iTerm2. Цена ошибки: два места при ручном обновлении (install.sh обновляет оба).
  • 2026-10-18 (план) — Текст — #3b4a50 (светлый фон) или #aacace (как в профиле, тёмный фон); выбирается тот, у которого выше коэффициент контраста WCAG (Lmax+0.05)/(Lmin+0.05) с фоном (было: порог яркости > 0.4, на средних тонах давал ~1.5:1 — находка I-1). Цена ошибки: на отдельных средних фонах контраст всё равно не дотянет до 4.5:1; подобрать другие цвета текста.
  • 2026-10-18 (план) — Область нажатия ⚙, образцов и кнопок — не меньше 36×36 CSS px. Цена ошибки: поправить размер после приёмки на iPad.
  • 2026-10-18 (план) — В этой ветке заодно закрываются O-1 (устаревший docstring) и O-3 (__pycache__/ в .gitignore) из issue #3: оба файла и так меняются. Цена ошибки: нет.
  • 2026-10-18 (план) — Приёмка (Task 6) выполняется человеком, не сабагентами: она меняет launchd, ~/.claude/settings.json и настройки iTerm2 на рабочем Mac.
  • 2026-10-18 (исполнение) — R-1 (design #12): test_old_theme_keys_ignored и test_open_worktree_non_tmux_is_noop прошли до реализации Task 1 (старые ключи и раньше не давали ошибки, _app был None); оставлены как тесты-фиксаторы поведения после удаления. Цена ошибки: нет — остальные тесты Task 1 падали корректно.
  • 2026-10-18 (исполнение) — R-2 (design #13): build_tree оставлен с параметром live (вызывается с None) и мёртвой веткой tmux_twins, потому что план требовал не менять build_tree. Цена ошибки: мёртвый код; уборка отложена (M-9).
  • 2026-10-18 (исполнение) — R-3 (design #14): RED для test_install показан временной порчей текста сообщения охраны, а не запуском скриптов без охраны (это задело бы настоящий launchd и порт 8766). Цена ошибки: тест защищён только проверяемой охраной — закрыто заглушками команд в PATH (M-7).
  • 2026-10-18 (финальное ревью) — R-4 (design #15): из находок финального ревью исправлены в ветке I-1…I-4, M-1, M-2, M-3, M-5, M-7, M-8, M-12; отложены в follow-up M-4 (порт из config.json в install.sh/uninstall.sh), M-6 (ротация panel.log), M-9 (мёртвый код build_tree), M-11 (область нажатия 44 pt — оценить на приёмке, Task 6 Step 6). I-3 (нет проверки Host/Sec-Fetch-Site) существовал до ветки, но исправлен сразу: служба теперь работает постоянно и открывается из браузера. Цена ошибки: отложенные пункты — мелкие неудобства, не потеря данных.
  • 2026-10-18 (финальное ревью) — R-5 (design #16): запрос без заголовка Host или с Host без порта получает 403 (сравнение строго с 127.0.0.1:<порт> / localhost:<порт>, порт берётся из сокета). Обоснование: браузеры, web view iTerm2 и туннель Termius всегда шлют Host с портом. Цена ошибки: клиент без Host (голый HTTP/1.0) не откроет панель — ослабить проверку.
  • 2026-10-18 (финальное ревью) — R-6 (design #17): тесты к исправлениям написаны без отдельного шага RED; вместо этого новые тесты прогнаны на коде до исправлений (2e8ab31) — все 6 новых тестов (I-1, I-3 ×2, I-4, M-2, M-12) там падают. Цена ошибки: нет.
  • 2026-10-18 (приёмка) — R-7 (design #18): служба получает PATH той оболочки, из которой запущен install.sh (EnvironmentVariables.PATH в plist). Обоснование: tmux отдаёт новой панели PATH вызвавшего split-window, а у launchd PATH=/usr/bin:/bin:/usr/sbin:/sbin — exec lf не находился, панель «⎇ папка» мигала и закрывалась (H-1). Так панели, открытые службой, видят то же, что и терминал (редактор, превьюеры lf). Цена ошибки: если install.sh запущен из урезанного окружения, lf снова не найдётся — README подсказывает перезапустить установщик из обычного терминала.

Результаты приёмки (Task 6, 2026-10-18)

Проверял человек с iPad (Termius, SSH + tmux) и контроллер из сессии Claude Code на том же Mac.

  • Step 1 — ок: служба com.andrey.claude-agents запущена launchd (родитель PID 1, python из /opt/homebrew/bin/python3), один слушатель 127.0.0.1:8766; лимиты и баланс загрузились, запроса доступа к Keychain не было. Чужой Host → 403.
  • Step 2 — ок: iTerm2 закрыт (⌘Q через osascript), с iPad через туннель Termius видны дерево, лимиты и баланс; tmux-сервер отвязан от iTerm2 (PPID 1) и пережил закрытие.
  • Step 3 — «↗ вкладка» ок; «⎇ папка» сначала мигала разделителем и закрывалась (H-1, причина — PATH службы), после c810f5a и переустановки — ок.
  • Step 4 — ок: после kill служба снова отвечает 200 примерно через 2 с.
  • Step 5 — ок: после open -a iTerm поднялся один процесс регистрации, порт держит служба; Toolbelt → Claude показывает страницу службы (проверено человеком за Mac).
  • Step 6 — ок: светлый образец, автоперезагрузка не закрывает строку образцов (I-4), «сброс», «свой цвет» открывает палитру iPad. Размер областей нажатия (M-11) жалоб не вызвал.
  • Step 7 — ок: выбранный в Toolbelt фон сохраняется после перезапуска iTerm2 (localStorage web view переживает перезапуск).
  • Попутно: при проверке человек случайно завершил процесс iTerm2, вместе с которым погас tmux-сервер (его запускает интеграция tmux -CC). Служба этого не заметила — тот же PID, страница отвечает 200.
  • Step 8 — ок, три повторных запуска: одна служба, bootstrap без «Bootstrap failed» (M-5 не проявился), один процесс регистрации (M-1), 9 хуков в settings.json без дублей.
  • Step 9 — ок с SSH_CONNECTION=test: отказ «за самим Mac» до каких-либо действий. Наблюдение: в окне tmux, открытом из Termius, SSH_CONNECTION пуст, и охрана не срабатывает (подтверждает M-3); установка из такого окна проходит нормально, т. к. tmux-сервер живёт в GUI-сессии.
  • Step 10 — не выполнялся (по желанию).
  • Наблюдение O-3: во время приёмки ./install.sh один раз запустили из основного checkout (main, старая версия без охраны) — он перезаписал скрипт AutoLaunch старым сервером; launchd сразу поднял службу. Исправлено повторным install.sh из ветки; после merge неактуально.