feat: one agent indicator per workspace row, atomic needs-input, stale watchdog - #361
Merged
Merged
Conversation
AgentPresence (state, source, lastEventAt, sessionKey, isStale) becomes the stored per-surface truth; panelAgentStates and panelAgentStateSources are read-only mirrors so they cannot diverge. SidebarAgentIndicator is the one place that picks a glyph and label for a workspace. No UI or hook changes yet.
…in Agent Overview The row renders a single glyph plus short label from SidebarAgentIndicator and drops the duplicate Running/Waiting/Needs input status entries. The existing 30s PID sweep now clears presence for dead hook processes and republishes once when a presence turns stale so the row dims. Focus never clears needs-input. Agent Overview reads presence and dims stale rows.
…ity on state reports A needs-input hook used to make three independent socket calls (create the notification, set a Needs input status row, report blocked state); any one could fail alone and the row disagreed with itself. agent.needs_input does all of it in one main-thread hop. surface.report_agent_state and agent.event accept optional provider, session_id, and pid so the app can key presence to a process for the liveness sweep. Hooks no longer write Running or Needs input status rows; turn start only clears leftover verbose tool text.
…ition observable A hook report without a pid no longer drops the pid an earlier report set, so the liveness sweep can still clear the surface when the process dies. Turn start no longer clears the status row, since workspace.clear_status also unregisters the agent pid. The stale transition now writes a stored flag on the presence instead of an identical value that the sidebar publisher's removeDuplicates swallowed.
…e and Codex Both hooks ran the same blocked-or-plain branch; one helper in the adapters file now owns it, which also brings CLI+Hooks.swift back under its CI line budget.
…he unawaited drains
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
The sidebar stops showing agent state three different ways that could disagree. Each workspace row now has one indicator: Needs input, Working, or Idle. Needs input only clears when the agent actually resumes, not when you click the workspace, and the app no longer leaves a red badge lit forever when a hook dies: dead agent processes are cleared within 30 seconds, and anything silent for 10 minutes dims with a "(stale)" suffix instead of pretending to be current.
Summary
Design:
docs/plans/agent-state-unification.md. Review in this order:Sources/AgentActivityState.swift:AgentPresence(state, source, lastEventAt, sessionKey, isStale) is the stored per-surface truth;SidebarAgentIndicator.makeis the only place that picks a glyph and label.Sources/Workspace.swift,Sources/Workspace+SidebarTelemetry.swift:panelAgentStatesandpanelAgentStateSourcesbecome read-only mirrors ofpanelAgentPresence. Hooks still win over screen inference; a repeated identical report only refreshes the liveness clock.Sources/TerminalController+Telemetry.swift,contracts/v2/methods.json: newagent.needs_inputwrites blocked state, posts the notification, and updates supervision in one main-thread hop.surface.report_agent_stateandagent.eventaccept optionalprovider,session_id,pid. Wire shapes of existing fields are unchanged.CLI/CLI+Hooks.swift,CLI/CLI+AgentEventAdapters.swift: Claude, Codex, and OpenCode hooks send oneagent.needs_inputfor blocking notifications. Hooks no longer write "Running" or "Needs input" status rows; verbose tool text still shows whenclaudeCodeVerboseStatusis on. Installed hook commands and arguments are unchanged, no reinstall needed.Sources/TabItemView.swift: one glyph plus label; the old duplicate status rows are filtered even if an older CLI still sends them.==and.equatable()untouched.Sources/TabManager+GitMetadataPolling.swift: the existing 30s PID sweep clears presence for dead pids and republishes once when a presence turns stale.Sources/AgentOverviewWindow.swift: reads presence, dims stale rows.Relaunch starts with no agent state on purpose; a persisted "blocked" would have no writer and no clear path.
Test plan
AgentActivityStateTestsincludes 9 new behavioral tests (needs-input write, focus keeps blocked, resume clears, session exit clears, staleness and refresh, watchdog dead vs live pid, hooks-win, aggregation)scripts/check-v2-contract.shprints OK (contract, catalog, CLI names, Python client in sync)