Skip to content

Fix Claude needs input status acknowledgement - #4794

Closed
austinywang wants to merge 34 commits into
mainfrom
issue-2576-native-claude-detection-should
Closed

austinywang wants to merge 34 commits into
mainfrom
issue-2576-native-claude-detection-should

Conversation

@austinywang

@austinywang austinywang commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

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:

  1. 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. A structuredAgentStatusKey (socket --agent-status-key=, V2 agent_status_key, session restore) ties each "needs input" alert to a known sidebar status (e.g. claude_code).

  2. 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 through FeedCoordinator.surfaceBlockingDecisionAttention, which sets the sidebar status + per-panel needsInput lifecycle without creating a store notification. PATH A can never reach that state, so clicking in left the badge stuck. New FeedCoordinator.acknowledgeBlockingDecisionAttention(workspaceId:panelId:) — the feed-path analogue of the store mark-read acknowledgement — is invoked from TabManager.dismissNotification, the single shared focus/interaction seam every entrypoint (sidebar resume, panel click, terminal interaction) funnels through. Overlay teardown is factored into a shared clearResolvedBlockingDecisionOverlay helper reused by concludeBlockingDecisionAttention; 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:

  • PATH A (notification hook) → markRead → demote to Idle (unchanged).
  • PATH B (feed-routed blocking decision) → 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

Testing

  • Not run locally per workspace policy; CI gates compilation and execution.
  • Two-commit regression structure for the feed-path fix: failing test 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).
  • Existing PATH A coverage retained in TerminalNotificationClearAllTests / TerminalNotificationQueueTests.

Fixes #2576. Part of umbrella #1027.

Summary by CodeRabbit

  • New Features

    • Sidebar agent-status badges are demoted to Idle when related notifications are read, cleared, or removed; acknowledgments are batched and applied consistently at panel and workspace scopes.
    • Notifications (CLI and app) can carry an optional structured agent-status key so status-aware behavior is preserved across delivery and actions.
    • Added APIs to view and clear pending “Needs input” attention overlays; focusing a panel can clear them.
  • Localization

    • Expanded translations for the agent "Idle" status to many additional languages.
  • Tests

    • Added tests covering demotion/non-demotion scenarios, panel attribution, and attention-clearing behavior.

