"Idle" was set by SessionStart and by nothing else, and there was no path back into it. So it never meant "sitting unused" -- it meant "opened and never asked anything yet", a state a few seconds long that you would almost never catch. Meanwhile a session that finished an hour ago and was forgotten showed as waiting, which is correct but leaves the fourth state with nothing to describe. A session that has just opened is waiting for your first prompt exactly as one that finished a turn is waiting for your next. They are the same thing, and now they are the same state. Three glyphs instead of four, which also gives the remaining three more room to be told apart in a monochrome panel. Files written by the previous hook still say "idle", and a session open across the upgrade must not disappear, so unrecognised states now read as waiting rather than being treated as unknown. Covered by a test that feeds an "idle" file to the store and asserts it comes back as waiting, sorted by age among the others. The "hide when nothing is running" setting goes with it. Its condition was "no sessions, or all of them idle"; with idle gone the second half is unreachable and the first was already unconditional, so the switch could no longer change anything. A control that does nothing is worse than no control. The compaction test also got stronger in passing: it now checks that a mid-turn SessionStart leaves a *busy* session alone, which is the case that matters. It used to assert from waiting, where the state it was guarding against happened to be the state already stored.
107 lines
4.3 KiB
JavaScript
107 lines
4.3 KiB
JavaScript
import Adw from 'gi://Adw';
|
|
import Gtk from 'gi://Gtk';
|
|
import Gio from 'gi://Gio';
|
|
import GLib from 'gi://GLib';
|
|
|
|
import { ExtensionPreferences, gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
|
|
|
|
export default class ClaudeCodeStatusPreferences extends ExtensionPreferences {
|
|
fillPreferencesWindow(window) {
|
|
const settings = this.getSettings();
|
|
|
|
const page = new Adw.PreferencesPage({
|
|
title: _('General'),
|
|
icon_name: 'utilities-terminal-symbolic',
|
|
});
|
|
window.add(page);
|
|
|
|
const dispGroup = new Adw.PreferencesGroup({ title: _('Panel') });
|
|
page.add(dispGroup);
|
|
|
|
dispGroup.add(this._switchRow(settings, 'show-project-name',
|
|
_('Label chips with the project'),
|
|
_('Name the sessions, not just their states.')));
|
|
dispGroup.add(this._switchRow(settings, 'abbreviate-names',
|
|
_('Shorten names to three characters'),
|
|
_('“dev-skills” becomes “ds”. Collisions get a digit by seniority, so a label already on screen never changes.')));
|
|
dispGroup.add(this._switchRow(settings, 'show-age',
|
|
_('Show time in state'),
|
|
_('Shown on the most urgent session only.')));
|
|
|
|
const zellijGroup = new Adw.PreferencesGroup({
|
|
title: _('zellij'),
|
|
description: _('Sessions running inside zellij can be located by tab name instead of by path.'),
|
|
});
|
|
page.add(zellijGroup);
|
|
|
|
zellijGroup.add(this._switchRow(settings, 'zellij-integration',
|
|
_('Resolve tab names'),
|
|
_('Show the zellij tab in the menu and switch to it on click. Ignored when zellij is not installed.')));
|
|
|
|
// --- Hooks ---------------------------------------------------------
|
|
// The indicator is only as good as the hooks feeding it, and a silent
|
|
// panel looks identical whether nothing is running or nothing is
|
|
// installed. Say which it is.
|
|
const hooksGroup = new Adw.PreferencesGroup({
|
|
title: _('Hooks'),
|
|
description: _('Claude Code writes one state file per session; the panel watches them.'),
|
|
});
|
|
page.add(hooksGroup);
|
|
|
|
hooksGroup.add(new Adw.ActionRow({
|
|
title: _('Status'),
|
|
subtitle: this._hooksStatus(),
|
|
}));
|
|
hooksGroup.add(new Adw.ActionRow({
|
|
title: _('State directory'),
|
|
subtitle: this._stateDir(),
|
|
}));
|
|
|
|
const installRow = new Adw.ActionRow({
|
|
title: _('Install command'),
|
|
subtitle: `${this.path}/hooks/install.py`,
|
|
});
|
|
const copyButton = new Gtk.Button({
|
|
icon_name: 'edit-copy-symbolic',
|
|
valign: Gtk.Align.CENTER,
|
|
tooltip_text: _('Copy to clipboard'),
|
|
});
|
|
copyButton.connect('clicked', () => {
|
|
window.get_clipboard().set(`${this.path}/hooks/install.py`);
|
|
});
|
|
installRow.add_suffix(copyButton);
|
|
hooksGroup.add(installRow);
|
|
}
|
|
|
|
_stateDir() {
|
|
const base = GLib.getenv('XDG_STATE_HOME') ||
|
|
GLib.build_filenamev([GLib.get_home_dir(), '.local', 'state']);
|
|
return GLib.build_filenamev([base, 'claude-code-status']);
|
|
}
|
|
|
|
_hooksStatus() {
|
|
const path = GLib.build_filenamev([GLib.get_home_dir(), '.claude', 'settings.json']);
|
|
try {
|
|
const [ok, bytes] = GLib.file_get_contents(path);
|
|
if (!ok)
|
|
return _('~/.claude/settings.json is unreadable');
|
|
const settings = JSON.parse(new TextDecoder().decode(bytes));
|
|
const events = Object.entries(settings.hooks ?? {})
|
|
.filter(([, groups]) => groups.some(g =>
|
|
(g.hooks ?? []).some(h => (h.command ?? '').includes('claude-status-hook.py'))))
|
|
.map(([event]) => event);
|
|
if (!events.length)
|
|
return _('Not installed — run the install command below, then restart your sessions');
|
|
return `${_('Installed for')}: ${events.join(', ')}`;
|
|
} catch (e) {
|
|
return _('~/.claude/settings.json could not be parsed');
|
|
}
|
|
}
|
|
|
|
_switchRow(settings, key, title, subtitle) {
|
|
const row = new Adw.SwitchRow({ title, subtitle });
|
|
settings.bind(key, row, 'active', Gio.SettingsBindFlags.DEFAULT);
|
|
return row;
|
|
}
|
|
}
|