← back to Desktop Dotbar
dotbar: mark orphaned sessions (live claude, no iTerm2 tab) so open-terminal only shows for focusable tabs
d56df0fca4c516cf8ccb751baa5f04ad55a4aff8 · 2026-09-23 12:35:47 -0700 · Steve Abrams
Cross-references iTerm2's attached-session ttys (split-pane-safe index walk) against
allcolordots' process-based live list. Rows whose claude survives under iTermServer with
no tab now render 'orphaned - no tab' instead of a dead-end 'open terminal' button.
Fixes 'open terminal never works' (79% of rows were orphans).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M public/index.htmlM server.js
Diff
commit d56df0fca4c516cf8ccb751baa5f04ad55a4aff8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 23 12:35:47 2026 -0700
dotbar: mark orphaned sessions (live claude, no iTerm2 tab) so open-terminal only shows for focusable tabs
Cross-references iTerm2's attached-session ttys (split-pane-safe index walk) against
allcolordots' process-based live list. Rows whose claude survives under iTermServer with
no tab now render 'orphaned - no tab' instead of a dead-end 'open terminal' button.
Fixes 'open terminal never works' (79% of rows were orphans).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/index.html | 13 +++++++++++--
server.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++----
2 files changed, 57 insertions(+), 6 deletions(-)
diff --git a/public/index.html b/public/index.html
index 6ff543c..54c69d1 100644
--- a/public/index.html
+++ b/public/index.html
@@ -107,6 +107,9 @@
.open { -webkit-app-region:no-drag; cursor:pointer; font-size:11px; padding:5px 9px; border-radius:7px;
border:1px solid var(--line); background:var(--bg2); color:var(--fg); white-space:nowrap; }
.open:hover { background:#2a2e37; }
+ /* Orphaned row: live claude process whose iTerm2 tab was closed — nothing to focus, so no open button. */
+ .orphan { -webkit-app-region:no-drag; font-size:11px; padding:5px 9px; border-radius:7px;
+ border:1px dashed var(--line); background:transparent; color:var(--dim); white-space:nowrap; opacity:.7; }
.empty { padding:14px; color:var(--dim); }
</style>
</head>
@@ -261,7 +264,11 @@ function renderPanel(){
+ `<span class="tk">${s.ticket || s.id || '—'}</span>`
+ `<span class="doing" title="${(s.doing||'').replace(/"/g,'"')}">${s.doing || ('('+s.kind+')')}</span>`
+ `<span class="tty">${s.tty || s.kind}</span>`
- + (s.tty ? `<button class="open" onclick='reveal(${JSON.stringify(s.tty)})'>open terminal ↗</button>` : '')
+ + (s.tty
+ ? (s.attached === false
+ ? `<span class="orphan" title="Live process, but its iTerm2 tab was closed — nothing to focus.">orphaned · no tab</span>`
+ : `<button class="open" onclick='reveal(${JSON.stringify(s.tty)})'>open terminal ↗</button>`)
+ : '')
+ `</div>`;
}
panel.innerHTML = html; return;
@@ -276,7 +283,9 @@ function renderPanel(){
+ `<span class="tk">${s.ticket || '—'}</span>`
+ `<span class="doing" title="${(s.doing||'').replace(/"/g,'"')}">${s.doing || '(no label)'}</span>`
+ `<span class="tty">${s.tty}</span>`
- + `<button class="open" onclick='reveal(${JSON.stringify(s.tty)})'>open terminal ↗</button>`
+ + (s.attached === false
+ ? `<span class="orphan" title="Live process, but its iTerm2 tab was closed — nothing to focus.">orphaned · no tab</span>`
+ : `<button class="open" onclick='reveal(${JSON.stringify(s.tty)})'>open terminal ↗</button>`)
+ `</div>`;
}
panel.innerHTML = html;
diff --git a/server.js b/server.js
index a7f15af..57ce537 100755
--- a/server.js
+++ b/server.js
@@ -45,11 +45,49 @@ function ticketOf(label) {
return m ? m[1].toUpperCase() : '';
}
+// Set of tty basenames (e.g. "ttys014") that iTerm2 actually has an ATTACHED session for.
+// A claude process can be "live" (allcolordots sees it via a ps scan) yet ORPHANED — its
+// iTerm2 tab/window was closed, so the process survives under iTermServer with no session to
+// focus. reveal() (iTerm2-only) can never open an orphan, so we cross-reference here and mark
+// each row `attached`; the UI then hides the dead-end "open terminal" button for orphans.
+// Uses the split-pane-SAFE index walk: the `repeat with s in sessions of t` element-reference
+// form throws -1719 on split-pane tabs and silently drops those sessions (undercount), so we
+// index by number with a per-session try, exactly like reveal().
+async function iterm2AttachedTtys() {
+ const script = `set out to ""
+tell application "iTerm2"
+ repeat with wi from 1 to (count of windows)
+ set w to window wi
+ repeat with ti from 1 to (count of tabs of w)
+ set t to tab ti of w
+ repeat with si from 1 to (count of sessions of t)
+ try
+ set out to out & (tty of (session si of t)) & " "
+ end try
+ end repeat
+ end repeat
+ end repeat
+end tell
+return out`;
+ const { err, stdout } = await run('osascript', ['-e', script], 12000);
+ const set = new Set();
+ if (err) return null; // iTerm2 unreachable -> don't mark anything orphaned (fail-open)
+ for (const dev of String(stdout || '').trim().split(/\s+/)) {
+ const b = dev.split('/').pop();
+ if (b) set.add(b);
+ }
+ return set;
+}
+const ttyBase = (t) => String(t || '').split('/').pop();
+
async function getDots() {
// allcolordots scans every terminal; on a busy/loaded box (many sessions) it can take 60-90s.
// The default 8s cap timed out every refresh -> empty output -> the bar showed "0 live". Give a
// generous cap; keep-last-good (in refresh()) covers the interim so the bar never flaps to 0.
const { stdout } = await run('bash', [ALLCOLORDOTS, '--json'], 120000);
+ // Which live rows are actually attached to an iTerm2 tab (vs orphaned). null = iTerm2 unreachable.
+ const attachedSet = await iterm2AttachedTtys();
+ const isAttached = (tty) => attachedSet === null ? true : attachedSet.has(ttyBase(tty));
let rows = [];
try { rows = JSON.parse(stdout || '[]'); } catch { rows = []; }
// Durable PARKED group straight from the registry (both tab + ticket kinds),
@@ -74,6 +112,7 @@ async function getDots() {
groups[color].push({
tty: r.tty,
pid: r.pid,
+ 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),
});
@@ -81,10 +120,13 @@ async function getDots() {
const out = ORDER
.map(color => ({ color, ...META[color], count: groups[color].length, sessions: groups[color] }))
.filter(g => g.count > 0 || g.color === 'green'); // always show green; hide empty others
- const parkedItems = parked.map(e => ({
- kind: e.kind, id: e.id, ticket: e.kind === 'ticket' ? e.id : ticketOf(e.label),
- tty: e.tty || (e.kind === 'tab' ? e.id : ''), doing: cleanLabel(e.label), parked_at: e.parked_at,
- }));
+ const parkedItems = parked.map(e => {
+ const tty = e.tty || (e.kind === 'tab' ? e.id : '');
+ return {
+ kind: e.kind, id: e.id, ticket: e.kind === 'ticket' ? e.id : ticketOf(e.label),
+ tty, attached: tty ? isAttached(tty) : true, doing: cleanLabel(e.label), parked_at: e.parked_at,
+ };
+ });
return { updated: Date.now(), total: rows.length, groups: out,
parked: { count: parkedItems.length, items: parkedItems } };
}
← cf77e2f Jev classifies dotbar dot colors ($0 builtin default; paid T
·
back to Desktop Dotbar
·
auto-data-snapshot: 2026-09-23T12:42:16 (1 data files) — sta de2776c →