Skip to content

CmuxIPCService: extract AppDelegate multi-window CLI routing (MultiWindowRouter behind MultiWindowRouting) - #5920

Merged
azooz2003-bit merged 28 commits into
mainfrom
feat-ipc-service
Jun 13, 2026
Merged

azooz2003-bit merged 28 commits into
mainfrom
feat-ipc-service

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jun 11, 2026 •

Copy link
Copy Markdown
Collaborator

Wave-2 slice of the AppDelegate decomposition (blueprint "PR 2", the canonical small service extraction): the multi-window CLI-over-socket routing capability moves out of Sources/AppDelegate.swift into a new Packages/CmuxIPCService.

Based on feat-ctl-coordinator-3c-1 on purpose; retarget to main after #5816 merges.

What moved

  • MultiWindowRouteCLIResult (private struct) + runMultiWindowRouteCLI (private free function, synchronous Process.run + waitUntilExit) deleted from Sources/AppDelegate.swift.
  • New package CmuxIPCService: MultiWindowRouting (seam protocol), MultiWindowRouteResult (Sendable DTO), MultiWindowRouteLaunchError, MultiWindowRouter (production conformer; cliURL/socketPath/environment constructor-injected). Swift 6 manifest per conventions (6.0, .macOS(.v14), one .library, per-target swiftLanguageMode(.v6) + ExistentialAny + InternalImportsByDefault), one major type per file, DocC on every public symbol.
  • The only caller, runMultiWindowWindowRouteCLIIfNeeded (UI-test scaffolding gated on CMUX_UI_TEST_WINDOW_ROUTE_CLI=1, exercised end to end by cmuxUITests/MultiWindowNotificationsUITests.swift), constructs the router behind its existing guards and forwards.

Two-commit structure per the faithful-lift discipline:

  1. Faithful lift (718e4b1): synchronous contract preserved; route body byte-identical modulo seam spellings, verified by normalized machine-diff against git show HEAD:Sources/AppDelegate.swift.
  2. Modernization: sync Process.run + waitUntilExit becomes async throws (deltas analyzed below).

Line delta and tests

  • Sources/AppDelegate.swift: 18044 -> 18003 (-41; budget tsv updated, monotonic shrink held through both commits).
  • Package: 4 one-type-per-file sources + manifest, 7 behavior tests (import Testing), green via swift test from a clean .build. Tests spawn a scripted CLI stand-in to pin: --socket prepending and argument forwarding, wholesale environment replacement (injected keys visible, parent env not inherited), stderr/exit-status capture, the throwing launch-failure contract with String(describing:) text preservation, the legacy -1 capture encoding, non-UTF-8 output collapsing to empty, and >pipe-buffer output completing without deadlock.
  • Also carries a one-line fix for an inherited duplicate sessionID parameter label in Sources/TerminalController+ControlWorkspaceContext.swift (base-branch file; label equals name so no caller changes; keeps the local Xcode 26.5 warning budget green, CI Xcode 16.4 never emits it).

