Fix #5286: surface needs-input attention for feed-routed blocking decisions
Problem
When Claude Code blocks mid-turn on a PermissionRequest (tool permission, ExitPlanMode, AskUserQuestion), the bundled wrapper fires cmux hooks feed --source claude, which sends a blocking feed.push and waits. cmux surfaced nothing: no sidebar "Needs input" status, no bell, no tab/workspace elevation (and no banner while the app is active). The command blocked until the 125s timeout and Claude continued without the user ever knowing it asked. The cmux hooks claude notification|stop paths work in the same setup — the break is specific to the feed decision bridge. (#5286)
Minimal local repro (no Claude Code needed)
echo '{"session_id":"manual-test","hook_event_name":"PermissionRequest","tool_name":"Write","tool_input":{"file_path":"/tmp/x"}}' \
| "$CMUX_BUNDLED_CLI_PATH" hooks feed --source claude
Blocks waiting for a decision, but nothing surfaces in the UI.
Root cause
The reporter's diagnosis is correct. The feed.push blocking bridge and the in-app attention surface diverged during the PreToolUse→PermissionRequest migration. FeedCoordinator.ingestBlocking ingested the Feed card and (only when the app is inactive) posted a desktop banner, but never drove the central attention path — agent lifecycle needsInput + "Needs input" sidebar status + workspace reorder + bell — that the working cmux hooks <agent> notification hook uses.
This is a class of bug: every event type routed through feed (PermissionRequest, ExitPlanMode, AskUserQuestion, and future kinds) silently swallowed in-app attention.
Fix
Converge at the single choke point every feed-routed blocking decision flows through. FeedCoordinator.ingestBlocking now calls surfaceBlockingDecisionAttention(event:) for any blocking-decision event, which:
- flips the owning workspace's agent lifecycle to
.needsInputand sets the localized "Needs input" sidebar status, keyed by a source→status-key map (claude→claude_code) so the existing per-agent resume hooks (e.g. Claude'spre-tool-use) clear it once the agent continues; - elevates the workspace to the top of the sidebar when Reorder on Notification is enabled;
- rings the bell (
requestUserAttention).
This fires regardless of app focus. The existing inactive-app feed banner — with its inline allow/deny actions — remains the desktop surface and the decision channel. Doing the surfacing once, for every blocking-decision event, eliminates the whole class rather than patching PermissionRequest alone.
Tests
Two-commit red/green:
- Failing test —
FeedCoordinatorTests.testBlockingIngestSurfacesNeedsInputAttentionForPermissionRequestasserts that a blockingPermissionRequestrouted throughingestBlockingrequests in-app attention surfacing. Red before the fix. - Fix — green after.
Localization
Added feed.status.needsInput (en: "Needs input", ja: "入力待ち") to Resources/Localizable.xcstrings. No other new user-facing strings.
🤖 Generated with Claude Code
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.
[!NOTE]
Medium Risk
Touches main-thread UI state, workspace targeting, and blocking-hook timing; wrong targeting or overlay cleanup could hide or stick badges, but scope is localized to FeedCoordinator with refcounting and conservative teardown.
Overview
Blocking feed.push decisions (permissions, plan approval, questions) now drive the same in-app attention path as agent notification hooks: Needs input sidebar status, .needsInput lifecycle, optional workspace reorder, and dock attention—even when the app is focused (inactive-only banners were insufficient).
FeedCoordinator resolves workspace/panel targets (preferring live workspace_id, refcounting overlapping decisions), records targets on waiters during ingest, and clears overlays on reply or timeout without clobbering agent-updated status. Adds feed.status.needsInput localization and tests for the blocking ingest path, decision predicate, and claude → claude_code status key mapping.
Reviewed by Cursor Bugbot for commit 5988f8950284ea796fe91cb5d8f559b3905167a3. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Surfaces in-app “Needs input” for all feed-routed blocking decisions and clears it on resolve or timeout. Fixes #5286 by aligning feed.push with notification hooks and tightening workspace/panel targeting.
- Bug Fixes
- Drive attention in
FeedCoordinator.ingestBlockingforPermissionRequest,ExitPlanMode, andAskUserQuestion: set.needsInput, show localized “Needs input”, ring the bell, and optionally reorder; inactive-app banner remains. - Clear on resolve or timeout with refcounted, race-free logic: record an
AttentionTargeton the waiter inside ingestmain.sync, conclude exactly once in reply/timeout, and don’t remove the shared workspace badge if another panel under the same status key still pends. - Resolve the target accurately and off-main: pre-resolve before
main.sync, prefer liveworkspace_id, fall back to the session store only when it matches the resolved workspace, map surface → owning panel, DEBUG-log when a workspace can’t be resolved, and mapclaude→claude_code; addfeed.status.needsInputand tests for the blocking path, decision predicate, and lifecycle key mapping. - Convert attention tests to Swift Testing and refresh the file-length budget.
- Drive attention in
Summary by CodeRabbit
-
New Features
- In-app "Needs input" attention indicator and transient overlay that surfaces pending actions; can reorder tabs and request system attention when enabled.
-
Localization
- Added English ("Needs input") and Japanese ("入力待ち") translations for the new status label.
-
Tests
- Added automated tests to validate attention-surfacing behavior and to ensure test observers are cleared between runs.