spec: headless panel service and client-side background (OpenSpec change)

Refs #4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-18 20:31:09 +04:00
parent 3ae32fadf9
commit fe43d2ea7b
5 changed files with 363 additions and 0 deletions

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-18

View File

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

View File

@ -0,0 +1,44 @@
# Proposal
## Why
Панель «Claude» сейчас живёт только внутри iTerm2: её HTTP-сервер на `127.0.0.1:8766` поднимает
скрипт AutoLaunch, и когда iTerm2 закрыт, страницы нет. Пользователь смотрит панель с iPad —
Termius держит SSH-туннель к `127.0.0.1:8766` на Mac, страница открыта в Safari, а сами сессии
Claude Code крутятся в tmux, которому iTerm2 не нужен. Панель должна быть доступна, пока Mac
включён и пользователь вошёл в систему, независимо от iTerm2. Заодно тема по умолчанию
(Solarized dark) в Safari на iPad «мозолит глаза», и её нужно менять прямо на странице.
## What Changes
- Новый фоновый режим панели (`--headless`): HTTP-сервер, дерево сессий, лимиты и баланс без
iTerm2. Запускается LaunchAgent'ом при входе в систему, перезапускается при падении.
- Порт 8766 всегда занимает только фоновый сервер. Скрипт AutoLaunch в iTerm2 больше не поднимает
сервер, а только регистрирует web view «Claude» в Toolbelt на тот же адрес.
- **BREAKING**: подстройка цветов и шрифта под профиль iTerm2 убрана; ключи `use_profile_theme`
и `theme_variant` в `config.json` больше ничего не делают.
- **BREAKING**: возможности, которым нужна живая связь с iTerm2, уходят: названия вкладок iTerm2 для
сессий вне tmux, переход во вкладку iTerm2 для сессий вне tmux, переключение панелей `tmux -CC`,
открытие `lf` в разделённой панели iTerm2 для сессий вне tmux. Сессии в обычном tmux работают
как раньше (имена, переход, `lf` — через tmux).
- Новая настройка фона на клиенте: шестерёнка ⚙ в нижней строке панели раскрывает образцы фона,
«свой цвет» и «сброс». Выбор хранится на устройстве (браузере/web view), текст подстраивается.
- `install.sh` ставит и запускает LaunchAgent, `uninstall.sh` его останавливает и удаляет.
## Capabilities
### New Capabilities
- `panel-service`: где и как работает сервер панели — фоновая служба без iTerm2, единственный
владелец порта, регистрация в Toolbelt, установка/удаление.
- `panel-appearance`: как пользователь меняет фон панели на своём устройстве.
### Modified Capabilities
<!-- openspec/specs/ пуст — изменяемых спек нет -->
## Impact
- `files/claude_agents.py`: точка входа с двумя режимами (фоновый сервер / регистратор Toolbelt),
удаление `theme_poller` и чтения профиля, клиентский JS/CSS для фона в `render_page`.
- `install.sh`, `uninstall.sh`: LaunchAgent (`~/Library/LaunchAgents/`), лог службы.
- `files/config.example.json`, README.md: убрать ключи темы, описать фоновую службу и шестерёнку.
- Внешние зависимости не добавляются (только стандартная библиотека Python, `launchctl`).

View File