Isolation decision (deviation from the blueprint's "actor" naming, justified)

The blueprint sketches MultiWindowRouter as an actor. The router holds only immutable Sendable configuration (CLI URL, socket path, child environment); there is no mutable state to isolate, and an actor would serialize unrelated route calls while protecting nothing. Per the LEARNINGS ruling ("don't keep a clearly-stateless actor"; GitMetadataService actor -> struct on owner review) and the closest prior shape (CmuxProcess.CommandRunner, a stateless Sendable struct), MultiWindowRouter is a Sendable struct with a nonisolated async method. Seam and DTO names stay blueprint-faithful.

Router construction happens at the call site rather than as a stored AppDelegate property: the dependencies (bundled CLI URL, socket path, merged env) are resolved behind the existing call-time guards that produce the CLI-observable failure markers (missing_cli, socket_not_ready); a half-configured router stored at launch would add an optional property with no consumer. Constructor injection of a Sendable value where its configuration is known.

Why this does not reuse CmuxProcess.CommandRunner

CommandRunning's contract differs from the route's exact semantics: it resolves executables against PATH/fallbacks (the route uses an exact bundled-CLI URL), it lets the child inherit the app environment (the route replaces the environment wholesale, which the tests pin), it nulls stdin, and it has its own timeout machinery (the route's timeout is the CLI's own CMUXTERM_CLI_RESPONSE_TIMEOUT_SEC). Extending the shared CommandRunning seam to carry env-replacement semantics would change a protocol other call sites depend on, exceeding this slice. Convergence can be evaluated later if the two contracts grow together.

Deltas with observer analysis

Commit 1 (lift):

  • Pipe read helper: the package cannot import the app-target ProcessPipeReader (shared by other app sources, stays put), so the router carries the minimal equivalent read loop (chunked read, EINTR retry, partial data + logged warning on error). Identical data path; the only delta is the os_log category and terser warning text. Observable only in unified-log diagnostics, never in CLI output or the UI-test data file.

Commit 2 (modernization):

  • Launch failure now throws MultiWindowRouteLaunchError instead of returning a -1 result. The caller uses routeCapturingLaunchFailure (package protocol extension, the CommandRunning.runStandardOutput convenience precedent), which folds the throw back into the legacy encoding: termination status -1, String(describing:) of the error in stderr. The error's CustomStringConvertible preserves the underlying Foundation error text verbatim, so the test-data file bytes are identical; each of the three calls still runs independently after an earlier launch failure, exactly like the legacy loop.
  • waitUntilExit (blocking a global-queue thread) becomes a terminationHandler continuation; the caller's DispatchQueue.global + DispatchQueue.main.async pair becomes one MainActor-inherited Task(priority: .userInitiated) with sequential awaits. Ordering (create, then window-2 list, then window-1 list), the [weak self]-gated final write, and the write landing on the main thread are all preserved. No code between the calls does blocking work, so nothing observable moves on or off main.
  • DTO field status: String becomes terminationStatus: Int32; the caller writes String(result.terminationStatus), producing identical strings for every reachable value.
  • Streams are now drained concurrently with the child (detached readers keyed by raw fd, the CommandRunner pattern) instead of read after exit. Captured bytes are identical for every run that completed before; the only behavior change is that CLI output larger than the 64 KiB pipe buffer no longer deadlocks the call forever (previously the worker thread hung and the UI test timed out). Strict improvement, pinned by a test.

End-to-end verification

MultiWindowNotificationsUITests (sets CMUX_UI_TEST_WINDOW_ROUTE_CLI=1 and asserts on the windowRoute* entries the extracted router writes into the shared test-data file) ran green on this branch via hosted CI: https://github.com/manaflow-ai/cmux/actions/runs/27384249666. That run covered the modernized router (commit 3c4273e); the one later commit (9cbcddd) only adds a nonisolated annotation to a file-scoped logger constant, with no codegen or behavior change.

The one thing not verified

No human has launched the tagged build (ipcsvc) to confirm general app behavior is unaffected. Nothing outside the CMUX_UI_TEST_WINDOW_ROUTE_CLI=1 UI-test path executes this code, so the residual risk is link-time only (new package product wired into the app target). Manual check: open the tagged build, create a second window, and confirm workspaces open and route normally between windows.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Introduced a reusable multi-window routing service and packaged it for the app.
  • Bug Fixes

    • Improved process launch/error handling with a dedicated launch error.
    • Enhanced stdout/stderr capture to avoid deadlocks, preserve outputs, and handle non-UTF8 data.
  • Tests

    • Added comprehensive tests for routing, environment isolation, launch failure handling, and large-output scenarios.
  • Chores

    • Integrated the new routing package into the app and updated UI-test routing to use it.

azooz2003-bit and others added 19 commits June 10, 2026 12:13
Extract the window RPC domain (window.list/current/focus/create/close/displays/
display) out of TerminalController into a new @mainactor @observable
ControlCommandCoordinator in CmuxControlSocket, behind the read-only
ControlCommandContext seam (app target conforms; package never imports the app
target). The coordinator owns the kind:N ControlHandleRegistry (RPC selection
state per the decomposition plan); TerminalController delegates its ensureRef/
resolveRef/removeRef to it so refs stay consistent across moved and not-yet-
moved domains.

Faithful lift: the window bodies build ControlCallResult/JSONValue payloads
whose Foundation object is identical to the legacy [String: Any] dictionaries,
so the encoded wire bytes match. Dispatch runs on the main actor inside the
existing withSocketCommandPolicy scope, so the per-read v2MainSync hops the
legacy bodies used become plain in-isolation calls and disappear. window.current
preserves both distinct legacy errors (unavailable vs not_found) via
ControlCurrentWindowResolution.

TerminalController.swift 22074 -> 21921 (budget ratcheted). 17 new package
tests (128 total) drive every window method through a fake context, asserting
byte-identical payloads, ref minting, routing-selector parsing, and the two
window.current failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Restructure the seam into a per-domain protocol umbrella (ControlCommandContext:
ControlWindowContext, ...) so each domain can be built in its own files, and
port the shared TerminalControllerV2ParamParsingSupport pure helpers + ref
minting (workspaceRefs/tabRef/workspacePaneAndSurfaceRefs) into the coordinator
as JSONValue twins. Foundation for moving the remaining RPC domains.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the app-focus (app.focus_override.set, app.simulate_active), main-actor feed
(feed.jump, feed.list), and notification (create/create_for_surface/create_for_target/
list/dismiss/mark_read/open/jump_to_unread/clear) domains into the coordinator
behind their per-domain seams (ControlAppFocusContext/ControlFeedContext/
ControlNotificationContext), composed into the ControlCommandContext umbrella. The
core handle(_:) now chains per-domain handleX dispatchers.

Worker-lane methods stay app-side: feed.push/permission.reply/question.reply/
exit_plan.reply, and notification.create_for_caller (its own resolver).

Faithfulness: byte-identical payloads/errors (live socket sweep on ctl3c1 confirms
every result + error shape). Notification localized strings are resolved in the app
conformance (app bundle) and passed through ControlNotificationStrings, because
String(localized:) inside the package would bind to the package bundle and silently
drop the Japanese translations — a wire change for non-English locales.

Test fakes get benign defaults for non-window seams via ControlCommandContextTestStubs
so each fake implements only the domain it exercises (128 package tests still green).
TerminalController.swift 21952 -> 21522 (budget ratcheted).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move workspace.group.* (17 methods), pane.* (9 methods), and mobile.host.status/
mobile.workspace.list/mobile.terminal.* (+terminal.* aliases) into the coordinator
behind ControlWorkspaceGroupContext/ControlPaneContext/ControlMobileHostContext,
composed into the umbrella; core handle(_:) chains the new handlers.

Workspace Groups + Pane are full lifts (bodies deleted, payloads rebuilt as JSONValue,
localized group strings routed app-side via ControlWorkspaceGroupStrings). Mobile Host
is a faithful pass-through: its 8 bodies are SHARED with the mobile data-plane
(mobileHostHandleRPC) so they stay in TerminalController (relaxed private->internal);
the coordinator decouples via the seam and the conformance bridges V2CallResult.

Pane folds the resize support helpers (kept app-side: Bonsplit-coupled); v2SurfaceMove
relaxed private->internal for pane.join forwarding. Live socket sweep on ctl3c1 confirms
faithful payloads + errors (group create/list, pane list/create split, mobile host status).

TerminalController.swift 21522 -> 20296. 128 package tests green. Two new Pane files
>500 lines get budget entries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regression found by the no-regression code review of the moved domains: the
ported int() did Int(value) on a JSON double, which TRAPS (crashes) on overflow/
NaN — reachable via pane.resize amount or workspace.group.move to_index with e.g.
1e30 — whereas legacy v2Int went through (params[key] as? NSNumber).intValue,
which clamps. Also int()/double() didn't coerce a JSON boolean to a number the
way the legacy as? NSNumber path did.

Both now route doubles/bools through NSNumber.intValue/.doubleValue, matching
v2Int/v2Double exactly (truncate-toward-zero, clamp out-of-range, bool->1/0).
5 regression tests cover truncation, overflow/NaN no-trap, and bool coercion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Workspace (21 methods incl. remote.*) and Surface (25 methods + debug.terminals)
move into ControlCommandCoordinator behind ControlWorkspaceContext/
ControlSurfaceContext. ~2640 lines deleted from TerminalController.swift
(20296 -> ~17650). Worker-lane workspace.remote.pty_* stay app-side.

Two shared bodies the drafting agents wrongly flagged for deletion were RESTORED
(internal/private): v2WorkspaceCreate(params:tabManager:) is still driven by the
mobile data-plane v2MobileWorkspaceCreate; workspaceCloseProtectedMessage() by the
v1 close path. surface.move + debug.terminals forward to the still-shared
v2SurfaceMove/v2DebugTerminals (relaxed internal), like pane.join. Relaxed to
internal for the conformances: tabManager, socketFastPathState, orderedPanels,
readTerminalTextRawSnapshot.

Live socket sweep on ctl3c1 confirms faithful payloads + errors across both
domains (workspace list/current/create/rename/select/next/close, surface list/
current/health/send_text+read_text round-trip/resume.get, error shapes).
133 package tests green.

KNOWN FOLLOW-UPS: workspace.create logic is duplicated (conformance reimplements +
restored shared body) — dedupe by forwarding; the 2 Workspace files >500 lines
(budget entries added) should be split; adversarial code-review verification of
these 2 domains still pending (8 prior domains verified clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Coordinator/ had grown to 105 files. Move each domain's coordinator extension,
seam protocol, and value/resolution/snapshot types into a per-domain subfolder
(Window/AppFocus/Feed/Notification/Pane/Surface/Workspace/WorkspaceGroup/
MobileHost). The 4 shared core files stay at the Coordinator/ root:
ControlCommandContext (umbrella), ControlCommandCoordinator (core dispatch +
handle registry), ControlCommandCoordinator+Params (shared param/ref helpers),
ControlRoutingSelectors. SwiftPM globs sources recursively, so this is purely
organizational — no Package.swift/import changes. Budget paths updated for the
moved Pane/Workspace coordinator files; TC.swift budget corrected to 17680
(the two restored shared bodies grew it after the last bump).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Workspace lift had reimplemented workspace.create logic in the conformance
while the original v2WorkspaceCreate(params:tabManager:) was restored for the
mobile data-plane caller -- two copies that could diverge. Replace the typed
reimplementation with a passthrough that forwards to the single shared
v2WorkspaceCreate (relaxed private->internal) and bridges its Foundation result,
exactly like surface.move/debug.terminals/mobile. Deletes the now-unused
ControlWorkspaceCreateInputs/ControlWorkspaceCreateResolution. One source of
truth, byte-identical wire output.

Comprehensive socket sweep on ctl3c1 (all 10 domains, 38 ok + 13 expected
validation errors, zero crashes) confirms no regression: workspace.create happy
path + its cwd/layout validation errors preserved; pane.resize amount=1e30 now
clamps (invalid_state) instead of trapping (the int/double NSNumber fix). 133
package tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…view

Surface (4): surface.clear_history with a present-but-invalid surface_id silently
cleared the FOCUSED surface instead of returning not_found (wrong-target side
effect; hasSurfaceIDParam now crosses the seam like send_text); surface.split
with an unrecognized direction returned unavailable instead of invalid_params
'Missing or invalid direction (left|right|up|down)' (coordinator now validates
the parseSplitDirection token set + a drift-safe .invalidDirection case);
surface.split error precedence restored (direction -> agent-session -> divider;
the agent-session token check moved before divider parsing); surface.resume.*
explicit target restored to surface_id ?? tab_id ONLY (terminal_id is a general
routing alias but was never a resume target) and the window branch now requires
a RESOLVABLE window_id like origin.

Workspace (4): select/close/rename get the routing precheck so unresolvable
routing returns unavailable before param validation (legacy TabManager-first
order, matching reorder); workspace.current with a stale selectedTabId returns
.ok with workspace:null again instead of not_found. Dead code removed
(JSONValue.isControlNull, surfaceIDForInput).

All confirmed by live socket sweep on the rebuilt ctl3c1 (each previously-wrong
response now byte-matches origin). 133 package tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…3c-1

# Conflicts:
#	.github/swift-file-length-budget.tsv
…ce conformance

The tests-build-and-lag job failed solely on the Swift WARNING budget: the
Workspace conformance's controlWorkspaceRemotePTYAttachEnd declared
'sessionID sessionID: String' (extraneous duplicate). Behavior identical; the
job's build and lag phases were green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s (drafts integrated)

Five domains drafted by the orchestrator's agents (handed off), repaired
(browserNavContext accessor, allocateElementRef state call, v1 handlers
unhooked from the v2 chain), wired into the umbrella + dispatch, with test
stubs completed. 140 package tests green. App-side surgery follows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e coordinator

TerminalController.swift 18,033 -> 10,748 (-7,285). The five remaining domains
now dispatch through ControlCommandCoordinator: System (identify/tree/auth.login/
session.restore/settings.open/feedback.open/extension snapshot/workspace.action/
tab.action/drag_to_split/split_off), Project (project.* + markdown.open +
file.open), Debug (39 debug.* methods), Sidebar v1 (44 verbs via a new
handleSidebarV1 hook ahead of the v1 switch), Browser panel v1 (8 verbs), and
all 89 main-actor browser.* methods.

Browser per-surface state moved off the controller: ControlBrowserAutomationState
(package) + dialog responders keyed by dialogID app-side (the Sendable
V2BrowserPendingDialog redesign); cleanupSurfaceState purges the new state,
faithfully mirroring the legacy eviction. Two conformances the drafts never
included (ControlBrowserContext, ControlBrowserPanelContext) were authored
byte-faithfully from the legacy bodies. Shared bodies kept + relaxed to internal
(v2Identify, v2WorkspaceAction, v2SurfaceSplitOff, v2FileOpen, the 18 v1-debug
impls, the JS pump, the worker-lane browser.download.wait cluster).

Deliberate deltas (documented): controlFeedbackOpen drops the deprecated
.activateIgnoringOtherApps activation option (documented no-op on macOS 14+,
the project floor; keeping it fails the new-file warning budget); a sequence id
bridges Int64->Int (lossless on arm64).

Gates: package swift build + 140/140 tests; tagged app build BUILD SUCCEEDED;
live socket sweep green across all domains (system.tree, auth.login parity,
browser.open_split -> get.title returns the real page title end-to-end,
project validation errors, v1 set-status via the new hook, debug.terminals,
plus regression of the ten prior domains); zero new warnings; both budgets pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The System+Project adversarial review found file.open had been reimplemented in
the coordinator/conformance while the original v2FileOpen stayed behind (it is
driven directly by FilePreviewReviewFeedbackTests and MarkdownPanelTests) - two
copies that could drift, and a stale dispatcher comment claiming forwarding.
file.open now forwards to the single shared body and bridges its result, like
workspace.create; the reimplementation and its now-unused
ControlFileOpenResolution/ControlFileOpenSurface types are deleted.

Review verdicts so far: System+Project all faithful (this was the only finding,
not a behavior bug); Debug (39 verbs) + Sidebar v1 (44) + Browser-panel v1 (8)
all faithful, zero divergences, #if DEBUG gating verified end-to-end.

140 package tests green; app build green; live probe of file.open through the
shared body (happy path + both error shapes) byte-faithful.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…h residue

The Browser adversarial review (87/89 faithful) found its only two divergences
share one root cause: focus_mode.set and zoom.set validated mode/direction
BEFORE the TabManager/handle guards (legacy order: guards first). The shared
browserFocusedAction helper gains a post-guard validate step; both methods'
validation moves there. Live-verified: double-fault now returns
unavailable/'TabManager not available', single-fault the mode/direction error.

Residue: socketFastPathState drops its 'nonisolated' (after the cutover its
only callers are the @mainactor sidebar/surface conformances; the worker-thread
fast path retired with the legacy dispatcher). ServerEventTarget's @unchecked
Sendable and the V2CallResult/V2SocketRequest twins stay deliberately: they
serve the worker-lane and kept-shared bodies, which move in a later wave (the
target itself dissolves with TerminalControlComposition in Wave 5).

Verification totals for the five stacked domains: 143 methods/verbs reviewed
per-method vs the pre-deletion originals; 141 faithful as-lifted, 2 fixed here.
140 package tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…uard)

Inherited from the base branch; label equals name so the external signature
is unchanged and no callers move. Keeps the local Swift warning budget green
(documented toolchain skew: CI Xcode 16.4 accepts this silently).

Net line delta: 0. Tests: n/a (no behavior change).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extract the multi-window CLI-over-socket capability (blueprint Wave-2
"PR 2") out of Sources/AppDelegate.swift into a new CmuxIPCService
package: MultiWindowRouting seam, MultiWindowRouteResult DTO, and
MultiWindowRouter (Sendable struct; cliURL/socketPath/environment
constructor-injected). The private MultiWindowRouteCLIResult struct and
runMultiWindowRouteCLI free function are deleted; the only caller
(runMultiWindowWindowRouteCLIIfNeeded, UI-test scaffolding) constructs
the router behind its existing guards and forwards.

Faithful lift: the route body is byte-identical modulo the seam
spellings (verified by normalized machine-diff against
git show HEAD:Sources/AppDelegate.swift); the synchronous
Process.run + waitUntilExit contract and the global-queue caller are
preserved. The async-throws modernization lands in the next commit.
The router carries the minimal equivalent of the app-side
ProcessPipeReader read loop (chunked read, EINTR retry, partial data +
logged warning) because that type stays app-target for its other
callers; only the os_log category differs.

MultiWindowRouter is a stateless Sendable struct, not the blueprint's
sketched actor: it holds only immutable configuration, so an actor
would serialize unrelated route calls while protecting nothing
(LEARNINGS ruling; CmuxProcess.CommandRunner precedent).

Net line delta: Sources/AppDelegate.swift 18044 -> 18003 (-41);
+331 package lines (Package.swift + 3 sources + tests).
Tests: 5 new behavior tests (swift test green; spawn a scripted CLI
stand-in to pin --socket prepending, exact-env replacement,
stderr/exit-status capture, the legacy "-1" launch-failure encoding,
and non-UTF-8 output collapsing).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 11, 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 Jun 13, 2026 7:20pm
cmux-staging Building Building Preview, Comment Jun 13, 2026 7:20pm

@coderabbitai

coderabbitai Bot commented Jun 11, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c4705ef6-36c5-49ec-b81e-dcdb68f81e08

📥 Commits

Reviewing files that changed from the base of the PR and between e9e22c1 and 18958a9.

⛔ Files ignored due to path filters (1)
  • .github/swift-file-length-budget.tsv is excluded by !**/*.tsv
📒 Files selected for processing (1)
  • cmux.xcodeproj/project.pbxproj

📝 Walkthrough

Walkthrough

Extracts multi-window CLI routing into a new CmuxIPCService Swift package (protocol, router, types, and tests), replaces the AppDelegate’s legacy Process helper with async/await routing, and wires the package into the cmux app target.

Changes

CmuxIPCService Package & Integration

Layer / File(s) Summary
Package manifest and public contracts
Packages/CmuxIPCService/Package.swift, Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRoute{LaunchError,Result}.swift
Package manifest declares macOS v14, Swift 6 language mode, and upcoming features. Adds MultiWindowRouteLaunchError (description) and MultiWindowRouteResult (terminationStatus, stdout, stderr).
MultiWindowRouting protocol
Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRouting.swift
Defines async throwing route(arguments:) and routeCapturingLaunchFailure that converts launch errors into a MultiWindowRouteResult with terminationStatus -1 and the error description in stderr.
MultiWindowRouter implementation
Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRouter.swift
Implements a router that spawns the cmux CLI with --socket and provided args, replaces child environment, concurrently drains stdout/stderr via detached raw-fd readers to avoid pipe-buffer deadlocks, captures termination status, and returns MultiWindowRouteResult. Includes a raw-fd reader with EINTR retry and logging; launch failures throw MultiWindowRouteLaunchError.
Router test coverage
Packages/CmuxIPCService/Tests/CmuxIPCServiceTests/MultiWindowRouterTests.swift
Tests cover socket flag injection and argument forwarding, stdout/stderr capture and exit status, environment isolation, launch-failure and legacy capture behavior, non-UTF8 stdout handling, and deadlock-free draining for large stdout.
AppDelegate integration
Sources/AppDelegate.swift
Imports CmuxIPCService, removes the legacy runMultiWindowRouteCLI helper, switches to Task(priority: .userInitiated) + routeCapturingLaunchFailure(...), and persists terminationStatus, stdout, stderr for UI-test data.
Build configuration
cmux.xcodeproj/project.pbxproj
Adds local Swift package reference for Packages/CmuxIPCService and links the CmuxIPCService product into the cmux target via packageProductDependencies and frameworks build phase entries.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A package born, with async grace,
Routes the cmux through socket's space.
Tasks replace queues, no pipes will clog,
Readers drift gently, not lost in the fog.
Tests hop along — all outputs logged and safe.


Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Concurrency ❌ Error AppDelegate.swift introduces an unstructured fire-and-forget Task(priority: .userInitiated) { [weak self] in ... } for the async router calls without storing/awaiting the Task. Return/await the Task instead of fire-and-forget (e.g., make runMultiWindowWindowRouteCLIIfNeeded async and await the route calls, or store the Task so lifecycle/cancellation is explicit).
Cmux Swift @Concurrent ❌ Error MultiWindowRouter.route is a new async method doing process/pipe I/O but has no @concurrent; AppDelegate is @MainActor and calls it from UI test Task closure. Annotate the heavy async entrypoint(s) (e.g., MultiWindowRouter.route and/or routeCapturingLaunchFailure) with @concurrent or add an explicit hop (Task.detached) so process/pipe work can’t run on the caller actor.
Cmux User-Facing Error Privacy ❌ Error FAIL: MultiWindowRouteLaunchError/routeCapturingLaunchFailure preserve unredacted upstream/Foundation error text via String(describing: error) into public description and result.stderr. Sanitize launch-failure errors before putting them into public API/result strings (e.g., generic "launch failed"); keep raw String(describing:) only in internal logs or test-only code paths.
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.
✅ Passed checks (17 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: extracting multi-window CLI routing from AppDelegate into a new CmuxIPCService package behind the MultiWindowRouting protocol and MultiWindowRouter implementation.
Description check ✅ Passed The PR description is comprehensive and covers the template requirements: what changed (multi-window routing moved to new package), why (AppDelegate decomposition), testing (package tests and UI tests passed), and implementation details with architectural decisions documented.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed New CmuxIPCService production code has no @MainActor; MultiWindowRouter is Sendable and uses nonisolated file-scoped logger; detached readers only capture Sendable values (fd/ints), avoiding MainAc...
Cmux Swift Blocking Runtime ✅ Passed CmuxIPCService production code uses process.terminationHandler + checked continuation and Task.detached stdout/stderr readers; no DispatchSemaphore, Task.sleep, main.sync, or asyncAfter in the new...
Cmux Expensive Synchronous Load ✅ Passed PR diff (a9ce013…→9cbcdddf…) changes only CmuxIPCService routing; added lines contain no RestorableAgentSessionIndex.load/sysctl/syscall index loader on main/interactive paths.
Cmux Cache Substitution Correctness ✅ Passed AppDelegate’s UI-test persistence uses fresh file reads (Data(contentsOf:)) with cold-cache fallback to [:]; new CmuxIPCService routing introduces no cache/opportunistic reads in persistence/histor...
Cmux No Hacky Sleeps ✅ Passed PR #5920 changes only Swift/PBXProj/TSV files (no .ts/.js/.sh or build/runtime scripts). No matching hacky-sleep/delay patterns found; rule scope not applicable.
Cmux Algorithmic Complexity ✅ Passed Checked new CmuxIPCService production code: MultiWindowRouter reads stdout/stderr with linear chunked while-loops (64KiB) and has no nested scans/sorts/filters in hot paths; AppDelegate routing hel...
Cmux Swift File And Package Boundaries ✅ Passed Routing logic is extracted into the new Packages/CmuxIPCService SwiftPM package; production files are small (e.g., MultiWindowRouter.swift 147 lines) and AppDelegate no longer contains the old rout...
Cmux Swift Logging ✅ Passed PR adds a nonisolated private let Swift Logger in MultiWindowRouter and introduces no print/debugPrint/dump/NSLog or ad hoc stdout/stderr production logging in the Swift diff.
Cmux Full Internationalization ✅ Passed PR #5920 changes only new CmuxIPCService package files, AppDelegate.swift, swift-file-length-budget.tsv, and pbxproj—no .xcstrings/.strings or web/i18n/routing.ts touched; added strings are debug/l...
Cmux Swiftui State Layout ✅ Passed Changed diff (origin/main..HEAD) only extracts multi-window CLI routing into CmuxIPCService; no new SwiftUI state patterns (ObservableObject/@Published/@StateObject/@observable), GeometryReader mea...
Cmux Architecture Rethink ✅ Passed Routing extraction/modernization uses async terminationHandler + detached EOF draining; AppDelegate now awaits sequential route results via Task. No sleeps/delays, polling, locks, observers, or dup...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Scans of PR-related Swift files (new CmuxIPCService sources/tests and modified AppDelegate block) show no NSWindow/NSPanel/WindowGroup code or cmux.* auxiliary window identifier assignments; change...
Cmux Source Artifacts ✅ Passed PR changes only add/modify source, Swift package manifest, unit tests, and Xcode project config; no generated logs/build/DerivedData/temp/recordings/artifact directories were added per review rules.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-ipc-service

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 Jun 11, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extracts the multi-window CLI-over-socket routing capability from Sources/AppDelegate.swift into a new Packages/CmuxIPCService SwiftPM package, following the AppDelegate decomposition blueprint. As part of the lift, the synchronous waitUntilExit-based implementation is modernized to async throws with a terminationHandler continuation, fixing both a blocking-thread-pool hazard and a potential pipe-buffer deadlock.

  • New package CmuxIPCService introduces four types (MultiWindowRouting protocol, MultiWindowRouteResult DTO, MultiWindowRouteLaunchError, and MultiWindowRouter struct) with DocC comments, Swift 6 settings, and 7 behavior tests that spawn a scripted CLI stand-in.
  • AppDelegate caller replaces DispatchQueue.global + DispatchQueue.main.async with a single MainActor-inherited Task(priority: .userInitiated) that sequentially awaits three routeCapturingLaunchFailure calls and writes results back on-main, preserving all observable behavior.

Confidence Score: 5/5

Safe to merge; the extraction is behavior-preserving and the modernization strictly improves correctness by removing a blocking-thread-pool wait and fixing a pipe-buffer deadlock.

The new package follows all established house patterns: Sendable struct router, nonisolated async route, terminationHandler continuation instead of waitUntilExit, concurrent detached pipe readers matching CommandRunner, correct MainActor inheritance in the AppDelegate caller, and nonisolated file-scoped logger. Seven behavior tests with a scripted CLI stand-in cover every behavioral claim in the PR description. Previously flagged issues (nonisolated logger, public Foundation import, blocking readers) have all been addressed in the head commits or correctly explained as house-pattern-consistent.

No files require special attention. The only residual item the PR itself flags — manual smoke-test of the tagged build with a real second window — is a product-QA step, not a code defect.

Important Files Changed

Filename Overview
Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRouter.swift Core production conformer; clean async/await with terminationHandler continuation, concurrent pipe readers (matching CommandRunner pattern), correct Sendable struct isolation, and nonisolated file-scoped logger.
Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRouting.swift Seam protocol and routeCapturingLaunchFailure convenience that folds launch errors into the legacy -1 encoding; well-documented and correctly nonisolated.
Packages/CmuxIPCService/Package.swift Swift 6 manifest with macOS 14 platform, single library product, and per-target swiftLanguageMode(.v6) + ExistentialAny + InternalImportsByDefault; consistent with house conventions.
Sources/AppDelegate.swift Removes private MultiWindowRouteCLIResult and runMultiWindowRouteCLI; replaces DispatchQueue.global+main.async with a MainActor-inherited Task using the new router; result field rename String(terminationStatus) produces identical output.
Packages/CmuxIPCService/Tests/CmuxIPCServiceTests/MultiWindowRouterTests.swift 7 tests with scripted CLI stand-in covering argument forwarding, environment isolation, stderr/exit-status capture, launch failure encoding, non-UTF8 collapse, and >pipe-buffer deadlock prevention.
Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRouteLaunchError.swift Sendable, Equatable, CustomStringConvertible error type preserving verbatim legacy String(describing:) encoding; minimal and correct.
Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRouteResult.swift Sendable, Equatable result DTO with Int32 terminationStatus replacing the legacy String status field; fully public with DocC.
cmux.xcodeproj/project.pbxproj Wires CmuxIPCService as an XCLocalSwiftPackageReference and framework dependency for the app target; no spurious entries.
.github/swift-file-length-budget.tsv AppDelegate budget updated 18118→18077 (−41 lines), monotonically decreasing; consistent with the PR's stated delta.

Sequence Diagram

sequenceDiagram
    participant AD as AppDelegate (MainActor)
    participant T as Task (MainActor-inherited)
    participant R as MultiWindowRouter
    participant P as Process (cmux CLI)
    participant DR as Detached Readers (2x)

    AD->>T: Task(priority: .userInitiated)
    T->>R: await routeCapturingLaunchFailure(create args)
    R->>DR: Task.detached stdout/stderr readers
    R->>P: process.run() + close write ends
    P-->>R: terminationHandler fires
    DR-->>R: "EOF -> Data"
    R-->>T: MultiWindowRouteResult (create)
    T->>R: await routeCapturingLaunchFailure(window2 args)
    R->>DR: Task.detached readers
    R->>P: process.run() + close write ends
    P-->>R: terminationHandler fires
    DR-->>R: "EOF -> Data"
    R-->>T: MultiWindowRouteResult (window2List)
    T->>R: await routeCapturingLaunchFailure(window1 args)
    R->>DR: Task.detached readers
    R->>P: process.run() + close write ends
    P-->>R: terminationHandler fires
    DR-->>R: "EOF -> Data"
    R-->>T: MultiWindowRouteResult (window1List)
    T->>AD: writeMultiWindowNotificationTestData (on MainActor)
Loading

Reviews (7): Last reviewed commit: "Merge origin/main into feat-ipc-service:..." | Re-trigger Greptile

Comment thread Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRouter.swift Outdated
Comment thread Packages/CmuxIPCService/Sources/CmuxIPCService/MultiWindowRouter.swift Outdated
Convert the lifted synchronous contract in a separated commit per the
faithful-lift discipline:

- MultiWindowRouting.route becomes async throws; launch failure now
  throws MultiWindowRouteLaunchError (new one-type file) whose
  CustomStringConvertible preserves the underlying error's
  String(describing:) text verbatim.
- MultiWindowRouter replaces waitUntilExit (which blocked a
  global-queue thread) with a terminationHandler continuation, and
  drains stdout/stderr concurrently with the child via detached
  readers keyed by raw fd (the CmuxProcess.CommandRunner pattern).
- MultiWindowRouteResult.status: String becomes terminationStatus:
  Int32; the caller formats String(terminationStatus), producing
  identical strings for every reachable value.
- routeCapturingLaunchFailure protocol-extension convenience (the
  CommandRunning.runStandardOutput precedent) folds the throw back
  into the legacy capture encoding (-1 status, error description in
  stderr) so each of the three UI-test calls still runs independently
  and the test-data file bytes stay identical.
- AppDelegate's DispatchQueue.global + DispatchQueue.main.async pair
  becomes one MainActor-inherited Task(priority: .userInitiated) with
  sequential awaits; ordering, the weak-self-gated write, and the
  main-thread write destination are preserved.

Observer analysis: the only behavior change is that CLI output larger
than the 64 KiB pipe buffer no longer deadlocks the call forever
(legacy read the pipes only after exit); pinned by a new test.

Net line delta: Sources/AppDelegate.swift 18003 -> 18003 (0);
package +91 lines. Tests: 7 (was 5; +launch-error description
preservation, +capture encoding, +pipe-buffer no-deadlock), green from
a clean .build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
stage 3c (stacked): final five domains — System/Project/Debug/Sidebar/Browser
azooz2003-bit and others added 2 commits June 11, 2026 16:47
…est, unused imports)

- @ObservationIgnored on the coordinator's handles registry: it is a struct
  mutated by ref() on nearly every response, so tracking it would invalidate
  any observer on every socket command (greptile).
- windowCloseOkAndNotFound now also asserts the not_found branch (coderabbit).
- Drop unused Foundation imports from ControlAppFocusContext and
  ControlMobileHostContext (coderabbit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
House-style shape for file-scoped os.Logger constants (matches the
app-side ProcessPipeReader precedent); no behavior change.

Net line delta: 0. Tests: 7 (unchanged, green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
azooz2003-bit and others added 2 commits June 11, 2026 19:42
…ert the coordinator's v2 browser.* domain

PR 5778 moved the JS-evaluating browser.* methods onto a nonisolated
socket-worker lane while this branch had lifted the (pre-5778) browser
domain into the @mainactor coordinator. The two designs are incompatible
and main's is the behavioral reference, so this merge takes main's
browser implementation wholesale and deletes the coordinator's browser-v2
domain (package files, app conformances, umbrella members, tests). The
v1 browser-panel and sidebar handlers and the other 13 coordinator
domains are untouched by main and stay. mobile.terminal.paste (new in
PR 5876) dispatches from the legacy v2 switch. The browser domain gets
re-lifted in a follow-up against the worker-lane architecture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Base moved to 2be538e (main sync: socket-worker browser JS lane,
v2 browser.* revert, final-five 3c domains).

Conflict resolution: .github/swift-file-length-budget.tsv only —
union of both sides, conflicted entries recomputed to actual merged
line counts (ContentView 19256, AppDelegate 17980). AppDelegate.swift
and pbxproj auto-merged; pbxproj normalized and checked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@azooz2003-bit
azooz2003-bit changed the base branch from feat-ctl-coordinator-3c-1 to main June 12, 2026 17:33
Main brought 16 commits since the sync point, including the 3c
coordinator merge (6743387), renderer realization GPU reclaim plus
ghostty submodule bump to 5697db8 (aeb8847), browser proxy and
download fixes, agent launch capture trust, iOS View as Text, and
v0.64.15.

Resolutions:
- TerminalController+ControlWorkspaceContext.swift (add/add): took
  main's version verbatim; the branch side differed only by a trailing
  blank line inherited from feat-ctl-coordinator-3c-1.
- swift-file-length-budget.tsv: regenerated from actual line counts
  (--write-budget), which also drops the 4 stale entries for the 3c
  browser-v2 coordinator files that exist on neither side.
- ghostty submodule: main's 5697db8 (branch pointer 34cbf18 was
  inherited, no slice commits).

Packages/CmuxControlSocket is identical to origin/main (zero-path
diff); no inherited 3c-only socket content to strip.

Gates: budget, lint-ios-package-conventions, swift build
CmuxIPCService, xcodebuild Debug compile all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	.github/swift-file-length-budget.tsv
…5907

Resolve two shared-infra conflicts brought by CmuxTerminalCore (#5894) and
CmuxSidebarGit (#5907):

- cmux.xcodeproj/project.pbxproj: union the XCSwiftPackageProductDependency
  block (keep both CmuxIPCService and main's CmuxTerminalCore entries);
  normalize + check-pbxproj + xcodebuild -list all pass, braces balanced.
- .github/swift-file-length-budget.tsv: regenerate from the merged tree via
  swift_file_length_budget.py --write-budget; budget respected (exit 0).

ghostty submodule pointer matches origin/main. Local app build
(xcodebuild -project cmux.xcodeproj -scheme cmux build) = BUILD SUCCEEDED,
0 errors, confirming no silent cross-file break from the merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@azooz2003-bit
azooz2003-bit merged commit 69f3129 into main Jun 13, 2026
24 checks passed
azooz2003-bit added a commit that referenced this pull request Jun 13, 2026
origin/main moved forward after the first resync (#6052 sidebar row cleanups,
#5920 CmuxIPCService MultiWindowRouter extraction). Only conflict was the
regenerable swift-file-length-budget.tsv; AppDelegate/ContentView/pbxproj
auto-merged. Budget regenerated from the merged tree; pbxproj normalized +
parses; runMultiWindowRouteCLI confirmed single-homed in CmuxIPCService.

Local Debug app build: ** BUILD SUCCEEDED **.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
azooz2003-bit added a commit that referenced this pull request Jun 13, 2026
…/5920/5921)

Resolve conflicts from the parallel main-side decomposition:

- Workspace.swift: drop the WorkspaceRemote* inline types kept on main; 5896
  lifted them to CmuxRemoteDaemon/CmuxRemoteSession/CmuxRemoteWorkspace.
- AppDelegate.swift: drop the re-added runMultiWindowRouteCLI; it lifted to
  CmuxIPCService.MultiWindowRouter on main (#5920).
- Import unions in ContentView / TerminalController / TerminalController+
  ControlWorkspaceContext / GhosttyConfigTests (CmuxCore + CmuxPanes/
  CmuxWorkspaces + CmuxBrowser + CmuxRemote* coexist).
- pbxproj: union the conflict regions, then drop stale wiring for files that
  main deleted via decomposition (SplitEqualizer.swift -> CmuxPanes;
  RemoteLoopbackProxyAlias.swift -> CmuxCore).
- swift-file-length-budget.tsv: regenerate from the merged tree.

Port #6056 (OpenWrt BusyBox remote platform probe) into its new home: main
added Sources/WorkspaceRemotePlatformProbeScript.swift as an extension on the
now-lifted WorkspaceRemoteSessionController. Re-home the fix into
RemoteSessionCoordinator+Bootstrap.swift (BusyBox-portable literal case
normalization instead of `tr '[:upper:]' '[:lower:]'`, version path-segment
sanitization, marker-stripped user-facing stdout, armv7 arch) and port the
regression tests into CmuxRemoteSession's test target. Delete the orphan
app-target probe + test files that referenced the deleted type.

ghostty pointer unchanged (matches origin/main). CmuxControlSocket stays
zero-diff vs main. RemoteSessionProcessRunner @suite(.serialized) preserved.

Gates: ensure-ghosttykit OK, swift_file_length_budget exit 0, lint exit 0,
xcodebuild app build SUCCEEDED, swift test CmuxRemoteSession 22/22 pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview – cmux — 18958a9e Deployed Jun 13, 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.

1 participant