diff --git a/README.md b/README.md
index 063e596..9237692 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,10 @@ Code ждёт меня прямо сейчас?**
текстом `«Claude needs your permission»`. Различить их можно было бы только через
`PreToolUse` — ценой записи файла на каждый вызов инструмента.
+Чипов показывается три (настраивается), остальные сворачиваются в `+N`. Панель
+живёт в центральном боксе рядом с часами, и без предела достаточно открытых
+сессий сдвинули бы часы с центра.
+
Чипы идут по срочности, а внутри одного состояния первой стоит **самая
давняя**: забывается та, что ждёт дольше всех, а не последняя. Время в
состоянии показывается только у первого чипа: пять счётчиков рядом — это
@@ -178,13 +182,17 @@ gnome-extensions enable claude-code-status@git.vakhrushev.me
## zellij
-Если сессии живут в табах zellij, меню показывает **имя таба**, а клик
-переключает на него. Поиск идёт через `zellij action dump-layout` — рабочий
-каталог сессии сопоставляется с каталогами пейнов, потому что в дампе раскладки
-нет id пейнов и `ZELLIJ_PANE_ID` для этого не годится. Отсюда следствия: две
-сессии в одном табе неразличимы, а сессия, сменившая `cwd` после открытия пейна,
-не найдётся. Строки, которые не разрешились, остаются некликабельными — вместо
-того чтобы делать вид, будто клик что-то делает.
+Если сессии живут в табах zellij, меню называет **имя таба** — это лучший ответ
+на «в какой терминал идти», чем путь. Поиск идёт через `zellij action
+dump-layout`: рабочий каталог сессии сопоставляется с каталогами пейнов, потому
+что в дампе раскладки нет id пейнов и `ZELLIJ_PANE_ID` для этого не годится.
+Отсюда следствия: две сессии в одном табе неразличимы, а сессия, сменившая
+`cwd` после открытия пейна, не найдётся.
+
+Меню **ничего не делает** — только показывает. Ни строки, ни чипы не кликабельны:
+переключение таба и подъём окна были написаны и убраны, потому что стоили заметной
+логики (окна gnome-terminal нельзя сопоставить по pid — все они под одним
+серверным процессом) ради экономии одного alt-tab.
Если zellij не используется, выключите в настройках: он стоит одного процесса
раз в пару минут.
@@ -198,11 +206,6 @@ gnome-extensions enable claude-code-status@git.vakhrushev.me
путается с «мёртв», — и такие записи истекают по возрасту, через 36 часов.
- **`Stop` не отличает «закончил» от «сдался».** Оба читаются как «ждёт ввода»,
и решение для вас в обоих случаях одно и то же.
-- **Фокус ведёт к табу zellij, а не к окну.** gnome-terminal держит все окна под
- одним общим серверным процессом, так что окно нельзя сопоставить по pid. Окно
- поднимается, только если в его заголовке есть имя zellij-сессии; когда
- совпадения нет, таб всё равно переключается, а фокус остаётся на месте —
- поднять произвольный терминал хуже, чем не поднимать никакой.
## Тесты
diff --git a/lib/indicator.js b/lib/indicator.js
index fa9649b..6929613 100644
--- a/lib/indicator.js
+++ b/lib/indicator.js
@@ -6,7 +6,6 @@ import Clutter from 'gi://Clutter';
import GLib from 'gi://GLib';
import Pango from 'gi://Pango';
-import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import { gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js';
@@ -27,11 +26,6 @@ function stateLabel(state) {
}
}
-const TERMINAL_CLASSES = [
- 'gnome-terminal', 'org.gnome.terminal', 'kitty', 'alacritty',
- 'foot', 'wezterm', 'konsole', 'xterm', 'ghostty',
-];
-
/** Paint a state glyph in the panel's own text colour.
*
* Nothing here picks a colour: the foreground comes from the theme node, so
@@ -164,6 +158,7 @@ class ClaudeStatusIndicator extends PanelMenu.Button {
_updatePanel(sessions) {
const showAge = this._settings.get_boolean('show-age');
const abbreviate = this._settings.get_boolean('abbreviate-names');
+ const maxChips = this._settings.get_int('max-chips');
this._chipLabels = assignChips(
sessions.map(s => ({
@@ -181,23 +176,38 @@ class ClaudeStatusIndicator extends PanelMenu.Button {
// than sitting there empty.
this.visible = sessions.length > 0;
+ // Chips are ordered by urgency, so cutting the tail keeps the ones that
+ // need you soonest. Without a cap the row grows without bound, and it
+ // sits in the centre box -- enough sessions would shove the clock off
+ // centre. Labels are still assigned over every session, so the menu and
+ // the panel agree and a chip does not change when the cap does.
+ const shown = sessions.slice(0, maxChips);
+ const hidden = sessions.length - shown.length;
+
// Age rides on the first chip only. Sessions are sorted by urgency, so
// that is the one whose age decides anything; five ages side by side
// would just be a wide row of numbers.
- const ageOnFirst = showAge && sessions.length > 0;
- const signature = sessions
+ const ageOnFirst = showAge && shown.length > 0;
+ const signature = shown
.map(s => `${s.sessionId}:${s.state}:${labelFor(s)}`)
- .join('|') + `|${ageOnFirst}`;
+ .join('|') + `|${ageOnFirst}|${hidden}`;
if (signature !== this._chipSignature) {
this._chipBox.destroy_all_children();
this._ageLabel = null;
- sessions.forEach((session, i) => {
+ shown.forEach((session, i) => {
const { chip, age } = this._buildChip(
session, labelFor(session), ageOnFirst && i === 0);
if (age)
this._ageLabel = { age, session };
this._chipBox.add_child(chip);
});
+ if (hidden > 0) {
+ this._chipBox.add_child(new St.Label({
+ style_class: 'ccs-overflow',
+ y_align: Clutter.ActorAlign.CENTER,
+ text: `+${hidden}`,
+ }));
+ }
this._chipSignature = signature;
}
@@ -210,8 +220,10 @@ class ClaudeStatusIndicator extends PanelMenu.Button {
// Rebuild only when the set of sessions or their states changed; ages
// alone are refreshed in place so an open menu does not flicker.
+ // The tab name is not in the signature: it only feeds the subtitle,
+ // which is refreshed in place below.
const signature = sessions
- .map(s => `${s.sessionId}:${s.state}:${this._tabFor(s) ?? ''}:${this._chipLabels?.get(s.sessionId) ?? ''}`)
+ .map(s => `${s.sessionId}:${s.state}:${this._chipLabels?.get(s.sessionId) ?? ''}`)
.join('|');
if (signature !== this._rowSignature) {
this._rebuildRows(sessions);
@@ -259,13 +271,12 @@ class ClaudeStatusIndicator extends PanelMenu.Button {
}
_buildRow(session) {
- const tab = this._tabFor(session);
- // Reactivity is decided at construction, not patched afterwards:
- // PopupBaseMenuItem latches _activatable in its constructor, so a row
- // switched to reactive=false later keeps the styling of a clickable one
- // and still looks like it does something.
+ // The menu reports; it does not act. Rows are built inert rather than
+ // switched off afterwards, because PopupBaseMenuItem latches
+ // _activatable in its constructor and a row demoted later keeps the
+ // styling of a clickable one.
const item = new PopupMenu.PopupBaseMenuItem(
- tab ? {} : { reactive: false, can_focus: false });
+ { reactive: false, can_focus: false });
item.add_style_class_name('ccs-row');
const column = new St.BoxLayout({ vertical: true, x_expand: true });
@@ -296,9 +307,6 @@ class ClaudeStatusIndicator extends PanelMenu.Button {
column.add_child(subtitle);
item.add_child(column);
- if (tab)
- item.connect('activate', () => this._switchTo(session, tab));
-
this._rows.push({ sessionId: session.sessionId, age, subtitle });
return item;
}
@@ -360,36 +368,6 @@ class ClaudeStatusIndicator extends PanelMenu.Button {
.catch(e => logError(e, 'claude-code-status: zellij refresh failed'));
}
- _switchTo(session, tab) {
- this._zellij.goToTab(session.zellijSession, tab);
- this._focusTerminal(session);
- }
-
- /** Best effort: raise a terminal window showing this zellij session.
- *
- * Matching by pid does not work for gnome-terminal, where every window
- * belongs to one shared server process, so the window title is the only
- * handle available -- and zellij puts the session name there.
- */
- _focusTerminal(session) {
- if (!session.zellijSession)
- return;
- // list_all_windows() rather than get_window_actors(): the latter is
- // deprecated from GNOME 46 on, and this has to work across 45-48.
- for (const win of global.display.list_all_windows()) {
- const wmClass = (win.get_wm_class() ?? '').toLowerCase();
- if (!TERMINAL_CLASSES.some(c => wmClass.includes(c)))
- continue;
- // Only a window that names this zellij session is raised. Falling
- // back to any terminal at all would raise an unrelated one, which
- // is worse than leaving focus where the user put it.
- if ((win.get_title() ?? '').includes(session.zellijSession)) {
- Main.activateWindow(win);
- return;
- }
- }
- }
-
// ---- Visuals --------------------------------------------------------
// ---- Teardown -------------------------------------------------------
diff --git a/lib/zellij.js b/lib/zellij.js
index b95af4f..3bf0e39 100644
--- a/lib/zellij.js
+++ b/lib/zellij.js
@@ -1,8 +1,8 @@
// Maps a session's working directory to the zellij tab it is running in.
//
// Knowing a session waits for you is only half the answer; the other half is
-// where to look. When sessions live in zellij tabs, the tab name is a better
-// answer than a path, and zellij can be told to switch to it.
+// where to look. When sessions live in zellij tabs, the tab name answers that
+// better than a path does.
//
// `zellij action dump-layout` prints tab names with each pane's cwd but no pane
// ids, so ZELLIJ_PANE_ID from the hook cannot be used for the lookup and the
@@ -115,17 +115,6 @@ export class ZellijTabs {
this._cache.clear();
this._inFlight.clear();
}
-
- /** Switch the given zellij session to a tab. Fire and forget. */
- goToTab(session, tab) {
- try {
- Gio.Subprocess.new(
- ['zellij', '--session', session, 'action', 'go-to-tab-name', tab],
- Gio.SubprocessFlags.STDOUT_SILENCE | Gio.SubprocessFlags.STDERR_SILENCE);
- } catch (e) {
- logError(e, 'claude-code-status: zellij go-to-tab-name failed');
- }
- }
}
/** Extract tab names and their pane working directories from a KDL layout.
diff --git a/prefs.js b/prefs.js
index 2851829..2926abb 100644
--- a/prefs.js
+++ b/prefs.js
@@ -24,6 +24,10 @@ export default class ClaudeCodeStatusPreferences extends ExtensionPreferences {
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._spinRow(settings, 'max-chips',
+ _('Chips shown'),
+ _('The most urgent sessions get a chip; the rest are counted as “+N”.'),
+ 1, 12));
dispGroup.add(this._switchRow(settings, 'show-age',
_('Show time in state'),
_('Shown on the most urgent session only.')));
@@ -36,7 +40,7 @@ export default class ClaudeCodeStatusPreferences extends ExtensionPreferences {
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.')));
+ _('Name the zellij tab each session runs in. Ignored when zellij is not installed.')));
// --- Hooks ---------------------------------------------------------
// The indicator is only as good as the hooks feeding it, and a silent
@@ -98,6 +102,17 @@ export default class ClaudeCodeStatusPreferences extends ExtensionPreferences {
}
}
+ _spinRow(settings, key, title, subtitle, lower, upper) {
+ const row = new Adw.SpinRow({
+ title, subtitle,
+ adjustment: new Gtk.Adjustment({
+ lower, upper, step_increment: 1, page_increment: 1,
+ }),
+ });
+ settings.bind(key, row, 'value', Gio.SettingsBindFlags.DEFAULT);
+ return row;
+ }
+
_switchRow(settings, key, title, subtitle) {
const row = new Adw.SwitchRow({ title, subtitle });
settings.bind(key, row, 'active', Gio.SettingsBindFlags.DEFAULT);
diff --git a/schemas/org.gnome.shell.extensions.claude-code-status.gschema.xml b/schemas/org.gnome.shell.extensions.claude-code-status.gschema.xml
index aa84ac3..8d9ae02 100644
--- a/schemas/org.gnome.shell.extensions.claude-code-status.gschema.xml
+++ b/schemas/org.gnome.shell.extensions.claude-code-status.gschema.xml
@@ -12,6 +12,12 @@
Shorten project names to three characters
Chips show initials ("dev-skills" becomes "ds") so a row of sessions stays narrow. Sessions that would collide, including two in the same project, get a digit by seniority: the older one keeps its label. Turn off to show full project names.
+
+ 3
+
+ How many sessions get a chip
+ Chips are ordered by urgency, so the ones shown are the ones that need you soonest; the rest are counted as "+N". Keeps the row from pushing the clock off centre when many sessions are open.
+
true
Show how long the session has been in this state
@@ -20,7 +26,7 @@
true
Resolve zellij tab names
- Look up which zellij tab each session runs in, show it in the menu, and let a click switch to that tab. Requires the zellij command; harmless when it is absent.
+ Look up which zellij tab each session runs in and name it in the menu, which answers "which terminal" better than a path does. Requires the zellij command; harmless when it is absent.
diff --git a/stylesheet.css b/stylesheet.css
index fa1dd75..afb6a41 100644
--- a/stylesheet.css
+++ b/stylesheet.css
@@ -34,6 +34,12 @@
font-feature-settings: "tnum";
}
+.ccs-overflow {
+ font-size: 0.9em;
+ opacity: 0.7;
+ font-feature-settings: "tnum";
+}
+
.ccs-summary {
font-weight: bold;
}
diff --git a/tests/test-prefs.js b/tests/test-prefs.js
index fdd8023..3be96ec 100644
--- a/tests/test-prefs.js
+++ b/tests/test-prefs.js
@@ -48,7 +48,7 @@ const rows = [];
const walk = widget => {
for (let c = widget.get_first_child?.(); c; c = c.get_next_sibling()) {
const type = c.constructor.$gtype.name;
- if (type === 'AdwSwitchRow' || type === 'AdwActionRow')
+ if (type === 'AdwSwitchRow' || type === 'AdwSpinRow' || type === 'AdwActionRow')
rows.push({ type, title: c.title, subtitle: c.subtitle });
walk(c);
}
@@ -65,11 +65,13 @@ function check(name, condition, detail = '') {
for (const row of rows)
print(` ${row.type.replace('Adw', '').padEnd(10)} ${row.title}`);
-// One switch per settings key, so a key added without a row is caught.
+// One control per settings key, so a key added without a row to change it is
+// caught here rather than by a user wondering why nothing happens.
const keys = schemas.lookup('org.gnome.shell.extensions.claude-code-status', true)
.list_keys().length;
-const switches = rows.filter(r => r.type === 'AdwSwitchRow').length;
-check('a switch for every settings key', switches === keys, `${switches} of ${keys}`);
+const controls = rows.filter(
+ r => r.type === 'AdwSwitchRow' || r.type === 'AdwSpinRow').length;
+check('a control for every settings key', controls === keys, `${controls} of ${keys}`);
// The hook status line is the reason this page is worth opening at all: a
// silent panel looks the same whether nothing runs or nothing is installed.