You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Session indicators are computed by different code paths depending on the source: PTY-attached local sessions get .cli-busy / .needs-attention / .response-ready from OSC parsing; remote sessions get an activity pip from the watch channel (#241, #242) plus a state + age text; local sessions launched outside Switchboard get nothing; subagents light their parent only through activeSessions. Each new source (a second host, OpenCode, Codex) would add another path.
public/session-activity.js:27 declares itself the single writer of .cli-busy / .response-ready. That rule holds by discipline only: the first version of the remote spinner alignment bypassed setActivity() from two files and neither lint nor tests went red. Nothing in the codebase makes a violation fail.
A full audit with file:line for every indicator, the divergences and the proposed shapes exists as a local note (.work-files/switchboard/audit-indicateurs-2026-09-10.md); its substance is below.
What
A pure domain module for session state, one projection, and enforcement.
Domain — session-state.js, dual-loadable like public/restore-plan.js (#239) and public/subagent-timing.js: no DOM, no IPC, no electron, loadable under node:test as is.
Invariants live here, not in CSS :not() chains: busy / waitingForInput / attention exclusivity, and the priority order (attention > response-ready > busy > agentsBusy > waitingForInput > idle+age > stale > archived).
Ports — the events a source may push. What each adapter can honestly emit:
event
local-pty
local-transcript
remote-ssh
ptyBusy / attention (OSC 0 / 9)
yes
never
only while attached
transcriptTouched(mtime)
yes
yes (only signal)
yes (watch channel)
descriptorStatus(status, at)
yes
no (no live CLI)
yes (already, main.js:539)
subagentSpawned / Completed
yes
no
no today
Only a PTY can tell "thinking" from "waiting for input". An adapter without one must never claim waitingForInput; it produces busy: unknown plus lastActivityAt.
Projection — renderSessionIcon(snapshot) returns classes, glyph and title for one icon slot per row. It absorbs today's .session-status-dot, the busy spinner, .has-busy-agents, and the remote state + age text. .remote-badge (identity) and .remote-host-dot (per-project reachability) legitimately stay separate.
Enforcement — a violation fails, it is not discouraged:
ESLint no-restricted-syntax forbidding classList.add/toggle with cli-busy / needs-attention / response-ready / has-busy-agents outside the projection file (no no-restricted-* rule exists in eslint.config.js today).
A boundary test asserting the domain module references neither document, window nor electron (same shape as test/main-ctx-db-wiring.test.js).
Split public/session-activity.js into state (the Maps/Sets + setActivity) and DOM (applyActivityClasses). This is the seam that does not exist yet.
Introduce session-state.js + renderSessionIcon() behind the existing applyActivityClasses call, local-pty only, with the ESLint rule active.
Move remote-activity-ui.js and remote-activity.js (already a pure factory) onto the remote-ssh adapter.
Add the local-transcript adapter: transcript growth via the existing ~/.claude/projects watcher, for sessions launched outside Switchboard.
Subagent attribution through the adapters (separate issue).
Not now
ssh transport (remote-transport.js, remote-mirror.js) — already isolated. Grid vs sidebar unification — separate, deliberate parallel implementations. The non-namespaced session_cache.sessionId primary key.
Cost, stated
main.js is ~2600 lines and mixes transport and decision (OSC parsing 1910-1974, annotateRemoteAttachable 520-545); the renderer is classic scripts sharing a global scope. The dual-load pattern already exists, so the new module is not a new cost; the split of session-activity.js and a unified session notion in main.js are.
Why
Session indicators are computed by different code paths depending on the source: PTY-attached local sessions get
.cli-busy/.needs-attention/.response-readyfrom OSC parsing; remote sessions get an activity pip from the watch channel (#241, #242) plus a state + age text; local sessions launched outside Switchboard get nothing; subagents light their parent only throughactiveSessions. Each new source (a second host, OpenCode, Codex) would add another path.public/session-activity.js:27declares itself the single writer of.cli-busy/.response-ready. That rule holds by discipline only: the first version of the remote spinner alignment bypassedsetActivity()from two files and neither lint nor tests went red. Nothing in the codebase makes a violation fail.A full audit with
file:linefor every indicator, the divergences and the proposed shapes exists as a local note (.work-files/switchboard/audit-indicateurs-2026-09-10.md); its substance is below.What
A pure domain module for session state, one projection, and enforcement.
Domain —
session-state.js, dual-loadable likepublic/restore-plan.js(#239) andpublic/subagent-timing.js: no DOM, no IPC, noelectron, loadable undernode:testas is.Invariants live here, not in CSS
:not()chains:busy/waitingForInput/attentionexclusivity, and the priority order (attention > response-ready > busy > agentsBusy > waitingForInput > idle+age > stale > archived).Ports — the events a source may push. What each adapter can honestly emit:
main.js:539)Only a PTY can tell "thinking" from "waiting for input". An adapter without one must never claim
waitingForInput; it producesbusy: unknownpluslastActivityAt.Projection —
renderSessionIcon(snapshot)returns classes, glyph and title for one icon slot per row. It absorbs today's.session-status-dot, the busy spinner,.has-busy-agents, and the remote state + age text..remote-badge(identity) and.remote-host-dot(per-project reachability) legitimately stay separate.Enforcement — a violation fails, it is not discouraged:
no-restricted-syntaxforbiddingclassList.add/togglewithcli-busy/needs-attention/response-ready/has-busy-agentsoutside the projection file (nono-restricted-*rule exists ineslint.config.jstoday).document,windownorelectron(same shape astest/main-ctx-db-wiring.test.js).renderSessionIcon(), never a replica (Five test files exercise a hand-written replica, not the production module #171).Migration order (least churn first)
public/session-activity.jsinto state (the Maps/Sets +setActivity) and DOM (applyActivityClasses). This is the seam that does not exist yet.session-state.js+renderSessionIcon()behind the existingapplyActivityClassescall, local-pty only, with the ESLint rule active.remote-activity-ui.jsandremote-activity.js(already a pure factory) onto theremote-sshadapter.local-transcriptadapter: transcript growth via the existing~/.claude/projectswatcher, for sessions launched outside Switchboard.Not now
ssh transport (
remote-transport.js,remote-mirror.js) — already isolated. Grid vs sidebar unification — separate, deliberate parallel implementations. The non-namespacedsession_cache.sessionIdprimary key.Cost, stated
main.jsis ~2600 lines and mixes transport and decision (OSC parsing 1910-1974,annotateRemoteAttachable520-545); the renderer is classic scripts sharing a global scope. The dual-load pattern already exists, so the new module is not a new cost; the split ofsession-activity.jsand a unified session notion inmain.jsare.