Repository navigation
Fix Claude needs input status acknowledgement - #4794
austinywang wants to merge 34 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughDemotes structured-agent "Needs input" sidebar statuses to Idle when related notifications are read, removed, or cleared by adding title↔status-key lookup, Workspace acknowledgment logic, TerminalNotificationStore integration, FeedCoordinator APIs, CLI payload tagging, expanded localization, and tests. ChangesStructured Agent Input Status Acknowledgment
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Poem
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (3 errors, 1 warning)
✅ Passed checks (17 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR fixes the user-reported bug where clicking into a workspace did not clear a Claude Code "Needs input" sidebar badge. It wires two cooperating acknowledgement paths: PATH A (store-notification mark-read/clear) now demotes the matching structured-agent status entry to Idle using a timestamp guard and sibling-panel scoping, and PATH B (feed-routed PermissionRequest/ExitPlanMode/AskUserQuestion) gains a new
Confidence Score: 4/5Merge with caution: the acknowledgement logic and hibernation fix are solid, but the xcstrings edit leaves Brazilian Portuguese and both Chinese script variants falling back to the English Idle string at runtime. The store and feed acknowledgement paths are well-designed with correct timestamp guards, sibling-panel scoping, and conservative transient-delivery blocking. The one real gap is in Resources/Localizable.xcstrings: this PR actively adds 15 locale entries to agent.generic.notification.status.idle but omits pt-BR, zh-Hans, and zh-Hant, all of which the catalog serves elsewhere. Those users see English-fallback Idle instead of a localized string whenever the badge is demoted. Resources/Localizable.xcstrings — the agent.generic.notification.status.idle key needs pt-BR, zh-Hans, and zh-Hant entries to complete the translation set. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User focuses / clicks workspace panel] --> B[TabManager.dismissNotification]
B --> C[PATH B: FeedCoordinator.acknowledgeBlockingDecisionAttention]
C --> D{pending attentionStates?}
D -- yes --> E[clearResolvedBlockingDecisionOverlay: lifecycle to .running, badge removed if no sibling pending]
D -- no --> F[return false]
B --> G[PATH A: TerminalNotificationStore.markRead / clearAll / removeNotification]
G --> H[acknowledgeStructuredAgentInputStatuses]
H --> I{remaining unread or transient inputs?}
I -- sibling still unread --> J[keep workspace badge lit]
I -- none remaining --> K[Workspace.acknowledgeStructuredAgentInputStatus: timestamp guard]
K -- entry.timestamp le notification.createdAt --> L[badge to Idle, icon to pause.circle.fill]
K -- agent re-raised newer status --> M[badge untouched: re-raise protection]
L --> N[demoteAcknowledgedAgentLifecycle: lifecycle to .idle, hibernatability restored]
Reviews (13): Last reviewed commit: "fix: initialize feedTarget before defer ..." | Re-trigger Greptile |
|
Addressed the remaining Greptile low-priority note in eee40ab by moving notification-title mapping into AgentHibernationLifecycleStatusKeys and deriving allowedStatusKeys from that shared registry. CodeRabbit's remaining low item is a rate-limit notice, not actionable code feedback. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/TerminalNotificationStore.swift`:
- Around line 1650-1653: The code uses notification.surfaceId ??
notification.panelId when resolving the workspace and when calling
acknowledgeStructuredAgentInputStatus, which causes non-panel surface IDs to be
treated as panel IDs; change these to prefer panelId first (use
notification.panelId ?? notification.surfaceId) in
workspaceForStructuredAgentInputAcknowledgement(...) and in the
acknowledgeStructuredAgentInputStatus(...) call so
workspaceContainingPanel(panelId:) always receives an actual panel UUID; apply
the same replacement for the other occurrences noted (the blocks around the
current uses at lines 1658-1659 and 1669-1673).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: a9a9bb78-9a55-4aa5-b9c3-fac75312182c
📒 Files selected for processing (4)
Sources/AgentHibernation/AgentHibernationLifecycleState.swiftSources/TerminalNotificationStore.swiftSources/Workspace+PanelLifecycle.swiftcmuxTests/TerminalNotificationClearAllTests.swift
Addressed in 9f1671d by preferring notification.panelId over surfaceId for structured-agent acknowledgement, with regression ea845d0 covering non-panel surface IDs. CodeRabbit re-reviewed the latest head as non-blocking.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d2a714a. Configure here.
…ude-detection-should
A Claude "Needs input" raised by a feed-routed blocking decision (FeedCoordinator.surfaceBlockingDecisionAttention) sets the sidebar status and needsInput lifecycle without creating a store notification. Focusing/interacting with the panel must still clear it, mirroring the notification-store acknowledgement path. This test fails today because TerminalController's dismissNotification bails out early when there is no store notification to mark read, stranding the badge on "Needs input". Refs #2576 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After the PreToolUse->PermissionRequest migration (#5313), Claude Code's blocking decisions surface "Needs input" through the feed path (FeedCoordinator.surfaceBlockingDecisionAttention), which sets the sidebar status and per-panel needsInput lifecycle WITHOUT creating a store notification. The notification-store acknowledgement added for #2576 can never reach that state, so clicking into the workspace left the badge stuck on "Needs input". Add FeedCoordinator.acknowledgeBlockingDecisionAttention(workspaceId:panelId:) — the feed-path analogue of the store mark-read acknowledgement — and call it from TabManager.dismissNotification, the single shared focus/interaction seam every entrypoint (sidebar resume, panel click, terminal interaction) funnels through. Targets are force-concluded regardless of refcount and scoped to the focused panel so sibling panels keep their badge; the decision's later concludeBlockingDecisionAttention becomes a safe no-op. Overlay teardown is factored into a shared clearResolvedBlockingDecisionOverlay helper reused by conclude and acknowledge. Fixes #2576 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Accept the legitimate growth from the feed-routed acknowledgement fix and its regression tests after merging main: FeedCoordinator (+46), TabManager (+7), and TerminalNotificationClearAllTests (+85 over the prior PR body), plus the merge's own length deltas. Refresh via swift_file_length_budget.py --write-budget, the guard's sanctioned path for accepted debt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The origin/main merge auto-resolved the ghostty submodule to the branch's older pointer (f78189a), rewinding past main's 34cbf18 (surface registry mutation serialization + cmd-click/link-open fixes). Fast-forward the pointer back to main's commit so the branch doesn't regress Ghostty behavior already shipped on main. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Claude hook sets agentLifecycleStatesByPanelId[panel][claude_code] to .needsInput before sending the notification. Acknowledging the notification must demote that per-panel lifecycle, not just the visible sidebar status — otherwise agentHibernationLifecycleState keeps returning .needsInput and hibernation stays blocked. This test fails today because acknowledgeStructuredAgentInputStatus only rewrites the status entry. Refs #2576 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
acknowledgeStructuredAgentInputStatus rewrote only the visible sidebar status entry, leaving agentLifecycleStatesByPanelId[panel][key] at .needsInput. agentHibernationLifecycleState then kept returning .needsInput, so hibernation stayed blocked for the agent even though the badge read Idle. Demote the matching per-panel lifecycle to .idle alongside the status entry (only when currently .needsInput, so a concurrent .running update wins), mirroring the feed acknowledgement path. Refs #2576 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Sources/Workspace+PanelLifecycle.swift (1)
156-168:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftDecouple panel lifecycle demotion from the shared sidebar entry rewrite.
statusEntriesis global per agent key, butagentLifecycleStatesByPanelIdis panel-scoped. If two panels both ownclaude_codeand panel B publishes a newer"Needs input"after panel A’s notification,entry.timestamp <= notificationCreatedAtfails here becausestatusEntries["claude_code"]now reflects panel B. Thatcontinuealso skipsdemoteAcknowledgedAgentLifecycle, so panel A stays.needsInputandagentHibernationLifecycleState(panelId: panelA, ...)remains blocked even though its notification was acknowledged.The sidebar rewrite and the panel-scoped lifecycle demotion need separate recency handling. Right now a sibling panel can keep the acknowledged panel stuck.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Sources/Workspace`+PanelLifecycle.swift around lines 156 - 168, The loop currently uses the global statusEntries timestamp for both the sidebar rewrite and the panel-scoped demotion, which allows another panel to override the global entry and prevent demoteAcknowledgedAgentLifecycle from running for the originating panel; fix this by separating the recency checks: keep the sidebar update using the current global statusEntries as now, but before calling demoteAcknowledgedAgentLifecycle(statusKey: panelId:), obtain the panel-scoped entry/timestamp (e.g. from agentLifecycleStatesByPanelId[panelId] or the per-panel snapshot you maintain) into a local variable (entryForDemotion) and compare entryForDemotion.timestamp <= notificationCreatedAt (instead of the global entry.timestamp) so the demotion decision is based on the panel-scoped snapshot; update the code paths that reference statusEntries, isStructuredAgentInputStatus, notificationCreatedAt, and demoteAcknowledgedAgentLifecycle accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@Sources/Workspace`+PanelLifecycle.swift:
- Around line 156-168: The loop currently uses the global statusEntries
timestamp for both the sidebar rewrite and the panel-scoped demotion, which
allows another panel to override the global entry and prevent
demoteAcknowledgedAgentLifecycle from running for the originating panel; fix
this by separating the recency checks: keep the sidebar update using the current
global statusEntries as now, but before calling
demoteAcknowledgedAgentLifecycle(statusKey: panelId:), obtain the panel-scoped
entry/timestamp (e.g. from agentLifecycleStatesByPanelId[panelId] or the
per-panel snapshot you maintain) into a local variable (entryForDemotion) and
compare entryForDemotion.timestamp <= notificationCreatedAt (instead of the
global entry.timestamp) so the demotion decision is based on the panel-scoped
snapshot; update the code paths that reference statusEntries,
isStructuredAgentInputStatus, notificationCreatedAt, and
demoteAcknowledgedAgentLifecycle accordingly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 61daf96e-4bb3-4437-a252-71211f296642
⛔ Files ignored due to path filters (1)
.github/swift-file-length-budget.tsvis excluded by!**/*.tsv
📒 Files selected for processing (2)
Sources/Workspace+PanelLifecycle.swiftcmuxTests/TerminalNotificationClearAllTests.swift
In a workspace with two Claude panels both stuck on needs-input, acknowledging one panel must demote that panel's per-panel lifecycle (so it can hibernate) while keeping the shared sidebar badge lit for the sibling panel that still needs input. This fails today because the workspace-level remaining-unread gate skips the whole acknowledgement when any panel is still pending, stranding the acknowledged panel's lifecycle on .needsInput. Refs #2576 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The acknowledgement gate keyed remaining unread inputs only by (tabId, statusKey), so two failure modes appeared in multi-panel, same-agent workspaces: - A sibling panel's still-unread notification made the workspace-level gate skip the whole acknowledgement, stranding the acknowledged panel's per-panel lifecycle on .needsInput and blocking its hibernation. - Feed-routed prompts (which have no store notification) were invisible to the gate, so reading one panel's notification could wipe the shared badge while a sibling panel still had an active feed prompt. Split the two concerns: the shared sidebar badge stays workspace-level (now also counting feed-routed pending via FeedCoordinator.pendingStructuredAgentInputWorkspaceKeys), while the per-panel lifecycle demotes as soon as the acknowledged panel itself has no remaining unread input, tracked with the new StructuredAgentInputPanelKey. The shared badge keeps its existing re-raise timestamp guard; the per-panel lifecycle relies on the panel-level remaining check instead, so a sibling's newer status write can't block it. Refs #2576 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ude-detection-should
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/TerminalNotificationStore.swift`:
- Around line 1708-1748: The panel lifecycle is being demoted based only on
persisted panel-counts; change the demotePanelLifecycle logic in
acknowledgeStructuredAgentInputStatuses(for:) to use the same pending-source set
as the workspace badge by checking remainingUnreadStructuredAgentInputKeys()
(remainingWorkspaceInputs) rather than
remainingUnreadStructuredAgentInputPanelKeys() alone: when computing
demotePanelLifecycle (the branch that currently builds a
StructuredAgentInputPanelKey and checks remainingPanelInputs), instead consider
whether StructuredAgentInputNotificationKey(tabId:notification.tabId,
statusKey:statusKey) is still present in remainingWorkspaceInputs (or use the
union of remainingWorkspaceInputs and remainingPanelInputs) so
queued/policy/feed-routed sources keep the panel lifecycle blocked; apply the
same fix to the duplicate block later that also computes demotePanelLifecycle.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 3dc345ff-46af-471b-9f8a-9bc94d0489c9
⛔ Files ignored due to path filters (1)
.github/swift-file-length-budget.tsvis excluded by!**/*.tsv
📒 Files selected for processing (5)
Sources/AgentHibernation/AgentHibernationLifecycleState.swiftSources/Feed/FeedCoordinator.swiftSources/TerminalNotificationStore.swiftSources/Workspace+PanelLifecycle.swiftcmuxTests/TerminalNotificationClearAllTests.swift
A live feed-routed blocking decision keeps a panel waiting for input even after a separate store notification on the same panel is acknowledged. Demoting the per-panel lifecycle there would let the agent be hibernated mid-wait. This fails before the fix because the panel-level remaining check only counts store notifications, not the still-pending feed prompt. Refs #2576 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-panel lifecycle demotion only consulted already-recorded store notifications, so a same-panel input that was still queued, in policy-hook evaluation, or pending as a feed-routed prompt could let the lifecycle drop to idle while the agent was still waiting — making it hibernatable mid-wait (and, for a re-raised needs-input, clearing newer state with an older ack). Feed prompts persist until resolved, so they are now panel-scoped precisely (FeedCoordinator.pendingStructuredAgentInputPanelKeys) and folded into the panel-level remaining set. Queued and policy-hook deliveries resolve within a drain tick and can't be panel-scoped cheaply, so they block lifecycle demotion conservatively at workspace granularity via transientWorkspaceStructuredAgentInputKeys — the safe direction. The shared badge gate is unchanged. Refs #2576 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
'constant feedTarget used before being initialized' — the defer captured the optional before its later assignment. Declare it as an initialized var so the cleanup path is well-defined if an XCTUnwrap throws first. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Sources/TerminalNotificationStore.swift`:
- Around line 1711-1716: Narrow the transient demotion guard so policy-hook
deliveries that already carry a panel id only block lifecycle demotion for that
specific panel instead of the whole workspace: inside the loop over
notificationsToAcknowledge (and the analogous sites at 1743–1746 and 1776–1784),
check NotificationPolicyContext.request.panelId (or equivalent on the
notification) and, when present, mark only that panel as having transient input
rather than using transientWorkspaceStructuredAgentInputKeys()/workspace-wide
blocking; fall back to the existing workspace-wide transientWorkspaceInputs
behavior only when no panelId is available.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 596a0dd9-52e8-4e60-b875-2fe910469139
⛔ Files ignored due to path filters (1)
.github/swift-file-length-budget.tsvis excluded by!**/*.tsv
📒 Files selected for processing (3)
Sources/Feed/FeedCoordinator.swiftSources/TerminalNotificationStore.swiftcmuxTests/TerminalNotificationClearAllTests.swift
| // Queued and policy-hook deliveries can't be panel-scoped cheaply, but | ||
| // they resolve within a drain tick, so block lifecycle demotion across | ||
| // the whole workspace while one is in flight — the safe direction (an | ||
| // agent still awaiting input must never become hibernatable). | ||
| let transientWorkspaceInputs = transientWorkspaceStructuredAgentInputKeys() | ||
| for notification in notificationsToAcknowledge { |
There was a problem hiding this comment.
Keep pending policy-hook input panel-scoped when the request already has a panelId.
workspaceHasTransientInput now blocks lifecycle demotion for every panel in the workspace, but the policy-hook path already knows the owning panel through NotificationPolicyContext.request.panelId. If panel A is waiting on a slow hook and the user acknowledges panel B, this keeps panel B in .needsInput until A’s hook returns, which breaks the panel-scoped acknowledgement contract and can keep sibling panels non-hibernatable longer than necessary.
💡 Narrow the transient guard to the owning panel when possible
- let transientWorkspaceInputs = transientWorkspaceStructuredAgentInputKeys()
+ let transientWorkspaceInputs = transientWorkspaceStructuredAgentInputKeys()
+ let transientPanelInputs = transientPanelStructuredAgentInputKeys()
...
- let workspaceHasTransientInput = transientWorkspaceInputs.contains(
- StructuredAgentInputNotificationKey(tabId: notification.tabId, statusKey: statusKey)
- )
- demotePanelLifecycle = !panelHasRemainingInput && !workspaceHasTransientInput
+ let panelHasTransientInput = transientPanelInputs.contains(
+ StructuredAgentInputPanelKey(
+ tabId: notification.tabId,
+ panelId: acknowledgementPanelId,
+ statusKey: statusKey
+ )
+ )
+ demotePanelLifecycle = !panelHasRemainingInput && !panelHasTransientInputYou can still keep a workspace-wide fallback for truly unscoped queued inputs, but the policy-hook path already has enough information to avoid cross-panel bleed-through.
Also applies to: 1743-1746, 1776-1784
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Sources/TerminalNotificationStore.swift` around lines 1711 - 1716, Narrow the
transient demotion guard so policy-hook deliveries that already carry a panel id
only block lifecycle demotion for that specific panel instead of the whole
workspace: inside the loop over notificationsToAcknowledge (and the analogous
sites at 1743–1746 and 1776–1784), check
NotificationPolicyContext.request.panelId (or equivalent on the notification)
and, when present, mark only that panel as having transient input rather than
using transientWorkspaceStructuredAgentInputKeys()/workspace-wide blocking; fall
back to the existing workspace-wide transientWorkspaceInputs behavior only when
no panelId is available.

