213 lines
23 KiB
Markdown
213 lines
23 KiB
Markdown
# 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 глазами не проверен (человек не за Mac).
|
||
- Step 6 — ок: светлый образец, автоперезагрузка не закрывает строку образцов (I-4), «сброс»,
|
||
«свой цвет» открывает палитру iPad. Размер областей нажатия (M-11) жалоб не вызвал.
|
||
- Step 7 — не проверен: сохраняется ли фон в Toolbelt после перезапуска iTerm2, видно только на
|
||
экране Mac.
|
||
- 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 неактуально.
|