Skip to content

webterm: fix wall session disconnects, stale-tab cropping, and sidebar status lag#11

Merged
Devail1 merged 4 commits into
mainfrom
dev
Jun 8, 2026
Merged

webterm: fix wall session disconnects, stale-tab cropping, and sidebar status lag#11
Devail1 merged 4 commits into
mainfrom
dev

Conversation

@Devail1

@Devail1 Devail1 commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Four related fixes to the agent terminal wall, all stemming from how grouped tmux sessions share windows and how status is polled.

Commits

  • e251b74 — drop the activity-based client reaper that disconnected live panes.
    The reaper (a3a0ef2) freed --max-clients slots by detaching wall clients idle past a timeout, keyed on tmux client_activity. But that only advances on terminal I/O, not on the WebSocket keepalive pings that keep a quiet pane's socket warm — so an agent terminal that simply produced no output for an hour got force-detached, dropping a live pane to ttyd's reconnect screen (observed on the ccbot wall ~1h in). tmux can't tell an abandoned tab from a live-but-quiet one, so any activity-based reaper kills real sessions. Removed it; genuinely dead sockets are already reclaimed by the keepalive path, and --max-clients=10 absorbs the rest.

  • 5ffb43d — release ttyd clients when the wall tab is backgrounded.
    Grouped tmux sessions share one real window with a single size; under window-size largest, a wall left open in a backgrounded tab keeps its WebSocket alive forever (keepalive pings auto-pong while hidden) and pins every shared window to that ghost's dimensions — cropping the wall you're actively viewing. Page Visibility can distinguish a hidden tab from a quiet-but-watched one: on visibilitychange to hidden, after a 45s grace, blank the ttyd iframes (closing their sockets so the tmux clients drop) and reconnect on return. Fixes the cropping without reintroducing the disconnect.

  • a2f2ecb — sync sidebar status dots to the wall's 4s tick.
    Sidebar dots only re-rendered on the global 30s refresh() loop while the wall pane dots recolour every 4s, both off the same /api/agents session_status. So an agent that went idle stayed green "busy" in the sidebar for up to 30s after the wall went grey. Added syncSidebarDots() (in-place recolour, no list rebuild) called from termTick off the same poll. Now they track in lockstep.

  • c81e481 — only release backgrounded panes that have >1 viewer (contention gate).
    Refines the teardown so it never disturbs a sole viewer. Blanking the only client of a window has no upside (a tmux window has one size shared by all its clients, so a lone client always fits — cropping needs 2+ differently-sized clients). New GET /api/term/clients returns per-wid ttyd client counts (one tmux list-clients, counted by grouped-session name, mirroring agent-terminals.sh); the wall blanks only panes whose window has >1 viewer. So a backgrounded single wall keeps its terminals and returns with no reconnect; teardown fires only when a second tab/device is also viewing the pane.

Notes

  • No schema changes. One new read-only endpoint (/api/term/clients); the rest is client-side JS + the terminal supervisor shell script.
  • Blanking an iframe only drops the ttyd WebSocket / tmux client — it never kills the tmux window or the processes (claude, shells) inside.
  • The separately-reported "carmel shows busy" case was diagnosed as expected behavior — claude agents --json reports a session with a live background shell as busy, not a dashboard bug.

🤖 Generated with Claude Code

Devail1 and others added 4 commits June 7, 2026 18:17
… panes

a3a0ef2 added reap_stale_clients to free --max-clients slots from abandoned
tabs, keyed on tmux client_activity past CLIENT_IDLE_MAX (1h). But client_activity
only advances on terminal I/O, NOT on the WebSocket keepalive pings (app.py
ping_interval=25) that keep a quiet pane's socket warm. A wall tile watching an
agent that produces no output for an hour — the normal case — looked idle and got
force-detached, dropping a live pane to ttyd's reconnect screen (observed on the
ccbot wall, disconnecting ~1h in). tmux can't tell an abandoned tab from a
live-but-quiet one, so any activity-based reaper kills real sessions.

Remove the reaper, its call, and CLIENT_IDLE_MAX; keep --max-clients=10. Dead
sockets are already reclaimed by the keepalive path (a failed ping raises in the
proxy pumps, ttyd drops the client, tmux frees the slot); the cap absorbs the
rest. Documented in a WHY-NO-REAPER note above the poll loop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to dropping the activity-based reaper. Grouped tmux sessions share one
real window with a single size; under `window-size largest`, a wall left open in
a backgrounded tab keeps its ttyd WebSocket alive forever (keepalive pings
auto-pong while hidden) and pins every shared window to that ghost's dimensions —
so the wall you're actively viewing gets its rows/cols cropped to the stale tab's
size (and a horizontal scrollbar appears). Terminal activity can't distinguish a
quiet-but-watched pane from an abandoned tab, which is exactly why the server-side
reaper was wrong. Page Visibility can: it reports when THIS tab is hidden.

On visibilitychange to hidden, after a 45s grace, blank the ttyd iframes
(src=about:blank) so their sockets close and the backing tmux clients drop,
freeing window-size; on return, restore each iframe's src in place so it
reconnects sized to the now-visible tile. termTick and _absorbFreshTerminals are
guarded so nothing reconnects an iframe while suspended; _restoreTermFrames
re-ticks to reconcile any adds/drops missed while hidden. A quick tab flip stays
under the grace, so there's no needless reload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…0s lag)

The sidebar agent dots only re-rendered on the global 30s refresh() loop, while
the wall pane dots recolour every 4s from termTick — both off the same
/api/agents session_status. So an agent that went idle (e.g. carmel) stayed
"busy" green in the sidebar for up to 30s after the wall had already gone grey.

Add syncSidebarDots(agents): recolour the sidebar .health-dot elements in place
(no list rebuild — rows are name-sorted, so status never reorders them) and call
it from termTick off the same poll that refreshes the wall dots. The sidebar now
tracks the wall in lockstep; row add/remove still rides the 30s refreshSidebar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion gate)

The visibility teardown blanked every ttyd iframe when the tab went hidden, which
needlessly churned the connection for the common case: a single wall watching its
agents, no second viewer. Blanking a sole viewer has no upside — a tmux window has
one size shared by all its clients, so a lone client always fits; cropping only
happens when 2+ differently-sized clients share a window.

Gate teardown on actual contention. New GET /api/term/clients returns per-wid
ttyd client counts (one `tmux list-clients` call, counted by grouped-session name,
mirroring scripts/agent-terminals.sh). On hide, the wall fetches it and blanks
only panes whose window has >1 viewer; sole-viewer panes stay connected, so a
backgrounded single wall keeps its terminals and returns with no reconnect. It
enters the suspended state only if it actually released something, and aborts if
the tab became visible mid-fetch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Devail1 Devail1 merged commit c254329 into main Jun 8, 2026
0 of 2 checks passed
Devail1 added a commit that referenced this pull request Jun 22, 2026
webterm: fix wall session disconnects, stale-tab cropping, and sidebar status lag
Devail1 added a commit that referenced this pull request Jun 23, 2026
webterm: fix wall session disconnects, stale-tab cropping, and sidebar status lag
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant