Files
claude-code-gnome-extension/README.md
T
av 223876a91a Fold NOTES.md into README.md
NOTES.md was written before the code, to hold the intent and the checked
facts about the environment. Everything in it that still applies now
lives in README.md, and the parts that do not -- the order of work, the
fields to verify against real hook input -- were answered by building it.

Two notes were kept rather than dropped: the rejection of desktop
notifications, which is a decision that would otherwise be re-litigated,
and the reasoning for hiding sub-agents. The original text stays in
history at 7fc7f63.
2026-08-09 18:25:19 +03:00

136 lines
5.7 KiB
Markdown

# Claude Code Status
GNOME Shell indicator that answers one question at a glance: **is any Claude
Code session waiting for me, and which one?**
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` 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.
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.
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.
## Install
```sh
git clone <this repo> ~/projects/private/claude-code-gnome-extension
cd ~/projects/private/claude-code-gnome-extension
# 1. hooks: teach Claude Code to publish session state
./hooks/install.py
# 2. extension: symlink, compile the schema, enable
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.
On Wayland, changing extension *code* still needs a logout; enabling it for the
first time does not.
`./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.
## How it works
Claude Code hooks write one small JSON file per session to
`~/.local/state/claude-code-status/<session_id>.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.
| Hook | Effect |
|---|---|
| `SessionStart` | session appears as `idle`, and dead sessions are swept |
| `UserPromptSubmit` | `busy` |
| `PostToolUse` | `busy` |
| `PreCompact` | `busy` |
| `Notification` | `blocked` or `waiting`, depending on `notification_type` |
| `Stop` | `waiting` |
| `SessionEnd` | file removed |
`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.
`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.
Hooks for one session run concurrently, so the whole read-decide-write runs
under an `flock` on `<session_id>.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.
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.
## 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.
Turn it off in Settings if you do not use zellij; it costs one process every
couple of minutes.
## Known rough edges
- **Stale sessions.** A killed terminal never sends `SessionEnd`. Liveness is
rechecked every 20 s against `/proc/<pid>` 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.
## Testing
```sh
gjs -m tests/test-sessions.js # state reading, ordering, liveness, monitoring
tests/test-hook.sh # event -> state machine, locking, teardown
```
`lib/sessions.js` imports nothing from `resource:///org/gnome/shell`, which is
what keeps it runnable outside the compositor.
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
```