av 75bfe77950 Survive a crash, and a reboot after one
kill, a closed window, a reboot: no SessionEnd arrives and the state file
stays. Each case was tried rather than reasoned about, and one of the
three was broken.

A killed session was already handled -- the process is gone, so the file
and its lock are removed within the 20 s liveness tick. An interrupted
hook write left its temporary file behind forever; those are now swept
once they are five minutes old, which is late enough that a hook part-way
through writing one does not lose the update.

The reboot case was the broken one. State files outlive a reboot and pids
are handed out afresh, so "does /proc/<pid> exist" only answers "is some
process wearing that number". Verified by giving an unrelated live process
the pid of a dead session: the ghost sat in the panel as a session waiting
for input, and would have stayed there forever, asking for an answer
nobody could give. The pid is now pinned to the process start time from
/proc/<pid>/stat, recorded when the state is written and compared when it
is read.

Files written before that field existed compare only on existence, as
before, so a session open across the upgrade is not evicted.

An abandoned flock needed nothing: the kernel drops it when the holder
dies, so there is no deadlock to recover from.
2026-08-09 19:43:31 +03:00

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

Установка

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 после открытия пейна, не найдётся.

Меню ничего не делает — только показывает. Ни строки, ни чипы не кликабельны: переключение таба и подъём окна были написаны и убраны, потому что стоили заметной логики (окна gnome-terminal нельзя сопоставить по pid — все они под одним серверным процессом) ради экономии одного alt-tab.

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

Аварийное завершение

kill, закрытое окно, перезагрузка — SessionEnd не приходит, и файл остаётся. Разбор завалов проверен на каждом случае отдельно:

Что осталось Что с этим происходит
файл убитой сессии процесса нет — файл и его .lock удаляются в пределах 20 с
файл из прошлой загрузки pid сверяется по времени старта процесса, а не только по наличию
оборванная запись хука (.tmp) удаляется, когда старше пяти минут
незакрытый flock ядро снимает блокировку при смерти процесса — тупика не бывает

Сверка по времени старта — не перестраховка. Файлы состояния переживают перезагрузку, а pid после неё раздаются заново: проверка «есть ли /proc/<pid>» отвечает лишь «какой-то процесс с таким номером есть». Без этой сверки сессия, погибшая в аварии, висела бы в панели вечно, требуя ответа, которого некому дать. Проверено подстановкой постороннего живого процесса на тот же pid.

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

Если хук вообще не смог опознать процесс claude, он пишет pid 0 — «неизвестно», что никогда не путается с «мёртв»; такие записи истекают по возрасту, через 36 часов.

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

  • Stop не отличает «закончил» от «сдался». Оба читаются как «ждёт ввода», и решение для вас в обоих случаях одно и то же.

Тесты

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.

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

touch ~/.local/state/claude-code-status/debug
S
Description
No description provided
Readme
410 KiB
Languages
JavaScript 63.9%
Python 24.8%
Shell 10.1%
CSS 1.2%