Additional fixes from review (this iteration)

  • Hibernation lifecycle now demoted on acknowledgement. acknowledgeStructuredAgentInputStatus previously rewrote only the visible sidebar status entry, leaving agentLifecycleStatesByPanelId[panel][key] at .needsInput — so agentHibernationLifecycleState kept returning .needsInput and 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 .running update wins), mirroring the feed path. Regression test testAcknowledgingClaudeNeedsInputDemotesAgentLifecycleForHibernation.
  • Ghostty submodule pointer restored to main's 34cbf180d after 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_async before set_status (Codex/Grok/OpenCode), the acknowledgement's entry.timestamp <= notificationCreatedAt guard treats the same-event status as "newer" than the notification and skips demotion. Claude is unaffected — its hook sends set_agent_lifecycle/set_status before notify (CLI cmux.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:

  • The shared badge is workspace-level: it stays lit while any panel (store, queued, policy-hook, or feed-routed) still needs input for the key.
  • The per-panel lifecycle demotes as soon as the acknowledged panel itself has no remaining input, so a sibling panel's prompt no longer strands the acknowledged panel on .needsInput (blocked hibernation).
  • The lifecycle is never demoted while the same panel still has a pending input: feed prompts (long-lived) are panel-scoped precisely; queued/policy-hook deliveries (sub-drain-tick) block conservatively at workspace granularity — always the safe direction (an agent awaiting input is never made hibernatable mid-wait).

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:

  • Completion/idle notifications counted as needs-input. The remaining-input set keys off the agent status key, but Claude's stop/idle notifications carry the same claude_code key as a real prompt (CLI cmux.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 / Incorrect Needs Input title 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.
  • Policy-hook reconciliation. A notification hook that suppresses delivery (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.

@vercel

vercel Bot commented May 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 11, 2026 1:07am
cmux-staging Building Building Preview, Comment Jun 11, 2026 1:07am

@coderabbitai

coderabbitai Bot commented May 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Demotes 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.

Changes

Structured Agent Input Status Acknowledgment

Layer / File(s) Summary
Status key lookup infrastructure
Sources/AgentHibernation/AgentHibernationLifecycleState.swift
notificationTitleByStatusKey is the canonical mapping. statusKey(forNotificationTitle:) does normalized reverse lookup and removes ambiguous normalized titles. normalizedAllowedStatusKey(_:) and normalizedNotificationTitle(_:) centralize validation and normalization.
Workspace acknowledgment methods
Sources/Workspace+PanelLifecycle.swift
Adds acknowledgeStructuredAgentInputStatus(...) to update matching structured-input SidebarStatusEntry items to Idle when their timestamp ≤ notificationCreatedAt; adds demoteAcknowledgedAgentLifecycle(...) and isStructuredAgentInputStatus(...) helpers.
FeedCoordinator overlay APIs
Sources/Feed/FeedCoordinator.swift
Extracts overlay clearing to clearResolvedBlockingDecisionOverlay, adds acknowledgeBlockingDecisionAttention(workspaceId:panelId:) and pendingStructuredAgentInputWorkspaceKeys()/pendingStructuredAgentInputPanelKeys() to manage and query pending feed-routed attention keys.
TerminalNotificationStore read/clear flow integration
Sources/TerminalNotificationStore.swift
TerminalNotification carries structuredAgentStatusKey; store tracks pending policy-driven structured-agent keys; multiple notification flows batch affected notifications and invoke acknowledgment after state updates (markRead, markRead variants, markAllRead, remove, clearAll, clearNotifications); keys are preserved across rebuild/rebind and test helpers clear pending counts.
Test coverage for status demotion
cmuxTests/TerminalNotificationClearAllTests.swift
Updates existing test to initialize claude_code status and agent PID contribution; adds tests for marking/removing Claude "Needs input" notifications demoting to Idle, demoting per-panel lifecycle when acknowledged, and negative tests for non-agent reads, older-notification reads, and sibling-panel reads; adds feed-routed attention clearing regression test.
CLI notification payloads with status key
CLI/cmux.swift
notificationPayload accepts optional structuredAgentStatusKey and prefixes the payload with --agent-status-key=<key> when provided; Claude notifications now include claudeCodeStatusKey.
Localization expansion
Resources/Localizable.xcstrings
Expands agent.generic.notification.status.idle localizations to many additional locales while keeping en/ja.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • manaflow-ai/cmux#5313: Related FeedCoordinator overlay refactor touching concludeBlockingDecisionAttention and overlay lifecycle.
  • manaflow-ai/cmux#4165: Related Agent Hibernation lifecycle/key infrastructure; both touch lifecycle and status-key plumbing.
  • manaflow-ai/cmux#4409: Updates structured-agent hook keys that will participate in this demotion/acknowledgment flow.

Poem

🐰 I sniff the titles, hop and peep,

I nudge "Needs input" to gentle sleep,
A pause, a dot, a muted chime,
I tidy badges, one at a time,
Hooray — the sidebar naps on time.


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Logging ❌ Error The PR adds unconditional NSLog calls in production app code (Sources/TerminalNotificationStore.swift). The logAuthorization() function calls NSLog("notification.auth %@", message) without #if DEBU... Replace NSLog calls in logAuthorization() and notification sound preparation code with terminalNotificationLogger.info() or terminalNotificationLogger.error() at appropriate levels, or guard NSLog calls with #if DEBUG if temporary diagno...
Cmux Full Internationalization ❌ Error The PR modified "agent.generic.notification.status.idle" in Resources/Localizable.xcstrings but failed to include translations for 3 supported locales: pt-BR, zh-Hans, zh-Hant. Add translations for pt-BR, zh-Hans, and zh-Hant locales to "agent.generic.notification.status.idle" in Resources/Localizable.xcstrings.
Cmux Architecture Rethink ❌ Error PR splits agent status UI lifecycle: sidebar badge (statusEntries→pause.circle.fill) and hibernation gate (agentLifecycleStatesByPanelId→idle) require separate demotion calls, leaving representable... Unify the status representation: either demote agentHibernationLifecycleState when statusEntries[key] icon changes, or expose a single source-of-truth demotion method that handles both consistently.
Docstring Coverage ⚠️ Warning Docstring coverage is 13.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (17 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: fixing the Claude needs input status acknowledgement mechanism when users click into the workspace.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed No Swift 6 actor isolation violations introduced. AgentHibernationLifecycleStatusKeys properly marked nonisolated, all MainActor contexts preserved, and new FeedCoordinator methods explicitly bound...
Cmux Swift Blocking Runtime ✅ Passed No new blocking/timing-based synchronization patterns (semaphores, blocking waits, sleeps, Task.sleep, DispatchQueue.main.sync, manual locks) introduced in production Swift code; all changes implem...
Cmux Expensive Synchronous Load ✅ Passed No expensive synchronous loaders added to main actor/interactive paths; new methods perform only lightweight in-memory collection operations on existing @MainActor classes.
Cmux Cache Substitution Correctness ✅ Passed All caches in this PR (statusKeyByNormalizedNotificationTitle, allowedStatusKeys) are computed from immutable compile-time constants and do not replace fresh authoritative reads. No cold/stale cach...
Cmux No Hacky Sleeps ✅ Passed PR changes only Swift and resource files. The runtime-no-hacky-sleeps rule explicitly scopes to TypeScript, JavaScript, shell, and build scripts; Swift is excluded and covered separately.
Cmux Algorithmic Complexity ✅ Passed PR uses pre-computed Sets with O(1) lookups; no nested collection scans, per-item rescans, or unbenchmarked algorithms on hot notification-handling paths. Batch acknowledgement pattern is efficient.
Cmux Swift Concurrency ✅ Passed PR introduces no legacy async patterns: no DispatchQueue.global, new Combine state, completion handlers, or unmanaged Tasks; all async work uses proper Swift Concurrency with MainActor and lifecycl...
Cmux Swift @Concurrent ✅ Passed No async functions introduced in PR; all new methods are synchronous. FeedCoordinator methods properly marked @MainActor; Workspace extension methods inherit @MainActor from class. Nonisolated enum...
Cmux Swift File And Package Boundaries ✅ Passed PR respects Swift file and package boundaries: TerminalNotificationStore (+202 lines to 2378-line file), FeedCoordinator (+89 lines to 1239-line file), and other changes all fall below the 250-line...
Cmux User-Facing Error Privacy ✅ Passed PR introduces only product names (Claude Code, Amp, etc.) and generic status terms (Idle) in user-facing text; no sensitive data, credentials, internal vendor details, or raw error messages are exp...
Cmux Swiftui State Layout ✅ Passed PR modifies only data models, stores, coordinators, CLI logic, tests, and resources—no SwiftUI view code. TerminalNotificationStore's @Published properties are pre-existing. No new @Observable, @St...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR does not introduce or materially change any standalone cmux-owned NSWindow, NSPanel, NSWindowController, SwiftUI Window, or WindowGroup instances; changes are confined to notification store logi...
Cmux Source Artifacts ✅ Passed All files in the PR (verified by git diff origin/main..HEAD) are legitimate: source code (.swift), tests (.py/.swift), localization (Localizable.xcstrings), and config (.github/swift-file-length-bu...
Description check ✅ Passed The PR description comprehensively covers all required sections with detailed explanations of changes, testing approach, and interactions with related work.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-2576-native-claude-detection-should

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented May 26, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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 acknowledgeBlockingDecisionAttention hook called from the shared TabManager.dismissNotification focus seam.

  • PATH A: TerminalNotificationStore gains acknowledgeStructuredAgentInputStatuses, called from every dismissal path (markRead, clearAll, removeNotification, clearAllForTab). A timestamp guard prevents an older notification from demoting a re-raised status, and pendingPolicyStructuredAgentInputCounts + TerminalMutationBus.inFlightNotificationDeliveries block premature lifecycle demotion during transient delivery.
  • PATH B: FeedCoordinator.acknowledgeBlockingDecisionAttention force-concludes pending feed attention targets for the focused panel, restoring the per-panel lifecycle to .running and removing the workspace badge when no sibling panels remain pending.
  • Hibernation fix: demoteAcknowledgedAgentLifecycle now also demotes agentLifecycleStatesByPanelId from .needsInput to .idle, so agentHibernationLifecycleState no longer blocks hibernation after the badge has been cleared.

Confidence Score: 4/5

Merge 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

Filename Overview
Resources/Localizable.xcstrings Adds 15 locale translations for agent.generic.notification.status.idle but omits pt-BR, zh-Hans, and zh-Hant — all three are present elsewhere in the catalog, so those users fall back to the English default at runtime.
Sources/TerminalNotificationStore.swift Adds structuredAgentStatusKey on TerminalNotification, acknowledgeStructuredAgentInputStatuses called from every dismissal path, and in-flight policy-hook tracking via pendingPolicyStructuredAgentInputCounts. Timestamp guard and sibling-panel scoping correctly prevent premature demotion.
Sources/Feed/FeedCoordinator.swift Adds acknowledgeBlockingDecisionAttention (PATH B feed-routed clearing) and key-exposure methods. clearResolvedBlockingDecisionOverlay is factored from concludeBlockingDecisionAttention and shared; re-raise safety and sibling-panel scoping are well-guarded.
Sources/Workspace+PanelLifecycle.swift Adds acknowledgeStructuredAgentInputStatus with timestamp-guarded badge demotion and demoteAcknowledgedAgentLifecycle to demote per-panel .needsInput lifecycle to .idle; icon-based detection replaces English string matching.
Sources/TerminalNotificationQueue.swift Adds structuredAgentStatusKey to QueuedTerminalNotification and inFlightNotificationDeliveries tracking for conservative lifecycle demotion blocking. Lock discipline is correct.
Sources/TerminalController.swift Introduces ParsedNotificationPayload struct and parseNotificationPayloadOptions to thread --agent-status-key= through every socket/CLI notification path. All notify call sites updated consistently.
Sources/TabManager.swift Adds acknowledgeBlockingDecisionAttention call in dismissNotification and includes its return value in the guard clause.
Sources/AgentHibernation/AgentHibernationLifecycleState.swift Converts allowedStatusKeys to a notificationTitleByStatusKey map with reverse-lookup and ambiguity tracking; adds workspace- and panel-scoped key value types.
CLI/cmux.swift Adds structuredAgentStatusKey to notificationPayload and passes Self.claudeCodeStatusKey at the two Claude hook notification sites.
Sources/SessionPersistence.swift Threads structuredAgentStatusKey through SessionNotificationSnapshot so the key survives session restore.

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]
Loading

Reviews (13): Last reviewed commit: "fix: initialize feedTarget before defer ..." | Re-trigger Greptile

Comment thread Sources/TerminalNotificationStore.swift Outdated
Comment thread Sources/Workspace+PanelLifecycle.swift
Comment thread Sources/Workspace+PanelLifecycle.swift
@austinywang

Copy link
Copy Markdown
Contributor Author

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.

Comment thread Sources/TerminalNotificationStore.swift
Comment thread Sources/TerminalNotificationStore.swift
coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 6, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5302202 and 2491b42.

📒 Files selected for processing (4)
  • Sources/AgentHibernation/AgentHibernationLifecycleState.swift
  • Sources/TerminalNotificationStore.swift
  • Sources/Workspace+PanelLifecycle.swift
  • cmuxTests/TerminalNotificationClearAllTests.swift

Comment thread Sources/TerminalNotificationStore.swift
Comment thread Sources/TerminalNotificationStore.swift
@austinywang
austinywang dismissed coderabbitai[bot]’s stale review June 6, 2026 09:37

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.

Comment thread Sources/TerminalNotificationStore.swift
Comment thread Sources/TerminalController.swift

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread Sources/TerminalNotificationQueue.swift
austinywang and others added 4 commits June 10, 2026 16:11
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>
austinywang and others added 4 commits June 10, 2026 16:44
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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 lift

Decouple panel lifecycle demotion from the shared sidebar entry rewrite.

statusEntries is global per agent key, but agentLifecycleStatesByPanelId is panel-scoped. If two panels both own claude_code and panel B publishes a newer "Needs input" after panel A’s notification, entry.timestamp <= notificationCreatedAt fails here because statusEntries["claude_code"] now reflects panel B. That continue also skips demoteAcknowledgedAgentLifecycle, so panel A stays .needsInput and agentHibernationLifecycleState(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

📥 Commits

Reviewing files that changed from the base of the PR and between afca982 and b523007.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (2)
  • Sources/Workspace+PanelLifecycle.swift
  • cmuxTests/TerminalNotificationClearAllTests.swift

austinywang and others added 4 commits June 10, 2026 17:08
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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between b523007 and 3c97954.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (5)
  • Sources/AgentHibernation/AgentHibernationLifecycleState.swift
  • Sources/Feed/FeedCoordinator.swift
  • Sources/TerminalNotificationStore.swift
  • Sources/Workspace+PanelLifecycle.swift
  • cmuxTests/TerminalNotificationClearAllTests.swift

Comment thread Sources/TerminalNotificationStore.swift
austinywang and others added 4 commits June 10, 2026 17:37
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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3c97954 and d90e083.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (3)
  • Sources/Feed/FeedCoordinator.swift
  • Sources/TerminalNotificationStore.swift
  • cmuxTests/TerminalNotificationClearAllTests.swift

Comment on lines +1711 to +1716
// 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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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 && !panelHasTransientInput

You 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.

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — e2d57262 Deployed Jun 11, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Native Claude detection should distinguish 'Needs Input' vs 'Idle' states

3 participants