Skip to content

Fix background workspace PTY startup for socket-created surfaces - #3876

Merged
austinywang merged 9 commits into
mainfrom
issue-3798-bg-workspace-pty
May 17, 2026
Merged

austinywang merged 9 commits into
mainfrom
issue-3798-bg-workspace-pty

Conversation

@austinywang

@austinywang austinywang commented May 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Verification

  • Reproduced before code changes with cmux new-split right --workspace workspace:43, cmux send --workspace workspace:43 --surface surface:117 "echo issue3798-repro-before-fix\n", cmux read-screen --workspace workspace:43 --surface surface:117 returning Terminal surface not found, then selecting the workspace caused the queued echo to run.
  • git diff --check
  • Local socket/UI tests not run per repository policy; CI should run the new regression test.

Notes

  • This intentionally does not add focus mutation to surface.create, surface.split, or surface.send_text; background socket control remains non-focusing.

Note

Low Risk
Low risk: this PR only adds a new v2 socket regression test and does not change production code paths.

Overview
Adds a new v2 socket regression test (test_background_split_send_text_starts_terminal.py) that creates a workspace without selecting it, splits a terminal in that background workspace with focus: false, then calls surface.send_text and verifies the command executed via a marker file.

The test also asserts the currently selected workspace never changes across workspace.create, surface.split, and surface.send_text, and includes cleanup for the temporary marker file and created workspace.

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


Summary by cubic

Start background terminal PTYs for socket-created surfaces on explicit runtime demand from the attached GhosttyNSView, even when off-window. Allows surface.send_text to start and run background splits without changing workspace focus (fixes #3798).

  • Bug Fixes
    • Keep visual attach lazy until an NSWindow exists; allow requestBackgroundSurfaceStartIfNeeded() to start the runtime from an off-window attached view.
    • Capture immutable portOrdinal in TerminalSurface init so all startup paths use the correct per-workspace port range.
    • Add a V2 socket regression test for background split surface.send_text; tighten assertions, remove a redundant check, and clean up logging.

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

Summary by CodeRabbit

  • Tests
    • Added a regression test that verifies creating a background split and sending text starts and executes the command in the split terminal without changing the currently selected workspace; includes polling for command completion, cleanup, and success/failure reporting.

Review Change Stack

Socket clients can create terminal surfaces in workspaces that are not selected. The existing workspace-create coverage only proves the first hidden terminal starts; it does not cover a later split surface that receives input while its view is still off-window.

This regression test drives the V2 socket API end to end: create a workspace without selecting it, split a terminal in that workspace, send text to the new surface, and assert the command executed by observing a marker file. On the 0.64.x regression path the input is accepted but remains queued until the user selects the workspace.

Constraint: Repository policy runs socket/UI tests in CI or VM, not locally.

Confidence: high

Scope-risk: narrow

Tested: Not run locally per repository policy; test is intended to fail before the fix in CI/VM.

Not-tested: Local socket execution.
@vercel

vercel Bot commented May 12, 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 17, 2026 2:33am
cmux-staging Building Building Preview, Comment May 17, 2026 2:33am

@coderabbitai

coderabbitai Bot commented May 12, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds a standalone regression test that opens the cmux socket, creates a background split terminal in a new workspace, sends a python3 one-liner via surface.send_text to write a marker file, verifies the marker executes without changing the selected workspace, and performs best-effort cleanup.

Changes

Background Split Send_text Regression Test

Layer / File(s) Summary
Test setup and constants
tests_v2/test_background_split_send_text_starts_terminal.py
Adds script header, imports, sys.path adjustment, and SOCKET_PATH constant derived from CMUX_SOCKET_PATH (default /tmp/cmux-debug.sock).
Helper utilities
tests_v2/test_background_split_send_text_starts_terminal.py
Adds _must(cond, msg), _wait_for_file_text(path, needle, timeout_s) to poll for a marker file, and _first_terminal_surface_id(payload) to extract a terminal surface id from surface.list payloads.
Test main flow and cleanup
tests_v2/test_background_split_send_text_starts_terminal.py
Adds main() which opens a cmux session, records baseline selected workspace, creates a workspace, finds a terminal surface, calls surface.split with focus: False, sends a unique python3 one-liner via surface.send_text that writes a token marker file, waits for the token, re-asserts the baseline workspace is still selected, prints PASS and exits 0; includes best-effort cleanup removing the marker file and closing the created workspace.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 In a quiet split where shadows dwell,
I whisper a token, a tiny shell.
It writes its mark without a peep,
The workspace sleeps, its focus deep.
Hooray — the rabbit nods: PASS, all is well.


Caution

Pre-merge checks failed

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

  • Ignore

❌ Failed checks (6 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Actor Isolation ❌ Error PR introduces actor isolation violations: TerminalSurface ObservableObject lacks @MainActor; TerminalController has nonisolated(unsafe) mutable static [String: Date]. Mark TerminalSurface @MainActor. Protect socketListenerFailureLastCapturedAt with lock or MainActor. Mutable reference types need explicit isolation per swift-actor-isolation.md rules.
Cmux Swift Concurrency ❌ Error PR introduces fire-and-forget Tasks with real lifecycle (browser open, surface teardown) and DispatchSemaphore blocking with spawned Tasks instead of async/await in new cmux code. Replace fire-and-forget Tasks with stored/tracked handles. Replace DispatchSemaphore + Task patterns with async/await or continuations.
Cmux Swift @Concurrent ❌ Error taskManagerTopPayload is a MainActor async function calling CPU-heavy CmuxTopProcessSnapshot.capture via group.addTask without explicit actor hop, violating swift-concurrent-annotation rules. Add explicit actor hop like group.addTask { @concurrent in ... } or mark function @concurrent for CPU-heavy process enumeration.
Cmux Swiftui State Layout ❌ Error PR introduces new SwiftUI state using legacy ObservableObject/@published pattern where @Observable is the modern shape per swiftui-state-layout.md rules. Convert AuthManager, CmuxConfigStore, and FeedPanelViewModel to use @Observable with value properties instead of ObservableObject with @Published.
Cmux Architecture Rethink ❌ Error Uses mutable flag backgroundSurfaceStartQueued with DispatchQueue.main.async for deferred PTY startup. Duplicates ownership and uses delayed dispatch as symptom patch instead of declarative state. Make PTY startup immediate and declarative when attached view exists. Remove mutable flag and deferred dispatch. Invariant: startup gates on attached view, not window membership.
Cmux Swift Auxiliary Window Close Shortcuts ❌ Error PR adds new NSWindow (headlessStartupWindow) in GhosttyTerminalView.swift. Though hidden/internal, it lacks documented exemption in IGNORED_IDENTIFIERS as required for bootstrap windows. Add "cmux.headless.pty.startup" to IGNORED_IDENTIFIERS in lint_auxiliary_window_close_shortcuts.py with explanatory comment about this internal bootstrap window for background PTY startup.
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (9 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix background workspace PTY startup for socket-created surfaces' directly and clearly describes the main change: fixing PTY startup for socket-created terminal surfaces in background workspaces, which aligns with the regression test and underlying fixes.
Linked Issues check ✅ Passed The PR fully addresses issue #3798 objectives: decouples PTY start from view.window gate, adds regression test that validates socket-driven background surface operations without focus change, and preserves non-focusing socket API behavior.
Out of Scope Changes check ✅ Passed All changes are scoped to the regression test file and production fixes directly required by issue #3798. The new test validates background workspace PTY startup, and the production changes (decoupling PTY from NSWindow, moving portOrdinal initialization) are necessary to support the test requirements.
Cmux Swift Blocking Runtime ✅ Passed No blocking Swift synchronization added. Python test uses time.sleep (allowed test scaffolding). Swift changes remove code, adding no DispatchSemaphore, Task.sleep, NSLock, or blocking patterns.
Cmux No Hacky Sleeps ✅ Passed Python test file uses time.sleep(0.1) in deadline-aware polling. Python is not in rule scope (TS/JS/shell). Sleep is in allowed test-only code. No production runtime violations.
Cmux Swift File And Package Boundaries ✅ Passed PR removes 252 lines from browser Swift files; no new production files. Changes focus on external navigation cleanup with no oversized file violations or mixed responsibilities.
Cmux Swift Logging ✅ Passed Test file is allowed per swift-logging.md. Python regression test uses print() for test output and logging module appropriately. No production Swift logging violations detected.
Cmux User-Facing Error Privacy ✅ Passed PR adds test file with generic error messages and no exposed credentials or sensitive details. Test files are explicitly allowed per rules.
Description check ✅ Passed The PR description comprehensively covers the changes, testing approach, and verification strategy with sufficient detail.
✨ 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-3798-bg-workspace-pty

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 12, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a V2 socket regression test (test_background_split_send_text_starts_terminal.py) that verifies surface.send_text starts a background split terminal and executes queued input without stealing workspace focus, closing issue #3798.

  • Creates a background workspace, splits a terminal with focus: false, sends a Python command that writes a unique token to a marker file, and polls for that file to confirm PTY startup and execution.
  • Asserts that current_workspace() remains unchanged at every step — after workspace.create, after surface.split, and after the background terminal executes the sent text.

Confidence Score: 5/5

Safe to merge — the change is a single new test file with no production code modifications.

The test logic, assertions, command construction, and marker-file polling are all correct. The only gap is in cleanup error handling: a workspace-close failure during an otherwise-passing run is silently swallowed rather than surfaced to the runner.

tests_v2/test_background_split_send_text_starts_terminal.py — specifically the finally block cleanup pattern.

Important Files Changed

Filename Overview
tests_v2/test_background_split_send_text_starts_terminal.py New regression test; logic and assertions are sound, but cleanup swallows workspace-close failures without re-raising when the test passes, inconsistent with the established pattern in the suite.

Sequence Diagram

sequenceDiagram
    participant T as Test
    participant S as cmux Socket
    participant BG as Background Workspace

    T->>S: "workspace.create {}"
    S-->>T: workspace_id (no focus change)
    T->>S: "surface.list {workspace_id}"
    S-->>T: surfaces (initial terminal surface)
    T->>S: "surface.split {workspace_id, surface_id, direction:right, focus:false}"
    S-->>T: split_surface_id (no focus change)
    T->>S: "surface.send_text {workspace_id, surface_id:split_surface, text:command}"
    S->>BG: start PTY (off-window, triggered by send_text)
    S-->>T: "{surface_id: split_surface}"
    BG->>BG: execute python3 -c ... write marker file
    T->>T: _wait_for_file_text (poll 0.1s, timeout 8s)
    T->>S: "workspace.current (assert == baseline)"
    T->>S: "workspace.close {workspace_id}"
Loading

Reviews (10): Last reviewed commit: "Remove redundant background send asserti..." | Re-trigger Greptile

Socket-driven terminal operations need to work as a control plane for background workspaces, but the previous eager off-window attach path made every model-only TerminalPanel start a PTY in unit tests. Keep the visual attach lifecycle lazy until an NSWindow exists, and route socket/read/send readiness through requestBackgroundSurfaceStartIfNeeded so explicit runtime demand can still create from the attached Ghostty view without selecting the workspace.

Constraint: Do not run local tests or xcodebuild; validation must come from CI and lightweight local diff checks.

Constraint: Background socket API must not mutate workspace focus.

Rejected: Auto-select the target workspace before send_text | violates the focus allowlist and steals user focus.

Rejected: Eagerly create every off-window attach | spawns PTYs for model-only/unit-test panels and timed out CircleCI unit tests.

Confidence: high

Scope-risk: moderate

Directive: Do not reintroduce NSWindow membership as a prerequisite in requestBackgroundSurfaceStartIfNeeded; visual attach can be window-lazy, but explicit socket/API runtime demand must be able to start off-window.

Tested: git diff --check

Not-tested: Local socket/UI/unit tests per repository policy; CI will rerun the regression and unit suites.
coderabbitai[bot]
coderabbitai Bot previously requested changes May 12, 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_background_split_send_text_starts_terminal.py`:
- Around line 109-112: The cleanup try/except that calls
c.close_workspace(created_workspace) currently swallows all exceptions; modify
the except block to log the failure (including exception details) instead of
silent pass — e.g., import logging (or use print(..., file=sys.stderr)) and call
logging.exception or logging.error with contextual text and the caught exception
when close_workspace fails so cleanup errors are visible during test runs.
🪄 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: c70a45f8-e557-4bc6-a654-6bdab2d51ade

📥 Commits

Reviewing files that changed from the base of the PR and between 5829da2 and 97cfe8b.

📒 Files selected for processing (3)
  • Sources/GhosttyTerminalView.swift
  • Sources/Panels/TerminalPanel.swift
  • tests_v2/test_background_split_send_text_starts_terminal.py

Comment thread tests_v2/test_background_split_send_text_starts_terminal.py Outdated

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

1 issue found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="tests_v2/test_background_split_send_text_starts_terminal.py">

<violation number="1" location="tests_v2/test_background_split_send_text_starts_terminal.py:75">
P2: Assert that `surface.split` returns a new surface ID. The current non-empty check can let this regression test pass even if no new split surface was created.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Re-trigger cubic

},
) or {}
split_surface = str(split_payload.get("surface_id") or "")
_must(bool(split_surface), f"surface.split returned no surface_id: {split_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.

P2: Assert that surface.split returns a new surface ID. The current non-empty check can let this regression test pass even if no new split surface was created.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests_v2/test_background_split_send_text_starts_terminal.py, line 75:

<comment>Assert that `surface.split` returns a new surface ID. The current non-empty check can let this regression test pass even if no new split surface was created.</comment>

<file context>
@@ -0,0 +1,124 @@
+                },
+            ) or {}
+            split_surface = str(split_payload.get("surface_id") or "")
+            _must(bool(split_surface), f"surface.split returned no surface_id: {split_payload}")
+            _must(
+                c.current_workspace() == baseline_workspace,
</file context>

@austinywang
austinywang merged commit 50b985b into main May 17, 2026
26 checks passed
@austinywang austinywang mentioned this pull request May 19, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 0af56526 Deployed May 17, 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.

Background workspace surfaces never start their PTY when driven via the socket API (regression in 0.64.x)

1 participant