Compare commits
2
Commits
be69bec17f
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d48a18ae75
|
||
|
|
f8e85d5703
|
@@ -0,0 +1,47 @@
|
|||||||
|
# Работа над этим расширением
|
||||||
|
|
||||||
|
## Никогда не писать в dconf живой сессии
|
||||||
|
|
||||||
|
**Запрещено** выполнять `gsettings set`, `gsettings reset`, `dconf write` и `dconf reset`
|
||||||
|
из bash этой сессии — ни голыми, ни с префиксом `XDG_CONFIG_HOME=...`.
|
||||||
|
|
||||||
|
Почему префикс не помогает: базу выбирает не клиент, а `dconf-service`, поднятый шиной
|
||||||
|
живой сессии, и берёт он её из **своего** окружения. Любая запись уходит в
|
||||||
|
`~/.config/dconf/user` Антона.
|
||||||
|
|
||||||
|
Чем это кончалось (дважды): затёрт `org.gnome.shell enabled-extensions` — во второй раз,
|
||||||
|
9 и 10 августа 2026, на работающем shell, отчего у него мгновенно отвалились dock, ding,
|
||||||
|
appindicators и tiling-assistant. Прежний список dconf не хранит, восстановить его точно
|
||||||
|
нельзя, только реконструировать.
|
||||||
|
|
||||||
|
Что делать вместо:
|
||||||
|
|
||||||
|
- нужно поменять настройку в **живой** сессии — отдать Антону команду, он выполнит её сам
|
||||||
|
через `! <команда>`;
|
||||||
|
- нужна настройка для **вложенного** shell — экспортировать `XDG_CONFIG_HOME` до форка
|
||||||
|
шины и звать `gsettings` уже внутри этой шины (см. рецепт ниже);
|
||||||
|
- читать (`dconf read`, `gsettings get`) можно свободно.
|
||||||
|
|
||||||
|
## Проверка изменений — только во вложенном shell
|
||||||
|
|
||||||
|
Живую сессию не трогаем вообще: расширение включает и перезагружает shell Антон сам.
|
||||||
|
У него несколько сессий Claude Code в zellij внутри gnome-terminal, и падение
|
||||||
|
gnome-shell унесёт весь его рабочий набор.
|
||||||
|
|
||||||
|
Рецепт для проверок только на чтение (GNOME 46, проверен 10.08.2026):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
XDG_STATE_HOME=$TMP/state timeout 50 dbus-run-session -- \
|
||||||
|
gnome-shell --nested --wayland > $TMP/nested.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
Подставные файлы сессий обязаны нести `event_ts` (плюс `since`, `started`): без него
|
||||||
|
`SessionStore` считает их древними и **удаляет** до первой отрисовки.
|
||||||
|
|
||||||
|
Скриншот из вложенного shell снять нельзя. Чтобы увидеть, что он нарисовал, добавляем
|
||||||
|
временный `log()` в код, читаем его из `nested.log` — и убираем инструментацию перед
|
||||||
|
коммитом.
|
||||||
|
|
||||||
|
## Коммиты
|
||||||
|
|
||||||
|
Без трейлера `Co-Authored-By`.
|
||||||
@@ -47,9 +47,12 @@ Code ждёт меня прямо сейчас?**
|
|||||||
достаточно открытых сессий сдвинули бы часы с центра.
|
достаточно открытых сессий сдвинули бы часы с центра.
|
||||||
|
|
||||||
Чипы идут по срочности, а внутри одного состояния первой стоит **самая
|
Чипы идут по срочности, а внутри одного состояния первой стоит **самая
|
||||||
давняя**: забывается та, что ждёт дольше всех, а не последняя. Время в
|
давняя**: забывается та, что ждёт дольше всех, а не последняя. Время
|
||||||
состоянии показывается только у первого чипа: пять счётчиков рядом — это
|
показывается у **работающих** чипов — у всех сразу: именно у них счётчик о
|
||||||
ряд чисел, а не ответ на вопрос.
|
чём-то говорит (пять минут — это сборка, сорок — сессия во что-то упёрлась).
|
||||||
|
У ждущих его нет: их возраст говорит лишь о том, когда вы перестали смотреть,
|
||||||
|
а это и так видно по самому чипу. Счётчик отключается в настройках
|
||||||
|
(«Show working time»); в меню возраст показывается у всех строк в любом случае.
|
||||||
|
|
||||||
## Место в панели
|
## Место в панели
|
||||||
|
|
||||||
@@ -159,7 +162,8 @@ gnome-extensions enable claude-code-status@git.vakhrushev.me
|
|||||||
| `PostToolUse` | `busy` |
|
| `PostToolUse` | `busy` |
|
||||||
| `PreCompact` | `busy` |
|
| `PreCompact` | `busy` |
|
||||||
| `Notification` | `blocked` или `waiting`, в зависимости от `notification_type` |
|
| `Notification` | `blocked` или `waiting`, в зависимости от `notification_type` |
|
||||||
| `Stop` | `waiting` |
|
| `Stop` | `waiting` — если не работают сабагенты (см. ниже) |
|
||||||
|
| `SubagentStop` | обновляет число работающих сабагентов; последний освобождает сессию |
|
||||||
| `SessionEnd` | файл удаляется |
|
| `SessionEnd` | файл удаляется |
|
||||||
|
|
||||||
`PostToolUse` не избыточен: это единственное событие, срабатывающее после выдачи
|
`PostToolUse` не избыточен: это единственное событие, срабатывающее после выдачи
|
||||||
@@ -205,31 +209,45 @@ Agent SDK и интеграции с редакторами.
|
|||||||
|
|
||||||
Но `Stop` при этом приходит раньше, чем сабагенты закончат. Если верить ему
|
Но `Stop` при этом приходит раньше, чем сабагенты закончат. Если верить ему
|
||||||
буквально, сессия покажется свободной ровно тогда, когда в неё лезть бессмысленно.
|
буквально, сессия покажется свободной ровно тогда, когда в неё лезть бессмысленно.
|
||||||
Поэтому сабагенты считаются явно:
|
|
||||||
|
|
||||||
| Событие | Что делает |
|
Сколько сабагентов ещё работает, хук не считает, а **берёт из самого события**:
|
||||||
|---|---|
|
`Stop` и `SubagentStop` несут поле `background_tasks` — список фоновых задач с их
|
||||||
| `PreToolUse` с матчером `^(Agent\|Task)$` | +1 к счётчику |
|
`status`. Оттуда и берётся число: задачи с `type: "subagent"` и `status:
|
||||||
| `SubagentStop` | −1; последний освобождает сессию, если ход уже закончен |
|
"running"`. Остальные события этого поля не несут, и тогда стоит последнее
|
||||||
| `UserPromptSubmit` | сбрасывает счётчик в 0 |
|
известное значение.
|
||||||
|
|
||||||
Пока счётчик больше нуля, состояние `waiting` невозможно: и `Stop`, и напоминание
|
Пока число больше нуля, состояние `waiting` невозможно: и `Stop`, и напоминание
|
||||||
`idle_prompt` дают `busy`. Освобождает сессию только уход последнего сабагента —
|
`idle_prompt` дают `busy`. Освобождает сессию только `SubagentStop`, пришедший в
|
||||||
и лишь если основной агент к тому времени остановился.
|
момент, когда список пуст, — и лишь если основной агент к тому времени
|
||||||
|
остановился.
|
||||||
|
|
||||||
Матчер якорный не для красоты: это регулярка, и голое `Task` поймало бы
|
Фоновая команда (`type: "bash"`) сабагентом не считается намеренно: поднятый
|
||||||
`TaskCreate`, `TaskUpdate` и прочее. Хук проверяет имя инструмента ещё раз, сам.
|
dev-сервер живёт часами и ничего не говорит о том, нужны вы сессии или нет, — а
|
||||||
Сброс на `UserPromptSubmit` ограничивает ущерб, если сабагент умрёт, не прислав
|
приравняв его к работе, чип пришлось бы держать «работает» всё это время.
|
||||||
`SubagentStop`: счётчик не переживёт следующего вашего сообщения.
|
|
||||||
|
### Почему не счётчик
|
||||||
|
|
||||||
|
Сначала счётчик и был: `+1` на `PreToolUse` с матчером `^(Agent|Task)$`, `−1` на
|
||||||
|
`SubagentStop`. Он ошибался в худшую сторону — гасил `busy` у занятой сессии, —
|
||||||
|
и вот почему. Запуск сабагента виден хуку только на верхнем уровне: когда свой
|
||||||
|
сабагент разворачивает сабагент, событие приходит с `agent_id` и отбрасывается.
|
||||||
|
А `SubagentStop` приходит **за каждого сабагента на любой глубине**. Каждый
|
||||||
|
вложенный вычитал единицу из батча, в который никогда не входил.
|
||||||
|
|
||||||
|
Замерено на живой сессии: один фоновый агент, четырнадцать вложенных `SubagentStop`
|
||||||
|
подряд — счётчик обнулился на первом же, и сессия с работающим батчем показалась
|
||||||
|
ждущей. Снимок вычитать нечего: он просто говорит, что запущено сейчас, и по той
|
||||||
|
же причине не течёт, если сабагент умрёт, не прислав `SubagentStop`.
|
||||||
|
|
||||||
Собственные вызовы инструментов сабагентов игнорируются — они долетают до хуков
|
Собственные вызовы инструментов сабагентов игнорируются — они долетают до хуков
|
||||||
родителя как `PostToolUse` с `agent_id`, но счётчик уже всё сказал, а они добавили
|
родителя как `PostToolUse` с `agent_id`, но сессия занята и без них, а записей на
|
||||||
бы только записи на диск. `Notification` — исключение: она означает, что нужен
|
диск от одного сабагента набежали бы сотни. `Notification` — исключение: она
|
||||||
человек, и это одинаково верно, в каком бы агенте ни заклинило.
|
означает, что нужен человек, и это одинаково верно, в каком бы агенте ни заклинило.
|
||||||
|
|
||||||
В меню число сабагентов показывается строкой «работает · 3 subagents». В панели —
|
В меню число сабагентов показывается строкой «работает · 3 subagents». В панели —
|
||||||
нет: батч из восьми задач остаётся одной строкой «работает 40 мин», и это
|
нет: батч из восьми задач остаётся одной строкой «работает 40 мин», и это
|
||||||
правильная строка.
|
правильная строка. Считаются фоновые: батч, которого основной агент дожидается
|
||||||
|
сам, и так виден по состоянию `busy`.
|
||||||
|
|
||||||
## zellij
|
## zellij
|
||||||
|
|
||||||
|
|||||||
+50
-28
@@ -48,10 +48,11 @@ NOTIFICATION_STATES = {
|
|||||||
"agent_completed": "waiting",
|
"agent_completed": "waiting",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Tools that spawn a subagent. Matched again here, not just in the hook
|
# Background tasks that mean the session is still working. A background shell
|
||||||
# registration: the matcher is a regex and a mistake there would silently
|
# is not one: a dev server left running says nothing about whether the session
|
||||||
# inflate the count with TaskCreate, TaskUpdate and the like.
|
# needs you, and counting it would pin the chip at "working" for as long as it
|
||||||
AGENT_TOOLS = {"Agent", "Task"}
|
# lives.
|
||||||
|
BUSY_TASK_TYPES = {"subagent"}
|
||||||
|
|
||||||
# Flags that mean "run one prompt and exit" rather than "open a session". See
|
# Flags that mean "run one prompt and exit" rather than "open a session". See
|
||||||
# is_headless: such a run has no terminal to be called over to.
|
# is_headless: such a run has no terminal to be called over to.
|
||||||
@@ -140,22 +141,51 @@ def derive_state(event):
|
|||||||
# and that is just as true when the agent that got stuck is a subagent.
|
# and that is just as true when the agent that got stuck is a subagent.
|
||||||
if name == "Notification":
|
if name == "Notification":
|
||||||
return NOTIFICATION_STATES.get(event.get("notification_type"))
|
return NOTIFICATION_STATES.get(event.get("notification_type"))
|
||||||
# SubagentStop is the counter's decrement and carries agent_id itself, so
|
# SubagentStop carries agent_id itself, so it has to pass the filter below.
|
||||||
# it has to pass the filter below. Its state is decided in apply_event,
|
# It is the one event that can free a session whose main agent stopped a
|
||||||
# which is where the count is known.
|
# long time ago; which way it goes needs the snapshot and is decided in
|
||||||
|
# apply_event.
|
||||||
if name == "SubagentStop":
|
if name == "SubagentStop":
|
||||||
return "busy"
|
return "busy"
|
||||||
# A subagent's own tool calls also reach the parent session's hooks
|
# A subagent's own tool calls also reach the parent session's hooks
|
||||||
# (measured: PostToolUse carrying agent_id and agent_type). They are
|
# (measured: PostToolUse carrying agent_id and agent_type). They are
|
||||||
# ignored, because the count already says a subagent is running and these
|
# ignored: the session is working either way, and a single subagent's shell
|
||||||
# would only add write traffic.
|
# commands alone would be hundreds of writes.
|
||||||
if event.get("agent_id"):
|
if event.get("agent_id"):
|
||||||
return None
|
return None
|
||||||
if name == "PreToolUse" and event.get("tool_name") not in AGENT_TOOLS:
|
|
||||||
return None
|
|
||||||
return EVENT_STATES.get(name)
|
return EVENT_STATES.get(name)
|
||||||
|
|
||||||
|
|
||||||
|
def running_agents(event):
|
||||||
|
"""How many subagents the session still has running, or None if this event
|
||||||
|
does not say.
|
||||||
|
|
||||||
|
Read from the event's own ``background_tasks`` rather than counted from
|
||||||
|
starts and stops. Counting was what this did first, and it was wrong in the
|
||||||
|
direction that matters: ``PreToolUse`` only ever sees a top-level launch --
|
||||||
|
a subagent spawning its own subagents does it through hooks carrying
|
||||||
|
``agent_id``, which are dropped above -- while ``SubagentStop`` arrives for
|
||||||
|
every subagent at every depth. Each nested one therefore subtracted from a
|
||||||
|
batch it had never joined, and the session was declared free with its work
|
||||||
|
still running. Measured live: one background agent, fourteen nested stops
|
||||||
|
behind it, and a session shown as waiting from the first of them on.
|
||||||
|
|
||||||
|
The field is present exactly where it decides something: on ``Stop`` and
|
||||||
|
``SubagentStop``, the two events that can end a turn. Everywhere else it is
|
||||||
|
absent, and the stored value stands.
|
||||||
|
"""
|
||||||
|
tasks = event.get("background_tasks")
|
||||||
|
if not isinstance(tasks, list):
|
||||||
|
return None
|
||||||
|
running = 0
|
||||||
|
for task in tasks:
|
||||||
|
if not isinstance(task, dict):
|
||||||
|
continue
|
||||||
|
if task.get("type") in BUSY_TASK_TYPES and task.get("status") == "running":
|
||||||
|
running += 1
|
||||||
|
return min(running, 999)
|
||||||
|
|
||||||
|
|
||||||
def read_environ(pid):
|
def read_environ(pid):
|
||||||
"""Environment of a process as a dict, empty if it is gone or not ours."""
|
"""Environment of a process as a dict, empty if it is gone or not ours."""
|
||||||
try:
|
try:
|
||||||
@@ -402,25 +432,17 @@ def apply_event(event, state, path, now, claude_pid):
|
|||||||
previous_ts = 0 # corrupt, and trusting it would freeze this session
|
previous_ts = 0 # corrupt, and trusting it would freeze this session
|
||||||
stale = previous_ts > now
|
stale = previous_ts > now
|
||||||
|
|
||||||
# The subagent count is different in kind: the mutations are deltas, and
|
# Applied whether or not this event lost the race above: a snapshot says
|
||||||
# deltas commute, so every one has to land whatever order it arrives in.
|
# what was running when the event fired, and even a slightly stale one is a
|
||||||
# Dropping a "+1" because its timestamp lost a race leaves the count one
|
# better answer than a number left over from an older event still. Nothing
|
||||||
# short, and the batch then frees the session while a subagent is still
|
# accumulates, so nothing leaks when a subagent dies without ever sending
|
||||||
# running -- the exact failure counting was added to prevent. The timestamps
|
# its SubagentStop -- the next event carrying a list corrects the count.
|
||||||
# cannot be trusted for this at all: each is taken when its hook process
|
snapshot = running_agents(event)
|
||||||
# starts, tens of milliseconds before it reaches the lock.
|
if snapshot is not None:
|
||||||
if name in ("SessionStart", "UserPromptSubmit"):
|
agents = snapshot
|
||||||
# A new turn from you starts a new batch. This also bounds the damage
|
|
||||||
# when a subagent dies without its SubagentStop ever arriving: the count
|
|
||||||
# cannot leak past the next thing you type.
|
|
||||||
agents = 0
|
|
||||||
elif name == "PreToolUse":
|
|
||||||
agents += 1
|
|
||||||
elif name == "SubagentStop":
|
|
||||||
agents = max(0, agents - 1)
|
|
||||||
|
|
||||||
if stale:
|
if stale:
|
||||||
# Keep the stored state and flag; the delta above still gets persisted.
|
# Keep the stored state and flag; the snapshot above still stands.
|
||||||
state = previous.get("state", state)
|
state = previous.get("state", state)
|
||||||
else:
|
else:
|
||||||
if name in ("SessionStart", "UserPromptSubmit"):
|
if name in ("SessionStart", "UserPromptSubmit"):
|
||||||
|
|||||||
+11
-9
@@ -23,17 +23,11 @@ HOOK = os.path.join(os.path.dirname(os.path.abspath(__file__)), "claude-status-h
|
|||||||
# before it writes -- observed with `claude -p`, which left a session stuck at
|
# before it writes -- observed with `claude -p`, which left a session stuck at
|
||||||
# "busy" forever. They fire at most once per turn, so ~20 ms is free; the
|
# "busy" forever. They fire at most once per turn, so ~20 ms is free; the
|
||||||
# per-tool-call events stay async so they never sit in the agent's loop.
|
# per-tool-call events stay async so they never sit in the agent's loop.
|
||||||
# (async, matcher). PreToolUse is matched to the agent-spawning tools alone:
|
# (async, matcher).
|
||||||
# unmatched it would fire on every tool call in every session, and the only
|
|
||||||
# thing it is here for is to count subagents as they start. The pattern is
|
|
||||||
# anchored because it is a regex -- a bare "Task" also matches TaskCreate,
|
|
||||||
# TaskUpdate and friends, which are not subagents. The hook re-checks the name
|
|
||||||
# anyway, in case a future matcher works differently.
|
|
||||||
EVENTS = {
|
EVENTS = {
|
||||||
"SessionStart": (True, ""),
|
"SessionStart": (True, ""),
|
||||||
"UserPromptSubmit": (True, ""),
|
"UserPromptSubmit": (True, ""),
|
||||||
"Notification": (True, ""),
|
"Notification": (True, ""),
|
||||||
"PreToolUse": (True, "^(Agent|Task)$"),
|
|
||||||
"PostToolUse": (True, ""),
|
"PostToolUse": (True, ""),
|
||||||
"PreCompact": (True, ""),
|
"PreCompact": (True, ""),
|
||||||
"SubagentStop": (True, ""),
|
"SubagentStop": (True, ""),
|
||||||
@@ -41,6 +35,14 @@ EVENTS = {
|
|||||||
"SessionEnd": (False, ""),
|
"SessionEnd": (False, ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Registered once, no longer. PreToolUse existed to count subagents as they
|
||||||
|
# started; the count now comes from the snapshot the events carry themselves
|
||||||
|
# (see running_agents in the hook). Listed rather than forgotten because both
|
||||||
|
# install and uninstall sweep these out: an entry left behind would go on
|
||||||
|
# spawning the hook on every agent launch for nothing, and an uninstall that
|
||||||
|
# leaves our registrations in settings.json is not an uninstall.
|
||||||
|
LEGACY_EVENTS = ("PreToolUse",)
|
||||||
|
|
||||||
|
|
||||||
def entry(spec):
|
def entry(spec):
|
||||||
async_, matcher = spec
|
async_, matcher = spec
|
||||||
@@ -90,9 +92,9 @@ def main():
|
|||||||
shutil.copymode(path, path + ".bak")
|
shutil.copymode(path, path + ".bak")
|
||||||
|
|
||||||
hooks = settings.setdefault("hooks", {})
|
hooks = settings.setdefault("hooks", {})
|
||||||
for event in EVENTS:
|
for event in list(EVENTS) + list(LEGACY_EVENTS):
|
||||||
groups = [g for g in hooks.get(event, []) if not is_ours(g)]
|
groups = [g for g in hooks.get(event, []) if not is_ours(g)]
|
||||||
if not uninstall:
|
if not uninstall and event in EVENTS:
|
||||||
groups.append(entry(EVENTS[event]))
|
groups.append(entry(EVENTS[event]))
|
||||||
if groups:
|
if groups:
|
||||||
hooks[event] = groups
|
hooks[event] = groups
|
||||||
|
|||||||
+22
-15
@@ -61,6 +61,7 @@ class ClaudeStatusIndicator extends PanelMenu.Button {
|
|||||||
this._store = new SessionStore();
|
this._store = new SessionStore();
|
||||||
this._zellij = new ZellijTabs();
|
this._zellij = new ZellijTabs();
|
||||||
this._chipLabels = new Map();
|
this._chipLabels = new Map();
|
||||||
|
this._ageLabels = new Map();
|
||||||
this._rows = [];
|
this._rows = [];
|
||||||
|
|
||||||
this._buildPanel();
|
this._buildPanel();
|
||||||
@@ -222,21 +223,24 @@ class ClaudeStatusIndicator extends PanelMenu.Button {
|
|||||||
const shown = sessions.slice(0, maxChips);
|
const shown = sessions.slice(0, maxChips);
|
||||||
const hidden = sessions.length - shown.length;
|
const hidden = sessions.length - shown.length;
|
||||||
|
|
||||||
// Age rides on the first chip only. Sessions are sorted by urgency, so
|
// Age rides on the working chips, and on all of them. A session that
|
||||||
// that is the one whose age decides anything; five ages side by side
|
// is working is the one you might be waiting on, and how long it has
|
||||||
// would just be a wide row of numbers.
|
// been at it is the whole question -- five minutes is a build, forty
|
||||||
const ageOnFirst = showAge && shown.length > 0;
|
// is a session stuck on something. The states where nothing is
|
||||||
|
// happening have no such clock: their age says how long ago you
|
||||||
|
// stopped looking, which the row's own presence already says.
|
||||||
|
const ageOn = session => showAge && session.state === 'busy';
|
||||||
const signature = shown
|
const signature = shown
|
||||||
.map(s => `${s.sessionId}:${s.state}:${labelFor(s)}`)
|
.map(s => `${s.sessionId}:${s.state}:${labelFor(s)}`)
|
||||||
.join('|') + `|${ageOnFirst}|${hidden}`;
|
.join('|') + `|${showAge}|${hidden}`;
|
||||||
if (signature !== this._chipSignature) {
|
if (signature !== this._chipSignature) {
|
||||||
this._chipBox.destroy_all_children();
|
this._chipBox.destroy_all_children();
|
||||||
this._ageLabel = null;
|
this._ageLabels = new Map();
|
||||||
shown.forEach((session, i) => {
|
shown.forEach(session => {
|
||||||
const { chip, age } = this._buildChip(
|
const { chip, age } = this._buildChip(
|
||||||
session, labelFor(session), ageOnFirst && i === 0);
|
session, labelFor(session), ageOn(session));
|
||||||
if (age)
|
if (age)
|
||||||
this._ageLabel = { age, sessionId: session.sessionId };
|
this._ageLabels.set(session.sessionId, age);
|
||||||
this._chipBox.add_child(chip);
|
this._chipBox.add_child(chip);
|
||||||
});
|
});
|
||||||
if (hidden > 0) {
|
if (hidden > 0) {
|
||||||
@@ -249,13 +253,16 @@ class ClaudeStatusIndicator extends PanelMenu.Button {
|
|||||||
this._chipSignature = signature;
|
this._chipSignature = signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._ageLabel) {
|
// Looked up again rather than captured: a session that returns to the
|
||||||
// Looked up again rather than captured: a session that returns to
|
// same state within one refresh keeps the signature unchanged, and a
|
||||||
// the same state within one refresh keeps the signature unchanged,
|
// captured object would then show an age that stopped moving.
|
||||||
// and a captured object would then show an age that stopped moving.
|
if (this._ageLabels.size) {
|
||||||
const current = sessions.find(s => s.sessionId === this._ageLabel.sessionId);
|
const byId = new Map(sessions.map(s => [s.sessionId, s]));
|
||||||
|
for (const [sessionId, label] of this._ageLabels) {
|
||||||
|
const current = byId.get(sessionId);
|
||||||
if (current)
|
if (current)
|
||||||
this._ageLabel.age.text = formatAge(this._ageOf(current));
|
label.text = formatAge(this._ageOf(current));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ export default class ClaudeCodeStatusPreferences extends ExtensionPreferences {
|
|||||||
_('The most urgent sessions get a chip; the rest are counted as “+N”.'),
|
_('The most urgent sessions get a chip; the rest are counted as “+N”.'),
|
||||||
1, 12));
|
1, 12));
|
||||||
dispGroup.add(this._switchRow(settings, 'show-age',
|
dispGroup.add(this._switchRow(settings, 'show-age',
|
||||||
_('Show time in state'),
|
_('Show working time'),
|
||||||
_('Shown on the most urgent session only.')));
|
_('A counter on every session that is working, so you can see how long it has been at it. The menu lists ages either way.')));
|
||||||
|
|
||||||
const zellijGroup = new Adw.PreferencesGroup({
|
const zellijGroup = new Adw.PreferencesGroup({
|
||||||
title: _('zellij'),
|
title: _('zellij'),
|
||||||
|
|||||||
@@ -42,8 +42,8 @@
|
|||||||
</key>
|
</key>
|
||||||
<key name="show-age" type="b">
|
<key name="show-age" type="b">
|
||||||
<default>true</default>
|
<default>true</default>
|
||||||
<summary>Show how long the session has been in this state</summary>
|
<summary>Show how long each working session has been at it</summary>
|
||||||
<description>How long a session has been working, or waiting, is what decides whether to switch to it now or leave it running.</description>
|
<description>A counter on every chip that is working, which is what says whether to wait for it or go and look: five minutes is a build, forty is a session stuck on something. The idle states get no counter — their age only says how long ago you stopped looking.</description>
|
||||||
</key>
|
</key>
|
||||||
<key name="zellij-integration" type="b">
|
<key name="zellij-integration" type="b">
|
||||||
<default>true</default>
|
<default>true</default>
|
||||||
|
|||||||
+36
-18
@@ -149,39 +149,57 @@ check "compaction does not reset the clock" "$since_before" "$(field since)"
|
|||||||
# The scenario this exists for: you ask for a batch, the main agent launches it
|
# The scenario this exists for: you ask for a batch, the main agent launches it
|
||||||
# and ends its turn, and the session waits for the results to consolidate them.
|
# and ends its turn, and the session waits for the results to consolidate them.
|
||||||
# It is working, not waiting for you, and must not call you over.
|
# It is working, not waiting for you, and must not call you over.
|
||||||
|
#
|
||||||
|
# What is running is not counted from starts and stops but taken from the list
|
||||||
|
# the events carry, exactly as measured on a live session: Stop and
|
||||||
|
# SubagentStop bring "background_tasks", the rest of the events bring nothing.
|
||||||
|
bg() { # running-subagent-count -> the snapshot as those events carry it
|
||||||
|
tasks=""
|
||||||
|
for i in $(seq 0 $(($1 - 1))); do
|
||||||
|
tasks="$tasks${tasks:+,}{\"id\":\"sub-$i\",\"type\":\"subagent\",\"status\":\"running\"}"
|
||||||
|
done
|
||||||
|
echo "\"background_tasks\":[$tasks]"
|
||||||
|
}
|
||||||
|
|
||||||
emit "$(ev UserPromptSubmit '"prompt":"launch a batch"')"
|
emit "$(ev UserPromptSubmit '"prompt":"launch a batch"')"
|
||||||
check "a new turn resets the count" "0" "$(field agents)"
|
emit "$(ev Stop "$(bg 2)")"
|
||||||
|
|
||||||
emit "$(ev PreToolUse '"tool_name":"Agent"')"
|
|
||||||
emit "$(ev PreToolUse '"tool_name":"Agent"')"
|
|
||||||
check "two launches counted" "2" "$(field agents)"
|
|
||||||
|
|
||||||
emit "$(ev PreToolUse '"tool_name":"TaskCreate"')"
|
|
||||||
check "a tool that merely starts with Task is not a subagent" "2" "$(field agents)"
|
|
||||||
|
|
||||||
emit "$(ev Stop)"
|
|
||||||
check "main agent stopping does not free a running batch" "busy" "$(field state)"
|
check "main agent stopping does not free a running batch" "busy" "$(field state)"
|
||||||
|
check "the snapshot is what gets stored" "2" "$(field agents)"
|
||||||
|
|
||||||
emit "$(ev SubagentStop '"agent_id":"sub-1","agent_type":"general-purpose"')"
|
emit "$(ev SubagentStop "\"agent_id\":\"sub-0\",\"agent_type\":\"general-purpose\",$(bg 1)")"
|
||||||
check "one down, still working" "busy" "$(field state)"
|
check "one down, still working" "busy" "$(field state)"
|
||||||
check "count decremented" "1" "$(field agents)"
|
check "the count follows the snapshot" "1" "$(field agents)"
|
||||||
|
|
||||||
emit "$(ev SubagentStop '"agent_id":"sub-2","agent_type":"general-purpose"')"
|
# The regression this replaced counting for: a subagent's own subagent stops,
|
||||||
|
# and its SubagentStop reaches this session just like a top-level one -- while
|
||||||
|
# its *launch* never did, because that event carried agent_id and was dropped.
|
||||||
|
# Subtracting it freed a session whose batch was still running.
|
||||||
|
emit "$(ev SubagentStop "\"agent_id\":\"nested-1\",\"agent_type\":\"\",$(bg 1)")"
|
||||||
|
check "a nested subagent stopping does not free the batch" "busy" "$(field state)"
|
||||||
|
check "and does not touch the count" "1" "$(field agents)"
|
||||||
|
|
||||||
|
emit "$(ev SubagentStop "\"agent_id\":\"sub-1\",$(bg 0)")"
|
||||||
check "last subagent finishing frees the session" "waiting" "$(field state)"
|
check "last subagent finishing frees the session" "waiting" "$(field state)"
|
||||||
check "count back to zero" "0" "$(field agents)"
|
check "count back to zero" "0" "$(field agents)"
|
||||||
|
|
||||||
|
# A background shell is a background task too, and must not be mistaken for
|
||||||
|
# work: a dev server left running would pin the chip at "working" for good.
|
||||||
|
emit "$(ev UserPromptSubmit '"prompt":"serve"')"
|
||||||
|
emit "$(ev Stop '"background_tasks":[{"id":"b1","type":"bash","status":"running"}]')"
|
||||||
|
check "a background shell is not a subagent" "waiting" "$(field state)"
|
||||||
|
|
||||||
# A batch that finishes while the main agent is still mid-turn must not free it.
|
# A batch that finishes while the main agent is still mid-turn must not free it.
|
||||||
emit "$(ev UserPromptSubmit '"prompt":"again"')"
|
emit "$(ev UserPromptSubmit '"prompt":"again"')"
|
||||||
emit "$(ev PreToolUse '"tool_name":"Agent"')"
|
emit "$(ev SubagentStop "\"agent_id\":\"sub-3\",$(bg 0)")"
|
||||||
emit "$(ev SubagentStop '"agent_id":"sub-3"')"
|
|
||||||
check "batch done mid-turn leaves the session working" "busy" "$(field state)"
|
check "batch done mid-turn leaves the session working" "busy" "$(field state)"
|
||||||
|
|
||||||
# An idle nudge while a batch runs must not claim the session is free either.
|
# An idle nudge while a batch runs must not claim the session is free either.
|
||||||
emit "$(ev PreToolUse '"tool_name":"Agent"')"
|
# It carries no snapshot of its own, so this also checks the stored one stands.
|
||||||
emit "$(ev Stop)"
|
emit "$(ev Stop "$(bg 1)")"
|
||||||
emit "$(ev Notification '"notification_type":"idle_prompt","message":"still there?"')"
|
emit "$(ev Notification '"notification_type":"idle_prompt","message":"still there?"')"
|
||||||
check "idle nudge cannot free a running batch" "busy" "$(field state)"
|
check "idle nudge cannot free a running batch" "busy" "$(field state)"
|
||||||
emit "$(ev SubagentStop '"agent_id":"sub-4"')"
|
check "an event without a snapshot leaves the count alone" "1" "$(field agents)"
|
||||||
|
emit "$(ev SubagentStop "\"agent_id\":\"sub-4\",$(bg 0)")"
|
||||||
check "and it frees properly once the batch ends" "waiting" "$(field state)"
|
check "and it frees properly once the batch ends" "waiting" "$(field state)"
|
||||||
|
|
||||||
# --- concurrency -----------------------------------------------------------
|
# --- concurrency -----------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user