Fix #5286: surface needs-input attention for feed-routed blocking decisions

Merged
AU
5mo ago
+515-2

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 .needsInput and 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's pre-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:

  1. Failing test — FeedCoordinatorTests.testBlockingIngestSurfacesNeedsInputAttentionForPermissionRequest asserts that a blocking PermissionRequest routed through ingestBlocking requests in-app attention surfacing. Red before the fix.
  2. 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


View with Codesmith Autofix with Codesmith
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.ingestBlocking for PermissionRequest, ExitPlanMode, and AskUserQuestion: 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 AttentionTarget on the waiter inside ingest main.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 live workspace_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 map claude → claude_code; add feed.status.needsInput and tests for the blocking path, decision predicate, and lifecycle key mapping.
    • Convert attention tests to Swift Testing and refresh the file-length budget.

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.