Skip to content

Fix background new-workspace commands - #4137

Merged
lawrencecchen merged 7 commits into
mainfrom
issue-4090-new-workspace-command-unfocused
May 15, 2026
Merged

lawrencecchen merged 7 commits into
mainfrom
issue-4090-new-workspace-command-unfocused

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 14, 2026 •

Copy link
Copy Markdown
Contributor

Closes #4090

Summary:

  • Allows explicit socket/API runtime demand to start a terminal from its attached Ghostty view while the workspace remains off-window.
  • Starts layout command terminals when their pending input observer is installed, so cmux new-workspace --layout commands do not expire in the background.
  • Captures terminal port ordinal at surface construction so off-window startup keeps the correct per-workspace port range.

Red failure:

  • CMUXTERM_CLI="$(cat /tmp/cmux-last-cli-path)" CMUX_SOCKET_PATH=/tmp/cmux-debug-i4090.sock python3 tests_v2/test_cli_new_workspace_layout_command_queue.py failed before the fix: Layout command marker file was not created.
  • Existing main regression also failed before the fix: test_cli_new_workspace_command_queue.py did not create its command marker file.

Green verification:

  • ./scripts/reload.sh --tag i4090f --launch
  • CMUXTERM_CLI="$(cat /tmp/cmux-last-cli-path)" CMUX_SOCKET_PATH=/tmp/cmux-debug-i4090f.sock python3 tests_v2/test_cli_new_workspace_command_queue.py passed.
  • CMUXTERM_CLI="$(cat /tmp/cmux-last-cli-path)" CMUX_SOCKET_PATH=/tmp/cmux-debug-i4090f.sock python3 tests_v2/test_cli_new_workspace_layout_command_queue.py passed.
  • CMUX_SOCKET_PATH=/tmp/cmux-debug-i4090f.sock python3 tests_v2/test_workspace_create_background_starts_terminal.py passed.
  • git diff --check passed.

Cloud proof:


Note

Medium Risk
Changes terminal surface startup conditions to allow off-window runtime creation for socket/API-triggered commands, which could affect PTY lifecycle and resource usage if mis-triggered. Mitigated by keeping the normal UI-driven attach path window-gated and adding regression tests for new-workspace command/layout queuing.

Overview
Fixes background new-workspace command execution by letting explicit socket/API requests start a TerminalSurface runtime from an attached view even when it is not yet in a window (via requestBackgroundSurfaceStartIfNeeded(allowOffWindow:)), while keeping the normal visual attach path window-gated.

Makes per-workspace port range selection deterministic by capturing portOrdinal at TerminalSurface construction (and wiring it through TerminalPanel). Updates workspace pending-input wiring to trigger background surface starts when observers are installed, and adds/extends Python regression tests with configurable CLI timeouts plus a new --layout command-queue test.

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

Summary by CodeRabbit

  • Bug Fixes

    • More deterministic terminal startup and background-start behavior to reduce race conditions and improve reliability when creating terminals.
  • Behavior

    • Clarified and enforced deferred visual/runtime surface creation until a view is present, while allowing controlled off-window startup when needed.
  • Tests

    • Added regression tests covering workspace/layout/command execution and introduced configurable timeouts for more reliable CI validation.

Review Change Stack

@vercel

vercel Bot commented May 14, 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 May 15, 2026 1:43pm
cmux-staging Building Building Preview, Comment May 15, 2026 1:43pm

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented May 14, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Port ordinal is now immutable and set at construction; TerminalSurface registers with the registry before hosted attachment; background/off-window surface start is exposed via allowOffWindow and threaded from Workspace send/prime paths; tests add configurable CLI timeouts and a regression validating unfocused layout commands execute without stealing focus.

Changes

Unfocused terminal command execution

