← back to Desktop Dotbar
dotbar: hide dead sessions from ALL panels (colour groups + parked), fix claude.exe liveness, never hide parked tickets (TK-12236, TK-12174)
d2b3a95e693303c063311d211479c69c12f863f3 · 2026-09-26 10:01:01 -0700 · Steve Abrams
- isClaudeComm accepted only /claude(\s|$)/, so the native installer's .../bin/claude.exe
made a LIVE parked tab read as 'recycled' and vanish; now claude/claude.exe/node/codex
- filterSessions: same ps probe drops dead-tty rows from the colour groups; ps failure = fail-open
- parked TICKETS are never hidden (always-show rule); closed -> liveness 'closed', off-board -> unknown
- negative tests: fake dead tty hidden, live kept, unknown kept; mutation-verified red
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GEE4jWy9RBmz5QnR2xcvno
Files touched
A evidence/dotbar-parked-tk12236.pngM live-filter.jsM server.jsM test/live-filter.test.js
Diff
commit d2b3a95e693303c063311d211479c69c12f863f3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Sep 26 10:01:01 2026 -0700
dotbar: hide dead sessions from ALL panels (colour groups + parked), fix claude.exe liveness, never hide parked tickets (TK-12236, TK-12174)
- isClaudeComm accepted only /claude(\s|$)/, so the native installer's .../bin/claude.exe
made a LIVE parked tab read as 'recycled' and vanish; now claude/claude.exe/node/codex
- filterSessions: same ps probe drops dead-tty rows from the colour groups; ps failure = fail-open
- parked TICKETS are never hidden (always-show rule); closed -> liveness 'closed', off-board -> unknown
- negative tests: fake dead tty hidden, live kept, unknown kept; mutation-verified red
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GEE4jWy9RBmz5QnR2xcvno
---
evidence/dotbar-parked-tk12236.png | Bin 0 -> 39643 bytes
live-filter.js | 29 ++++++++++++++++++----
server.js | 12 +++++++---
test/live-filter.test.js | 48 +++++++++++++++++++++++++++++++++----
4 files changed, 77 insertions(+), 12 deletions(-)
diff --git a/evidence/dotbar-parked-tk12236.png b/evidence/dotbar-parked-tk12236.png
new file mode 100644
index 0000000..b5d7181
Binary files /dev/null and b/evidence/dotbar-parked-tk12236.png differ
diff --git a/live-filter.js b/live-filter.js
index 10d2f81..124787a 100644
--- a/live-filter.js
+++ b/live-filter.js
@@ -14,7 +14,10 @@ const tkKey = (id) => { const m = /^(TK-\d+)/i.exec(String(id || '')); return m
// ON A REAL TTY is a CLAIMED daemon spare, i.e. the interactive session itself (cc-daemon
// keeps the spare's argv after the claim; verified 2026-09-25: pid 90349 on ttys004, S+
// foreground, 8m CPU). UNCLAIMED spares sit on tty "??" and never count (see parsePs).
-function isClaudeComm(comm) { return /(^|\/)claude(\s|$)/.test(String(comm || '').trim()); }
+// TK-12236 fix 2026-09-26: the native installer runs as ".../claude-code/bin/claude.exe" — the old
+// /claude(\s|$)/ test rejected it, so a LIVE parked tab (pid 1634 on ttys002) was hidden as
+// "recycled". A session REPL is claude / claude.exe / node (npm-installed claude) / codex.
+function isClaudeComm(comm) { return /(^|\/)(claude(\.exe)?|node|codex)(\s|$)/.test(String(comm || '').trim()); }
// Parse `ps -axo pid=,tty=,comm=`. Returns null when the probe produced nothing usable
// (ps failed / empty) so callers treat it as NOT-MEASURED rather than "nothing alive".
@@ -60,9 +63,9 @@ function ticketLiveness(e, tickets) {
const k = tkKey(e.id);
if (!tickets) return { state: 'unknown', reason: 'ticket board unreachable' };
if (!k) return { state: 'unknown', reason: 'no TK id' };
- if (!tickets.has(k)) return { state: 'dead', reason: `${k} not on board (archived done)` };
+ if (!tickets.has(k)) return { state: 'unknown', reason: `${k} not in board summary` };
const status = tickets.get(k);
- if (CLOSED.has(status)) return { state: 'dead', reason: `${k} is ${status}`, status };
+ if (CLOSED.has(status)) return { state: 'closed', reason: `${k} is ${status}`, status };
return { state: 'live', reason: `${k} is ${status}`, status };
}
@@ -76,7 +79,9 @@ function buildParked(entries, { procs, tickets, isAttached = () => true, cleanLa
const lv = isTicket ? ticketLiveness(e, tickets)
: e.kind === 'tab' ? tabLiveness(e, procs)
: { state: 'unknown', reason: `unknown kind ${e.kind}` };
- if (lv.state === 'dead') { hidden.push({ kind: e.kind, id: e.id, reason: lv.reason }); continue; }
+ // Only SESSION rows (tabs) are ever hidden. Parked TICKETS always show (Steve's always-show-every-
+ // state rule; TK-12174) — a closed one is rendered with liveness 'closed', never dropped.
+ if (!isTicket && lv.state === 'dead') { hidden.push({ kind: e.kind, id: e.id, reason: lv.reason }); continue; }
const base = { kind: e.kind, id: e.id, doing: cleanLabel(e.label), parked_at: e.parked_at,
liveness: lv.state, liveness_reason: lv.reason };
if (isTicket) {
@@ -90,4 +95,18 @@ function buildParked(entries, { procs, tickets, isAttached = () => true, cleanLa
return { count: items.length, items, hidden };
}
-module.exports = { isClaudeComm, parsePs, ticketStatusMap, tabLiveness, ticketLiveness, buildParked, tkKey };
+// TK-12236: the ACTIVE colour-group rows (allcolordots --json) get the same session probe, so a
+// terminal whose claude died drops out of every panel, not just PARKED. rows: [{tty,pid,...}].
+// procs null (ps failed) -> everything kept (fail-open). Returns { kept, hidden }.
+function filterSessions(rows, procs) {
+ const kept = [], hidden = [];
+ for (const r of Array.isArray(rows) ? rows : []) {
+ if (!r) continue;
+ const lv = tabLiveness({ tty: r.tty, pid: r.pid }, procs);
+ if (lv.state === 'dead') hidden.push({ tty: ttyBase(r.tty), pid: r.pid || null, reason: lv.reason });
+ else kept.push({ ...r, liveness: lv.state });
+ }
+ return { kept, hidden };
+}
+
+module.exports = { filterSessions, isClaudeComm, parsePs, ticketStatusMap, tabLiveness, ticketLiveness, buildParked, tkKey };
diff --git a/server.js b/server.js
index 0404305..c627204 100755
--- a/server.js
+++ b/server.js
@@ -102,6 +102,12 @@ async function getDots() {
// A durably-parked live tab peels OUT of the active colour groups into the PARKED
// section, so the active dots stay uncluttered (allcolordots --json carries `parked`).
rows = rows.filter(r => r && r.live && !r.parked);
+ // TK-12236: ONE batched ps per refresh (never per row). A session row whose claude died drops out of
+ // EVERY panel (colour groups here, PARKED below); ps failure -> procs null -> nothing hidden (fail-open).
+ const { err: psErr, stdout: psOut } = await run('ps', ['-axo', 'pid=,tty=,comm='], 8000, 8 << 20);
+ const procs = psErr ? null : liveFilter.parsePs(psOut);
+ const sess = liveFilter.filterSessions(rows, procs);
+ rows = sess.kept;
// Re-decide each ACTIVE dot's COLOR through Jev (System One typed choice). Scoped to
// the dotbar's own read path — allcolordots.sh is untouched. Jev unavailable/capped/
// errors -> we keep allcolordots' original heuristic color (never blank, never throw).
@@ -119,6 +125,7 @@ async function getDots() {
attached: isAttached(r.tty), // false => orphaned (live claude, no iTerm2 tab): UI hides the dead-end open button
ticket: ticketOf(r.label),
doing: cleanLabel(r.label),
+ liveness: r.liveness, // 'live' | 'unknown' (dead rows never reach here)
});
}
// Folder tag per session (DW vs Non-DW) so the bar can be scoped to one folder. Fail-open to 'other'.
@@ -130,13 +137,12 @@ async function getDots() {
.filter(g => g.count > 0 || g.color !== 'none'); // ALWAYS show every real colour (dimmed at 0, Steve 2026-09-25); hide only an empty 'no dot'
// TK-12236: parked rows show only if LIVE. One batched ps per refresh (never per row);
// ticket parks resolve against the board status map refreshed by refreshTickets().
- const { err: psErr, stdout: psOut } = await run('ps', ['-axo', 'pid=,tty=,comm='], 8000, 8 << 20);
- const procs = psErr ? null : liveFilter.parsePs(psOut);
if (!ticketStatus.map) await refreshTickets();
const tickets = ticketStatus.map && Date.now() - ticketStatus.at < TICKET_STATUS_MAX_AGE_MS ? ticketStatus.map : null;
const parkedOut = liveFilter.buildParked(parked, { procs, tickets, isAttached, cleanLabel, ticketOf });
await tagAll(parkedOut.items); // TK-12234 folder tag on the LIVE parked rows only
- return { updated: Date.now(), total: rows.length, groups: out, parked: parkedOut };
+ return { updated: Date.now(), total: rows.length, groups: out, parked: parkedOut,
+ hidden_sessions: sess.hidden, liveness_measured: !!procs };
}
// Bring the iTerm2 session whose tty matches to the front.
diff --git a/test/live-filter.test.js b/test/live-filter.test.js
index 5276fca..d4c4fb8 100644
--- a/test/live-filter.test.js
+++ b/test/live-filter.test.js
@@ -14,6 +14,9 @@ const PS = [
' 400 ttys030 claude', // a claude, but on a different tty than recorded
' 500 ttys021 claude', // holds ttys021 (for the no-pid fallback)
' 600 ?? /usr/sbin/cfprefsd',
+ ' 1634 ttys002 /Users/x/.npm-global/lib/node_modules/@anthropic-ai/claude-code/bin/claude.exe',
+ ' 1700 ttys040 node',
+ ' 1800 ttys041 /bin/zsh',
].join('\n');
const procs = lf.parsePs(PS);
const tickets = lf.ticketStatusMap([
@@ -50,18 +53,21 @@ test('tab with no pid falls back to "some claude holds that tty"', () => {
assert.deepEqual(ids(out), ['ttys021']);
});
-test('NEGATIVE: closed / archived tickets hidden; open tickets kept with no tty rendered', () => {
+test('parked TICKETS are NEVER hidden (TK-12174 / always-show rule): open, closed, off-board all shown', () => {
const out = lf.buildParked([
tkt('TK-11155', 'ttys026'), tkt('TK-12103', 'ttys016'),
tkt('TK-9001', 'ttys026'), tkt('TK-9002', 'ttys026'), tkt('TK-4', 'ttys026'),
], { procs, tickets });
- assert.deepEqual(ids(out), ['TK-11155', 'TK-12103']);
+ assert.deepEqual(ids(out), ['TK-11155', 'TK-12103', 'TK-9001', 'TK-9002', 'TK-4']);
+ assert.equal(out.hidden.length, 0, 'no ticket park is ever hidden');
+ const by = Object.fromEntries(out.items.map(i => [i.id, i]));
+ assert.equal(by['TK-11155'].liveness, 'live');
+ assert.equal(by['TK-9001'].liveness, 'closed'); assert.equal(by['TK-9001'].status, 'done');
+ assert.equal(by['TK-4'].liveness, 'unknown', 'not in board summary = NOT-MEASURED, shown');
for (const i of out.items) {
assert.equal(i.tty, '', 'ticket tty is provenance, never rendered as a session');
- assert.equal(i.parked_from, i.id === 'TK-11155' ? 'ttys026' : 'ttys016');
assert.equal(i.attached, undefined, 'no orphan/attached state on a ticket park');
}
- assert.deepEqual(out.hidden.map(h => h.id), ['TK-9001', 'TK-9002', 'TK-4']);
});
test('FAIL-OPEN: probe failures show the row with liveness unknown (never hidden, never live)', () => {
@@ -102,3 +108,37 @@ test('NEGATIVE: an UNCLAIMED daemon spare (tty ??) never makes a tty or pid row
const out = lf.buildParked([tab('ttys099'), tab('ttys098', 700)], { procs: p, tickets });
assert.deepEqual(ids(out), [], 'no-pid row on a spare-less tty and a pid that is an unclaimed spare elsewhere are both hidden');
});
+
+test('REGRESSION: native-installer comm ".../bin/claude.exe" counts as live (was hidden as "recycled")', () => {
+ assert.ok(lf.isClaudeComm('/Users/x/.npm-global/lib/node_modules/@anthropic-ai/claude-code/bin/claude.exe'));
+ assert.ok(!lf.isClaudeComm('/usr/bin/vim'));
+ assert.ok(!lf.isClaudeComm('/bin/zsh'));
+ const out = lf.buildParked([tab('ttys002', 1634)], { procs, tickets });
+ assert.deepEqual(ids(out), ['ttys002']);
+ assert.equal(out.items[0].liveness, 'live');
+});
+
+test('NEGATIVE (all panels): filterSessions hides a fake dead-tty row, keeps a live one, keeps unknown', () => {
+ const rows = [
+ { tty: 'ttys020', pid: '100', color: 'green' }, // live claude
+ { tty: 'ttys040', pid: '1700', color: 'yellow' }, // live node session
+ { tty: 'ttys041', pid: '1800', color: 'pink' }, // pid is a bare shell -> claude gone -> dead
+ { tty: 'ttys077', pid: '4242', color: 'purple' }, // FAKE dead tty: pid not running
+ { tty: 'ttys078', color: 'orange' }, // no pid, no claude on tty -> dead
+ ];
+ const { kept, hidden } = lf.filterSessions(rows, procs);
+ assert.deepEqual(kept.map(r => r.tty), ['ttys020', 'ttys040']);
+ assert.ok(kept.every(r => r.liveness === 'live'));
+ assert.deepEqual(hidden.map(h => h.tty).sort(), ['ttys041', 'ttys077', 'ttys078']);
+ // UNKNOWN liveness (ps probe failed) -> every row shown, none hidden (fail-open)
+ const blind = lf.filterSessions(rows, null);
+ assert.equal(blind.kept.length, rows.length);
+ assert.equal(blind.hidden.length, 0);
+ assert.ok(blind.kept.every(r => r.liveness === 'unknown'));
+});
+
+test('server wires filterSessions into the colour groups (not just PARKED)', () => {
+ const src = fs.readFileSync(path.join(__dirname, '..', 'server.js'), 'utf8');
+ assert.match(src, /liveFilter\.filterSessions\(rows, procs\)/);
+ assert.match(src, /rows = sess\.kept;/);
+});
← a5d20f8 bar: add dw / non-dw ticket chips under the five state chips
·
back to Desktop Dotbar
·
auto-data-snapshot: 2026-09-26T11:13:09 (1 data files) — .cl 7956e2f →