Andrey Epifancev fe43d2ea7b spec: headless panel service and client-side background (OpenSpec change)
Refs #4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:31:09 +04:00

142 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/`.
Цена ошибки: переименование при следующей установке.