# 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` — ценой записи файла на каждый вызов инструмента. Чипов показывается три (настраивается), остальные сворачиваются в `+N`. Панель живёт в центральном боксе рядом с часами, и без предела достаточно открытых сессий сдвинули бы часы с центра. Чипы идут по срочности, а внутри одного состояния первой стоит **самая давняя**: забывается та, что ждёт дольше всех, а не последняя. Время в состоянии показывается только у первого чипа: пять счётчиков рядом — это ряд чисел, а не ответ на вопрос. ## Метки чипов Имя проекта сжимается до трёх знаков: если в имени несколько сегментов (`-`, `_`, 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`, записать своё устаревшее решение после него. ## Сабагенты Типичный сценарий: вы просите запустить батч, основной агент разворачивает его и **заканчивает ход**, а сессия ждёт результатов, чтобы свести их воедино. Звать вас туда не надо — она занята. Но `Stop` при этом приходит раньше, чем сабагенты закончат. Если верить ему буквально, сессия покажется свободной ровно тогда, когда в неё лезть бессмысленно. Поэтому сабагенты считаются явно: | Событие | Что делает | |---|---| | `PreToolUse` с матчером `^(Agent\|Task)$` | +1 к счётчику | | `SubagentStop` | −1; последний освобождает сессию, если ход уже закончен | | `UserPromptSubmit` | сбрасывает счётчик в 0 | Пока счётчик больше нуля, состояние `waiting` невозможно: и `Stop`, и напоминание `idle_prompt` дают `busy`. Освобождает сессию только уход последнего сабагента — и лишь если основной агент к тому времени остановился. Матчер якорный не для красоты: это регулярка, и голое `Task` поймало бы `TaskCreate`, `TaskUpdate` и прочее. Хук проверяет имя инструмента ещё раз, сам. Сброс на `UserPromptSubmit` ограничивает ущерб, если сабагент умрёт, не прислав `SubagentStop`: счётчик не переживёт следующего вашего сообщения. Собственные вызовы инструментов сабагентов игнорируются — они долетают до хуков родителя как `PostToolUse` с `agent_id`, но счётчик уже всё сказал, а они добавили бы только записи на диск. `Notification` — исключение: она означает, что нужен человек, и это одинаково верно, в каком бы агенте ни заклинило. В меню число сабагентов показывается строкой «работает · 3 subagents». В панели — нет: батч из восьми задач остаётся одной строкой «работает 40 мин», и это правильная строка. ## zellij Если сессии живут в табах zellij, меню называет **имя таба** — это лучший ответ на «в какой терминал идти», чем путь. Поиск идёт через `zellij action dump-layout`: рабочий каталог сессии сопоставляется с каталогами пейнов, потому что в дампе раскладки нет id пейнов и `ZELLIJ_PANE_ID` для этого не годится. Отсюда следствия: две сессии в одном табе неразличимы, а сессия, сменившая `cwd` после открытия пейна, не найдётся. Меню **ничего не делает** — только показывает. Ни строки, ни чипы не кликабельны: переключение таба и подъём окна были написаны и убраны, потому что стоили заметной логики (окна gnome-terminal нельзя сопоставить по pid — все они под одним серверным процессом) ради экономии одного alt-tab. Если zellij не используется, выключите в настройках: он стоит одного процесса раз в пару минут. ## Аварийное завершение `kill`, закрытое окно, перезагрузка — `SessionEnd` не приходит, и файл остаётся. Разбор завалов проверен на каждом случае отдельно: | Что осталось | Что с этим происходит | |---|---| | файл убитой сессии | процесса нет — файл и его `.lock` удаляются в пределах 20 с | | файл из прошлой загрузки | pid сверяется по времени старта процесса, а не только по наличию | | оборванная запись хука (`.tmp`) | удаляется, когда старше пяти минут | | незакрытый `flock` | ядро снимает блокировку при смерти процесса — тупика не бывает | Сверка по времени старта — не перестраховка. Файлы состояния переживают перезагрузку, а pid после неё раздаются заново: проверка «есть ли `/proc/`» отвечает лишь «какой-то процесс с таким номером есть». Без этой сверки сессия, погибшая в аварии, висела бы в панели вечно, требуя ответа, которого некому дать. Проверено подстановкой постороннего живого процесса на тот же pid. Порог в пять минут для `.tmp` тоже осмысленный: хук может писать такой файл прямо сейчас, и удаление свежего стоило бы потерянной записи. Если хук вообще не смог опознать процесс claude, он пишет pid 0 — «неизвестно», что никогда не путается с «мёртв»; такие записи истекают по возрасту, через 36 часов. ## Известные шероховатости - **`Stop` не отличает «закончил» от «сдался».** Оба читаются как «ждёт ввода», и решение для вас в обоих случаях одно и то же. ## Тесты ```sh gjs -m tests/test-sessions.js # чтение состояния, порядок, живость, слежение gjs -m tests/test-abbrev.js # метки чипов: сжатие, коллизии, липкость gjs -m tests/test-prefs.js # окно настроек строится и находит хуки tests/test-hook.sh # события -> состояния, блокировка, снос файлов ``` `prefs.js` живёт в отдельном процессе, а не в композиторе, поэтому проверка во вложенном шелле до него не достаёт — без этого теста он единственный файл, который впервые исполняется у пользователя. `lib/sessions.js` намеренно ничего не импортирует из `resource:///org/gnome/shell`, а `lib/abbrev.js` — вообще ничего. Именно это позволяет гонять их вне композитора — в `gjs`, а `abbrev` и в `node`. Чтобы посмотреть сырые события хуков, создайте файл-маркер, и каждое событие будет дописываться в него: ```sh touch ~/.local/state/claude-code-status/debug ```