Summary
Fixes the user-reported symptom: "When using Claude Code, the 'Needs input' status doesn't get cleared when I click into the workspace."
Two cooperating mechanisms, both keyed to the same user action — focusing/interacting with a workspace panel:
Store-notification acknowledgement (PATH A). Demote acknowledged structured-agent input statuses to neutral Idle from the shared notification acknowledgement paths (
markRead,removeNotification,clearAll, …), scoped to the agent runtime owned by that panel so sibling panels keep their Needs input state. AstructuredAgentStatusKey(socket--agent-status-key=, V2agent_status_key, session restore) ties each "needs input" alert to a known sidebar status (e.g.claude_code).Feed-routed acknowledgement (PATH B) — added after merging
main. After the PreToolUse→PermissionRequest migration (Fix #5286: surface needs-input attention for feed-routed blocking decisions #5313, merged today), Claude Code's blocking decisions (PermissionRequest / ExitPlanMode / AskUserQuestion) surface Needs input throughFeedCoordinator.surfaceBlockingDecisionAttention, which sets the sidebar status + per-panelneedsInputlifecycle without creating a store notification. PATH A can never reach that state, so clicking in left the badge stuck. NewFeedCoordinator.acknowledgeBlockingDecisionAttention(workspaceId:panelId:)— the feed-path analogue of the store mark-read acknowledgement — is invoked fromTabManager.dismissNotification, the single shared focus/interaction seam every entrypoint (sidebar resume, panel click, terminal interaction) funnels through. Overlay teardown is factored into a sharedclearResolvedBlockingDecisionOverlayhelper reused byconcludeBlockingDecisionAttention; the decision's later conclude becomes a safe no-op.Composition with
main(#5313, feed-routed needs-input)Re-verified that clicking into the workspace now clears both paths:
markRead→ demote to Idle (unchanged).acknowledgeBlockingDecisionAttention→ force-conclude, drop the badge, restore lifecycle to.running, scoped to the focused panel. The underlying feed decision card remains for allow/deny; only the attention badge is collapsed, mirroring "I've seen it" semantics.Reconciliation with overlapping PRs
turnIddrift): Complementary, intentionally not duplicated here. That PR fixes a no-click failure mode at the CLI layer (ClaudeHookSessionStore.isCurrentOrClearsStaleNeedsInput): the agent's own clearing hooks (stop/prompt-submit/pre-tool-use) are dropped when the follow-up event'sturnIddrifts. This PR fixes the click/focus path at the app layer. They address different triggers at different layers and compose cleanly; copying its CLI gate here would create the overlapping-merge situation we're trying to avoid. Credit to @RubiconPerform for the turnId-drift analysis.stopidle_promptrelighting Needs input), which is the false-trigger facet that overlaps fix: suppress false Needs Input on turn-end notifications #4900 / IncorrectNeeds Inputtitle after compaction #2910. This PR clears a legitimately-raised Needs input on user acknowledgement. Different mechanisms; Fix stale Claude sidebar needs-input status #4742 can continue independently (or be folded into fix: suppress false Needs Input on turn-end notifications #4900's territory).Needs Inputtitle after compaction #2910 (recap/turn-end false triggers): out of scope here — only noted for the relationship. Not touched.Testing
testFocusingWorkspaceClearsFeedRoutedNeedsInputAttention(cmuxTests/TerminalNotificationClearAllTests.swift) added first, fix second. It seeds a real feed-routed PermissionRequest attention, drives the focus/interaction seam, and asserts the sidebar status + per-panel lifecycle clear (structural assertions, no timing).TerminalNotificationClearAllTests/TerminalNotificationQueueTests.Fixes #2576. Part of umbrella #1027.
Summary by CodeRabbit
New Features
Localization
Tests
Additional fixes from review (this iteration)
acknowledgeStructuredAgentInputStatuspreviously rewrote only the visible sidebar status entry, leavingagentLifecycleStatesByPanelId[panel][key]at.needsInput— soagentHibernationLifecycleStatekept returning.needsInputand hibernation stayed blocked even though the badge read Idle. It now demotes the matching per-panel lifecycle to.idle(only when currently.needsInput, so a concurrent.runningupdate wins), mirroring the feed path. Regression testtestAcknowledgingClaudeNeedsInputDemotesAgentLifecycleForHibernation.34cbf180dafter the merge auto-resolved it to the branch's older commit.Known limitation (out of scope, follow-up)
For generic agents that send
notify_target_asyncbeforeset_status(Codex/Grok/OpenCode), the acknowledgement'sentry.timestamp <= notificationCreatedAtguard treats the same-event status as "newer" than the notification and skips demotion. Claude is unaffected — its hook sendsset_agent_lifecycle/set_statusbeforenotify(CLIcmux.swift), so the guard holds. The root cause is the CLI hook-ordering inconsistency for non-Claude agents, not the app-side guard; aligning those hooks to status-before-notify (matching Claude) is the correct fix and is left as a focused follow-up to keep this PR scoped to #2576 and avoid weakening the deliberate re-raise-protection guard (covered by an existing test).Hibernation lifecycle correctness (review-driven, this iteration)
Acknowledgement now reconciles the per-panel agent lifecycle (which gates hibernation) in addition to the shared sidebar badge, and keeps the two correctly scoped:
.needsInput(blocked hibernation).Known limitations (out of scope for #2576, follow-up)
These are pre-existing properties of the agent-notification model that the autoreview surfaced; they do not affect the reported click-into-workspace symptom (which marks every workspace notification read together) and fixing them belongs in adjacent work:
claude_codekey as a real prompt (CLIcmux.swift). In a multi-panel workspace where one panel's prompt is acknowledged individually while another panel still has an unread completion notification, the shared badge can stay lit. Distinguishing needs-input from completion/idle notifications is Fix stale Claude sidebar needs-input status #4742 / IncorrectNeeds Inputtitle after compaction #2910 / fix: suppress false Needs Input on turn-end notifications #4900 territory (the idle_prompt/recap classification), explicitly out of this PR's scope.record=false/markUnread=false) can, if the user marks the prior notification read mid-evaluation, leave no replacement notification to retrigger acknowledgement. This is a pre-existing edge in the policy-pending tracking and needs a hook-completion reconcile; left as a focused follow-up to avoid widening this PR into the notification-policy pipeline.