# Claude Code Status Индикатор для GNOME Shell, отвечающий на один вопрос: **какая сессия Claude Code ждёт меня прямо сейчас?** Когда открыто несколько сессий в разных проектах, дорого не «не знать, чем каждая занята», а не заметить, что одна из них встала час назад. Справа от часов стоит по чипу на сессию — значок состояния и трёхбуквенное имя проекта: ``` ◉ ds ● dc ● ds2 ○ pps ○ umb 9:41 │ │ │ │ └ umbar, работает │ │ │ └ pet-project-server, работает │ │ └ вторая сессия в dev-skills, ждёт │ └ dev-conventions, ждёт └ dev-skills, спрашивает ``` ## Состояния | Значок | Состояние | Что значит | |---|---|---| | диск в кольце | `blocked` | спрашивает — разрешение или вопрос с вариантами; строка ввода занята | | закрашенный диск | `waiting` | ход закончен, строка ввода свободна | | кольцо | `busy` | работает | `blocked` и `waiting` разведены намеренно. Слитые в одно «требует внимания», законченная задача выглядит так же срочно, как заблокированная, — а именно это различие и решает, переключаться сейчас или после текущей мысли. Состояний ровно три. Четвёртое, «тихо», было и убрано: оно ставилось только событием `SessionStart` и в него не было возврата, так что означало не «давно без дела», а «сессию открыли и ещё ни разу ничего не спросили» — состояние длиной в несколько секунд, занимавшее форму в панели. Свежая сессия ждёт первого промпта ровно так же, как доделавшая ход ждёт следующего, и теперь обе называются `waiting`. Дальше этого деление не идёт, и не по лени: запрос разрешения и вопрос с вариантами приходят под одним и тем же `notification_type`, с одинаковым родовым текстом `«Claude needs your permission»`. Различить их можно было бы только через `PreToolUse` — ценой записи файла на каждый вызов инструмента. Чипы идут по срочности, а внутри одного состояния первой стоит **самая давняя**: забывается та, что ждёт дольше всех, а не последняя. Время в состоянии показывается только у первого чипа: пять счётчиков рядом — это ряд чисел, а не ответ на вопрос. ## Метки чипов Имя проекта сжимается до трёх знаков: если в имени несколько сегментов (`-`, `_`, camelCase) — инициалы, иначе первые буквы. | Проект | Чип | |---|---| | `dev-skills` | `ds` | | `dev-conventions` | `dc` | | `pet-project-server` | `pps` | | `claude-code-gnome-extension` | `ccg` | | `jellybit` | `jel` | Инициалы, а не просто префикс, важны больше, чем кажется: префикс слил бы `dev-skills` и `dev-conventions` в один `dev` — ровно тот случай, который надо развести. При совпадении — включая две сессии в одном проекте, где `cwd` один и тот же — добавляется цифра по старшинству: `ds`, `ds2`, `ds3`. Цифра съедает базу, а не удлиняет метку, чтобы ряд не расползался. Два правила держат метки на месте, и без них вся затея рассыпается: - Назначение идёт **от самой давней сессии**, так что цифру берёт только что запущенная, а не та, на которую ты сейчас смотришь. - Выданная метка закреплена за сессией до её конца — даже после того, как закрылась сессия, из-за которой появилась цифра. Метка, переехавшая под рукой, хуже метки с цифрой, которая уже не выглядит нужной. Сокращение отключается в настройках — тогда в чипах полные имена проектов. В меню строка начинается с той же метки, чтобы соответствие «`ds` — это dev-skills» читалось, а не угадывалось. **Уведомлений на рабочий стол нет** — сознательно. `notify-send` из хука было бы куда дешевле сделать, но форма неверная: сессия, ждущая двадцать минут, должна оставаться на виду, а уведомление исчезает в момент, когда его смахнули. Панель здесь — единственный канал. ## Установка ```sh git clone <этот репозиторий> ~/projects/private/claude-code-gnome-extension cd ~/projects/private/claude-code-gnome-extension # 1. хуки: научить Claude Code публиковать состояние сессий ./hooks/install.py # 2. расширение: симлинк, компиляция схемы, включение ln -s "$PWD" ~/.local/share/gnome-shell/extensions/claude-code-status@git.vakhrushev.me glib-compile-schemas schemas/ gnome-extensions enable claude-code-status@git.vakhrushev.me ``` Уже запущенные сессии подхватывают хуки без перезапуска — Claude Code перечитывает `settings.json` по мере изменения. На Wayland правки *кода* расширения по-прежнему требуют релогина; первое включение — нет. `./hooks/install.py --uninstall` снимает регистрацию хуков, не трогая остальное в `~/.claude/settings.json`. Копия `.bak` пишется при каждом запуске. ## Как это работает Хуки Claude Code пишут по одному небольшому JSON-файлу на сессию в `~/.local/state/claude-code-status/.json`; расширение следит за этим каталогом через `Gio.FileMonitor`. Ничего не опрашивается, демона нет — смена состояния доходит до панели, как только хук завершился. | Хук | Действие | |---|---| | `SessionStart` | сессия появляется как `waiting`, мёртвые подчищаются | | `UserPromptSubmit` | `busy` | | `PostToolUse` | `busy` | | `PreCompact` | `busy` | | `Notification` | `blocked` или `waiting`, в зависимости от `notification_type` | | `Stop` | `waiting` | | `SessionEnd` | файл удаляется | `PostToolUse` не избыточен: это единственное событие, срабатывающее после выдачи разрешения, — без него сессия остаётся `blocked` в панели до конца хода. Пишет он только при реальной смене состояния, так что обычный случай стоит запуска процесса и нулевого ввода-вывода. `Stop` и `SessionEnd` зарегистрированы синхронно, в отличие от остальных. Оба срабатывают, когда процесс вот-вот затихнет, и асинхронный хук, проигравший гонку с выходом, убивается раньше, чем успевает записать: у `claude -p` это наблюдалось как сессия, навсегда застрявшая в `busy`. Хуки одной сессии выполняются параллельно, поэтому весь цикл «прочитать — решить — записать» идёт под `flock` на `.json.lock`, а событие старше сохранённого отвергается. Нужно и то, и другое: одна лишь проверка меток времени всё ещё позволяет хуку, прочитавшему старое состояние до `Stop`, записать своё устаревшее решение после него. Сабагенты не показываются, и это не про экономию строк. Их вызовы инструментов **долетают** до хуков родительской сессии — как `PostToolUse` с полями `agent_id` и `agent_type` (проверено запуском). Учитывать их нельзя: при фоновых сабагентах основной агент заканчивает ход первым (`Stop`, то есть `waiting`), а сабагенты продолжают работать, и их события перебили бы состояние обратно в `busy`. Панель показывала бы «работает» у сессии, которая на самом деле ждёт вас, — ровно та подмена, ради предотвращения которой всё и затевалось. Поэтому любое событие с `agent_id` игнорируется. Кроме `Notification`: она означает, что нужен человек, и это одинаково верно, в каком бы агенте ни заклинило. Батч из восьми задач остаётся одной строкой «работает 40 мин», и это правильная строка: пятый воркер из восьми ни о чём не просит. ## zellij Если сессии живут в табах zellij, меню показывает **имя таба**, а клик переключает на него. Поиск идёт через `zellij action dump-layout` — рабочий каталог сессии сопоставляется с каталогами пейнов, потому что в дампе раскладки нет id пейнов и `ZELLIJ_PANE_ID` для этого не годится. Отсюда следствия: две сессии в одном табе неразличимы, а сессия, сменившая `cwd` после открытия пейна, не найдётся. Строки, которые не разрешились, остаются некликабельными — вместо того чтобы делать вид, будто клик что-то делает. Если zellij не используется, выключите в настройках: он стоит одного процесса раз в пару минут. ## Известные шероховатости - **Протухшие сессии.** Убитый терминал не присылает `SessionEnd`. Живость перепроверяется каждые 20 с по `/proc/`, файл удаляется — убитая сессия исчезает в пределах этого окна, а не висит вечно. Если хук вообще не смог опознать процесс claude, он записывает pid 0 — «неизвестно», что никогда не путается с «мёртв», — и такие записи истекают по возрасту, через 36 часов. - **`Stop` не отличает «закончил» от «сдался».** Оба читаются как «ждёт ввода», и решение для вас в обоих случаях одно и то же. - **Фокус ведёт к табу zellij, а не к окну.** gnome-terminal держит все окна под одним общим серверным процессом, так что окно нельзя сопоставить по pid. Окно поднимается, только если в его заголовке есть имя zellij-сессии; когда совпадения нет, таб всё равно переключается, а фокус остаётся на месте — поднять произвольный терминал хуже, чем не поднимать никакой. ## Тесты ```sh gjs -m tests/test-sessions.js # чтение состояния, порядок, живость, слежение gjs -m tests/test-abbrev.js # метки чипов: сжатие, коллизии, липкость tests/test-hook.sh # события -> состояния, блокировка, снос файлов ``` `lib/sessions.js` намеренно ничего не импортирует из `resource:///org/gnome/shell`, а `lib/abbrev.js` — вообще ничего. Именно это позволяет гонять их вне композитора — в `gjs`, а `abbrev` и в `node`. Чтобы посмотреть сырые события хуков, создайте файл-маркер, и каждое событие будет дописываться в него: ```sh touch ~/.local/state/claude-code-status/debug ```