Skip to content

feat: one agent indicator per workspace row, atomic needs-input, stale watchdog - #361

Merged
arzafran merged 11 commits into
mainfrom
feat/agent-presence
Sep 18, 2026
Merged

arzafran merged 11 commits into
mainfrom
feat/agent-presence

Conversation

@arzafran

Copy link
Copy Markdown
Member

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:

  1. Sources/AgentActivityState.swift: AgentPresence (state, source, lastEventAt, sessionKey, isStale) is the stored per-surface truth; SidebarAgentIndicator.make is the only place that picks a glyph and label.
  2. Sources/Workspace.swift, Sources/Workspace+SidebarTelemetry.swift: panelAgentStates and panelAgentStateSources become read-only mirrors of panelAgentPresence. Hooks still win over screen inference; a repeated identical report only refreshes the liveness clock.
  3. Sources/TerminalController+Telemetry.swift, contracts/v2/methods.json: new agent.needs_input writes blocked state, posts the notification, and updates supervision in one main-thread hop. surface.report_agent_state and agent.event accept optional provider, session_id, pid. Wire shapes of existing fields are unchanged.
  4. CLI/CLI+Hooks.swift, CLI/CLI+AgentEventAdapters.swift: Claude, Codex, and OpenCode hooks send one agent.needs_input for blocking notifications. Hooks no longer write "Running" or "Needs input" status rows; verbose tool text still shows when claudeCodeVerboseStatus is on. Installed hook commands and arguments are unchanged, no reinstall needed.
  5. 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.
  6. Sources/TabManager+GitMetadataPolling.swift: the existing 30s PID sweep clears presence for dead pids and republishes once when a presence turns stale.
  7. 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

  • unit-tests shards green; AgentActivityStateTests includes 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.sh prints OK (contract, catalog, CLI names, Python client in sync)
  • socket-integration-tests green
  • Manual, tagged Debug build: run Claude Code in a workspace, trigger a permission prompt, confirm the row shows "Needs input" and only that; click the row, confirm it stays; approve, confirm it flips to "Working"

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.
@arzafran
arzafran merged commit 09e86a5 into main Sep 18, 2026
16 checks passed
@arzafran
arzafran deleted the feat/agent-presence branch September 18, 2026 15:55
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