Skip to content

Fix background new-workspace command startup - #4115

Closed
austinywang wants to merge 11 commits into
mainfrom
issue-4090-new-workspace-command-dropped
Closed

austinywang wants to merge 11 commits into
mainfrom
issue-4090-new-workspace-command-dropped

Conversation

@austinywang

@austinywang austinywang commented May 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Fixes Regression in 0.64.4: cmux new-workspace with --command or --layout silently drops the command when invoked unfocused #4090.
  • Allows requested background terminal startup to create the Ghostty runtime surface from an attached off-window view instead of deferring forever on view.window != nil.
  • Routes layout per-pane startup commands through TerminalSurface.sendInput, so they are retained on the surface pending-input queue instead of being dropped after a 3 second readiness timeout.
  • Extends tests_v2/test_cli_new_workspace_command_queue.py to cover both cmux new-workspace --command and cmux new-workspace --layout per-pane commands without selecting the created workspace.

Repro Steps Run

From this cmux workspace terminal, using the currently running app's bundled CLI:

"$CMUX_BUNDLED_CLI_PATH" --version
# cmux 0.64.5 (85) [278b31209]

"$CMUX_BUNDLED_CLI_PATH" new-workspace --name "_repro_4090_cmd_<ts>" --cwd /tmp --command "echo HELLO_FROM_COMMAND"
"$CMUX_BUNDLED_CLI_PATH" tree --workspace workspace:53 --json
"$CMUX_BUNDLED_CLI_PATH" read-screen --workspace workspace:53 --surface surface:105 --lines 5
sleep 3
"$CMUX_BUNDLED_CLI_PATH" read-screen --workspace workspace:53 --surface surface:105 --lines 5

"$CMUX_BUNDLED_CLI_PATH" new-workspace --name "_repro_4090_layout_<ts>" --cwd /tmp --layout '{"direction":"horizontal","split":0.5,"children":[{"pane":{"surfaces":[{"type":"terminal","command":"echo HELLO_FROM_LAYOUT_A"}]}},{"pane":{"surfaces":[{"type":"terminal","command":"echo HELLO_FROM_LAYOUT_B"}]}}]}'
"$CMUX_BUNDLED_CLI_PATH" tree --workspace workspace:54 --json
"$CMUX_BUNDLED_CLI_PATH" read-screen --workspace workspace:54 --surface surface:106 --lines 5
"$CMUX_BUNDLED_CLI_PATH" read-screen --workspace workspace:54 --surface surface:107 --lines 5

I closed the temporary _repro_4090* workspaces after the repro.

Observed Baseline Behavior

On current main at 278b31209, the unfocused --command workspace created workspace:53 / surface:105, but the surface stayed titled Terminal, had tty=<nil>, and read-screen returned:

Error: internal_error: ERROR: Terminal surface not found

The same error persisted after 3 seconds. The layout repro created workspace:54 with surface:106 and surface:107; both surfaces had tty=<nil> and both read-screen calls returned Terminal surface not found.

I also ran the --focus true control from the issue. In this already-running 0.64.5 app/session it also produced a nil-tty surface, so I did not use that as the regression assertion. The unfocused command/layout failure reproduced deterministically and matches #4090.

Observed After Fix

Pending CI verification on this PR. The expected observable behavior after this fix is that the marker-file commands in tests_v2/test_cli_new_workspace_command_queue.py are created from both --command and --layout without changing the selected workspace.

Regression Test

Failing-then-passing regression test: tests_v2/test_cli_new_workspace_command_queue.py.

Before the fix commit, I ran it against the baseline running app with:

CMUX_SOCKET_PATH="$CMUX_SOCKET_PATH" CMUXTERM_CLI="$CMUX_BUNDLED_CLI_PATH" python3 tests_v2/test_cli_new_workspace_command_queue.py

It failed waiting for the --command marker file:

cmux.cmuxError: Command marker file was not created: /var/folders/.../T/cmux_new_workspace_command_5442.txt

Relationship To #3798 And #3876

#3798 and #4090 share the same lifecycle root cause: background terminal work is requested while the SwiftUI/AppKit view is attached but not in an NSWindow, and requestBackgroundSurfaceStartIfNeeded() used to return before creating the Ghostty runtime surface/PTY.

PR #3876 is still open and fixes the #3798 surface.send_text queue by removing that background-start view.window != nil gate. This PR applies the same background-start principle for the new-workspace command case and adds the missing #4090-specific piece: layout per-pane command fields were not using the durable surface input queue and could be silently dropped by Workspace.sendInputWhenReady after 3 seconds. This branch removes that timeout path and queues those inputs on the TerminalSurface instead.


Note

Medium Risk
Changes terminal startup-command delivery by removing the Workspace-level readiness observer/timeout mechanism and sending commands directly to TerminalPanel.sendInput, which could affect when/if commands execute during surface initialization. Coverage is improved with expanded CLI regression tests, but the behavior touches workspace/terminal lifecycle paths.

Overview
Fixes background new-workspace startup commands by removing Workspace’s pending-terminal-input observer/3s timeout path and routing both --command and per-pane --layout command fields through TerminalPanel.sendInput (so input is queued on the surface rather than dropped when the runtime surface isn’t ready).

Cleans up associated lifecycle bookkeeping (pending observer maps, deinit/prune/close cleanup hooks, and background-start “work pending” checks) and extends tests_v2/test_cli_new_workspace_command_queue.py to assert --command and --layout commands execute without switching focus and that the CLI returns quickly.

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


Summary by cubic

Fixes #4090 by making background cmux new-workspace --command and per-pane --layout commands run reliably without focusing the new workspace or dropping input.

  • Bug Fixes
    • Route startup commands through panel.sendInput so they queue on the terminal surface; remove readiness observers, the 3s drop timeout, all pending-input bookkeeping, and the stale observer gate in background-start checks.
    • Create background terminal surfaces on first attach when queued input exists; allow off-window PTY creation; passive attach still waits for a window.
    • Expand tests_v2/test_cli_new_workspace_command_queue.py to cover --command and two-pane --layout; assert quick CLI return (NEW_WORKSPACE_LAYOUT_TIMEOUT), exact per-pane tokens and OK response, and focus preservation.

Written for commit 0de8b11. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • Tests
    • Expanded test coverage for queued workspace creation via both command and layout configurations, verifying workspace focus preservation throughout execution.

Review Change Stack

@vercel

vercel Bot commented May 13, 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 22, 2026 3:16am
cmux-staging Building Building Preview, Comment May 22, 2026 3:16am

@coderabbitai

coderabbitai Bot commented May 13, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR removes the explicit observer deregistration call from the workspace panel discard path and expands CLI integration tests to validate queued command execution under both new-workspace --command and --layout scenarios while preserving focus state.

Changes

Queued Input Recovery for Unfocused Surfaces

Layer / File(s) Summary
Workspace panel lifecycle cleanup simplification
Sources/Workspace+PanelLifecycle.swift
Remove explicit removePendingTerminalInputObservers(forPanelId:) call from discardClosedPanelLifecycleState(...) since pending input is now handled at the surface level rather than via panel-based observers.
Expanded test coverage for queued command and layout execution
tests_v2/test_cli_new_workspace_command_queue.py
Add _wait_for_marker() synchronization helper, expand marker setup to track both command and layout-pane outputs, extend test to verify --command and --layout queued execution with focus preservation assertions, and enhance cleanup to remove both workspace types and all marker files.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • manaflow-ai/cmux#2916: Adds the --layout functionality to new-workspace CLI that this PR's expanded tests now exercise.
  • manaflow-ai/cmux#3744: Implements pane teardown and cleanup lifecycle work in the same discardClosedPanelLifecycleState(...) helper where observer deregistration is now removed.

Poem

🐰 A command queued without a care,
No focus needed, runs from air.
Observers fade, the surface feeds,
Tests now dance through both command creeds!
Clean and swift, the cleanup flows,
As panel discard gently goes. 🎉


Important

Pre-merge checks failed

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

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux Architecture Rethink ❓ Inconclusive No result was produced after verification. Marking as INCONCLUSIVE. Re-run the check or adjust instructions to produce a final result.
✅ Passed checks (14 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix background new-workspace command startup' clearly summarizes the main fix, addressing #4090 where unfocused new-workspace commands were being dropped.
Linked Issues check ✅ Passed The code changes in both modified files directly address #4090's core requirements: background terminal startup now creates the Ghostty runtime surface from off-window attached views, and per-pane layout commands are routed through TerminalSurface.sendInput instead of being dropped after timeout.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to #4090: the lifecycle fix removes a single problematic observer-cleanup call, and test expansion directly validates the fix with comprehensive --command and --layout coverage.
Cmux Swift Actor Isolation ✅ Passed PR doesn't introduce new Swift 6 actor isolation mistakes. TerminalPanel and Workspace are @MainActor isolated. TerminalSurface adds a mutable field following existing patterns. No material worsening.
Cmux Swift Blocking Runtime ✅ Passed Production Swift only removes code, no blocking primitives added. Test scaffolding uses allowed deterministic polling. No semaphores, locks, sleeps, or sync queues in production code.
Cmux No Hacky Sleeps ✅ Passed PR introduces no new production-code sleeps. Test-only time.sleep(0.05) existed before and was refactored, not worsened. No new timing primitives in Swift changes.
Cmux Swift Concurrency ✅ Passed No new legacy async patterns. Python test only. Workspace+PanelLifecycle removes cleanup. Dispatch/Task patterns in Workspace.swift unchanged.
Cmux Swift @Concurrent ✅ Passed The PR removes a synchronous cleanup call from a synchronous @MainActor function. No @concurrent annotations are added, missing, or invalid. No async/concurrent isolation violations occur.
Cmux Swift File And Package Boundaries ✅ Passed Swift change is minimal (1 line removed from 224-line extension file), represents focused bug fix, no new oversized files, no mixing of responsibilities. Complies with all file/package boundary rules.
Cmux Swift Logging ✅ Passed No Swift logging violations detected. Production code changes contain only debug logs within #if DEBUG guards. Test file print statements are allowed.
Cmux User-Facing Error Privacy ✅ Passed Test file and production code cleanup only. Test assertions are exempt. Production code removes a function call with no new user-facing errors. No vendor names, credentials, or sensitive data exposed.
Cmux Swiftui State Layout ✅ Passed No SwiftUI View changes in PR. Only a Python test file and Swift model extension with cleanup function removal. No new SwiftUI state, GeometryReader, or render-time mutations.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR does not add or materially change any standalone windows. The sole Swift change modifies panel lifecycle cleanup code with zero window-related operations. The check is not applicable.
Description check ✅ Passed The pull request description is comprehensive and follows the template structure, covering Summary, Testing (via regression test), Repro Steps, and detailed technical context.
✨ 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-dropped

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@greptile-apps

greptile-apps Bot commented May 13, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes background new-workspace --command and --layout per-pane command execution by removing the fragile sendInputWhenReady observer/timeout path and routing all startup commands through TerminalPanel.sendInput, which enqueues text in GhosttyTerminalView's existing durable pendingSocketInputQueue and triggers requestBackgroundSurfaceStartIfNeeded.

  • Deletes WorkspacePendingTerminalInputObserver, WorkspacePendingTerminalInputReason, WorkspacePendingTerminalInputPolicy, and pendingTerminalInputObserversByPanelId, along with all panel-close/deinit/prune cleanup for that state; hasBackgroundSurfaceStartWork is simplified to only check hasDeferredStartupWorkForBackgroundStart(), which returns pendingSocketInputBytes > 0 — correctly non-zero whenever sendInput has enqueued a startup command.
  • Removes the redundant requestBackgroundSurfaceStartIfNeeded() call from the old sendInputWhenReady body (previously flagged as a no-op); TerminalSurface.sendInput owns both queueing and background startup now.
  • Expands the regression test to cover --command and two-pane --layout, adding exact-token marker assertions, a configurable quick-return timing check, and contextlib.suppress-based cleanup.

Confidence Score: 5/5

Safe to merge; the change removes dead observer bookkeeping and delegates to an already-tested durable input queue with no new timing dependencies.

The three call sites that previously used sendInputWhenReady now call panel.sendInput directly. GhosttyTerminalView.sendInputResult already handles the nil-surface case by enqueuing text and calling requestBackgroundSurfaceStartIfNeeded, so no delivery guarantee is lost. The hasBackgroundSurfaceStartWork simplification is correct because pendingSocketInputBytes > 0 captures exactly the panels that have queued work. All deleted state (observer registrations, timeouts, panel-close cleanup) is unreachable after the routing change. The regression test covers both --command and --layout without focus change, which directly validates the bug fix.

No files require special attention.

Important Files Changed

Filename Overview
Sources/Workspace.swift Removes sendInputWhenReady, WorkspacePendingTerminalInputObserver, and all associated bookkeeping; routes per-pane startup commands directly through panel.sendInput, which enqueues text in GhosttyTerminalView's durable pendingSocketInputQueue and triggers requestBackgroundSurfaceStartIfNeeded. hasBackgroundSurfaceStartWork is simplified to only check hasDeferredStartupWorkForBackgroundStart (pendingSocketInputBytes > 0), which is now correct since sendInput fills that queue.
Sources/Workspace+PanelLifecycle.swift Removes the removePendingTerminalInputObservers call from the panel-close path; since that bookkeeping structure no longer exists, this is a correct cleanup with no functional gap.
tests_v2/test_cli_new_workspace_command_queue.py Extends regression test to cover both --command and two-pane --layout workspace creation; adds _wait_for_marker helper with 12s polling, exact-token assertions, quick-return timing check, and robust temp-file cleanup using contextlib.suppress.

Sequence Diagram

sequenceDiagram
    participant CLI as cmux CLI
    participant WS as Workspace
    participant TP as TerminalPanel
    participant TS as TerminalSurface (GhosttyTerminalView)
    participant Q as pendingSocketInputQueue

    CLI->>WS: new-workspace --command / --layout
    WS->>TP: newTerminalSurface(focus: false)
    TP-->>WS: "panel (surface == nil)"
    WS->>TP: panel.sendInput(command + newline)
    TP->>TS: surface.sendInput(text)
    TS->>TS: "surface == nil, allowsRuntimeSurfaceCreation() == true"
    TS->>Q: enqueuePendingSocketInput(text)
    TS->>TS: requestBackgroundSurfaceStartIfNeeded()
    Note over TS: DispatchQueue.main.async: attachedView present but no window, scheduleHeadlessRuntimeStartIfNeeded
    TS->>TS: createSurface(headless window)
    TS->>Q: flush pendingSocketInputQueue to PTY
    Note over Q: Command executes in background terminal
Loading

Reviews (12): Last reviewed commit: "merge: sync main into issue-4090-new-wor..." | Re-trigger Greptile

Comment thread Sources/Workspace.swift Outdated
coderabbitai[bot]
coderabbitai Bot previously requested changes May 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 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_command_queue.py`:
- Around line 172-181: Replace the bare try/except blocks used around
c.close_workspace calls with contextlib.suppress(Exception) to make the "ignore
errors during cleanup" intent explicit: import suppress from contextlib, then
wrap the conditional close calls for created_layout_ws_id and created_ws_id each
in a with suppress(Exception): block calling c.close_workspace(id) so that
cleanup remains best-effort but is clearer; target the existing
created_layout_ws_id and created_ws_id variables and the c.close_workspace(...)
calls.
- Around line 89-93: Replace the try/except blocks around the cleanup loop that
calls path.unlink(missing_ok=True) (iterating over marker, layout_left_marker,
layout_right_marker) with contextlib.suppress(OSError) to make the intent
explicit and the code more concise; import suppress from contextlib and wrap the
unlink call in a with suppress(OSError): block in both occurrences (the loop
around marker/layout_left_marker/layout_right_marker and the similar block at
the second location) so only OSError is suppressed while keeping other behavior
unchanged.
🪄 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: e77cdbe6-8ba5-41cb-a41e-115b5210071b

📥 Commits

Reviewing files that changed from the base of the PR and between 278b312 and 8b57832.

📒 Files selected for processing (3)
  • Sources/GhosttyTerminalView.swift
  • Sources/Workspace.swift
  • tests_v2/test_cli_new_workspace_command_queue.py

Comment thread tests_v2/test_cli_new_workspace_command_queue.py Outdated
Comment thread tests_v2/test_cli_new_workspace_command_queue.py Outdated
Comment thread Sources/GhosttyTerminalView.swift Outdated
@austinywang
austinywang force-pushed the issue-4090-new-workspace-command-dropped branch from 44dd7fd to 5ec29db Compare May 13, 2026 20:29
@vercel
vercel Bot temporarily deployed to Preview – cmux May 13, 2026 20:36 Inactive
coderabbitai[bot]
coderabbitai Bot previously requested changes May 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests_v2/test_cli_new_workspace_command_queue.py`:
- Around line 73-74: The polling in _wait_for_marker currently breaks on any
non-empty observed content which can catch a file mid-write; change the loop to
continue polling until observed == token (or timeout) instead of breaking on
truthy observed, i.e., only exit when the observed string exactly equals the
expected token (preserving the existing timeout behavior and any sleep/retry
logic).
🪄 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: 1b45f147-a7d1-4aef-a3a8-768f00de6dc1

📥 Commits

Reviewing files that changed from the base of the PR and between 355f1fc and 5ec29db.

📒 Files selected for processing (4)
  • Sources/GhosttyTerminalView.swift
  • Sources/Workspace+PanelLifecycle.swift
  • Sources/Workspace.swift
  • tests_v2/test_cli_new_workspace_command_queue.py
💤 Files with no reviewable changes (1)
  • Sources/Workspace+PanelLifecycle.swift

Comment thread tests_v2/test_cli_new_workspace_command_queue.py Outdated
@austinywang
austinywang dismissed stale reviews from coderabbitai[bot] and coderabbitai[bot] May 13, 2026 20:42

Stale CodeRabbit review on an older commit. The marker polling issue was fixed in 7f0a094 and acknowledged on the review thread; latest CodeRabbit check is passing.

coderabbitai[bot]
coderabbitai Bot previously requested changes May 17, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests_v2/test_cli_new_workspace_command_queue.py`:
- Line 181: Replace the hardcoded timeout check in
tests_v2/test_cli_new_workspace_command_queue.py that uses _must(elapsed < 1.5,
...) with a configurable threshold obtained via the existing _float_env helper
(e.g. timeout = _float_env("NEW_WORKSPACE_LAYOUT_TIMEOUT", 1.5)); then use
_must(elapsed < timeout, f"new-workspace --layout should return quickly, took
{elapsed:.2f}s (threshold {timeout}s)"). Update the assertion site where _must
is called and ensure the env var name matches naming convention used for other
test timeouts (it should be referenced where --command timeout is already
handled).
🪄 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: 26358688-5784-4ef6-a70f-9498644591ed

📥 Commits

Reviewing files that changed from the base of the PR and between 5ec29db and 3ab9fc5.

📒 Files selected for processing (1)
  • tests_v2/test_cli_new_workspace_command_queue.py

Comment thread tests_v2/test_cli_new_workspace_command_queue.py Outdated
Comment thread tests_v2/test_cli_new_workspace_command_queue.py Outdated

@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 014dc07. Configure here.

Comment thread Sources/Workspace+PanelLifecycle.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 2 files

Re-trigger cubic

@austinywang
austinywang dismissed coderabbitai[bot]’s stale review May 18, 2026 07:04

Superseded by the current CodeRabbit approval on the latest commit. The layout timeout feedback was addressed by replacing the hardcoded threshold with NEW_WORKSPACE_LAYOUT_TIMEOUT in the current code.

@austinywang austinywang reopened this May 19, 2026
@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 — 0de8b112 Deployed May 22, 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.

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

3 participants