@ -0,0 +1,65 @@
# Spec Delta
## Purpose
Позволяет пользователю выбрать фон панели прямо на странице, отдельно на каждом устройстве
(Safari на iPad, Toolbelt на Mac), чтобы панель не резала глаза рядом с его терминалом.
## ADDED Requirements
### Requirement: Шестерёнка настройки фона
Нижняя строка панели (рядом с «обновлено ЧЧ:ММ:СС») SHALL содержать значок ⚙. Тап по нему SHALL
раскрывать строку с образцами фона, кнопкой «свой цвет» и кнопкой «сброс»; повторный тап SHALL её
сворачивать. В свёрнутом виде, кроме значка, ничего нового на странице не видно.
#### Scenario: Раскрыть и свернуть
- **WHEN** пользователь нажимает ⚙
- **THEN** появляется строка образцов; при повторном нажатии она скрывается
#### Scenario: Попадание пальцем
- **WHEN** страница открыта в Safari на iPad
- **THEN** ⚙, образцы и кнопки нажимаются пальцем без промахов (не мельче привычных элементов
управления iPadOS)
### Requirement: Выбор фона
Выбор образца или своего цвета SHALL сразу менять фон всей панели, включая окно сообщений. Цвет
основного текста, приглушённого текста, линий дерева и разделителей SHALL подстраиваться под
яркость фона, чтобы оставаться читаемым. Цвета статусов и лимитов (зелёный, жёлтый, красный)
SHALL оставаться прежними.
#### Scenario: Светлый фон
- **WHEN** пользователь выбирает светлый образец
- **THEN** фон становится светлым, а текст — тёмным и читаемым
#### Scenario: Свой цвет
- **WHEN** пользователь нажимает «свой цвет» и выбирает цвет в системной палитре
- **THEN** фон панели становится этим цветом, текст подстраивается
### Requirement: Выбор хранится на устройстве
Выбранный фон SHALL сохраняться средствами того браузера или web view, где он выбран, и SHALL
применяться при каждом открытии и автоматической перезагрузке страницы без мелькания темы по
умолчанию. Выбор на одном устройстве SHALL NOT влиять на другие. Если клиент не сохраняет данные
страницы между своими перезапусками (возможно, web view Toolbelt), после перезапуска клиента
показывается тема по умолчанию; на сервере выбор не хранится.
#### Scenario: Клиент без постоянного хранилища
- **WHEN** выбор фона сделан в Toolbelt, а web view Toolbelt не сохраняет данные между
перезапусками iTerm2, и iTerm2 перезапущен
- **THEN** панель в Toolbelt показывает тему по умолчанию
#### Scenario: Перезагрузка страницы
- **WHEN** пользователь выбрал фон в Safari на iPad и страница перезагрузилась (сама раз в минуту
или вручную)
- **THEN** сразу отображается выбранный фон
#### Scenario: Разные устройства
- **WHEN** в Safari на iPad выбран светлый фон
- **THEN** панель в Toolbelt на Mac сохраняет свой фон
### Requirement: Сброс к теме по умолчанию
«Сброс» SHALL удалять сохранённый выбор и возвращать тему, которую отдаёт сервер. Тема по умолчанию
SHALL выглядеть как нынешний профиль iTerm2 пользователя (`Chat`: фон `#002b36`, текст `#aacace`).
#### Scenario: Сброс
- **WHEN** пользователь нажимает «сброс»
- **THEN** панель показывает тему по умолчанию, и после перезагрузки она остаётся по умолчанию

View File

