Files
claude-code-gnome-extension/README.md
T
av 5398f32826 Count subagents, and keep a batch from looking free
Corrected from the previous commit, which had it backwards. When a batch
is running the session is working, not waiting: the main agent will pick
the results up and consolidate them itself, so sending you to that
terminal wastes the trip. That is the common shape of the work here --
ask for a batch, let it run.

Simply letting subagent tool calls set "busy" would mostly work and was
tempting, but it leaves a hole. Stop fires before the batch finishes, so
the session shows as free from the moment the turn ends until the first
subagent tool call lands -- and longer whenever the subagents are thinking
rather than calling tools. So subagents are counted instead:

  PreToolUse, matched to ^(Agent|Task)$   +1
  SubagentStop                            -1
  UserPromptSubmit                        reset to 0

While the count is above zero the session cannot read as waiting; Stop and
an idle_prompt nudge both leave it working. The session is freed by the
last subagent leaving, and only if the main agent has stopped by then.

The matcher is anchored because it is a regex: a bare "Task" also matches
TaskCreate and friends, which are not subagents. The hook re-checks the
tool name itself in case a future matcher behaves differently, and the
reset on UserPromptSubmit bounds a count that leaks because a subagent
died without its SubagentStop.

Measured, not assumed: a matched PreToolUse fires only on agent launches,
SubagentStop arrives once per subagent carrying agent_id, and a real
three-subagent run walks the count 0-1-2-3-2-0 before Stop frees it.

The menu shows the number, as asked. The panel does not: a batch of eight
is still one line saying "working 40 min", which is the right line.
2026-08-09 19:26:25 +03:00

16 KiB
Raw Blame History

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 из хука было бы куда дешевле сделать, но форма неверная: сессия, ждущая двадцать минут, должна оставаться на виду, а уведомление исчезает в момент, когда его смахнули. Панель здесь — единственный канал.

Установка

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/<session_id>.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 на <session_id>.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 после открытия пейна, не найдётся. Строки, которые не разрешились, остаются некликабельными — вместо того чтобы делать вид, будто клик что-то делает.

Если zellij не используется, выключите в настройках: он стоит одного процесса раз в пару минут.

Известные шероховатости

  • Протухшие сессии. Убитый терминал не присылает SessionEnd. Живость перепроверяется каждые 20 с по /proc/<pid>, файл удаляется — убитая сессия исчезает в пределах этого окна, а не висит вечно. Если хук вообще не смог опознать процесс claude, он записывает pid 0 — «неизвестно», что никогда не путается с «мёртв», — и такие записи истекают по возрасту, через 36 часов.
  • Stop не отличает «закончил» от «сдался». Оба читаются как «ждёт ввода», и решение для вас в обоих случаях одно и то же.
  • Фокус ведёт к табу zellij, а не к окну. gnome-terminal держит все окна под одним общим серверным процессом, так что окно нельзя сопоставить по pid. Окно поднимается, только если в его заголовке есть имя zellij-сессии; когда совпадения нет, таб всё равно переключается, а фокус остаётся на месте — поднять произвольный терминал хуже, чем не поднимать никакой.

Тесты

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.

Чтобы посмотреть сырые события хуков, создайте файл-маркер, и каждое событие будет дописываться в него:

touch ~/.local/state/claude-code-status/debug