Layer / File(s) Summary
Deterministic port ordinal at construction
Sources/GhosttyTerminalView.swift, Sources/Panels/TerminalPanel.swift
TerminalSurface.portOrdinal becomes private let captured from a new portOrdinal: Int = 0 initializer parameter; TerminalPanel passes portOrdinal into TerminalSurface at construction.
Lazy lifecycle and proactive background surface start
Sources/GhosttyTerminalView.swift, Sources/Workspace.swift
TerminalSurface registers with TerminalSurfaceRegistry before calling hostedView.attachSurface; attachToView(_:) and requestBackgroundSurfaceStartIfNeeded(allowOffWindow:) document lazy visual lifecycle and permit explicit off-window runtime creation; debug logging expanded; Workspace.sendInputWhenReady and background-priming now call requestBackgroundSurfaceStartIfNeeded(allowOffWindow: true).
CLI tests: configurable timeouts and new layout regression
tests_v2/test_cli_new_workspace_command_queue.py, tests_v2/test_cli_new_workspace_layout_command_queue.py
Adds _float_env for configurable timeouts; _run_cli enforces subprocess.run timeout and surfaces TimeoutExpired with stdout/stderr; introduces an executable regression test that runs new-workspace --layout with a terminal command writing a marker file and asserts the selected workspace is unchanged.

Sequence Diagram(s)

sequenceDiagram
  participant Workspace
  participant TerminalPanel.surface as Panel.surface
  participant TerminalSurface
  participant TerminalSurfaceRegistry
  Workspace->>Panel.surface: requestBackgroundSurfaceStartIfNeeded(allowOffWindow: true)
  Panel.surface->>TerminalSurface: requestBackgroundSurfaceStartIfNeeded(allowOffWindow: true)
  TerminalSurface->>TerminalSurfaceRegistry: register(surface)
  TerminalSurface->>TerminalSurface: hostedView.attachSurface(self) (visual attach)
  TerminalSurface->>TerminalSurface: create runtime PTY (allowed off-window when allowOffWindow==true)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Possibly related issues

  • #4090: Regression where new-workspace --command/--layout drops create-time commands when unfocused — this PR's off-window start path and tests address the same symptom and gate discussed in that issue.

Poem

