Proposal: redefine session activity → status → pill (collapse to a 4-state model, surface no_signal) #331
harshitsinghbhandari
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Summary
The per-session status the dashboard shows (the pill, the board column, the sidebar dot) is harder to reason about than it should be. One raw signal (
activity.state) is expanded into a 13-valueSessionStatus, which is then collapsed again by three independent mappings and rendered across four surfaces, two of which duplicate the same lookup table. Several distinct backend states (a busy agent, a quiet-but-fine agent, and an agent the daemon thinks may be stuck) all render identically.This proposes collapsing the per-session pill to a 4-state model (
Working,Needs input,Ready,Idle), consolidating the mappings into one place, and surfacing theno_signal("possibly stuck") state the backend already computes but the UI currently throws away. It also lists the open decisions, because there are more than a handful.This is a design discussion, not a finished spec. The open questions at the bottom need answers before implementation.
How it works today
Pipeline (claude-code shown, but the post-hook path is shared by all harnesses):
Two distinct vocabularies, which is fine and correct in principle:
domain.ActivityState(stored):active | idle | waiting_input | exited. The raw agent-liveness signal. Only these four are accepted atPOST /activityand only these are persisted.domain.SessionStatus(never stored, computed every read inderiveStatus):working, pr_open, draft, ci_failed, review_pending, changes_requested, approved, mergeable, merged, needs_input, idle, terminated, no_signal. The display status, which mixes the activity signal with GitHub PR/CI/review facts.The problem is everything downstream of
SessionStatus.Three parallel mappings off
SessionStatus, infrontend/src/renderer/types/workspace.tsworkerDisplayStatus()->WorkerDisplayStatus(working | needs_you | mergeable | ci_failed | done). Drives the pill and the card badge.attentionZone()->AttentionZone(merge | action | pending | working | done). Drives the kanban columns and the sidebar dot.sessionNeedsAttention()(true for needs_input, changes_requested, review_pending, ci_failed),sessionIsActive()(false only for merged/terminated),workerStatusPulses()(true for working, needs_you).These three do not agree with each other. Example: a
ci_failedsession isci_failedto the pill,actionto the board, andtruetosessionNeedsAttention. Areview_pendingsession isneeds_youto the pill butpendingto the board. There is no single answer to "what state is this session in."Four render surfaces
SessionsBoard.tsx: 4 columns byattentionZone(Working / Needs you / In review / Ready to merge) plus aBADGEmap byworkerDisplayStatus.ShellTopbar.tsx: aSTATUS_PILLmap byworkerDisplayStatus.SessionInspector.tsx: a second, separately-definedSTATUS_PILLmap (same values today, free to drift) plus a special-case sostatus === "idle"shows "Idle", plusactivityDetail().Sidebar.tsx:SessionDot, orange+breathing ifattentionZone === "working", grey otherwise.Concrete problems
idle,no_signal, and an actively-workingworkingall collapse to "Working" on the board and topbar. A stuck agent, a finished-and-quiet agent, and a busy agent look identical.no_signalis computed and then discarded. The backend spends real machinery on it (FirstSignalAt, a 90snoSignalGrace, a dedicated CDC trigger onfirst_signal_at, thesignalCapablegate) to answer "has this hook-capable agent gone silent and possibly broken?". But the frontendSessionStatusunion does not even containno_signal, sotoSessionStatus()falls back to"working". The one state that flags a possibly-stuck agent never reaches a human.waiting_input, a permission or idle prompt) and "a reviewer asked for changes / a PR is awaiting review" (changes_requested,review_pending). Those are very different asks.STATUS_PILLdefinitions (topbar and inspector) plus a third color map in the board. Identical today, but nothing keeps them in sync.deriveStatuschecks "has an open PR" before "agent is active", so an agent actively coding on top of an open PR shows the PR status, not "Working".Proposed direction: a 4-state pill
Collapse the per-session pill to four glanceable states. Each answers a clear question.
Delta from today, applied to the existing
SessionStatusvalues:working(activity active)ci_failedneeds_input(waiting_input)changes_requestedreview_pendingapprovedmergeablemergedterminatedidlepr_opendraftno_signalNet effect: "Needs input" becomes exactly "the agent is blocked", and the PR pipeline collapses into a single "Ready" bucket.
Open decisions (the actual work of this discussion)
Each needs a call. My recommendation is listed, but these are the points to argue about.
1. Where do
pr_openanddraftgo?pr_openis "open PR, no review signal yet";draftis a draft PR. Options: (a) Working, (b) Ready.Recommendation: Working. A draft means the agent is still iterating; an un-reviewed PR has nothing for you to do yet. "Ready" should mean "ready for your attention".
2. Should
no_signalbe visible, and how?Options: (a) give it its own pill state ("No signal" / "Stuck", grey or red, non-breathing); (b) fold into Idle; (c) fold into Working (status quo).
Recommendation: (a), its own visible state. Folding it into Working or Idle defeats the entire
FirstSignalAt/ grace mechanism. If we are not willing to surface it, we should delete that machinery instead of paying for it invisibly. This is the highest-value fix in the proposal.3. Precedence: active agent with an open PR.
When
activity.state == activeAND an open PR exists, show Working or the PR status? Today PR wins (deriveStatusorder). Options: (a) keep PR-wins, (b) active-wins (agent busy = Working until it stops).Recommendation: lean active-wins, but this is a genuine product call. It is a one-line reorder in
deriveStatus.4. Does the kanban board collapse to match the pill, or stay a separate triage axis?
The board (
attentionZone, 5 columns) answers "sort my N sessions by what needs me", which is a different question from the per-session pill. Options: (a) keep the board as-is and only change the pill, (b) re-derive the board from the same 4-state model, (c) redesign the board columns to match.Recommendation: decide explicitly. If we keep both, they must at least be derived from one source so they cannot contradict (see #7).
5. What counts as "needs attention"?
sessionNeedsAttention()currently includesci_failed,changes_requested,review_pending. If those move to Working/Ready, do they still drive sidebar emphasis / any counters? Options: tie the predicate to the new buckets (e.g. attention =Needs inputonly, orNeeds input+Ready).Recommendation: attention =
Needs input+no_signal(things that are actually blocked), withReadyas a softer "your move" that does not nag.6. Idle vs Working on the board.
Today
idleshows as "Working" on the board and only the inspector distinguishes "Idle". With a real Idle pill, should the board show idle sessions distinctly too?Recommendation: yes, otherwise Idle is invisible exactly where you scan for it.
7. Single source of truth.
Collapse the duplicated
STATUS_PILLmaps (topbar + inspector) and the parallelworkerDisplayStatus/attentionZone/ predicate functions into one module with one input -> one mapping. Recommendation: yes, non-negotiable for this to be maintainable. Whatever the buckets end up being, define them once.8. Backend
no_signalsemantics (only if #2 = surface it).no_signalcurrently requiressignalCapable(true for claude-code and the other hook harnesses) and 90s of silence withFirstSignalAtzero. Confirm the 90s grace and that it should only ever apply to hook-capable harnesses (a hook-less harness must never show "stuck").What stays the same (non-goals)
ActivityStatederivation (claudecode/activity.goand siblings). That layer is correct; this is all downstream of it.exited.NotificationNeedsInputalready fires only on the transition intowaiting_input, which is consistent with the proposed "Needs input = agent blocked" meaning.ready_to_merge/pr_merged/pr_closed_unmergedare separate and untouched.POST /activityvalidation and the persistedactivity_stateCHECK set stay at the fourActivityStatevalues.Implementation blast radius
Mostly frontend.
no_signaldoes not need an OpenAPI change (the APIstatusfield is already a plainstring); it only needs to be added to the frontendSessionStatusunion so it stops falling back to "working".backend/internal/service/session/status.go—deriveStatusprecedence (Tracking: Lifecycle Manager + Session Manager lane #3), only if changed.frontend/src/renderer/types/workspace.ts— the core:SessionStatusunion + set (addno_signal),toSessionStatus,workerDisplayStatus,attentionZone,sessionNeedsAttention,workerStatusPulses,workerStatusLabel. This is where the consolidation (feat(session): Session Manager — spawn/kill/list/get/send/restore/cleanup #7) lands.frontend/src/renderer/components/SessionsBoard.tsx—COLUMNS,BADGE.frontend/src/renderer/components/ShellTopbar.tsx—STATUS_PILL.frontend/src/renderer/components/SessionInspector.tsx—STATUS_PILL,activityDetail, theidlespecial-case.frontend/src/renderer/components/Sidebar.tsx—SessionDot.frontend/src/renderer/types/workspace.test.ts— update expectations.If the direction (4-state pill + surface
no_signal+ one source of truth) is acceptable, the next step is to lock answers to the eight questions above and turn them into a single mapping table that all surfaces derive from. Feedback welcome on the buckets themselves, not just the edge cases.Beta Was this translation helpful? Give feedback.
All reactions