diff --git a/README.md b/README.md index e5860be..2103a76 100644 --- a/README.md +++ b/README.md @@ -1,134 +1,134 @@ # Claude Code Status -GNOME Shell indicator that answers one question at a glance: **is any Claude -Code session waiting for me, and which one?** +Индикатор для GNOME Shell, отвечающий на один вопрос: **какая сессия Claude +Code ждёт меня прямо сейчас?** -With several sessions open in different projects, the cost is not knowing what -each one is doing — it is noticing that one of them stopped an hour ago. The -panel names the session, not just the state. +Когда открыто несколько сессий в разных проектах, дорого не «не знать, чем +каждая занята», а не заметить, что одна из них встала час назад. Панель +называет сессию, а не только состояние. -## States +## Состояния -| Panel | State | Meaning | +| В панели | Состояние | Что значит | |---|---|---| -| disc inside a ring | `blocked` | stuck on a permission prompt — it cannot proceed without you | -| filled disc | `waiting` | turn finished, waiting for your input | -| ring | `busy` | working | -| dim dashed ring | `idle` | started, nothing asked yet, or nothing running | +| диск в кольце | `blocked` | упёрлась в запрос разрешения — без вас не сдвинется | +| закрашенный диск | `waiting` | ход закончен, ждёт вашего ввода | +| кольцо | `busy` | работает | +| тусклое пунктирное кольцо | `idle` | запущена, но ничего не просили, либо ничего не запущено | -`blocked` and `waiting` are kept apart on purpose. Merged into one "needs you", -a finished task looks as urgent as a blocked one, and the distinction is exactly -what decides whether to switch now or after the current thought. +`blocked` и `waiting` разведены намеренно. Слитые в одно «требует внимания», +законченная задача выглядит так же срочно, как заблокированная, — а именно это +различие и решает, переключаться сейчас или после текущей мысли. -The panel shows the **highest-priority** session and, when others share that -state, a `+N` count. Within a state the **oldest** one wins: the session you -have forgotten about is the one that has been waiting longest, never the latest. +Панель показывает **самую приоритетную** сессию и счётчик `+N`, если в том же +состоянии есть другие. Внутри состояния выигрывает **самая давняя**: забывается +та, что ждёт дольше всех, а не последняя. -There are deliberately **no desktop notifications**. `notify-send` from a hook -would have been far cheaper to build, and it is the wrong shape: a session that -has been waiting twenty minutes needs to stay visible, and a notification is -gone the moment it is dismissed. The panel is the whole channel. +**Уведомлений на рабочий стол нет** — сознательно. `notify-send` из хука было бы +куда дешевле сделать, но форма неверная: сессия, ждущая двадцать минут, должна +оставаться на виду, а уведомление исчезает в момент, когда его смахнули. Панель +здесь — единственный канал. -## Install +## Установка ```sh -git clone ~/projects/private/claude-code-gnome-extension +git clone <этот репозиторий> ~/projects/private/claude-code-gnome-extension cd ~/projects/private/claude-code-gnome-extension -# 1. hooks: teach Claude Code to publish session state +# 1. хуки: научить Claude Code публиковать состояние сессий ./hooks/install.py -# 2. extension: symlink, compile the schema, enable +# 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 ``` -Running sessions pick the hooks up without restarting — Claude Code re-reads -`settings.json` as it changes. +Уже запущенные сессии подхватывают хуки без перезапуска — Claude Code перечитывает +`settings.json` по мере изменения. -On Wayland, changing extension *code* still needs a logout; enabling it for the -first time does not. +На Wayland правки *кода* расширения по-прежнему требуют релогина; первое включение +— нет. -`./hooks/install.py --uninstall` removes the hook registration and leaves the -rest of `~/.claude/settings.json` untouched. A `.bak` copy is written on every -run. +`./hooks/install.py --uninstall` снимает регистрацию хуков, не трогая остальное в +`~/.claude/settings.json`. Копия `.bak` пишется при каждом запуске. -## How it works +## Как это работает -Claude Code hooks write one small JSON file per session to -`~/.local/state/claude-code-status/.json`; the extension watches -that directory with `Gio.FileMonitor`. Nothing polls, and there is no daemon — -a state change reaches the panel as soon as the hook returns. +Хуки Claude Code пишут по одному небольшому JSON-файлу на сессию в +`~/.local/state/claude-code-status/.json`; расширение следит за этим +каталогом через `Gio.FileMonitor`. Ничего не опрашивается, демона нет — смена +состояния доходит до панели, как только хук завершился. -| Hook | Effect | +| Хук | Действие | |---|---| -| `SessionStart` | session appears as `idle`, and dead sessions are swept | +| `SessionStart` | сессия появляется как `idle`, мёртвые подчищаются | | `UserPromptSubmit` | `busy` | | `PostToolUse` | `busy` | | `PreCompact` | `busy` | -| `Notification` | `blocked` or `waiting`, depending on `notification_type` | +| `Notification` | `blocked` или `waiting`, в зависимости от `notification_type` | | `Stop` | `waiting` | -| `SessionEnd` | file removed | +| `SessionEnd` | файл удаляется | -`PostToolUse` is not redundant: it is the only event that fires after a -permission is granted, so without it a session stays `blocked` in the panel for -the rest of the turn. It writes only when the state actually changes, so the -usual case costs a process spawn and no I/O. +`PostToolUse` не избыточен: это единственное событие, срабатывающее после выдачи +разрешения, — без него сессия остаётся `blocked` в панели до конца хода. Пишет он +только при реальной смене состояния, так что обычный случай стоит запуска процесса +и нулевого ввода-вывода. -`Stop` and `SessionEnd` are registered synchronously. Both fire as the process -is about to go quiet, and an async hook racing that exit gets killed before it -writes — `claude -p` was observed leaving a session pinned at `busy` forever. +`Stop` и `SessionEnd` зарегистрированы синхронно, в отличие от остальных. Оба +срабатывают, когда процесс вот-вот затихнет, и асинхронный хук, проигравший гонку +с выходом, убивается раньше, чем успевает записать: у `claude -p` это наблюдалось +как сессия, навсегда застрявшая в `busy`. -Hooks for one session run concurrently, so the whole read-decide-write runs -under an `flock` on `.json.lock`, and an event older than the stored -one is refused. Both are needed: the timestamp guard alone still lets a hook -that read the old state before a `Stop` write its stale decision afterwards. +Хуки одной сессии выполняются параллельно, поэтому весь цикл «прочитать — решить — +записать» идёт под `flock` на `.json.lock`, а событие старше +сохранённого отвергается. Нужно и то, и другое: одна лишь проверка меток времени +всё ещё позволяет хуку, прочитавшему старое состояние до `Stop`, записать своё +устаревшее решение после него. -Sub-agents are not shown. A batch of eight tasks is one line, "working 40 min", -which is the right line: the fifth of eight workers is not asking for anything. +Сабагенты не показываются. Батч из восьми задач — это одна строка «работает +40 мин», и это правильная строка: пятый воркер из восьми ни о чём не просит. ## zellij -When sessions run in zellij tabs, the menu shows the **tab name** and a click -switches to it. The lookup goes through `zellij action dump-layout`, matching a -session's working directory against pane directories — the layout dump carries -no pane ids, so `ZELLIJ_PANE_ID` cannot be used for it. Two sessions in one tab -are therefore indistinguishable, and a session whose `cwd` has moved since the -pane opened will not match. Rows that cannot be resolved stay inert rather than -pretending a click does something. +Если сессии живут в табах zellij, меню показывает **имя таба**, а клик +переключает на него. Поиск идёт через `zellij action dump-layout` — рабочий +каталог сессии сопоставляется с каталогами пейнов, потому что в дампе раскладки +нет id пейнов и `ZELLIJ_PANE_ID` для этого не годится. Отсюда следствия: две +сессии в одном табе неразличимы, а сессия, сменившая `cwd` после открытия пейна, +не найдётся. Строки, которые не разрешились, остаются некликабельными — вместо +того чтобы делать вид, будто клик что-то делает. -Turn it off in Settings if you do not use zellij; it costs one process every -couple of minutes. +Если zellij не используется, выключите в настройках: он стоит одного процесса +раз в пару минут. -## Known rough edges +## Известные шероховатости -- **Stale sessions.** A killed terminal never sends `SessionEnd`. Liveness is - rechecked every 20 s against `/proc/` and the file is deleted, so a - killed session disappears within that window rather than lingering. When the - hook cannot identify the claude process at all it records pid 0 — "unknown", - never confused with "dead" — and those entries expire on age instead, after - 36 h. -- **`Stop` cannot tell "finished" from "gave up".** Both read as "waiting for - input", which is the same decision for you either way. -- **Focus follows the zellij tab, not the window.** gnome-terminal runs every - window under one shared server process, so a window cannot be matched by pid. - A window is raised only when its title names the zellij session; when no - window matches, the tab still switches and focus is left alone, because - raising an arbitrary terminal is worse than raising none. +- **Протухшие сессии.** Убитый терминал не присылает `SessionEnd`. Живость + перепроверяется каждые 20 с по `/proc/`, файл удаляется — убитая сессия + исчезает в пределах этого окна, а не висит вечно. Если хук вообще не смог + опознать процесс claude, он записывает pid 0 — «неизвестно», что никогда не + путается с «мёртв», — и такие записи истекают по возрасту, через 36 часов. +- **`Stop` не отличает «закончил» от «сдался».** Оба читаются как «ждёт ввода», + и решение для вас в обоих случаях одно и то же. +- **Фокус ведёт к табу zellij, а не к окну.** gnome-terminal держит все окна под + одним общим серверным процессом, так что окно нельзя сопоставить по pid. Окно + поднимается, только если в его заголовке есть имя zellij-сессии; когда + совпадения нет, таб всё равно переключается, а фокус остаётся на месте — + поднять произвольный терминал хуже, чем не поднимать никакой. -## Testing +## Тесты ```sh -gjs -m tests/test-sessions.js # state reading, ordering, liveness, monitoring -tests/test-hook.sh # event -> state machine, locking, teardown +gjs -m tests/test-sessions.js # чтение состояния, порядок, живость, слежение +tests/test-hook.sh # события -> состояния, блокировка, снос файлов ``` -`lib/sessions.js` imports nothing from `resource:///org/gnome/shell`, which is -what keeps it runnable outside the compositor. +`lib/sessions.js` намеренно ничего не импортирует из `resource:///org/gnome/shell` +— именно это позволяет гонять его в обычном `gjs`, вне композитора. -To watch the raw hook events, create the marker file and every event will be -appended to it: +Чтобы посмотреть сырые события хуков, создайте файл-маркер, и каждое событие +будет дописываться в него: ```sh touch ~/.local/state/claude-code-status/debug