@ -0,0 +1,111 @@
# Spec Delta
## Purpose
Определяет, где и как работает сервер панели «Claude»: фоновая служба пользователя, не зависящая
от iTerm2, единственный владелец порта панели, тонкая регистрация в Toolbelt и установка/удаление.
## ADDED Requirements
### Requirement: Сервер панели работает без iTerm2
Сервер панели SHALL отдавать страницу, дерево сессий, лимиты и баланс на `127.0.0.1:<port>`
(по умолчанию 8766), пока пользователь вошёл в систему, независимо от того, запущен ли iTerm2.
#### Scenario: iTerm2 не запущен
- **WHEN** пользователь вошёл в систему, iTerm2 не запущен, а сессия Claude Code работает в tmux
- **THEN** `http://127.0.0.1:8766/` отвечает 200 и показывает эту сессию с её субагентами, моделью,
контекстом и worktree, а нижний блок показывает лимиты и баланс
#### Scenario: Имена сессий в tmux
- **WHEN** сессия Claude Code работает в панели tmux
- **THEN** её имя берётся из заголовка панели tmux или из `сессия:окно имя_окна`, как и раньше
### Requirement: Служба стартует при входе и перезапускается при падении
Сервер панели SHALL запускаться автоматически при входе пользователя в систему и SHALL
перезапускаться, если его процесс завершился.
#### Scenario: Вход в систему
- **WHEN** пользователь входит в систему на Mac после перезагрузки
- **THEN** сервер панели запускается без открытия iTerm2 и каких-либо действий пользователя
#### Scenario: Падение процесса
- **WHEN** процесс сервера панели завершился (ошибка, `kill`)
- **THEN** он снова запущен и отвечает на порту в пределах нескольких десятков секунд
### Requirement: Порт панели занимает один процесс
На порту панели SHALL работать ровно один сервер; лимиты и баланс SHALL опрашиваться одним
процессом, чтобы не удваивать запросы к API.
#### Scenario: iTerm2 запускается при работающей службе
- **WHEN** служба уже отвечает на порту и пользователь запускает iTerm2
- **THEN** второй сервер не запускается, в API лимитов идёт один поток запросов, а в Toolbelt
появляется панель «Claude», показывающая ту же страницу
### Requirement: Toolbelt показывает панель фоновой службы
Скрипт панели в iTerm2 SHALL регистрировать web view «Claude» в Toolbelt, указывающий на адрес
фоновой службы, и SHALL NOT поднимать собственный сервер.
#### Scenario: Toolbelt при запущенной службе
- **WHEN** iTerm2 запущен и пользователь выбирает Toolbelt → Claude
- **THEN** панель показывает ту же страницу, что и `http://127.0.0.1:8766/` в браузере
#### Scenario: Служба не отвечает при старте iTerm2
- **WHEN** iTerm2 стартует, а служба на порту не отвечает
- **THEN** скрипт просит launchd запустить службу и всё равно регистрирует web view
### Requirement: Сервер доступен только локально
Сервер панели SHALL слушать только `127.0.0.1`; доступ с других устройств возможен только через
туннель (например, SSH local forward).
#### Scenario: Запрос из локальной сети
- **WHEN** устройство в той же сети обращается к Mac по его LAN-адресу на порт панели
- **THEN** соединение не устанавливается
### Requirement: Действия по клику работают через tmux
Переход к сессии и открытие worktree по клику SHALL выполняться средствами tmux для сессий в tmux,
независимо от iTerm2 и от того, на каком устройстве открыта страница.
#### Scenario: Переход к сессии с iPad
- **WHEN** пользователь с iPad (страница через туннель) нажимает «↗ вкладка» у сессии в tmux
- **THEN** клиент tmux переключается на окно и панель этой сессии
#### Scenario: Открытие worktree с iPad
- **WHEN** пользователь нажимает `⎇ папка` у сессии в tmux
- **THEN** в том же окне tmux открывается новая панель с файловым менеджером в этой папке
### Requirement: Установка и удаление службы
`install.sh` SHALL устанавливать, (пере)запускать службу и останавливать прежний сервер, державший
порт; повторный запуск SHALL быть безопасным. `uninstall.sh` SHALL останавливать и удалять службу.
#### Scenario: Обновление старой установки
- **WHEN** у пользователя стоит прежняя версия (сервер внутри iTerm2) и он запускает `install.sh`
- **THEN** прежний сервер остановлен, служба установлена и отвечает на порту, а iTerm2-скрипт
обновлён до регистратора
#### Scenario: Повторная установка
- **WHEN** `install.sh` запускается второй раз подряд
- **THEN** служба одна, отвечает на порту, записи хуков в `settings.json` не задублированы
#### Scenario: Запуск по SSH
- **WHEN** `install.sh` или `uninstall.sh` запущен из SSH-сессии
- **THEN** скрипт сообщает, что его нужно запускать за самим Mac, и завершается с ненулевым кодом,
ничего не изменив: прежний сервер продолжает работать
#### Scenario: Удаление
- **WHEN** пользователь запускает `uninstall.sh`
- **THEN** служба остановлена, её описание удалено, порт свободен и после повторного входа в систему
служба не запускается
## REMOVED Requirements
### Requirement: Тема панели из профиля iTerm2
**Reason**: Сервер больше не связан с iTerm2; фон задаётся на клиенте (см. `panel-appearance`).
**Migration**: Выставить фон шестерёнкой ⚙ внизу панели; ключи `use_profile_theme` и
`theme_variant` в `config.json` удалить (они игнорируются).
### Requirement: Возможности iTerm2 для сессий вне tmux
**Reason**: Нужна живая связь сервера с iTerm2, а сервер теперь фоновый; пользователь держит все
сессии в tmux.
**Migration**: Запускать `claude` внутри tmux. Для сессий вне tmux имя берётся из заголовка сессии
Claude Code или папки, переход во вкладку и `lf` в разделённой панели iTerm2 недоступны; режим
`tmux -CC` не поддерживается для перехода.