diff --git a/CLAUDE.md b/CLAUDE.md index 8046a8f..156602e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -73,7 +73,7 @@ docs/ - **Crash-safety** — every HTTP route dispatch is wrapped in try/catch → 500 (one bad session never takes down the server); `findSessionFile` looks up its index with `Object.prototype.hasOwnProperty.call(...)` to avoid prototype-pollution DoS; delete / bulk-delete validate `SAFE_SESSION_ID` - **Desktop app is a thin shell** — `desktop/main.js` spawns the *unmodified* server as a Node child and points a `BrowserWindow` at it. Keep the server desktop-agnostic; desktop-only capabilities are exposed through `preload.js` (`window.codbashDesktop`) and detected at runtime in the frontend (e.g. the native folder picker is only wired up when `window.codbashDesktop.pickFolder` exists) - **View-aware chrome** — `render()` stamps `document.body` with `data-view`; the session toolbar is hidden in Overview/Workspace via `body[data-view="workspace"|"overview"] .toolbar { display:none }` -- **Running-agents sidebar tree** (Workspace) is built from `activeSessions` grouped by real `cwd` and labeled by agent — do NOT reconstruct it from static project config. It lists agents in **external native terminals** only: `getActiveSessions()` tags each with `local` (true = descends from a codbash browser-pty pane, false = external), and the tree shows `!local` — codbash's own panes are already visible as tabs. Clicking a row raises that real terminal window via `POST /api/focus` (`focusTerminalByPid`); it must NEVER spawn a blank in-app terminal (an empty shell isn't the agent, and `claude --continue` on a live agent would fork a second instance). A still-running agent's PTY cannot be mirrored/attached from the browser terminal — focus the real window instead. See `docs/design/running-agents-external.md`. +- **Running-agents sidebar tree** (persistent, in the main sidebar — not just Workspace) is built from `activeSessions`, a live `ps`-scan-backed map (`GET /api/active`, polled every 5s), so a truly-exited agent simply isn't in it — no "ghost" filtering needed. It shows **every** currently-running agent, both inside codbash and in external native terminals, as a **3-level tree**: `_wsRunningTree(mode)` (`workspace.js`) nests project → agent → sessions (`mode:'project'`, default) or the mirror agent → project → sessions (`mode:'agent'`) — user-toggleable via the compact segmented control in the tree header, persisted to `localStorage['codedash-running-group']`. A project+agent pair with exactly one live session collapses to a single leaf row (no redundant 1-child nesting); 2+ sessions get one leaf row each, labeled via `_wsSessionLeafLabel` (pane name if local and matched, else session-id prefix, else pid) so same-project/same-agent sessions read as distinct. Top-level (L1) groups are an **accordion, collapsed by default** — `_wsRunExpanded` (in-memory, resets on reload) tracks open/closed per `mode|groupKey`; `_wsToggleRunGroup` flips a `collapsed` class directly on the group's wrapper DOM node rather than forcing a full rebuild, and a later rebuild re-reads `_wsRunExpanded` so open/closed choices survive it. Because the L1 header's click means "toggle", it does NOT jump to a session — that action lives entirely on leaf rows (`.ws-run-l2.ws-run-leaf` / `.ws-run-l3`). `getActiveSessions()` tags each agent with `local` (true = descends from a codbash browser-pty pane, false = external); rows are colored by where they run (blue = inside codbash, orange = external; dimmed = idle, not gone). Clicking a leaf dispatches on `local`: local → `jumpToWorkspacePane` (found by matching `cwd` against live panes); external → `POST /api/focus` (`focusTerminalByPid`) to raise the real window. It must NEVER spawn a blank in-app terminal as a stand-in (an empty shell isn't the agent, and `claude --continue` on a live agent would fork a second instance). See `docs/design/running-agents-external.md`. - **Saved layouts round-trip the full pane** — `sanitizePane` preserves `cmd` + `prefill` + `cwd` (not just `cmd`); dropping any of these silently loses the user's launch command on restore - **No `window.prompt` in Electron** — use `codbashPrompt()` (app.js) for any text input; the native prompt is a no-op in the desktop shell - **Two update paths, mutually exclusive** — the npm CLI self-updates via `POST /api/update` (`npm i -g codbash-app@latest` + restart). The **desktop app updates in-place via `electron-updater`** (download-on-click → restart, driven by the frontend banner over `window.codbashDesktop.updater` IPC and `main.js`). `desktop/main.js` sets `CODBASH_DESKTOP=1` so the server **refuses `/api/update` (400)** — running `npm i -g` inside the signed, read-only app bundle would update an unrelated global copy and the restart would land back on the bundled old version. macOS in-place update needs the **`.zip` target + `latest-mac.yml`** (Squirrel.Mac can't apply a DMG) and a signed build; on failure the banner falls back to opening the releases page (`codbash:open-releases`). See `desktop/RELEASE.md` §4. diff --git a/docs/design/running-agents-external.md b/docs/design/running-agents-external.md index abf63b5..a1a4004 100644 --- a/docs/design/running-agents-external.md +++ b/docs/design/running-agents-external.md @@ -1,5 +1,13 @@ # Running Agents = agents in external terminals (focus, don't spawn) +> **Amendment (unified tree):** the sidebar tree now shows local (in-codbash) +> AND external agents together, colored differently, with a project/agent +> grouping toggle. The `local` tag and the "focus by PID, never spawn a blank +> terminal" rule below are unchanged for external agents — only the "external +> only" scoping was reversed. See "Unified local + external tree" at the +> bottom of this doc for the current design; the sections above it describe +> the external-focus mechanics, which still apply verbatim. + ## Goal The Workspace "Running agents" sidebar should list agents actually running in @@ -127,3 +135,89 @@ regression; a full keyboard-navigable list is a follow-up, `deferred_to: issue`) raises the native terminal window. **Touch targets:** tree rows keep their existing height (unchanged). + +## Unified local + external tree + +### Goal + +Users report the "external-only" tree was confusing: it silently hid agents +running in codbash's own panes, so a user watching a project with both an +in-app Claude session and an iTerm one saw only half the picture. Merge them +into one tree, colored by where each agent runs, and let the user pick whether +the tree groups by project (default) or by agent kind. + +A first pass grouped project/agent as a flat 2-level list — one header, then +every session underneath as a same-labeled row ("Claude", "Claude", …). Users +found that read as noise/duplication rather than a real hierarchy, so it grew +a third level: outer group → inner group → individual sessions. + +### Data model + +No server change: `getActiveSessions()` already tags every live agent with +`local`. The frontend just stops dropping `local:true` entries. + +`_wsRunningTree(mode)` (`workspace.js`) builds a 3-level tree: +- `mode: 'project'` (default) — project → agent kind → sessions. +- `mode: 'agent'` — agent kind → project → sessions (the mirror nesting, not + just a relabel — switching the toggle re-parents the whole tree). + +Built via a small generic `_wsGroupBy(items, keyFn)` applied twice (outer key, +then inner key within each outer group). Each item carries `{agent, cwd, +projName, kind}` so either grouping direction can label its rows correctly. + +A subgroup (project+agent pair) holding exactly one live session collapses +its leaf row into the subgroup row itself — no redundant single-child row — +and takes that session's true color/idle state directly (`.ws-run-leaf`). +Once a subgroup holds 2+ sessions, it renders as a real subheader (neutral +green dot, session count) with one leaf row per session underneath, each +labeled by `_wsSessionLeafLabel` (a live pane's user-given name if local and +matched, else the session id prefix — same convention as the session cards' +"Resume last session (id12345)" — else a bare pid) so same-project, +same-agent sessions read as distinct instead of repeating the same label. + +### Click dispatch + +`jumpToRunningAgent(cwd, sessionId, kind, pid, local)`: +- `local === true` → `_wsFindLivePaneForCwd(cwd)` looks up a live (connected, + not exited) pane whose shell cwd matches, then `jumpToWorkspacePane(tabId, + paneId)`. If no pane is found (a stale tag right after a tab closed), falls + back to `setView('workspace')` rather than doing nothing. +- `local === false` — unchanged: `POST /api/focus` by PID. + +### Grouping preference + +Stored in `localStorage['codedash-running-group']` (`'project'` | `'agent'`), +not a server setting — it's a per-browser display toggle, not something that +needs to sync across machines. A compact 2-button segmented control sits in +the tree header (`.ws-run-mode`). + +### Accordion (L1 collapsed by default) + +Top-level groups start collapsed; clicking a project (project-mode) or agent +kind (agent-mode) header expands it to reveal its running sessions. Expand +state is in-memory only (`_wsRunExpanded`, resets on reload), keyed +`mode|groupKey` so project-mode and agent-mode expand choices don't collide. +Toggling flips a `collapsed` class directly on the group's wrapper DOM node +(`.ws-run-group`) rather than forcing a full tree rebuild — cheap, and a later +rebuild triggered by a real `activeSessions` change re-reads `_wsRunExpanded` +so the user's open/closed choices survive it. Because the header's click now +means "toggle", the "jump to a session" action moved entirely to leaf rows +(`.ws-run-l2.ws-run-leaf` / `.ws-run-l3`) — there is no single-click shortcut +from an L1 header to a specific session anymore, by design (an accordion +header disclosing multiple children has no unambiguous single default action). + +### "No ghost sessions" + +The tree was never actually showing ghosts in the sense of dead processes — +`activeSessions` is a live `ps` scan re-polled every 5s (1s while Workspace is +open), so an exited process drops out on the next poll. The dimmed rows in the +old design were `status: 'waiting'` (idle — low CPU, sleeping — but still a +live process), which reads as "maybe gone" without a legend. The new tree +keeps that dimming for idle but adds an explicit color legend (blue/orange) +for *where* an agent runs, so dimmed no longer doubles as an ambiguous signal. + +### Color legend + +- Blue dot — running inside a codbash browser-pty pane (`local: true`). +- Orange dot — running in an external native terminal (`local: false`). +- Either dimmed to muted gray — idle (waiting for input), not exited. diff --git a/src/frontend/styles.css b/src/frontend/styles.css index ca847ad..0c12374 100644 --- a/src/frontend/styles.css +++ b/src/frontend/styles.css @@ -3074,33 +3074,84 @@ body[data-view="overview"] .toolbar { display: none; } .ws-running-tree { margin: 8px 8px 4px; padding-top: 8px; border-top: 1px solid var(--border); } +.ws-run-head { + display: flex; align-items: center; justify-content: space-between; gap: 6px; + padding: 0 8px 4px; +} .ws-run-title { font-size: 10px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; - color: var(--text-muted); padding: 0 8px 4px; + color: var(--text-muted); } -.ws-run-proj { +/* Compact 2-way segmented control — the "grouping" setting the user toggles + inline instead of hunting for it in a settings dialog. */ +.ws-run-mode { + display: inline-flex; border: 1px solid var(--border); border-radius: 5px; overflow: hidden; flex-shrink: 0; +} +.ws-run-mode-btn { + font-size: 9px; font-weight: 600; letter-spacing: 0.02em; line-height: 1; + padding: 3px 6px; background: transparent; color: var(--text-muted); + border: none; cursor: pointer; +} +.ws-run-mode-btn + .ws-run-mode-btn { border-left: 1px solid var(--border); } +.ws-run-mode-btn:hover { color: var(--text-primary); background: rgba(255,255,255,0.05); } +.ws-run-mode-btn.active { color: #fff; background: var(--accent-blue); } +/* Accordion wrapper for one L1 group + its body. Collapsed by default (see + _wsRunExpanded) — .ws-run-l1-body is hidden until the header is clicked. */ +.ws-run-group + .ws-run-group { margin-top: 1px; } +.ws-run-l1-body { display: block; } +.ws-run-group.collapsed .ws-run-l1-body { display: none; } +.ws-run-l1-chevron { + font-size: 8px; color: var(--text-muted); flex-shrink: 0; + transition: transform 0.15s; transform: rotate(90deg); /* expanded: pointing down */ +} +.ws-run-group.collapsed .ws-run-l1-chevron { transform: rotate(0deg); } /* collapsed: pointing right */ +/* Level 1 — outer group (project, or agent kind in agent-mode). */ +.ws-run-l1 { display: flex; align-items: center; gap: 6px; padding: 4px 8px; font-size: 12px; color: var(--text-primary); cursor: pointer; border-radius: 6px; } -.ws-run-proj:hover { background: rgba(255,255,255,0.06); } +.ws-run-l1:focus-visible { outline: 2px solid var(--accent-blue); outline-offset: -2px; } +.ws-run-l1:hover { background: rgba(255,255,255,0.06); } .ws-run-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent-green); flex-shrink: 0; } .ws-run-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: Menlo, Monaco, monospace; } .ws-run-count { font-size: 10px; color: var(--text-muted); background: var(--bg-hover); border-radius: 8px; padding: 0 6px; min-width: 16px; text-align: center; } -.ws-run-term { - position: relative; - padding: 2px 8px 2px 22px; font-size: 11px; color: var(--text-secondary); +/* Level 2 — inner group (agent kind, or project in agent-mode). Neutral green + dot while it holds 2+ sessions (a real subheader); collapses to the leaf's + own color (.ws-run-leaf) when it holds exactly one, so a single running + instance doesn't get a redundant extra row underneath it. */ +.ws-run-l2 { + display: flex; align-items: center; gap: 6px; padding: 3px 8px 3px 20px; + font-size: 11px; color: var(--text-secondary); cursor: pointer; border-radius: 6px; +} +.ws-run-l2:hover { background: rgba(255,255,255,0.05); color: var(--text-primary); } +.ws-run-l2-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--accent-green); flex-shrink: 0; } +.ws-run-l2-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +/* Level 3 — one row per individual running session. Also reused by .ws-run-l2 + when it collapses to a single-session leaf. Dot color is the terminal-type + legend: orange = external native terminal (default), blue = inside codbash. */ +.ws-run-l3, .ws-run-l2.ws-run-leaf .ws-run-l2-dot { position: relative; } +.ws-run-l3 { + padding: 2px 8px 2px 32px; font-size: 10.5px; color: var(--text-secondary); cursor: pointer; border-radius: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.ws-run-term::before { - content: ''; position: absolute; left: 11px; top: 50%; transform: translateY(-50%); - width: 5px; height: 5px; border-radius: 50%; background: var(--accent-green); -} -.ws-run-term.ws-run-idle { color: var(--text-muted); } -.ws-run-term.ws-run-idle::before { background: var(--text-muted); } -.ws-run-term:hover { background: rgba(255,255,255,0.05); color: var(--text-primary); } +.ws-run-l3::before { + content: ''; position: absolute; left: 21px; top: 50%; transform: translateY(-50%); + width: 5px; height: 5px; border-radius: 50%; background: var(--accent-orange, #f59e0b); +} +.ws-run-l3.ws-run-local::before { background: var(--accent-blue); } +.ws-run-l3:hover { background: rgba(255,255,255,0.05); color: var(--text-primary); } +.ws-run-l2.ws-run-leaf .ws-run-l2-dot { background: var(--accent-orange, #f59e0b); } +.ws-run-l2.ws-run-leaf.ws-run-local .ws-run-l2-dot { background: var(--accent-blue); } +/* Idle (waiting for input, still a live process — not "gone") always wins over + the terminal-type color so a dimmed dot reliably means "idle", not a second + meaning collision with local/external. */ +.ws-run-l3.ws-run-idle, +.ws-run-l2.ws-run-leaf.ws-run-idle { color: var(--text-muted); } +.ws-run-l3.ws-run-idle::before, +.ws-run-l2.ws-run-leaf.ws-run-idle .ws-run-l2-dot { background: var(--text-muted); } /* In-app prompt (codbashPrompt) — replaces window.prompt (no-op in Electron). */ .cb-prompt-overlay { diff --git a/src/frontend/workspace.js b/src/frontend/workspace.js index 3c2b525..ae33173 100644 --- a/src/frontend/workspace.js +++ b/src/frontend/workspace.js @@ -271,32 +271,131 @@ function _wsToolLabel(kind) { return _WS_TOOL_LABELS[kind] || (kind.charAt(0).toUpperCase() + kind.slice(1)); } -// Group *running agents in EXTERNAL native terminals* (from the live /api/active -// map, not Workspace panes) by their real project folder. Agents running inside -// a codbash browser-pty pane (tagged `local` server-side) are excluded — they -// are already visible as Workspace tabs, so surfacing them here too is just -// noise. See docs/design/running-agents-external.md. Used by the sidebar tree. -function _wsRunningByProject() { +// Grouping mode for the sidebar tree — 'project' (project → agents, default) +// or 'agent' (agent kind → projects). A per-browser preference, not a synced +// setting: it's a display toggle, not something that needs to follow the user +// across machines. +var WS_RUN_GROUP_KEY = 'codedash-running-group'; +function _wsGetRunningGroupMode() { + try { + return window.localStorage.getItem(WS_RUN_GROUP_KEY) === 'agent' ? 'agent' : 'project'; + } catch (e) { return 'project'; } +} +function _wsSetRunningGroupMode(mode) { + mode = mode === 'agent' ? 'agent' : 'project'; + if (_wsGetRunningGroupMode() === mode) return; + try { window.localStorage.setItem(WS_RUN_GROUP_KEY, mode); } catch (e) {} + _wsRunTreeSig = ''; // force a rebuild even though activeSessions didn't change + _wsRenderRunningTree(); +} + +// Accordion state for the tree's top-level (L1) groups — collapsed by default +// so the tree opens compact; expanding a project (or agent, in agent-mode) +// reveals its running sessions. In-memory only (resets on reload), keyed by +// `mode|groupKey` so project-mode and agent-mode expand state don't collide. +var _wsRunExpanded = {}; +function _wsRunGroupKey(mode, g) { + return mode + '|' + (mode === 'agent' ? g.kind : g.cwd); +} +// Toggles the DOM directly (cheap, no full rebuild) and records the choice so +// a later full rebuild (triggered by a real activeSessions change) preserves it. +function _wsToggleRunGroup(rowEl) { + var wrap = rowEl.closest ? rowEl.closest('.ws-run-group') : null; + if (!wrap) return; + var key = wrap.getAttribute('data-key') || ''; + var collapsed = wrap.classList.toggle('collapsed'); + _wsRunExpanded[key] = !collapsed; + rowEl.setAttribute('aria-expanded', String(!collapsed)); +} +function _wsRunGroupKeydown(ev, rowEl) { + if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); _wsToggleRunGroup(rowEl); } +} + +// Find a live (connected, not exited) Workspace pane whose shell sits in `cwd`. +// Used to make an agent tagged `local:true` (running inside a codbash pty) +// clickable — jump straight to its tab/pane instead of the external-focus path. +function _wsFindLivePaneForCwd(cwd) { + if (!cwd || typeof _wsAllPanes !== 'function') return null; + var all = _wsAllPanes(); + for (var i = 0; i < all.length; i++) { + var x = all[i]; + if (x.pane && !x.pane.exited && x.pane.cwd === cwd) return x; + } + return null; +} + +// Plain two-key groupBy: returns [{key, items}] in first-seen order. +function _wsGroupBy(items, keyFn) { + var map = {}, order = []; + items.forEach(function (it) { + var k = keyFn(it); + if (!Object.prototype.hasOwnProperty.call(map, k)) { map[k] = []; order.push(k); } + map[k].push(it); + }); + return order.map(function (k) { return { key: k, items: map[k] }; }); +} + +// A short, stable per-instance label for a running-agent leaf row — needed +// once a project+agent pair has more than one live session, so the rows read +// as distinct sessions instead of the same label repeated. Prefers a pane's +// user-given name (local agents), falls back to the session id prefix (same +// convention as the session cards' "Resume last session (id12345)"), then a +// bare pid. +function _wsSessionLeafLabel(x) { + var a = x.agent; + if (a.local) { + var hit = _wsFindLivePaneForCwd(x.cwd); + if (hit && hit.pane && hit.pane.name) return hit.pane.name; + } + if (a.sessionId) return a.sessionId.slice(0, 8); + if (a.pid) return 'pid ' + a.pid; + return 'session'; +} + +// Build the 3-level running-agents tree: outer group → inner group → individual +// sessions. `mode: 'project'` (default) nests project → agent → sessions; +// `mode: 'agent'` nests agent → project → sessions — same data, mirrored +// nesting order, so switching the toggle re-parents instead of just relabeling +// a flat list. Built from the live /api/active map (a fresh `ps` scan each +// poll — an exited agent simply isn't in it, so there is no separate "ghost" +// entry to filter). Each agent carries `local` (server-tagged: true = running +// inside a codbash browser-pty pane, false = an external native terminal) so +// the render step can color and route clicks correctly. See +// docs/design/running-agents-external.md for the external-focus half of this; +// the local half jumps to the matching pane. +function _wsRunningTree(mode) { var map = (typeof activeSessions === 'object' && activeSessions) || {}; - var groups = {}, order = []; + var items = []; Object.keys(map).forEach(function (k) { var a = map[k]; - // Skip codbash's own panes: this tree is for agents in external terminals, - // the ones that have no other UI home. (Undefined `local` — an older server - // payload — is treated as external so nothing silently disappears.) - if (a && a.local === true) return; var cwd = (a && a.cwd) || ''; - // Only skip entries with no cwd (can't group or focus a window). - if (!cwd) return; + if (!cwd) return; // nothing to group or act on without a known folder var isHome = /^(\/Users\/[^/]+|\/home\/[^/]+|\/root)\/?$/.test(cwd); - var key = cwd; // group by full path so two same-named folders don't merge - if (!groups[key]) { - groups[key] = { name: isHome ? '~' : _wsProjectBasename(cwd), cwd: cwd, items: [] }; - order.push(key); - } - groups[key].items.push(a); + items.push({ agent: a, cwd: cwd, projName: isHome ? '~' : _wsProjectBasename(cwd), kind: a.kind || '' }); + }); + + var outerKeyFn = mode === 'agent' ? function (it) { return it.kind || 'agent'; } : function (it) { return it.cwd; }; + var innerKeyFn = mode === 'agent' ? function (it) { return it.cwd; } : function (it) { return it.kind || 'agent'; }; + + return _wsGroupBy(items, outerKeyFn).map(function (outer) { + var rep = outer.items[0]; + return { + name: mode === 'agent' ? _wsToolLabel(rep.kind) : rep.projName, + cwd: mode === 'agent' ? '' : rep.cwd, + kind: mode === 'agent' ? rep.kind : '', + count: outer.items.length, + rep: rep, + subgroups: _wsGroupBy(outer.items, innerKeyFn).map(function (inner) { + var rep2 = inner.items[0]; + return { + name: mode === 'agent' ? rep2.projName : _wsToolLabel(rep2.kind), + count: inner.items.length, + rep: rep2, + sessions: inner.items, + }; + }), + }; }); - return order.map(function (k) { return groups[k]; }); } // Sanitize a pid for an inline onclick arg: a positive integer, else 0. Mirrors @@ -307,15 +406,24 @@ function _wsPidArg(pid) { return (Number.isInteger(n) && n > 0) ? String(n) : '0'; } -// Click a running-agent row. These rows are agents in EXTERNAL native terminals -// (codbash's own panes are filtered out of the tree), so the honest action is to -// raise that real terminal window by PID — reusing /api/focus (focusTerminalByPid, -// same path the session cards' "Focus Terminal" uses). We deliberately do NOT -// open a blank in-app terminal as a stand-in: an empty shell is not the agent, -// and resuming (claude --continue) would spawn a SECOND instance of a live agent. -// `cwd`/`kind` are kept in the signature for call-site stability but unused here -// (focus is keyed purely by pid); `sessionId` is forwarded for the server log. -function jumpToRunningAgent(cwd, sessionId, kind, pid) { +// Click a running-agent row. Dispatches on where the agent actually runs: +// - local (inside a codbash browser-pty pane) → jump straight to that tab/pane. +// We look the pane up by cwd at click time (not baked into the onclick) so a +// tab opened/closed after the tree last rendered is still found correctly. +// - external (native terminal — iTerm/Terminal.app/Warp/cmux…) → raise that +// real window by PID via /api/focus (focusTerminalByPid, same path the +// session cards' "Focus Terminal" uses). We deliberately do NOT open a blank +// in-app terminal as a stand-in: an empty shell is not the agent, and +// resuming (claude --continue) would spawn a SECOND instance of a live agent. +function jumpToRunningAgent(cwd, sessionId, kind, pid, local) { + if (local) { + var hit = _wsFindLivePaneForCwd(cwd); + if (hit) { jumpToWorkspacePane(hit.tab.id, hit.pane.id); return; } + // The agent is tagged local but we can't find its pane (e.g. a stale tag + // right after a tab closed) — land on Workspace rather than doing nothing. + if (typeof setView === 'function') setView('workspace'); + return; + } var n = typeof pid === 'number' ? pid : parseInt(pid, 10); if (!Number.isInteger(n) || n <= 0) { if (typeof showToast === 'function') showToast('No terminal window to focus for this agent.'); @@ -340,38 +448,101 @@ function jumpToRunningAgent(cwd, sessionId, kind, pid) { }); } -// Render a compact tree at the bottom of the sidebar: each project folder with a -// running agent, the agents underneath labeled by agent name — click to jump. +// One