🐰 When workspaces spawn behind the shade,
The rabbit nudges runtime, plans well-laid,
Ordinals fixed, no command awry,
Background PTYs hum without a sigh,
A quiet hop — the workflows stayed.


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (2 errors, 2 warnings)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error PR introduces DispatchQueue.main.asyncAfter (3s) timeout in Workspace.sendInputWhenReady for input path. Delayed dispatch flagged by swift-blocking-runtime rules without documented justification. Replace asyncAfter with Task cancellation or async deadline mechanism. Document justification if pattern must remain.
Cmux Architecture Rethink ❌ Error Introduces observer-based timing repair and delayed dispatch (DispatchQueue.main.async) for off-window surface creation, creating notification side-channels to work around lifecycle/focus races. Queue initial commands at TerminalSurface init rather than via observers in Workspace. Remove view.window gate and reconcile Ghostty's requirements with off-window creation needs in a single lifecycle model.
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The PR description covers the key changes and provides clear red/green verification, but omits required template sections for Testing, Demo Video, and Checklist. Add Testing section with manual verification steps, note whether demo video is applicable, and complete the Checklist section with acknowledgment of test execution.
✅ Passed checks (12 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR comprehensively addresses issue #4090 by: enabling off-window surface startup via new allowOffWindow parameter [#4090], capturing portOrdinal immutably to preserve port ranges [#4090], installing pending-input observers before workspace creation to prevent command loss [#4090], and adding regression tests for both --command and --layout paths [#4090].
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing background new-workspace command execution: TerminalSurface initialization/lifecycle, Workspace pending-input handling, and regression tests targeting the --command and --layout paths from issue #4090.
Cmux Swift Actor Isolation ✅ Passed No actor isolation issues introduced. portOrdinal improved (var→let). requestBackgroundSurfaceStartIfNeeded properly threads MainActor contexts. Callers are @MainActor.
Cmux No Hacky Sleeps ✅ Passed No violations found. Production code uses proper async dispatch, not sleeps. Test polling with bounded deadlines is allowed per rule.
Cmux Swift Concurrency ✅ Passed PR preserves existing DispatchQueue.main.async pattern (not new). No legacy async patterns (background queues, new Combine, completion-handlers, fire-and-forget Tasks) introduced.
Cmux Swift @Concurrent ✅ Passed PR does not violate swift-concurrent-annotation rules. No new @concurrent annotations, nonisolated async functions, or violations of actor isolation patterns found in code changes.
Cmux Swift File And Package Boundaries ✅ Passed Small focused changes (15 and 5 lines) to already-oversized files. Qualifies as allowed exception: "incidental touch" and "focused bug fix" preserving extraction path per rule.
Cmux Swift Logging ✅ Passed All Swift logging changes are DEBUG-guarded. Only modification is cmuxDebugLog() enhancement adding inWindow state in GhosttyTerminalView.swift. No production logging violations.
Cmux User-Facing Error Privacy ✅ Passed PR contains internal API changes (portOrdinal, allowOffWindow) with no new user-facing error messages. Test files are explicitly allowed per rules.
Cmux Swiftui State Layout ✅ Passed No new SwiftUI state patterns added. Only existing ObservableObject model classes modified. Changes to constructor signatures and methods in non-View classes. No @Published/@observable violations.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No new standalone cmux-owned windows added. Changes only modify TerminalSurface initialization order and background startup logic for existing terminal surfaces.
Title check ✅ Passed The title 'Fix background new-workspace commands' directly and concisely describes the main change: resolving an issue with background workspace creation commands.
✨ 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-4090-new-workspace-command-unfocused

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.

@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 `@tests_v2/test_cli_new_workspace_layout_command_queue.py`:
- Line 98: The test currently asserts a hard latency bound using _must(elapsed <
1.5, ...) which is CI-flaky; change it to use a configurable threshold (e.g.,
read float from an env var like NEW_WORKSPACE_LAYOUT_TIMEOUT with a sensible
default such as 3.0) and assert _must(elapsed < timeout, f"... took
{elapsed:.2f}s") so the threshold can be increased in noisy environments; update
the call site in tests_v2/test_cli_new_workspace_layout_command_queue.py around
the elapsed check and ensure the env var parsing uses a safe float fallback.
- Around line 53-61: The subprocess.run call in
test_cli_new_workspace_layout_command_queue.py should be bounded to avoid hung
tests: add a timeout parameter (e.g., timeout=30) to the subprocess.run
invocation and wrap the call in a try/except subprocess.TimeoutExpired block; on
timeout raise cmuxError with a clear message that includes the command, the
timeout value and the TimeoutExpired details (e.output / e.stderr or str(e)) so
CI fails fast and reports useful diagnostics for the failing call to
subprocess.run.
🪄 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: c2324fa8-23a9-4d14-bb45-2e26862adcd7

📥 Commits

Reviewing files that changed from the base of the PR and between f7bd659 and a5fc811.

📒 Files selected for processing (4)
  • Sources/GhosttyTerminalView.swift
  • Sources/Panels/TerminalPanel.swift
  • Sources/Workspace.swift
  • tests_v2/test_cli_new_workspace_layout_command_queue.py

Comment thread tests_v2/test_cli_new_workspace_layout_command_queue.py Outdated
Comment thread tests_v2/test_cli_new_workspace_layout_command_queue.py Outdated
@greptile-apps

greptile-apps Bot commented May 14, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes the background new-workspace --command and new-workspace --layout flows so queued terminal commands execute without requiring the created workspace to be selected or presented. portOrdinal is promoted to a private let captured at construction, and requestBackgroundSurfaceStartIfNeeded gains an allowOffWindow flag that allows socket/API callers to start a PTY from an attached GhosttyNSView even when that view has no backing window.

  • portOrdinal is now a private let initialized in the TerminalSurface constructor and threaded through TerminalPanel, fixing a window where off-window startup could use port ordinal 0 before the caller set the field.
  • requestBackgroundSurfaceStartIfNeeded(allowOffWindow:) merges concurrent invocations via queuedBackgroundSurfaceStartAllowsOffWindow before the queued DispatchQueue.main.async fires, preventing the pending-input observer install and the background-prime path from blocking on a window.
  • New Python regression test (test_cli_new_workspace_layout_command_queue.py) and updated test_cli_new_workspace_command_queue.py cover both paths with configurable timeouts.

Confidence Score: 5/5

Safe to merge. The changes are well-scoped: portOrdinal is now immutable, the allowOffWindow accumulator is cleanly self-contained within TerminalSurface, and the Workspace call sites explicitly opt in to off-window startup only for socket/API-triggered and background-prime paths.

All three Swift files make targeted, locally-bounded changes with clear ownership. The portOrdinal fix eliminates a real initialization-ordering gap. The queuedBackgroundSurfaceStartAllowsOffWindow flag is private, reset before every createSurface call, and serialized on the main queue — no new shared mutable state escapes TerminalSurface.

Sources/GhosttyTerminalView.swift — the off-window startup path still falls back to NSScreen.main backing scale when the view has no window; scale is only partially reconciled on window presentation (display ID update only, no scale push). Worth revisiting if multi-display rendering artifacts appear.

Important Files Changed

Filename Overview
Sources/GhosttyTerminalView.swift Promotes portOrdinal to private let; adds queuedBackgroundSurfaceStartAllowsOffWindow accumulator flag; extends requestBackgroundSurfaceStartIfNeeded with allowOffWindow parameter. Logic is sound; the scale-factor reconciliation gap on off-window startup remains unaddressed.
Sources/Panels/TerminalPanel.swift Threads portOrdinal through TerminalSurface constructor instead of setting it as a mutable field post-construction; removes the racy post-construction assignment.
Sources/Workspace.swift Calls requestBackgroundSurfaceStartIfNeeded(allowOffWindow: true) when installing pending-input observers and in requestBackgroundPrimeTerminalSurfaceStartIfNeeded, enabling PTY startup without waiting for workspace presentation.
tests_v2/test_cli_new_workspace_command_queue.py Adds configurable subprocess timeout via _float_env, improves CLI timeout error reporting, and widens the quick-return assertion threshold via NEW_WORKSPACE_COMMAND_TIMEOUT env var.
tests_v2/test_cli_new_workspace_layout_command_queue.py New regression test for the --layout command path; uses pid-scoped marker files, 12s deadline with 50ms polling, configurable timeouts, and workspace cleanup in finally block.

Sequence Diagram

sequenceDiagram
    participant API as Socket/API Caller
    participant WS as Workspace
    participant TP as TerminalPanel
    participant TS as TerminalSurface
    participant MQ as Main Queue

    API->>WS: addPendingTerminalInputObserver(panelId, text)
    WS->>WS: append registration to pendingTerminalInputObserversByPanelId
    WS->>TS: requestBackgroundSurfaceStartIfNeeded(allowOffWindow: true)
    TS->>TS: "queuedBackgroundSurfaceStartAllowsOffWindow |= true"
    TS->>TS: "backgroundSurfaceStartQueued = true"
    TS->>MQ: async createSurface for attachedView
    note over TS,MQ: surface starts even with no window
    MQ-->>TS: fire reads and clears queuedBackgroundSurfaceStartAllowsOffWindow
    TS->>TS: createSurface for view off-window allowed
    TS-->>WS: surface ready callback fires
    WS->>TP: sendInput text pending observer fires
Loading

Reviews (5): Last reviewed commit: "Preserve explicit background surface sta..." | Re-trigger Greptile

@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/GhosttyTerminalView.swift (1)

5690-5724: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve the strongest queued off-window start request.

Once backgroundSurfaceStartQueued is set, later calls are ignored, so a queued allowOffWindow == false request can suppress a subsequent explicit allowOffWindow == true request. The async block then still hits the Line 5708 no-window guard, leaving background CLI/socket startup deferred until some later retry. This regresses the new explicit off-window path whenever the weaker request wins the race.

💡 Proposed fix
     private var backgroundSurfaceStartQueued = false
+    private var queuedBackgroundSurfaceStartAllowOffWindow = false
@@
     func requestBackgroundSurfaceStartIfNeeded(allowOffWindow: Bool = false) {
         if !Thread.isMainThread {
             DispatchQueue.main.async { [weak self] in
                 self?.requestBackgroundSurfaceStartIfNeeded(allowOffWindow: allowOffWindow)
             }
             return
         }
 
         guard allowsRuntimeSurfaceCreation() else { return }
         guard surface == nil, attachedView != nil else { return }
-        guard !backgroundSurfaceStartQueued else { return }
+        queuedBackgroundSurfaceStartAllowOffWindow =
+            queuedBackgroundSurfaceStartAllowOffWindow || allowOffWindow
+        guard !backgroundSurfaceStartQueued else { return }
         backgroundSurfaceStartQueued = true
 
         DispatchQueue.main.async { [weak self] in
             guard let self else { return }
+            let allowOffWindow = self.queuedBackgroundSurfaceStartAllowOffWindow
+            self.queuedBackgroundSurfaceStartAllowOffWindow = false
             self.backgroundSurfaceStartQueued = false
             guard self.allowsRuntimeSurfaceCreation() else { return }
             guard self.surface == nil, let view = self.attachedView else { return }
             guard allowOffWindow || view.window != nil else {
🤖 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/GhosttyTerminalView.swift` around lines 5690 - 5724, The
backgroundSurfaceStartQueued boolean currently suppresses later stronger
requests (allowOffWindow == true); change the logic to preserve the strongest
queued request by introducing a companion stored flag (e.g.
queuedAllowOffWindow: Bool) or an enum, set queuedAllowOffWindow =
queuedAllowOffWindow || allowOffWindow when you mark
backgroundSurfaceStartQueued = true inside
requestBackgroundSurfaceStartIfNeeded, and in the DispatchQueue.main.async
closure read and reset queuedAllowOffWindow (and backgroundSurfaceStartQueued)
and use that value instead of the original allowOffWindow parameter when
checking the no-window guard and calling createSurface(for:). Ensure all
references to allowOffWindow inside the async block use the preserved
queuedAllowOffWindow and that both flags are cleared appropriately.
🤖 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/GhosttyTerminalView.swift`:
- Around line 5690-5724: The backgroundSurfaceStartQueued boolean currently
suppresses later stronger requests (allowOffWindow == true); change the logic to
preserve the strongest queued request by introducing a companion stored flag
(e.g. queuedAllowOffWindow: Bool) or an enum, set queuedAllowOffWindow =
queuedAllowOffWindow || allowOffWindow when you mark
backgroundSurfaceStartQueued = true inside
requestBackgroundSurfaceStartIfNeeded, and in the DispatchQueue.main.async
closure read and reset queuedAllowOffWindow (and backgroundSurfaceStartQueued)
and use that value instead of the original allowOffWindow parameter when
checking the no-window guard and calling createSurface(for:). Ensure all
references to allowOffWindow inside the async block use the preserved
queuedAllowOffWindow and that both flags are cleared appropriately.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6ac417cb-ad19-407e-843c-d375c6829ca5

📥 Commits

Reviewing files that changed from the base of the PR and between 4bfe182 and 6d72f9b.

📒 Files selected for processing (2)
  • Sources/GhosttyTerminalView.swift
  • Sources/Workspace.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 6d72f9b. Configure here.

Comment thread Sources/GhosttyTerminalView.swift

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 5 files

@lawrencecchen
lawrencecchen merged commit f18bd91 into main May 15, 2026
25 checks passed
@lawrencecchen
lawrencecchen deleted the issue-4090-new-workspace-command-unfocused branch May 15, 2026 13:48

This branch was successfully deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Regression in 0.64.4: cmux new-workspace with --command or --layout silently drops the command when invoked unfocused

1 participant