The panel showed a single aggregate: the most urgent session, plus a "+N" for the others. That answers "what is the worst thing happening", which is not the question with five projects open -- "what is each of them doing" needs each of them on screen. Each session now gets a chip: state glyph plus a three-character project label, in the centre box just right of the clock. Time in state stays on the first chip only; five counters side by side are a row of numbers, not an answer. Labels are initials for multi-segment names, first letters otherwise. Initials rather than a prefix, because a prefix collapses dev-skills and dev-conventions onto the same "dev" -- exactly the pair that has to stay apart. Collisions, including two sessions in one project where the cwd is identical, take a digit: ds, ds2, ds3. Two rules keep a label still, and the feature is worthless without them. Assignment runs oldest-session-first, so a session starting now takes the suffix instead of displacing one already on screen; and a label belongs to its session until it ends, even after whatever forced the digit has closed. A label that moves under your hand is worse than one carrying a digit that no longer looks necessary. The zellij tab name was considered as the label source and dropped. It arrives asynchronously from dump-layout, so a sticky label would freeze whatever the project name produced first and never adopt it. Digits already separate same-project sessions, so the tab name buys nothing here and stays where it is useful, in the menu. Menu rows now lead with the same label, so the mapping from "ds" to dev-skills is read rather than guessed.
110 lines
4.5 KiB
JavaScript
110 lines
4.5 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.')));
|
|
dispGroup.add(this._switchRow(settings, 'hide-when-idle',
|
|
_('Hide when nothing is running'),
|
|
_('Remove the indicator from the panel while no session is active.')));
|
|
|
|
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;
|
|
}
|
|
}
|