Skip to content

Fix visible helper setup targeting - #6494

Closed
lawrencecchen wants to merge 3 commits into
mainfrom
issue-6491-visible-helper-setup
Closed

lawrencecchen wants to merge 3 commits into
mainfrom
issue-6491-visible-helper-setup

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add helper.visible and cmux visible-helper as the focused-workspace helper setup path.
  • Make caller-vs-focused divergence explicit in responses, with target_workspace_source=focused and caller_focused_diverged.
  • Treat only surface.health in_window=true as visible, waiting on a hosted-view window event for newly created surfaces and failing loudly when structural helper state is not actually visible.
  • Reuse a visible right-side helper surface when possible, without duplicating panes or stealing focus.

Testing

  • swift test --package-path Packages/macOS/CmuxControlSocket --filter ControlCommandCoordinatorHelperTests passed, 12 tests.
  • swift test --package-path Packages/macOS/CmuxControlSocket --filter ControlCommandExecutionPolicyTests passed, 5 tests.
  • ./scripts/reload-cloud.sh --tag vh6491 passed, run https://github.com/manaflow-ai/cmux/actions/runs/27866417817.
  • Tagged app proof on /tmp/cmux-debug-vh6491.sock: fake caller workspace:8, focused workspace:9; visible-helper returned caller_focused_diverged=true, target_workspace_source=focused, workspace_ref=workspace:9, surface_ref=surface:13, surface_health_in_window=true, and surface_window_event_observed=true.
  • Reuse proof: repeated visible-helper reused pane:13/surface:13, created_pane=false, created_surface=false; focused pane count stayed 2.
  • Visibility proof: surface-health --workspace workspace:9 showed surface:13 type=terminal in_window=true; read-screen --workspace workspace:9 --surface surface:13 showed 6491-event-visible-helper; caller workspace:8 still had only its original pane.
  • Screenshot proof: /var/folders/rr/vmfx6xh12dz2tlvgtmyvjmf80000gn/T/cmux-screenshots/vh6491-visible-helper-proof_2026-06-20T09-10-17Z_1DF84E3B.png.

Issues

Summary by CodeRabbit

Release Notes

  • New Features

    • Added cmux visible-helper CLI subcommand to create or reuse helper panes/surfaces and to send an optional initial command only after the target becomes visible/in-window (supports terminal and browser).
    • Added helper.visible to the socket v2 capability set to coordinate helper placement and visibility before sending commands.
    • Added a new notification event when a hosted surface view moves into a window.
  • Documentation

    • Updated CLI help, usage text, and command/flag listings for visible-helper.
  • Tests

    • Added comprehensive helper-visible test coverage for success, reuse, and error flows.

Note

Medium Risk
Changes pane/split layout and automation routing (focused vs caller workspace) with async visibility gates; risk is mitigated by reusing existing pane/surface paths and broad unit tests, but wrong targeting could still confuse agents or duplicate panes in edge layouts.

Overview
Adds cmux visible-helper and the helper.visible socket v2 method so automation can create or reuse a right-side helper pane in the visually focused workspace (not the caller’s CMUX_* context), with responses that spell out target_workspace_source=focused and caller_focused_diverged.

The coordinator reuses a visible right-side pane/surface of the requested type when possible, otherwise pane.create with direction=right and focus=false. Success requires surface.health in_window=true; new surfaces await a surfaceHostedViewDidMoveToWindow notification (terminal + browser) before verifying health and optionally surface.send_text for --command / initial_command. helper.visible runs on the socket worker via handleAsync so waits do not block the main actor.

CLI help, command lists, and printV2Output extraction are included; 12 coordinator helper tests cover reuse, create, visibility failures, and command send errors.

Reviewed by Cursor Bugbot for commit 4d4ef1f. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel

vercel Bot commented Jun 20, 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 Jul 18, 2026 10:18pm

@coderabbitai

coderabbitai Bot commented Jun 20, 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

Adds a visible-helper CLI command and helper.visible socket flow with focused-workspace routing, helper reuse or right-pane creation, visibility verification, window-move notifications, and coverage for success and failure paths.

Changes

helper.visible feature

Layer / File(s) Summary
CLI command and output
CLI/cmux.swift, CLI/CMUXCLI+V2Output.swift, CLI/CMUXCLI+VisibleHelper.swift
Adds command registration, help text, option parsing, caller-context extraction, request submission, and JSON or fallback output.
Socket dispatch and capability wiring
Sources/TerminalController.swift, Sources/TerminalController+SocketWorkerHelperVisible.swift, Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/..., Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Wire/...
Adds v2 handling and capability advertisement, converts request/results, routes async helper dispatch, and assigns socket-worker execution.
Placement and visibility flow
Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/*, Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/*, Sources/TerminalController+ControlSurfaceContext.swift
Validates focused helper requests, selects reuse or creation, verifies UI visibility, waits for hosted-window events, sends optional commands, and returns structured results.
Window signaling and platform integration
Sources/NotificationName+ControlSurfaceWindow.swift, Sources/GhosttyTerminalView.swift, Sources/Panels/*, Sources/TerminalController+ControlPaneContext.swift
Adds hosted-view notifications, terminal and browser notification emission, browser visibility checks, remote-mirror metadata, and browser-creation availability reporting.
Tests and project wiring
Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/*, cmux.xcodeproj/project.pbxproj
Adds configurable test context and broad scenario coverage, updates execution-policy tests, and registers new sources in Xcode.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

  • manaflow-ai/cmux#7357: Refactors the same socket-worker and main-thread dispatch machinery used by helper.visible.

Suggested reviewers: azooz2003-bit


Important

Pre-merge checks failed

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

❌ Failed checks (5 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error Production code adds ContinuousClock().sleep in SurfaceHostedWindowWait.wait(timeout:) plus a fixed 1.5s timeout, which is timing-based synchronization. Replace the timeout/sleep with an owner-held completion signal or async callback from the hosted view, then confirm visibility via health.
Cmux Algorithmic Complexity ❌ Error FAIL: helperVisiblePlacement linearly rescans each pane’s surfaceIDs inside a pane loop and sorts candidates, creating O(panes×surfaces) work on a socket path with no bound/benchmark. Precompute a surfaceID index/Set or reduce in one pass; if the input is truly tiny, document the bound and add a benchmark note.
Cmux User-Facing Error Privacy ❌ Error helper.visible returns raw command plus downstream send_error.message/data in a user-facing error body, violating error privacy rules. Drop command text and nested downstream message/data from API errors; keep only stable codes and safe metadata, and log diagnostics server-side.
Cmux Full Internationalization ❌ Error The PR adds user-facing English help/error copy in CLI and socket responses without localized APIs or matching xcstrings entries. Localize the new visible-helper help and helper.visible error copy with String(localized:defaultValue:) and add matching Resources/Localizable.xcstrings entries for every supported locale.
Cmux Architecture Rethink ❌ Error controlSurfaceWaitForInWindow adds a runtime NotificationCenter wait plus 1.5s timeout, which the rule forbids as a lifecycle repair path. Replace the notification/timeout wait with owner-held readiness completion from the hosted-view owner, then confirm visibility via controlSurfaceHealth.
Docstring Coverage ⚠️ Warning Docstring coverage is 5.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (19 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: improving visible helper setup targeting.
Description check ✅ Passed The description covers summary, testing, and linked issue closure, with only optional template sections missing.
Linked Issues check ✅ Passed The PR satisfies #6491 by targeting the focused workspace, supporting reuse, and adding divergence-focused tests and verification.
Out of Scope Changes check ✅ Passed The added CLI, coordinator, visibility, and test changes all support the visible-helper flow and do not appear unrelated.
Cmux Swift Actor Isolation ✅ Passed PASS: new helper-visible code stays on @MainActor UI/coordinator types, and the nonisolated worker bridge hops to handleAsync; no new shared-mutable Sendable or background UI access was added.
Cmux Browser Automation Off-Main ✅ Passed helper.visible is socket-worker routed with an async main-actor handoff, and browser JS/wait commands remain worker-routed with policy tests; no new browser.* wait moved to main.
Cmux Expensive Synchronous Load ✅ Passed No changed path adds RestorableAgentSessionIndex.load() or similar heavy history scans; the new visible-helper flow only parses small payloads and runs async coordinator work.
Cmux Cache Substitution Correctness ✅ Passed No fresh authoritative read was replaced by a cached value; helper.visible uses live health checks plus an event-driven wait and fresh snapshot verification.
Cmux No Hacky Sleeps ✅ Passed PASS: The diff only changes Swift sources; this rule targets non-Swift runtime scripts, and the only timing primitive found is a Swift ContinuousClock.sleep covered elsewhere.
Cmux Swift Concurrency ✅ Passed The new async code is callback-boundary only: NotificationCenter wait + stored timeout Task, with no new background queues, Combine, or completion-handler APIs.
Cmux Swift @Concurrent ✅ Passed New async helpers are MainActor-bound or explicitly hopped from the socket worker; no added @concurrent misuse or un-hopped heavy async call sites were found.
Cmux Swift Package Boundaries ✅ Passed The new helper.visible/placement/visibility logic lives in Packages/macOS/CmuxControlSocket; app-target edits are CLI/socket/UI glue, which the rule allows.
Cmux Swiftpm Lockfiles ✅ Passed The diff only changes Swift source files; no .gitignore, Package.resolved, or Xcode SwiftPM package-reference hunks are present.
Cmux Swift Logging ✅ Passed No new or modified app/runtime logging was added; only allowed CLI output/test code changes, and existing BrowserPanel NSLog calls were unchanged.
Cmux Swiftui State Layout ✅ Passed PASS: The UI-touching diff only adds a lifecycle notification in an AppKit callback and a computed visibility helper; no new SwiftUI state, GeometryReader, or render-time mutation appears.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR only adds notification plumbing on existing BrowserPanel/Ghostty views; it introduces no new NSWindow/NSPanel/WindowGroup and doesn't touch cmuxAuxiliaryWindowIdentifiers.
Cmux Source Artifacts ✅ Passed All changed paths are source, tests, or build config; none match the rule’s banned artifact directories or temp paths.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The debugWebViewVisibleInUI and debugPortalVisibleInUI properties are not test seams. They are production members used by controlSurfaceHealth() to compute surface visibility for the `helper....
Cmux No Ambient Global State ✅ Passed All new behavior lives on existing owner types/extensions; no new top-level funcs, mutable globals, or singleton state were introduced.
✨ 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-6491-visible-helper-setup
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch issue-6491-visible-helper-setup

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.

@greptile-apps

greptile-apps Bot commented Jun 20, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds cmux visible-helper / helper.visible as a new automation path that creates or reuses a right-side helper pane in the visually focused workspace (not the caller's CMUX_* context), with explicit caller_focused_diverged and target_workspace_source=focused response fields. Visibility is gated on surface.health visibleInUI=true (composite check: in-window, non-hidden, non-zero bounds), and newly created surfaces wait for a shared surfaceHostedViewDidMoveToWindow notification before verifying health; commands are only sent after this gate.

  • New helper.visible socket command routes through ControlCommandCoordinator.handleAsync on the socket worker, with a mutation-start deadline to prevent stale creates; the coordinator reuses visible right-side surfaces of the requested type, blocks on invisible same-type right-side candidates, and creates a new right-side pane otherwise.
  • SurfaceHostedWindowWait is a @MainActor continuation-based helper that subscribes to surfaceHostedViewDidMoveToWindow with a 1.5 s bounded timeout; both terminal and browser panels now post the shared notification.
  • CLI additions in CMUXCLI+VisibleHelper.swift expose --type, --url, --cwd, --command, --window, --json, and -- <command> flags with proper mutual-exclusion validation.

Confidence Score: 4/5

Safe to merge for most cases; the window-event wait carries a small spurious-timeout risk for fast-layout newly created surfaces.

The event-driven SurfaceHostedWindowWait design is sound and correctly isolated to the main actor with no blocking primitives. The placement logic correctly filters candidate panes by requested type. The known concern — that surfaceHostedViewDidMoveToWindow can fire before controlSurfaceWaitForInWindow is reached from the socket worker, leaving the observer registered too late — can produce spurious not_visible timeouts for newly created surfaces when the host view enters the window faster than the socket-worker path completes its pane-creation round-trip. In practice the 1.5 s timeout and the initial visibleInUI fast-path check mitigate most real-world cases, but the edge case remains.

Sources/TerminalController+ControlSurfaceContext.swift — specifically the ordering of the initial visibility check vs. observer registration in controlSurfaceWaitForInWindow.

Important Files Changed

Filename Overview
Sources/TerminalController+ControlSurfaceContext.swift Adds controlSurfaceWaitForInWindow + SurfaceHostedWindowWait; the notification-based wait is correctly main-actor-isolated, but a timing gap between pane creation completing and the observer being installed can cause spurious not_visible timeouts for newly created surfaces.
Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator+Helper.swift New helper.visible coordinator entry-point; routes correctly through handleHelperAsync for socket-worker-only execution; blockedInvisible path now correctly filters by requestedType.
Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator+HelperVisibility.swift Verification, annotation, and error formatting for the helper-visible flow; correctly separates inWindow (diagnostic) from visibleInUI (gate); command-delivery path is clear.
Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator+HelperPlacement.swift Placement logic correctly prioritises right-side visible panes of the requested type, blocks on right-side invisible same-type candidates, and falls through to create when no matching right-side candidate exists.
Sources/TerminalController+SocketWorkerHelperVisible.swift Bridges V2 socket request to handleAsync; correctly stamps a mutation-start deadline before the async dispatch and converts results back to wire format.
Sources/GhosttyTerminalView.swift Posts both the new shared surfaceHostedViewDidMoveToWindow and the legacy terminalSurfaceHostedViewDidMoveToWindow notifications in viewDidMoveToWindow, preserving backward compatibility.
Sources/Panels/BrowserPanel.swift Adds debugWebViewVisibleInUI (production-called composite visibility check); naming uses the prohibited debug-prefix pattern for new production members.
Sources/Panels/BrowserPanelView.swift Posts surfaceHostedViewDidMoveToWindow inside the onDidMoveToWindow guard (host.window != nil), so the notification only fires on window entry, not removal.
CLI/CMUXCLI+VisibleHelper.swift Well-structured CLI subcommand; correctly wires caller context from environment variables, validates mutual exclusion of --command and -- command, and forwards all relevant params.
Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/ControlCommandCoordinator.swift Adds handleAsync public entry-point that dispatches to async helper domain first, then falls through to sync handle; clean layering with no regressions to existing domains.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant CLI as cmux CLI
    participant SW as Socket Worker
    participant CC as ControlCommandCoordinator
    participant MA as Main Actor (TerminalController)
    participant NC as NotificationCenter

    CLI->>SW: "helper.visible {target=focused, type, ...}"
    SW->>SW: stamp mutation deadline
    SW->>CC: handleAsync(request)
    CC->>MA: controlSystemIdentify → focusedWorkspaceID
    CC->>MA: controlPaneList(routing)
    CC->>MA: controlSurfaceHealth(routing)
    alt reuse visible right-side pane
        CC->>MA: "helperVisibleSurfaceVisibility(waitForWindowEvent=false)"
        MA-->>CC: visibleInUI snapshot
    else create new pane
        CC->>MA: "paneCreate(direction=right, focus=false)"
        MA-->>CC: pane_id / surface_id
        CC->>MA: controlSurfaceWaitForInWindow(surfaceID)
        MA->>NC: addObserver(surfaceHostedViewDidMoveToWindow)
        NC-->>MA: notification fires
        MA->>MA: isVisible() check
        MA-->>CC: "observed=true"
        CC->>MA: controlSurfaceHealth final verify
    end
    CC->>MA: surfaceSendText(command)
    CC-->>SW: ControlCallResult
    SW-->>CLI: JSON response
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant CLI as cmux CLI
    participant SW as Socket Worker
    participant CC as ControlCommandCoordinator
    participant MA as Main Actor (TerminalController)
    participant NC as NotificationCenter

    CLI->>SW: "helper.visible {target=focused, type, ...}"
    SW->>SW: stamp mutation deadline
    SW->>CC: handleAsync(request)
    CC->>MA: controlSystemIdentify → focusedWorkspaceID
    CC->>MA: controlPaneList(routing)
    CC->>MA: controlSurfaceHealth(routing)
    alt reuse visible right-side pane
        CC->>MA: "helperVisibleSurfaceVisibility(waitForWindowEvent=false)"
        MA-->>CC: visibleInUI snapshot
    else create new pane
        CC->>MA: "paneCreate(direction=right, focus=false)"
        MA-->>CC: pane_id / surface_id
        CC->>MA: controlSurfaceWaitForInWindow(surfaceID)
        MA->>NC: addObserver(surfaceHostedViewDidMoveToWindow)
        NC-->>MA: notification fires
        MA->>MA: isVisible() check
        MA-->>CC: "observed=true"
        CC->>MA: controlSurfaceHealth final verify
    end
    CC->>MA: surfaceSendText(command)
    CC-->>SW: ControlCallResult
    SW-->>CLI: JSON response
Loading

Reviews (4): Last reviewed commit: "Bound visible helper setup failure paths" | Re-trigger Greptile

Comment on lines +209 to +215
let hasAnyVisibleCandidate = orderedCandidates.contains { pane in
pane.surfaceIDs.contains { visibleEntriesByID[$0] != nil }
}
if hasAnyVisibleCandidate {
return .create
}
return .blockedInvisible(orderedCandidates.first ?? candidates[0])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 blockedInvisible does not filter by requested type, blocking creation of a different-type helper

hasAnyVisibleCandidate checks all surface IDs across all candidate panes with no type guard. If the only non-focused pane is an invisible browser helper and the caller requests type=terminal, hasAnyVisibleCandidate will be false and the function returns .blockedInvisible(browser pane). The caller then gets a not_visible error and cannot create a terminal helper, even though no same-type helper pane exists to duplicate.

The reuse loop at line 201–207 correctly filters by requestedType, but the fallback guard at line 209–215 does not, making the type check asymmetric. An invisible same-type candidate should block creation; an invisible wrong-type candidate should allow it.

@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 `@CLI/cmux.swift`:
- Line 34323: The compact usage line for the visible-helper command is missing
the --working-directory flag that is actually parsed and documented elsewhere in
the codebase. Update the visible-helper usage line to include
--working-directory (or its short form if one exists) between the existing flags
to ensure consistency with the actual parser implementation and documentation,
so users can discover this option from the compact command list.
🪄 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: 92f3edec-6ccd-4f34-b9ae-7626175299eb

📥 Commits

Reviewing files that changed from the base of the PR and between bbed84b and 539cbc6.

📒 Files selected for processing (5)
  • CLI/cmux.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/ControlCommandCoordinator.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator+Helper.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorHelperTests.swift
  • Sources/TerminalController.swift

Comment thread CLI/cmux.swift Outdated
@lawrencecchen
lawrencecchen force-pushed the issue-6491-visible-helper-setup branch from 539cbc6 to 6f5d333 Compare June 20, 2026 08:43
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Updated divergence dogfood after adding the bounded surface-health retry.

Proof:

  • swift test --package-path Packages/macOS/CmuxControlSocket --filter ControlCommandCoordinatorHelperTests passed, 8 tests.
  • Rebuilt tag vh6491 with ./scripts/reload-cloud.sh --tag vh6491, run https://github.com/manaflow-ai/cmux/actions/runs/27865991537.
  • In tagged app, created fake caller workspace:4 and focused workspace:5, then ran visible-helper with CMUX_WORKSPACE_ID/CMUX_SURFACE_ID from workspace:4 while workspace:5 was focused.
  • Result: caller_focused_diverged=true, workspace_ref=workspace:5, created_pane=true, surface_visible=true, surface_health_in_window=true, surface_health_attempts=2.
  • read-screen --workspace workspace:5 --surface surface:8 showed 6491-divergence-visible-helper-retry.
  • list-panes --workspace workspace:4 still had only the original caller pane.
  • Screenshot: /var/folders/rr/vmfx6xh12dz2tlvgtmyvjmf80000gn/T/cmux-screenshots/vh6491-divergence-helper_2026-06-20T08-49-43Z_18BDE937.png.

@lawrencecchen
lawrencecchen force-pushed the issue-6491-visible-helper-setup branch from 6f5d333 to fdd4f33 Compare June 20, 2026 09:10

@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
`@Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorHelperTests.swift`:
- Around line 162-171: The controlSurfaceWaitForInWindow method in the test
double always returns true, which prevents testing the branch where this method
returns false. Add a configurable property (such as surfaceWindowWaitResult) to
the test double class that defaults to true, then modify the
controlSurfaceWaitForInWindow method to return this configurable property
instead of the hardcoded true value. This will allow tests to set the property
to false and verify the code path where no in-window event is observed.
🪄 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: 0bd354ea-dc9a-4cd8-bca9-a8432b770507

📥 Commits

Reviewing files that changed from the base of the PR and between 6f5d333 and fdd4f33.

📒 Files selected for processing (12)
  • CLI/cmux.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/ControlCommandCoordinator.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator+Helper.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceContext.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCommandExecutionPolicy.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorHelperTests.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandExecutionPolicyTests.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/Panels/BrowserPanelView.swift
  • Sources/TerminalController+ControlSurfaceContext.swift
  • Sources/TerminalController.swift

Comment on lines +162 to +171
func controlSurfaceWaitForInWindow(
routing: ControlRoutingSelectors,
surfaceID: UUID
) async -> Bool {
surfaceWindowWaits.append((routing, surfaceID))
if let visibleAfterWindowEvent = createdSurfaceVisibleAfterWindowEvent {
createdSurfaceVisible = visibleAfterWindowEvent
}
return true
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Make wait outcome configurable in the test double.

controlSurfaceWaitForInWindow always returns true, so you can’t cover the branch where no in-window event is observed (surface_window_event_observed == false). Add a configurable return value (e.g., surfaceWindowWaitResult) and a test for that path.

🤖 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
`@Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorHelperTests.swift`
around lines 162 - 171, The controlSurfaceWaitForInWindow method in the test
double always returns true, which prevents testing the branch where this method
returns false. Add a configurable property (such as surfaceWindowWaitResult) to
the test double class that defaults to true, then modify the
controlSurfaceWaitForInWindow method to return this configurable property
instead of the hardcoded true value. This will allow tests to set the property
to false and verify the code path where no in-window event is observed.

@lawrencecchen
lawrencecchen force-pushed the issue-6491-visible-helper-setup branch from fdd4f33 to 6838a0b Compare June 20, 2026 09:24
Comment thread Sources/TerminalController+SocketWorkerHelperVisible.swift
if !rightSide.isEmpty {
return rightSide
}
return []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing frames skip helper reuse

Medium Severity

When the focused pane has a pixelFrame but no non-focused pane passes the right-side geometry filter (including when candidate panes lack pixelFrame), helperVisibleOrderedCandidatePanes returns an empty list. Placement then skips reuse and blocked-invisible checks and always chooses create, which can add an extra right split beside an existing structural helper pane.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6838a0b. Configure here.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Latest proof after commit 6838a0b:

  • ./scripts/reload-cloud.sh --tag vh6491 passed in https://github.com/manaflow-ai/cmux/actions/runs/27866917955.
  • Tagged app identified on /tmp/cmux-debug-vh6491.sock as com.cmuxterm.app.debug.vh6491.
  • Caller/focused divergence repro: caller workspace:10 / surface:14, visually focused workspace:11.
  • cmux visible-helper returned caller_focused_diverged=true, target_workspace_source=focused, workspace_ref=workspace:11, pane_ref=pane:16, surface_ref=surface:16, surface_health_in_window=true, and surface_window_event_observed=true.
  • Reuse pass returned placement_strategy=reused_right_pane, created_pane=false, created_surface=false; focused pane count stayed 2 -> 2.
  • Visibility proof: surface-health --workspace workspace:11 showed surface:16 type=terminal in_window=true, and read-screen --workspace workspace:11 --surface surface:16 showed vh6491-final-visible-helper.
  • Screenshot proof: /var/folders/rr/vmfx6xh12dz2tlvgtmyvjmf80000gn/T/cmux-screenshots/vh6491-final-visible-helper-proof_2026-06-20T09-29-47Z_D7D5C6D9.png.

This is the regression path from #6491: structural pane/surface existence is not accepted unless surface.health reports in_window=true.

@lawrencecchen
lawrencecchen force-pushed the issue-6491-visible-helper-setup branch from 6838a0b to 4d4ef1f Compare June 20, 2026 09:33

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

There are 2 total unresolved issues (including 1 from previous review).

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 4d4ef1f. Configure here.

reason: "didMoveToWindow"
) else { return }
guard host.window != nil else { return }
browserPanel.postSurfaceHostedViewDidMoveToWindow()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Browser visibility check races bind

High Severity

For browser helpers, helper.visible waits on surfaceHostedViewDidMoveToWindow, then requires surface.health in_window=true via webView.window. The browser posts that notification before portal bind, so the health read can run while webView.window is still nil and the command fails with not_visible despite a valid helper surface.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4d4ef1f. Configure here.

@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: 2

🤖 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
`@Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator`+HelperPlacement.swift:
- Around line 102-127: The helperVisibleOrderedCandidatePanes function returns
Array(candidates.reversed()) as a fallback when focusedPane or focusedFrame is
unavailable, but this behavior lacks explanation. Add a clear comment above the
return statement on line 126 explaining why the candidate order is reversed in
this fallback case, what it represents in the context of helper pane placement,
and whether it reflects a deliberate ordering strategy or serves as a neutral
default when focused pane information is not available.

In `@Sources/TerminalController`+ControlSurfaceContext.swift:
- Around line 175-185: There is a race condition between the health check at the
start of the function and the notification subscription. If the surface moves to
window between the one-time health check (line 175) and when the notification
observer actually starts listening (line 182), the event will be missed and the
code may wait indefinitely. Reorder the logic by setting up the notification
subscription with NotificationCenter.default.notifications before performing the
health check, so that any surface visibility changes are captured either by the
health check or by the notification observer, closing the window where events
can be missed between these two steps.
🪄 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: ab4e2a01-5f8d-4675-9b98-40019012d30f

📥 Commits

Reviewing files that changed from the base of the PR and between fdd4f33 and 6838a0b.

📒 Files selected for processing (21)
  • CLI/CMUXCLI+V2Output.swift
  • CLI/CMUXCLI+VisibleHelper.swift
  • CLI/cmux.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/ControlCommandCoordinator.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator+Helper.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator+HelperPlacement.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator+HelperVisibility.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceContext.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCommandExecutionPolicy.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorHelperTestSupport.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorHelperTests.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandExecutionPolicyTests.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/NotificationName+ControlSurfaceWindow.swift
  • Sources/Panels/BrowserPanel+SurfaceHostedWindowNotification.swift
  • Sources/Panels/BrowserPanelView.swift
  • Sources/TerminalController+ControlSurfaceContext.swift
  • Sources/TerminalController+SocketWorkerHelperVisible.swift
  • Sources/TerminalController.swift
  • cmux.xcodeproj/project.pbxproj

Comment on lines +102 to +127
private func helperVisibleOrderedCandidatePanes(
_ candidates: [ControlPaneSummary],
in snapshot: ControlPaneListSnapshot,
focusedPaneID: UUID?
) -> [ControlPaneSummary] {
if let focusedPane = focusedPaneID.flatMap({ id in snapshot.panes.first { $0.paneID == id } }),
let focusedFrame = focusedPane.pixelFrame {
let rightSide = candidates.filter { pane in
guard let frame = pane.pixelFrame else { return false }
return frame.x >= focusedFrame.x + (focusedFrame.width * 0.5)
}.sorted { lhs, rhs in
let lhsFrame = lhs.pixelFrame!
let rhsFrame = rhs.pixelFrame!
if lhsFrame.x == rhsFrame.x {
return lhsFrame.y < rhsFrame.y
}
return lhsFrame.x < rhsFrame.x
}
if !rightSide.isEmpty {
return rightSide
}
return []
}

return Array(candidates.reversed())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for comments or related logic explaining the reversed() fallback.

# Check for related comments or documentation
rg -nC5 'helperVisibleOrderedCandidatePanes|reversed.*candidate' --type=swift

Repository: manaflow-ai/cmux

Length of output: 3714


🏁 Script executed:

# Read the full file to understand the function context and purpose
wc -l "Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator+HelperPlacement.swift"

Repository: manaflow-ai/cmux

Length of output: 190


🏁 Script executed:

# Get full file with context around the function
head -n 140 "Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator+HelperPlacement.swift" | tail -n +30

Repository: manaflow-ai/cmux

Length of output: 4393


🏁 Script executed:

# Search for any design comments or related logic about ordering/placement strategy
rg -n 'placement|order|visible|reversed' "Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator+HelperPlacement.swift" -B2 -A2

Repository: manaflow-ai/cmux

Length of output: 2119


Add a comment explaining the reversed() fallback behavior.

When focusedPane or focusedFrame is unavailable (line 126), the fallback returns Array(candidates.reversed()) without describing why reversing the candidate order is the intended behavior. The right-side spatial logic (lines 109–123) is clear, but the fallback strategy needs a comment explaining what "reversed" represents in the context of helper pane placement and whether it reflects a deliberate ordering strategy or a neutral default.

🤖 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
`@Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator`+HelperPlacement.swift
around lines 102 - 127, The helperVisibleOrderedCandidatePanes function returns
Array(candidates.reversed()) as a fallback when focusedPane or focusedFrame is
unavailable, but this behavior lacks explanation. Add a clear comment above the
return statement on line 126 explaining why the candidate order is reversed in
this fallback case, what it represents in the context of helper pane placement,
and whether it reflects a deliberate ordering strategy or serves as a neutral
default when focused pane information is not available.

Comment on lines +175 to +185
if controlSurfaceHealth(routing: routing)?
.surfaces
.first(where: { $0.surfaceID == surfaceID })?
.inWindow == true {
return true
}

let notifications = NotificationCenter.default.notifications(
named: .surfaceHostedViewDidMoveToWindow,
object: nil
)

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

Close the visibility wait race between health read and notification subscription.

Line 175 does a one-time health check before Line 182 starts observing notifications. If the surface becomes in-window between those two steps, the event can be missed and this loop may wait indefinitely for a later move event. That can stall helper.visible even though the surface is already visible.

Suggested fix
 func controlSurfaceWaitForInWindow(
     routing: ControlRoutingSelectors,
     surfaceID: UUID
 ) async -> Bool {
-    if controlSurfaceHealth(routing: routing)?
-        .surfaces
-        .first(where: { $0.surfaceID == surfaceID })?
-        .inWindow == true {
-        return true
-    }
-
     let notifications = NotificationCenter.default.notifications(
         named: .surfaceHostedViewDidMoveToWindow,
         object: nil
     )
+    // Re-check after subscription starts to avoid missing a just-fired event.
+    if controlSurfaceHealth(routing: routing)?
+        .surfaces
+        .first(where: { $0.surfaceID == surfaceID })?
+        .inWindow == true {
+        return true
+    }
     for await notification in notifications {
         if Task.isCancelled {
             return false
         }
         guard let hostedSurfaceID = notification.userInfo?["surfaceId"] as? UUID,
               hostedSurfaceID == surfaceID else {
             continue
         }
         return true
     }
     return false
 }
🤖 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/TerminalController`+ControlSurfaceContext.swift around lines 175 -
185, There is a race condition between the health check at the start of the
function and the notification subscription. If the surface moves to window
between the one-time health check (line 175) and when the notification observer
actually starts listening (line 182), the event will be missed and the code may
wait indefinitely. Reorder the logic by setting up the notification subscription
with NotificationCenter.default.notifications before performing the health
check, so that any surface visibility changes are captured either by the health
check or by the notification observer, closing the window where events can be
missed between these two steps.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Final proof after commit 4d4ef1f5c6ffbb5845c0d6ad890226362f81b67b:

  • ./scripts/reload-cloud.sh --tag vh6491 passed in https://github.com/manaflow-ai/cmux/actions/runs/27867132348.
  • Fresh tagged process identified on /tmp/cmux-debug-vh6491.sock as com.cmuxterm.app.debug.vh6491.
  • Caller/focused divergence repro: caller workspace:12 / surface:17, visually focused workspace:13.
  • cmux visible-helper returned caller_focused_diverged=true, target_workspace_source=focused, workspace_ref=workspace:13, pane_ref=pane:19, surface_ref=surface:19, surface_health_in_window=true, and surface_window_event_observed=true.
  • Reuse pass returned placement_strategy=reused_right_pane, created_pane=false, created_surface=false, reused_pane=true; focused pane count stayed 2, caller pane count stayed 1.
  • Visibility proof: surface-health --workspace workspace:13 showed surface:19 type=terminal in_window=true; read-screen --workspace workspace:13 --surface surface:19 showed vh6491-final-4d4ef1f5c-visible-helper as actual terminal output.
  • Screenshot proof: /var/folders/rr/vmfx6xh12dz2tlvgtmyvjmf80000gn/T/cmux-screenshots/vh6491-final-4d4-visible-helper-proof_2026-06-20T09-38-23Z_1581D729.png.

This tests the exact failure mode from #6491: structural pane/surface existence is insufficient unless surface.health reports in_window=true.

Comment on lines +171 to +210
func controlSurfaceWaitForInWindow(
routing: ControlRoutingSelectors,
surfaceID: UUID
) async -> Bool {
if controlSurfaceHealth(routing: routing)?
.surfaces
.first(where: { $0.surfaceID == surfaceID })?
.inWindow == true {
return true
}

let (surfaceIDs, surfaceIDContinuation) = AsyncStream<UUID>.makeStream(
bufferingPolicy: .bufferingNewest(1)
)
let observer = NotificationCenter.default.addObserver(
forName: .surfaceHostedViewDidMoveToWindow,
object: nil,
queue: nil
) { notification in
guard let hostedSurfaceID = notification.userInfo?["surfaceId"] as? UUID else {
return
}
surfaceIDContinuation.yield(hostedSurfaceID)
}
defer {
surfaceIDContinuation.finish()
NotificationCenter.default.removeObserver(observer)
}

for await hostedSurfaceID in surfaceIDs {
if Task.isCancelled {
return false
}
guard hostedSurfaceID == surfaceID else {
continue
}
return true
}
return false
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 TOCTOU race: observer registered after health check, notification silently lost

The initial health check (line 175) fires before the NotificationCenter observer is installed (line 185). If surfaceHostedViewDidMoveToWindow posts between those two points — which is plausible because paneCreate dispatches UI work to the main actor and that work can complete before the socket worker resumes here — the notification is never yielded to the AsyncStream and the wait hangs until the 2-second outer timeout cancels the task, producing a spurious not_visible or timeout error even though the pane was created successfully.

The correct pattern is: create the stream and register the observer first, then do the initial health check. Any notification that fires after observer registration but before the for await begins is buffered (the policy already uses .bufferingNewest(1)), so the fast path still works without data races.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Additional tagged dogfood with real Claude/Codex sessions on vh6491:

  • Targeted only /tmp/cmux-debug-vh6491.sock on com.cmuxterm.app.debug.vh6491.
  • Created focused decoy workspace:14, then spawned background agent workspaces with --focus false: Claude workspace:15 / surface:21, Codex workspace:16 / surface:22.
  • Immediately after spawning, focus stayed on workspace:14; decoy pane count stayed 1 -> 1.
  • Claude Code rendered in workspace:15, wrote /tmp/vh6491-claude-agent-proof.txt, ran pwd, launched and completed a Task subagent, then completed a longer Task subagent loop.
  • Codex rendered in workspace:16, wrote /tmp/vh6491-codex-agent-proof.txt, ran pwd, spawned/completed a short subagent, then spawned a longer background subagent. top showed Codex process/tag ownership under workspace:16; decoy top had no Claude/Codex/tag processes.
  • Focusing Claude then Codex proved rendering: surface-health --workspace workspace:15 showed surface:21 in_window=true; surface-health --workspace workspace:16 showed surface:22 in_window=true.
  • Returned focus to decoy workspace:14; final pane counts were decoy=1, claude=1, codex=1. No agent/session activity created splits in the focused decoy workspace.
  • Screenshots: /var/folders/rr/vmfx6xh12dz2tlvgtmyvjmf80000gn/T/cmux-screenshots/vh6491-agent-claude-visible_2026-06-20T10-03-25Z_0699E314.png and /var/folders/rr/vmfx6xh12dz2tlvgtmyvjmf80000gn/T/cmux-screenshots/vh6491-agent-codex-visible_2026-06-20T10-03-28Z_59C93D43.png.

@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: 4

🤖 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 `@CLI/CMUXCLI`+VisibleHelper.swift:
- Around line 112-123: Update callerContextFromEnvironment() to return [String:
Any] instead of an optional dictionary, returning caller directly so empty
results produce an empty collection. Adjust its corresponding call site to
remove optional binding and handle the non-optional dictionary while preserving
existing caller-context behavior.

In
`@Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator`+HelperVisibility.swift:
- Around line 84-93: Update the failed-send handling in
helperVisibleCommandError and the related path around the additional referenced
branch so API responses contain only safe status metadata: remove the command
and any raw downstream sendResult/error payload from the returned response.
Preserve sent_command as false and keep detailed diagnostics only in sanitized
internal logs.
- Around line 101-103: Update helperVisibleCommandText to retrieve the raw
“command” or “initial_command” value without trimming and return it unchanged.
Perform any required non-empty validation using a trimmed copy, while preserving
all leading, trailing, and multiline whitespace in the returned command text.

In `@Sources/TerminalController`+ControlSurfaceContext.swift:
- Around line 183-199: Replace the SurfaceHostedWindowWait-based logic in
controlSurfaceWaitForInWindow with an async readiness completion owned by the
terminal/browser hosted-view lifecycle. Thread that completion through
hosted-view creation, await it after the initial visibility check, then confirm
readiness using controlSurfaceHealth. Remove the process-wide notification and
fixed 1.5-second timeout from this synchronization path, keeping lifecycle state
under the explicit owner.
🪄 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: 31d0c73a-762c-45e6-906f-00ea88b6154f

📥 Commits

Reviewing files that changed from the base of the PR and between 6838a0b and f448dcb.

📒 Files selected for processing (29)
  • CLI/CMUXCLI+V2Output.swift
  • CLI/CMUXCLI+VisibleHelper.swift
  • CLI/cmux.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/ControlCommandCoordinator.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator+Helper.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator+HelperPlacement.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator+HelperVisibility.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator+HelperVisibleIdentify.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator+HelperVisiblePlacement.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Pane/ControlPaneContext.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Pane/ControlPaneListSnapshot.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceContext.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceHealthEntry.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Surface/ControlSurfaceHealthSnapshot.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCommandExecutionPolicy.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorHelperTestSupport.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandCoordinatorHelperTests.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandExecutionPolicyTests.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/NotificationName+ControlSurfaceWindow.swift
  • Sources/Panels/BrowserPanel+SurfaceHostedWindowNotification.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/BrowserPanelView.swift
  • Sources/TerminalController+ControlPaneContext.swift
  • Sources/TerminalController+ControlSurfaceContext.swift
  • Sources/TerminalController+SocketWorkerHelperVisible.swift
  • Sources/TerminalController.swift
  • cmux.xcodeproj/project.pbxproj

Comment on lines +112 to +123
private func callerContextFromEnvironment() -> [String: Any]? {
let environment = ProcessInfo.processInfo.environment
var caller: [String: Any] = [:]
if let workspace = environment["CMUX_WORKSPACE_ID"]?.nilIfEmpty {
caller["workspace_id"] = workspace
}
if let surface = (environment["CMUX_SURFACE_ID"] ?? environment["CMUX_TAB_ID"])?.nilIfEmpty {
caller["surface_id"] = surface
caller["tab_id"] = surface
}
return caller.isEmpty ? nil : caller
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer empty collection over optional collection.

As highlighted by SwiftLint, it is idiomatic in Swift to return an empty collection rather than an optional collection. This simplifies the return type and avoids the need for optional binding at the call site.

♻️ Proposed refactor
-    private func callerContextFromEnvironment() -> [String: Any]? {
+    private func callerContextFromEnvironment() -> [String: Any] {
         let environment = ProcessInfo.processInfo.environment
         var caller: [String: Any] = [:]
         if let workspace = environment["CMUX_WORKSPACE_ID"]?.nilIfEmpty {
             caller["workspace_id"] = workspace
         }
         if let surface = (environment["CMUX_SURFACE_ID"] ?? environment["CMUX_TAB_ID"])?.nilIfEmpty {
             caller["surface_id"] = surface
             caller["tab_id"] = surface
         }
-        return caller.isEmpty ? nil : caller
+        return caller
     }

Update the corresponding call site (lines 31-34) to match:

         var params: [String: Any] = ["target": "focused"]
-        if let caller = callerContextFromEnvironment() {
+        let caller = callerContextFromEnvironment()
+        if !caller.isEmpty {
             params["caller"] = caller
         }
🧰 Tools
🪛 SwiftLint (0.65.0)

[Warning] 112-112: Prefer empty collection over optional collection

(discouraged_optional_collection)

🤖 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 `@CLI/CMUXCLI`+VisibleHelper.swift around lines 112 - 123, Update
callerContextFromEnvironment() to return [String: Any] instead of an optional
dictionary, returning caller directly so empty results produce an empty
collection. Adjust its corresponding call site to remove optional binding and
handle the non-optional dictionary while preserving existing caller-context
behavior.

Source: Linters/SAST tools

Comment on lines +84 to +93
guard case .ok(.object(let sendPayload)) = sendResult else {
payload["sent_command"] = .bool(false)
payload["command"] = .string(command)
return helperVisibleCommandError(
sendResult,
identify: identify,
focusedWorkspaceID: focusedWorkspaceID,
focusedWindowID: focusedWindowID,
payload: payload
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not echo commands or raw downstream errors in the API response.

A failed send currently returns the complete command plus unsanitized nested error message/data. Commands may contain credentials or private content; expose only safe status metadata and keep detailed diagnostics in sanitized logs.

As per coding guidelines, user-facing API errors must not expose raw payloads or upstream messages.

Proposed sanitization
-                payload["command"] = .string(command)
+                payload["command_present"] = .bool(true)
+                payload["command_bytes"] = .int(Int64(command.utf8.count))
...
-        if case .err(let code, let message, let data) = sendResult {
+        if case .err(let code, _, _) = sendResult {
             extra["send_error"] = .object([
                 "code": .string(code),
-                "message": .string(message),
-                "data": data ?? .null,
             ])
         }

Also applies to: 126-147

🤖 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
`@Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator`+HelperVisibility.swift
around lines 84 - 93, Update the failed-send handling in
helperVisibleCommandError and the related path around the additional referenced
branch so API responses contain only safe status metadata: remove the command
and any raw downstream sendResult/error payload from the returned response.
Preserve sent_command as false and keep detailed diagnostics only in sanitized
internal logs.

Source: Coding guidelines

Comment on lines +101 to +103
private func helperVisibleCommandText(_ params: [String: JSONValue]) -> String? {
string(params, "command") ?? string(params, "initial_command")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the command bytes instead of trimming them.

string(...) strips leading and trailing whitespace, changing shell semantics such as history-suppression prefixes and multiline commands. Validate with a trimmed copy but return the original value.

Proposed fix
 private func helperVisibleCommandText(_ params: [String: JSONValue]) -> String? {
-    string(params, "command") ?? string(params, "initial_command")
+    for key in ["command", "initial_command"] {
+        guard case .string(let raw)? = params[key] else { continue }
+        if !raw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
+            return raw
+        }
+    }
+    return nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private func helperVisibleCommandText(_ params: [String: JSONValue]) -> String? {
string(params, "command") ?? string(params, "initial_command")
}
private func helperVisibleCommandText(_ params: [String: JSONValue]) -> String? {
for key in ["command", "initial_command"] {
guard case .string(let raw)? = params[key] else { continue }
if !raw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return raw
}
}
return nil
}
🤖 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
`@Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Helper/ControlCommandCoordinator`+HelperVisibility.swift
around lines 101 - 103, Update helperVisibleCommandText to retrieve the raw
“command” or “initial_command” value without trimming and return it unchanged.
Perform any required non-empty validation using a trimmed copy, while preserving
all leading, trailing, and multiline whitespace in the returned command text.

Comment on lines +183 to +199
func controlSurfaceWaitForInWindow(
routing: ControlRoutingSelectors,
surfaceID: UUID
) async -> Bool {
if controlSurfaceIsVisibleInTargetUI(routing: routing, surfaceID: surfaceID) {
return true
}

return await SurfaceHostedWindowWait(
surfaceID: surfaceID,
isVisible: { [weak self] in
self?.controlSurfaceIsVisibleInTargetUI(routing: routing, surfaceID: surfaceID) == true
}
).wait(
timeout: .milliseconds(1_500)
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Replace the notification-and-timeout readiness repair with an owner-held completion signal.

This waits on a process-wide notification and a fixed 1.5-second clock. If the move event arrives before layout makes isVisible true, it is discarded and readiness depends on the timeout, causing latency or false failures. Thread an async readiness completion from the terminal/browser hosted-view owner through creation, then confirm with controlSurfaceHealth.

As per coding guidelines, lifecycle/rendering synchronization must not use notification waits or fixed delays to repair races, and lifecycle state must remain under one explicit owner.

Also applies to: 244-301

🤖 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/TerminalController`+ControlSurfaceContext.swift around lines 183 -
199, Replace the SurfaceHostedWindowWait-based logic in
controlSurfaceWaitForInWindow with an async readiness completion owned by the
terminal/browser hosted-view lifecycle. Thread that completion through
hosted-view creation, await it after the initial visibility check, then confirm
readiness using controlSurfaceHealth. Remove the process-wide notification and
fixed 1.5-second timeout from this synchronization path, keeping lifecycle state
under the explicit owner.

Source: Coding guidelines

@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 — f448dcba Deployed Jul 18, 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.

Make visible workspace setup hard to get wrong

2 participants