Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
94 changes: 94 additions & 0 deletions docs/design/running-agents-external.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
77 changes: 64 additions & 13 deletions src/frontend/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading