Skip to content

fix: create offscreen NSWindow for background workspace PTY initialization - #1522

Closed
EtanHey wants to merge 1 commit into
manaflow-ai:mainfrom
EtanHey:fix/background-workspace-pty-init
Closed

EtanHey wants to merge 1 commit into
manaflow-ai:mainfrom
EtanHey:fix/background-workspace-pty-init

Conversation

@EtanHey

@EtanHey EtanHey commented Mar 16, 2026 •

Copy link
Copy Markdown

Fixes #1472

Summary

  • Fix background workspaces created via the socket API (workspace.create with select: false) having dead PTY surfaces that reject send, read-screen, and send-key commands.
  • Add an offscreen NSWindow host so ghostty_surface_new() gets a valid Metal-backed window context even when the workspace has never been displayed.
  • The offscreen window is released automatically when the view migrates to a real window or on teardown.

Why

We built an MCP server on top of cmux for multi-agent AI orchestration — spawning Claude Code, Codex, and Gemini CLI sessions in separate workspaces programmatically. When creating workspaces via the socket API without selecting them, the surfaces are listed correctly but all commands fail with "Surface is not a terminal."

Root cause: requestBackgroundSurfaceStartIfNeeded() calls attachToView, which requires surfaceView.window != nil. Background workspaces have no NSWindow, so createSurface silently defers and the PTY is never forked.

Testing

  1. Create a background workspace: cmux new-workspace (without --focus)
  2. Immediately run cmux read-screen --surface <new-surface>
  3. Before fix: "Surface is not a terminal"
  4. After fix: Returns terminal content (shell prompt)

Could not verify local build — zig is required for GhosttyKit.xcframework. CI should handle build verification.

@vercel

vercel Bot commented Mar 16, 2026

Copy link
Copy Markdown

@EtanHey is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps greptile-apps Bot left a comment

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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented Mar 16, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This change introduces an offscreen NSWindow mechanism to enable background surface creation without a real window attachment. It extends the PortalLifecycleState enum to include closing and closed states, adds offscreen window lifecycle management during surface initialization, and clears resources during teardown.

Changes

Cohort / File(s) Summary
Offscreen Window Lifecycle & Portal State Management
Sources/GhosttyTerminalView.swift
Introduced offscreenHostWindow property to support background surface creation. Extended PortalLifecycleState enum with closing and closed cases. Added background-start path in requestBackgroundSurfaceStartIfNeeded to create offscreen window when surface lacks real window attachment. Updated teardown logic to clear offscreen window. Added surface reuse logic to release offscreen window when moving to real window.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

Poem

🐰 A window in the dark, unseen but true,
Metal needs a home, so we conjured one new,
Background workspaces bloom without a screen,
Now surfaces spin up where none have been!
The PTY lives—no more dead in the night. ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description includes a comprehensive summary of changes and detailed testing steps, but lacks information on how testing was performed locally and does not include demo video or a completed checklist. Add details about local testing performed (despite zig requirement), confirm whether added/updated tests exist, and complete the checklist items to clarify review readiness and testing coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and concisely summarizes the core fix: creating an offscreen NSWindow to enable PTY initialization for background workspaces.
Linked Issues check ✅ Passed Changes directly address issue #1472 by implementing the offscreen NSWindow approach to create live PTY surfaces for background workspaces without manual focus workarounds.
Out of Scope Changes check ✅ Passed All changes are confined to GhosttyTerminalView.swift and directly support the stated objective of fixing background workspace PTY initialization.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

CodeRabbit can enforce grammar and style rules using `languagetool`.

Configure the reviews.tools.languagetool setting to enable/disable rules and categories. Refer to the LanguageTool Community to learn more.

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

🧹 Nitpick comments (1)
Sources/GhosttyTerminalView.swift (1)

2743-2753: Optional: set explicit frame/autoresizing for offscreen host attachment.

This makes the temporary-host path more robust if default view sizing changes later.

Suggested patch
             offscreenHostWindow = window
-            window.contentView?.addSubview(hostedView)
+            if let contentView = window.contentView {
+                hostedView.frame = contentView.bounds
+                hostedView.autoresizingMask = [.width, .height]
+                contentView.addSubview(hostedView)
+            }
             // viewDidMoveToWindow fires on surfaceView → attachToView → createSurface
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/GhosttyTerminalView.swift` around lines 2743 - 2753, The offscreen
host window and its hostedView are added without an explicit frame or resizing
behavior which can break if default sizing changes; update the block that
creates offscreenHostWindow (the temporary NSWindow assigned to
offscreenHostWindow and its hostedView) to set a concrete frame for hostedView
(e.g., match window.contentView!.bounds) and configure autoresizingMask or Auto
Layout constraints so the hostedView always fills the contentView; ensure this
change is applied where window is created and before calling viewDidMoveToWindow
/ createSurface so the attachment has deterministic sizing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Sources/GhosttyTerminalView.swift`:
- Around line 2743-2753: The offscreen host window and its hostedView are added
without an explicit frame or resizing behavior which can break if default sizing
changes; update the block that creates offscreenHostWindow (the temporary
NSWindow assigned to offscreenHostWindow and its hostedView) to set a concrete
frame for hostedView (e.g., match window.contentView!.bounds) and configure
autoresizingMask or Auto Layout constraints so the hostedView always fills the
contentView; ensure this change is applied where window is created and before
calling viewDidMoveToWindow / createSurface so the attachment has deterministic
sizing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b2b51307-c4bb-42a1-9d16-286ad7eebd86

📥 Commits

Reviewing files that changed from the base of the PR and between 3b507d3 and 579040c.

📒 Files selected for processing (1)
  • 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 1 file


Since this is your first cubic review, here's how it works:

  • cubic automatically reviews your code and comments on bugs and improvements
  • Teach cubic by replying to its comments. cubic learns from your replies and gets better over time
  • Add one-off context when rerunning by tagging @cubic-dev-ai with guidance or docs links (including llms.txt)
  • Ask questions if you need clarification on any suggestion

@shuhei0866

Copy link
Copy Markdown

I have the same need — I'm building an orchestrator pattern where a conductor pane sends commands to other panes via cmux send-text, and hitting exactly the "Surface not ready" issue after cmux break / new-workspace.

Reviewed the diff and the approach looks solid:

  • Offscreen NSWindow at (-10000, -10000): clean way to satisfy Metal's requirement for a window-backed layer without visual side effects
  • Automatic cleanup: releasing the offscreen window in both attachToView (when migrating to a real window) and teardownSurface — no leak risk
  • Minimal change: only GhosttyTerminalView.swift touched, no API surface changes

One minor thought: the surface != nil check right after addSubview assumes viewDidMoveToWindow → createSurface fires synchronously. Worth confirming that's always the case in the Ghostty lifecycle, but from reading the existing code it looks like it is.

Would love to see this merged — it unblocks programmatic multi-workspace orchestration for both MCP servers and CLI-driven workflows. Happy to help test once a build is available.

…ation

Background workspaces created via `workspace.create` (socket API) have
dead PTY surfaces because ghostty_surface_new() requires a view backed
by a live NSWindow for Metal layer setup and backing context.

When requestBackgroundSurfaceStartIfNeeded() is called for a workspace
that hasn't been displayed, the surfaceView has no window — so the
attachToView → createSurface chain silently defers creation. The PTY is
never forked, leaving the terminal as a ghost: listed in surface health
but unable to accept input or return screen content.

Fix: when the surfaceView has no window, create a small offscreen
NSWindow positioned at (-10000, -10000) and temporarily host the
hostedView in it. This triggers the standard viewDidMoveToWindow →
attachToView → createSurface path with a valid window context. The
offscreen window is released when the view migrates to a real window
(workspace is displayed) or on teardown.

Fixes manaflow-ai#1472

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@EtanHey
EtanHey force-pushed the fix/background-workspace-pty-init branch from 579040c to 7b85f3f Compare April 9, 2026 16:10
@EtanHey

EtanHey commented Apr 9, 2026

Copy link
Copy Markdown
Author

Rebased onto current main, resolved conflicts from portal sync changes. Also applied CodeRabbit's frame/autoresizing suggestion. Issue #1472 is still open — would appreciate a maintainer review when you get a chance! 🙏

@EtanHey

EtanHey commented Apr 30, 2026

Copy link
Copy Markdown
Author

Rebased onto current main on April 9, 2026, and issue #1472 is still open. This still unblocks background workspace PTY initialization; would appreciate a maintainer review when convenient.

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Thanks for this! Background-workspace terminals now start via an offscreen host, the same idea as yours landed on main in #4233. You opened this first, so you got there first. Closing since main covers it now.

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 workspaces have dead PTYs — read-screen/send-key fail with 'Surface not ready'

3 participants