Repository navigation
Conversation
* Add browser import flow with installed-browser detection * Tone down empty browser import overlay * Make browser import a 2-step choice flow * Use single-window browser import wizard with close button * Mention extensions not yet supported in import note * Reapply "Merge pull request manaflow-ai#239 from manaflow-ai/issue-151-ssh-remote-port-proxying" This reverts commit f7cbbad. * Fix ssh stack review regressions * Address ssh stack review follow-ups * Optimize remote daemon builds and TCP latency * Add remote favicon proxy regression * Proxy remote browser favicon fetches * Add ssh profile-noise regression * Avoid sourcing profile in ssh bootstrap * Add ssh stack regression tests * Fix ssh stack review regressions * Fix ghostty deferred-init regression harness * Fix SSH workspace priming and restore state * Fix SSH transport dedupe and loopback review issues * Fix browser move and zsh bootstrap regressions * Add regressions for v1 panel focus preservation * Fix socket focus and startup env regressions * Add regression test for deferred terminal portal sync * Defer terminal portal sync past layout churn * Keep portal sync responsive during live resize * fix: show sidebar update banner from background checks (manaflow-ai#1543) * Update bonsplit for split transparency * Update bonsplit for split transparency * Support folder drops on dock icon (manaflow-ai#1571) * Fix sidebar PR badges for restored workspaces (manaflow-ai#1570) * test: cover sidebar PR explicit branch fallback * fix: restore sidebar PR badges for workspace branches * test: preserve sidebar PR badge on first prompt * fix: keep sidebar PR badges through first prompt * feat: add browser profile mapping import flow * Avoid blocking browser PR metadata updates (manaflow-ai#1564) * Fix manaflow-ai#1574: remove top update banner in sidebar (manaflow-ai#1575) * test: cover sidebar update indicator regression * fix: remove duplicate sidebar update banner * fix: address browser import review feedback * Stabilize SSH remote flow after merging main * Make remote proxy close idempotent * Fix UI test helper closure captures * Add remote CLI relay regressions * Fix nightly remote daemon and SSH relay wiring * Migrate CI/CD to WarpBuild, consolidate test jobs (manaflow-ai#1501) * Migrate CI/CD to WarpBuild, consolidate test jobs Replace all macOS runner labels across workflows: - depot-macos-latest → warp-macos-15-arm64-6x - macos-15 → warp-macos-15-arm64-6x - macos-14 → warp-macos-14-arm64-6x Consolidates tests + tests-depot into a single tests job that runs unit tests, regressions, UI tests, and lag tests sequentially on one WarpBuild runner. Ubuntu jobs remain on ubuntu-latest. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Upgrade stale zig on runners that have an outdated version pre-installed WarpBuild macos-14 ships zig 0.15.1 but the project requires 0.15.2. The install step skipped because zig was found, just outdated. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Pin zig 0.15.2 via direct tarball instead of Homebrew Homebrew's zig bottle for macOS 14 (Sonoma) is stuck at 0.15.1 but the ghostty submodule requires 0.15.2. Download zig directly from ziglang.org to guarantee the correct version on all runner images. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix zig tarball URL: arch-os order is aarch64-macos, not macos-aarch64 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Create /usr/local/bin and /usr/local/lib before copying zig WarpBuild runners don't have /usr/local/lib by default. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add 20-min timeout to WarpBuild jobs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix UI test hang: stream output instead of variable capture, use GitHub runner for macOS 14 The OUTPUT=$(...) pattern buffers all xcodebuild output into a bash variable. For the full cmux scheme (build + UI tests), this can be hundreds of MB, causing the shell to hang. Replace with tee streaming. macOS 14 on WarpBuild consistently hangs (unit tests timeout at 20min vs 4min on macOS 15, same M4 Pro hardware). Use GitHub-hosted macos-14 runner for compat tests instead, which works on main today. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Split UI tests to GitHub-hosted runner (WarpBuild can't activate GUI apps) WarpBuild macOS VMs leave XCUIApplication stuck in "Running Background" state, causing every UI test to burn ~62s waiting for activation and timing out the job. Root cause: WarpBuild ephemeral VMs don't provide a full GUI session for app activation. Split CI into parallel jobs: - tests: WarpBuild (unit tests + regressions, ~6 min) - tests-ui: GitHub-hosted macos-15 (UI tests + lag regression) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Move tests-ui to WarpBuild with TCC permission grants Grant accessibility, post-event, and screen capture TCC permissions to Xcode and XCTest processes on WarpBuild ephemeral VMs. This should fix "Failed to activate application (Running Background)" errors that prevent XCUITests from bringing the app to foreground. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add GUI session diagnostics and DevToolsSecurity for WarpBuild UI tests Add session diagnostics (who, console user, GUI domain, WindowServer, loginwindow) to understand WarpBuild VM session state. Also enable DevToolsSecurity and security authorizationdb for XCTest process control. Try bootstrapping GUI session if missing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix TCC permissions: use Xcode-Helper + user DB (CircleCI approach) Previous TCC grants used wrong client IDs (com.apple.dt.Xcode) and only wrote to the system database. CircleCI's proven approach grants: - kTCCServiceAccessibility to com.apple.dt.Xcode-Helper (not Xcode) - kTCCServiceDeveloperTool to com.apple.Terminal - Both system AND user-level TCC databases Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Reduce UI test timeout to 15s for WarpBuild expected failures WarpBuild Virtualization.framework VMs cannot activate macOS GUI apps (XCUIApplication stuck "Running Background"). Tests still execute and report expected failures. But the 62s per-test activation timeout makes 30+ tests take 30+ minutes total. Set per-test timeout to 15s so expected failures resolve quickly. Full interactive UI test coverage runs via test-e2e.yml on GitHub-hosted runners with proper display support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Replace XCUITest run with build + lag regression on WarpBuild WarpBuild Virtualization.framework VMs cannot activate macOS GUI apps (XCUIApplication stuck "Running Background" with 62s activation timeout per test). Tried TCC permissions, DevToolsSecurity, virtual display, reduced timeouts, nothing fixes the framework-level issue. Replace tests-ui job with tests-build-and-lag: - Build the full cmux scheme (verifies compilation) - Run workspace churn typing-lag regression (socket-based, no GUI) - XCUITests run via test-e2e.yml on GitHub-hosted runners Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Move macOS 14 compat to WarpBuild (no GitHub-hosted runners) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add diagnostic workflow to probe WarpBuild GUI activation Tests multiple app activation approaches on WarpBuild VMs: - open -a, NSWorkspace, NSRunningApplication.activate, osascript - Virtual display state before/after CGVirtualDisplay - TCC/accessibility permissions, Quartz session info - VM type detection This is a workflow_dispatch-only diagnostic to determine if XCUITest can work on WarpBuild with the right configuration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Trigger GUI probe on branch push (workflow_dispatch needs main) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Rewrite GUI probe with Swift (Python lacks AppKit on WarpBuild) v1 failed because WarpBuild's Python isn't a framework build and can't import AppKit/Quartz. v2 uses a compiled Swift binary to test NSRunningApplication.activate(), osascript, Quartz session state, display info, and AX trust. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * GUI probe v3: try 5 approaches to unlock WarpBuild screen 1. defaults write (screensaver, loginwindow, pmset) 2. automationmodetool enable-automationmode-without-authentication 3. CGSSessionSetScreenLocked private API + System Events keystroke 4. sysadminctl -screenLock off + keychain unlock 5. CGEvent simulation (mouse move + Return key to dismiss lock) Each approach is followed by an activation check to see if it worked. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Test GUI activation on macOS 14, 15, and 26 (Tahoe) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add DerivedData and GhosttyKit caching to CI workflows Major caching improvements across ci.yml and ci-macos-compat.yml: - Cache GhosttyKit.xcframework keyed on ghostty submodule SHA (skip download on cache hit) - Cache DerivedData keyed on OS + Xcode version + Package.resolved + project.pbxproj (enables incremental builds across runs) - Remove explicit DerivedData wipe (rely on cache key invalidation) - Use download-prebuilt-ghosttykit.sh in compat workflow too This should significantly speed up macOS 14 compat tests which were taking 20+ min due to full recompilation every run. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Bump macOS 14 compat timeout to 45 min for cold cache seeding The DerivedData cache wasn't saved because the job timed out at 30 min, causing the post-job cache save step to be skipped. 45 min gives enough headroom for the first uncached run to complete and seed the cache. Subsequent runs should be much faster with incremental builds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Use Depot runners for E2E tests (WarpBuild has screen lock on macOS 15/26) WarpBuild VMs on macOS 15 and 26 have CGSSessionScreenIsLocked=1, which prevents XCUIApplication activation. Depot runners have working GUI activation. Can switch back to WarpBuild once they fix the VM images. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Skip smoke test on macOS 14 compat, remove GUI diagnostic workflow macOS 14 was slow because it built the full app (cmux scheme) on top of unit tests (cmux-unit scheme). Unit tests are the real compat check; smoke test runs on macOS 15 only. Also removes the temporary test-warpbuild-gui.yml diagnostic workflow. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Replace Sonoma with Tahoe in compat matrix, drop macOS 14 Swap macOS 14 (Sonoma) for macOS 26 (Tahoe). Smoke test runs on macOS 15 only (WarpBuild screen lock blocks app activation on 26). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Drop macOS 26 from compat matrix (zig 0.15.2 linker failure) Zig 0.15.2 can't link against the macOS 26 (Tahoe) SDK: undefined symbols for basic libc functions (_abort, _free, _fork, etc.). The zig toolchain needs an update to support Tahoe. Keep macOS 15 only for now. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * Fix release browser portal compile * Add macOS 26 (Tahoe) compat tests, skip zig build via stub (manaflow-ai#1590) Zig 0.15.2's MachO linker can't resolve libSystem on macOS 26 (the version number jump from 15 to 26 breaks zig's SDK handling). The unit tests don't need the CLI helper binary at runtime, so we skip the zig build on macOS 26 by setting CMUX_SKIP_ZIG_BUILD=1, which creates a stub binary to satisfy the Xcode Run Script file check. Smoke test (full app build + launch) is skipped on macOS 26 since it needs the real CLI helper. Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add regression tests for SSH remote CLI follow-ups * Fix SSH remote CLI and loopback proxy follow-ups * Fix remote daemon build script using relative output path after cd (manaflow-ai#1595) The Go build runs in a subshell that cd's to daemon/remote/, but OUTPUT_DIR was relative to the repo root. Resolve to absolute path after mkdir so go build -o writes to the correct location. Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address SSH follow-up PR review comments * fix: restore Sparkle automatic update checks (manaflow-ai#1597) * feat: add native MCP protocol support to socket server Add MCP (Model Context Protocol) Content-Length framing support directly to the cmux socket server, enabling AI tools to connect via socat without needing a separate Node.js MCP wrapper process. Protocol detection on first read: "Content-Length:" → MCP mode, "{" → V2 JSON-RPC, else V1 plain text. All three protocols coexist on the same Unix socket. New files: - MCPServer.swift: Content-Length framing parser, MCPHandler with 3 JSON-RPC methods (initialize, tools/list, tools/call), and 20 MCP tool schemas that route to existing V2 socket methods - MCPServerTests.swift: 28 unit tests covering framing, handler logic, and tool routing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address MCP server review findings - Fix test initialization: tests calling tools/list and tools/call now send initialize first (XCTest creates fresh instances per test) - Add testToolsListBeforeInitializeReturnsError to cover the guard - Fix MCP message loop: continue parsing after each message instead of breaking after one, avoiding latency when multiple messages arrive in a single socket read - Forward press_enter param in send_input tool routing - Quote V1 command arguments to prevent injection via tokenizer - Add writeSocketData helper with EINTR/partial-write handling - Add encodeResponse fallback for non-serializable dicts - Require initialized handshake before tools/list and tools/call - Reject MCP connections when password auth is required - Close connection on corrupted data after MCP detection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com> Co-authored-by: Austin Wang <austinwang115@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
MCP tools use human-friendly names (surface, workspace, pane) but V2 handlers look for surface_id, workspace_id, pane_id. Without this fix, V2 falls back to the focused surface instead of using the specified one, causing browser_navigate to fail and other commands to target wrong surfaces. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ption)
Two fixes for the infinite constraint-update cycle that crashes the app
on first launch after granting accessibility permission:
1. Workspace.flushWorkspaceWindowLayouts: skip non-visible and off-screen
windows. The method iterated ALL NSApp.windows including SwiftUI system
windows (settings, about) positioned at {0, -5984} with height 7432.
Calling layoutSubtreeIfNeeded on those triggers a constraint cycle when
their SwiftUI layout hasn't converged.
2. TerminalNotificationStore.refreshAuthorizationStatus: defer initial
call from init with asyncAfter(0.2s) to land outside the window setup
constraint pass. The callback also uses asyncAfter(0.05s) instead of
plain async, and skips no-op @published updates to avoid triggering
SwiftUI invalidation when the value hasn't changed.
Repro: tccutil reset → launch → grant accessibility → crash.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Extract magic numbers to named constants: offscreenLayoutSkipYThreshold, initialAuthCheckDelay, authStateUpdateDelay - Add safety comments to ensureAuthorization and requestAuthorization mutation sites explaining why plain main.async is safe there (user-action- triggered, window constraints already converged) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(cmux): add Phase 4e gate script * fix(cmux): handle submodule init failure in gate
* test(cmux): add IOSurface leaks CLI harness * test(cmux): harden IOSurface leaks sentinel wait * fix(test): prevent sentinel PID leak on EXIT trap - Move sentinel PID to global SENTINEL_PID variable so EXIT trap can access it - Previously, local 'pid' variable in main() was out of scope when trap fired on normal exit - This caused the cmux sentinel process to be orphaned on successful test runs - Also fix integer comparison for IOSurface bytes (use -ne instead of !=) - Reduce vmmap polling interval from 0.1s to 0.2s to avoid excessive system calls Addresses Bugbot finding: EXIT trap loses local pid on success path Co-authored-by: Etan Heyman <EtanHey@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Etan Heyman <EtanHey@users.noreply.github.com>
Hook fell off end without exit 0. Sister fix to voicelayer manaflow-ai#182 across 5 layer repos. Recipe: brainbar-85cb1121-307.
|
@EtanHey is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThis PR introduces an MCP (Model Context Protocol) server implementation with JSON-RPC framing, adds pre-push regression testing via Git hooks and test scripts, implements IOSurface memory leak detection fixtures, and optimizes Auto Layout timing by deferring notification state updates and skipping off-screen window layout passes. Changes
Sequence DiagramsequenceDiagram
participant Client as MCP Client
participant TerminalCtrl as TerminalController
participant MCPFrame as MCPFraming
participant MCPHdlr as MCPHandler
participant Schemas as MCPToolSchemas
participant V2Exec as V2 Executor
Client->>TerminalCtrl: Send Content-Length framed message
TerminalCtrl->>MCPFrame: parse(data)
MCPFrame-->>TerminalCtrl: ParseResult (complete/needMore/notMCP)
alt Complete MCP Frame
TerminalCtrl->>MCPHdlr: handleRequest(jsonRpcDict)
alt Initialize Request
MCPHdlr-->>TerminalCtrl: Server info + tool capabilities
TerminalCtrl->>MCPFrame: encodeResponse(result)
MCPFrame-->>TerminalCtrl: Content-Length framed JSON
TerminalCtrl->>Client: Send framed response
else Tools/List Request
MCPHdlr->>Schemas: List available tools
Schemas-->>MCPHdlr: Tool array
MCPHdlr-->>TerminalCtrl: Tools response
TerminalCtrl->>MCPFrame: encodeResponse(result)
TerminalCtrl->>Client: Send framed response
else Tools/Call Request
MCPHdlr->>Schemas: route(toolName)
Schemas-->>MCPHdlr: V2 method + param mapping
MCPHdlr->>V2Exec: Execute mapped V2 method
V2Exec-->>MCPHdlr: Result or error
MCPHdlr-->>TerminalCtrl: MCP content response
TerminalCtrl->>MCPFrame: encodeResponse(result)
TerminalCtrl->>Client: Send framed response
end
else Non-MCP or Fallback
TerminalCtrl->>TerminalCtrl: Route to V1/V2 command logic
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
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. Comment |
Greptile SummaryThis PR adds an MCP (Model Context Protocol) JSON-RPC 2.0 server to cmux, introduces a pre-push regression harness gate (
Confidence Score: 3/5Not safe to merge until the broken routing tests in MCPServerTests.swift are corrected. A P1 defect — three XCTest assertions using wrong parameter key names that will always fail — directly breaks CI on the new MCPServer test suite. The rest of the change is well-structured, but the broken tests must be fixed first. cmuxTests/MCPServerTests.swift — routing test parameter key names must be corrected before merge. Important Files Changed
|
There was a problem hiding this comment.
Actionable comments posted: 11
🧹 Nitpick comments (1)
cmuxTests/MCPServerTests.swift (1)
170-199: Consider documenting the expected tool count or making it more resilient.The assertion
XCTAssertEqual(tools.count, 20)will fail if any tools are added or removed. This is intentional as a regression guard, but a brief comment explaining why exactly 20 tools are expected would help future maintainers understand whether this is a strict requirement or needs updating when tools change.Suggested improvement
func testToolsListReturns20Tools() { let request = mcpRequest(id: 2, method: "tools/list", params: [:]) let response = handler.handleRequest(request)! let result = response["result"] as! [String: Any] let tools = result["tools"] as! [[String: Any]] + // MCP spec exposes 20 tools; update this count when adding/removing tools XCTAssertEqual(tools.count, 20)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmuxTests/MCPServerTests.swift` around lines 170 - 199, The test testToolsListReturns20Tools has a brittle assertion XCTAssertEqual(tools.count, 20) that will break when tools are added/removed; either document why 20 is a strict requirement with a comment above that assertion referencing testToolsListReturns20Tools and tools.count, or make the check resilient by replacing the equality check with a minimum/threshold check (e.g., XCTAssertGreaterThanOrEqual(tools.count, 20)) and rely on the existing explicit existence assertions for required tools (the toolNames contains checks) to ensure critical tools remain present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cmuxTests/RapidSpawnKillFixtureTests.swift`:
- Around line 148-152: The test is accidentally inheriting any exported
CMUX_RAPID_SPAWN_KILL_* variables from the parent environment when runProcess
builds mergedEnvironment; update runProcess (where mergedEnvironment is created
and assigned to process.environment) to first remove any keys matching the
CMUX_RAPID_SPAWN_KILL_* prefix (e.g. CMUX_RAPID_SPAWN_KILL_EXECUTABLE_PATH) from
ProcessInfo.processInfo.environment before applying the test's own environment
overrides so the fixture cannot be pointed at the wrong binary.
- Around line 168-185: The timeout handling may hang because process.terminate()
(SIGTERM) can be ignored and readDataToEndOfFile() will block; update the logic
around exitSignal, timedOut, and process.terminate() so that after the 2-second
grace period you check if the process is still running and then send a SIGKILL
(e.g., via process.interrupt/kill or posix_kill) and wait for
process.waitUntilExit()/exitSignal again, and/or close
stdoutPipe.fileHandleForReading and stderrPipe.fileHandleForReading before
calling readDataToEndOfFile() to ensure reads do not block; keep using
exitSignal and the timedOut flag but ensure you always wait for process
termination (or forcibly kill) before constructing ProcessResult.
In `@scripts/run_tests.sh`:
- Around line 14-18: DERIVED_DATA_PATH is currently a fixed shared directory
causing stale artifacts; change its default to create a unique per-run temp
directory (e.g., use mktemp -d with a /tmp/cmux-run-tests-derived.XXXX template)
and make BUILT_APP_PATH, FIXTURE_PATH, LEAK_TEST_PATH and any other uses (the
other blocks around the indicated ranges) reference that variable so each
invocation uses its own DerivedData; ensure any setup/cleanup logic that assumes
the old path is updated to handle the dynamic DERIVED_DATA_PATH.
- Line 13: The default DESTINATION value is hard-coded to
"platform=macOS,arch=arm64", which forces arm64 and breaks Intel Macs; update
the DESTINATION assignment in scripts/run_tests.sh (the DESTINATION variable) to
use only "platform=macOS" as the default (like other scripts), and apply the
same change for the other occurrences around lines 45-53 so xcodebuild can pick
the native architecture.
In `@Sources/MCPServer.swift`:
- Around line 166-170: The code currently coerces params["arguments"] to an
empty dictionary which hides protocol errors; change the handling so that if
params["arguments"] is absent you set toolArgs = [:], but if it is present and
not a [String: Any] you return jsonRPCError(id: id, code: -32602, message:
"Invalid 'arguments' - expected object"); implement this by replacing the let
toolArgs = ... line with a conditional cast/check on params["arguments"] and
returning the jsonRPCError when the value exists but fails to cast, referencing
the existing params, toolArgs variable, and jsonRPCError(id:code:message:) call.
- Around line 181-189: The current case .ok handling only JSON-serializes
[String: Any]; update it to emit stable JSON for any JSON-compatible top-level
value: first check JSONSerialization.isValidJSONObject(payload) and if true
serialize with JSONSerialization.data(withJSONObject:payload), otherwise detect
primitive JSON types (String, Int, Double, Bool, NSNull/Optional.none) and
serialize those by casting and encoding each with JSONEncoder (e.g., encode the
String/Int/Double/Bool value) to produce proper JSON text; if all serialization
attempts fail, fall back to String(describing: payload). Ensure these changes
replace the logic that used only a [String: Any] cast so v2Execute payloads that
are arrays or primitives are emitted as valid JSON.
- Around line 47-56: The parser currently accepts any non-negative
Content-Length which allows a peer to advertise arbitrarily large frames; add a
sanity cap (e.g. MAX_CONTENT_LENGTH or mcpMaxContentLength) and check it
immediately after parsing contentLength in the MCPServer.swift block (the guard
that binds headerStr and contentLength and before computing
bodyStart/totalNeeded); if contentLength is greater than the cap, reject the
frame (return the appropriate failure like .notMCP or an explicit invalid-frame
result used elsewhere) instead of falling through to the .needMore path so the
connection cannot force unbounded buffering.
In `@Sources/TerminalController.swift`:
- Around line 1603-1644: The code currently decides MCP vs NDJSON from a single
read by checking only the first chunk (firstBytes) which can split the
"Content-Length:" header; change this to accumulate an undecided preamble buffer
(e.g., mcpPreamble) by appending buffer[0..<bytesRead] while mcpDetected ==
false && mcpHandler == nil, and only decide MCP when either the preamble
contains the full "Content-Length:" prefix (check using the full prefix length)
or the preamble grows past a safe threshold (e.g., 64 bytes) or contains a
newline that proves it is NDJSON; when the prefix is detected set mcpDetected =
true and create mcpHandler and move any remaining bytes from mcpPreamble into
mcpBuffer, otherwise once ruled out stop accumulating and fall back to the
newline/NDJSON parser using the preserved preamble content (do not emit errors
for partial headers). Ensure all references use the existing symbols
mcpDetected, mcpHandler, mcpBuffer and replace firstBytes logic with the new
mcpPreamble accumulation and checks.
- Around line 2068-2085: The V1 MCP handlers for "report_meta" and
"set_progress" currently accept a params["surface"] and append "--surface=..."
which silently targets the active tab instead of the surface's owning workspace;
change the logic in the case "report_meta" and case "set_progress" blocks to
either reject surface (return .error(code: "invalid_params", message: "surface
not supported for v1")) or resolve the surface to its owning workspace ID and
pass "--tab=<workspace_id>" instead of "--surface=..."; implement this by
checking params["surface"] early in each case, calling your existing
workspace/surface lookup routine (or add a helper like
resolveOwnerWorkspace(forSurface:)) to get the workspace id, return an error if
resolution fails, and append "--tab=\(workspaceId)" (or error out) rather than
appending "--surface=\(…)".
In `@Sources/Workspace.swift`:
- Around line 8048-8051: The hardcoded offscreen Y threshold
(offscreenLayoutSkipYThreshold and the check using window.frame.origin.y >=
-2000) can incorrectly skip visible windows on tall/multi-display setups;
replace that heuristic with a proper on-screen test: determine whether the
window's frame intersects any connected NSScreen frame (or use window.screen and
convert the window rect to screen coordinates) and only treat it as off-screen
when it does not intersect any NSScreen bounds. Update the logic in Workspace
where offscreenLayoutSkipYThreshold is referenced (the window/frame check block)
to use this intersection test and remove or deprecate the -2000 constant. Ensure
the change handles nil window.screen and considers converted screen coordinates
so visible windows on stacked displays are not skipped.
In `@tests/regression/test_iosurface_leak.sh`:
- Around line 263-266: The success path in the test_iosurface_leak.sh script
currently logs a PASS but falls off the end of the main function without an
explicit exit code; add an explicit exit 0 on the successful branch (i.e.,
immediately after the log "PASS IOSurface footprint stayed within threshold" in
the main function) so the script returns a deterministic success status
consistent with the existing exit "$status"/exit 1 patterns.
---
Nitpick comments:
In `@cmuxTests/MCPServerTests.swift`:
- Around line 170-199: The test testToolsListReturns20Tools has a brittle
assertion XCTAssertEqual(tools.count, 20) that will break when tools are
added/removed; either document why 20 is a strict requirement with a comment
above that assertion referencing testToolsListReturns20Tools and tools.count, or
make the check resilient by replacing the equality check with a
minimum/threshold check (e.g., XCTAssertGreaterThanOrEqual(tools.count, 20)) and
rely on the existing explicit existence assertions for required tools (the
toolNames contains checks) to ensure critical tools remain present.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: b23618fd-7437-4fc6-a39d-c77fd80f7558
📒 Files selected for processing (14)
.githooks/pre-pushCONTRIBUTING.mdGhosttyTabs.xcodeproj/project.pbxprojSources/MCPServer.swiftSources/TerminalController.swiftSources/TerminalNotificationStore.swiftSources/Workspace.swiftcmuxTests/MCPServerTests.swiftcmuxTests/RapidSpawnKillFixtureTests.swiftscripts/run_tests.shtests/fixtures/README.mdtests/fixtures/rapid_spawn_kill.shtests/regression/README.mdtests/regression/test_iosurface_leak.sh
| var mergedEnvironment = ProcessInfo.processInfo.environment | ||
| for (key, value) in environment { | ||
| mergedEnvironment[key] = value | ||
| } | ||
| process.environment = mergedEnvironment |
There was a problem hiding this comment.
Clear inherited CMUX_RAPID_SPAWN_KILL_* overrides before launching the fixture.
runProcess merges the parent environment wholesale. In tests/fixtures/rapid_spawn_kill.sh, CMUX_RAPID_SPAWN_KILL_EXECUTABLE_PATH takes precedence over the app path, so an exported shell override can make this test pass against the wrong binary.
Suggested fix
var mergedEnvironment = ProcessInfo.processInfo.environment
+ for key in [
+ "CMUX_RAPID_SPAWN_KILL_APP_PATH",
+ "CMUX_RAPID_SPAWN_KILL_EXECUTABLE_PATH",
+ "CMUX_RAPID_SPAWN_KILL_ITERATIONS",
+ "CMUX_RAPID_SPAWN_KILL_READY_TIMEOUT_MS",
+ "CMUX_RAPID_SPAWN_KILL_TMPDIR",
+ ] {
+ mergedEnvironment.removeValue(forKey: key)
+ }
for (key, value) in environment {
mergedEnvironment[key] = value
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| var mergedEnvironment = ProcessInfo.processInfo.environment | |
| for (key, value) in environment { | |
| mergedEnvironment[key] = value | |
| } | |
| process.environment = mergedEnvironment | |
| var mergedEnvironment = ProcessInfo.processInfo.environment | |
| for key in [ | |
| "CMUX_RAPID_SPAWN_KILL_APP_PATH", | |
| "CMUX_RAPID_SPAWN_KILL_EXECUTABLE_PATH", | |
| "CMUX_RAPID_SPAWN_KILL_ITERATIONS", | |
| "CMUX_RAPID_SPAWN_KILL_READY_TIMEOUT_MS", | |
| "CMUX_RAPID_SPAWN_KILL_TMPDIR", | |
| ] { | |
| mergedEnvironment.removeValue(forKey: key) | |
| } | |
| for (key, value) in environment { | |
| mergedEnvironment[key] = value | |
| } | |
| process.environment = mergedEnvironment |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cmuxTests/RapidSpawnKillFixtureTests.swift` around lines 148 - 152, The test
is accidentally inheriting any exported CMUX_RAPID_SPAWN_KILL_* variables from
the parent environment when runProcess builds mergedEnvironment; update
runProcess (where mergedEnvironment is created and assigned to
process.environment) to first remove any keys matching the
CMUX_RAPID_SPAWN_KILL_* prefix (e.g. CMUX_RAPID_SPAWN_KILL_EXECUTABLE_PATH) from
ProcessInfo.processInfo.environment before applying the test's own environment
overrides so the fixture cannot be pointed at the wrong binary.
| let exitSignal = DispatchSemaphore(value: 0) | ||
| DispatchQueue.global(qos: .userInitiated).async { | ||
| process.waitUntilExit() | ||
| exitSignal.signal() | ||
| } | ||
|
|
||
| let timedOut = exitSignal.wait(timeout: .now() + timeout) == .timedOut | ||
| if timedOut { | ||
| process.terminate() | ||
| _ = exitSignal.wait(timeout: .now() + 2) | ||
| } | ||
|
|
||
| return ProcessResult( | ||
| status: process.terminationStatus, | ||
| stdout: String(data: stdoutPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "", | ||
| stderr: String(data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "", | ||
| timedOut: timedOut | ||
| ) |
There was a problem hiding this comment.
❓ Verification inconclusive
Script executed:
git ls-files | grep -i "rapidspawn\|fixture.*test"Repository: manaflow-ai/cmux
Repository: manaflow-ai/cmux
Exit code: 0
stdout:
cmuxTests/RapidSpawnKillFixtureTests.swift
Script executed:
fd -i rapidspawnRepository: manaflow-ai/cmux
Repository: manaflow-ai/cmux
Exit code: 0
stdout:
cmuxTests/RapidSpawnKillFixtureTests.swift
Script executed:
fd "RapidSpawnKillFixtureTests" --type fRepository: manaflow-ai/cmux
Repository: manaflow-ai/cmux
Exit code: 0
stdout:
cmuxTests/RapidSpawnKillFixtureTests.swift
Script executed:
find . -name "*RapidSpawn*" -o -name "*FixtureTest*" 2>/dev/nullRepository: manaflow-ai/cmux
Repository: manaflow-ai/cmux
Exit code: 0
stdout:
./cmuxTests/RapidSpawnKillFixtureTests.swift
Script executed:
wc -l ./cmuxTests/RapidSpawnKillFixtureTests.swiftRepository: manaflow-ai/cmux
Repository: manaflow-ai/cmux
Exit code: 0
stdout:
187 ./cmuxTests/RapidSpawnKillFixtureTests.swift
Script executed:
cat -n ./cmuxTests/RapidSpawnKillFixtureTests.swift | sed -n '150,200p'Repository: manaflow-ai/cmux
Repository: manaflow-ai/cmux
Exit code: 0
stdout:
150 mergedEnvironment[key] = value
151 }
152 process.environment = mergedEnvironment
153 process.standardInput = FileHandle.nullDevice
154 process.standardOutput = stdoutPipe
155 process.standardError = stderrPipe
156
157 do {
158 try process.run()
159 } catch {
160 return ProcessResult(
161 status: -1,
162 stdout: "",
163 stderr: String(describing: error),
164 timedOut: false
165 )
166 }
167
168 let exitSignal = DispatchSemaphore(value: 0)
169 DispatchQueue.global(qos: .userInitiated).async {
170 process.waitUntilExit()
171 exitSignal.signal()
172 }
173
174 let timedOut = exitSignal.wait(timeout: .now() + timeout) == .timedOut
175 if timedOut {
176 process.terminate()
177 _ = exitSignal.wait(timeout: .now() + 2)
178 }
179
180 return ProcessResult(
181 status: process.terminationStatus,
182 stdout: String(data: stdoutPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "",
183 stderr: String(data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "",
184 timedOut: timedOut
185 )
186 }
187 }
Script executed:
cat -n ./cmuxTests/RapidSpawnKillFixtureTests.swift | sed -n '120,187p'Repository: manaflow-ai/cmux
Repository: manaflow-ai/cmux
Exit code: 0
stdout:
120 return nil
121 }
122
123 private static func parseIOSurfaceFootprintMB(from output: String) -> Double? {
124 let pattern = #"VM: IOSurface\s*=\s*([0-9]+(?:\.[0-9]+)?)\s*MB"#
125 guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil }
126 let range = NSRange(output.startIndex..<output.endIndex, in: output)
127 guard
128 let match = regex.firstMatch(in: output, range: range),
129 let valueRange = Range(match.range(at: 1), in: output)
130 else {
131 return nil
132 }
133 return Double(output[valueRange])
134 }
135
136 private func runProcess(
137 executablePath: String,
138 arguments: [String],
139 environment: [String: String],
140 timeout: TimeInterval
141 ) -> ProcessResult {
142 let process = Process()
143 let stdoutPipe = Pipe()
144 let stderrPipe = Pipe()
145
146 process.executableURL = URL(fileURLWithPath: executablePath)
147 process.arguments = arguments
148 var mergedEnvironment = ProcessInfo.processInfo.environment
149 for (key, value) in environment {
150 mergedEnvironment[key] = value
151 }
152 process.environment = mergedEnvironment
153 process.standardInput = FileHandle.nullDevice
154 process.standardOutput = stdoutPipe
155 process.standardError = stderrPipe
156
157 do {
158 try process.run()
159 } catch {
160 return ProcessResult(
161 status: -1,
162 stdout: "",
163 stderr: String(describing: error),
164 timedOut: false
165 )
166 }
167
168 let exitSignal = DispatchSemaphore(value: 0)
169 DispatchQueue.global(qos: .userInitiated).async {
170 process.waitUntilExit()
171 exitSignal.signal()
172 }
173
174 let timedOut = exitSignal.wait(timeout: .now() + timeout) == .timedOut
175 if timedOut {
176 process.terminate()
177 _ = exitSignal.wait(timeout: .now() + 2)
178 }
179
180 return ProcessResult(
181 status: process.terminationStatus,
182 stdout: String(data: stdoutPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "",
183 stderr: String(data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "",
184 timedOut: timedOut
185 )
186 }
187 }
The timeout enforcement is incomplete and can hang indefinitely.
The process.terminate() call sends only SIGTERM, which a process can ignore. After the 2-second grace period, readDataToEndOfFile() is called unconditionally. If the child process is still alive (refused SIGTERM or blocked on I/O), the call blocks indefinitely waiting for EOF, causing the helper to exceed the advertised timeout and leak subprocesses. Add SIGKILL after the grace period or close the pipes before reading.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cmuxTests/RapidSpawnKillFixtureTests.swift` around lines 168 - 185, The
timeout handling may hang because process.terminate() (SIGTERM) can be ignored
and readDataToEndOfFile() will block; update the logic around exitSignal,
timedOut, and process.terminate() so that after the 2-second grace period you
check if the process is still running and then send a SIGKILL (e.g., via
process.interrupt/kill or posix_kill) and wait for
process.waitUntilExit()/exitSignal again, and/or close
stdoutPipe.fileHandleForReading and stderrPipe.fileHandleForReading before
calling readDataToEndOfFile() to ensure reads do not block; keep using
exitSignal and the timedOut flag but ensure you always wait for process
termination (or forcibly kill) before constructing ProcessResult.
| PROJECT="${CMUX_RUN_TESTS_PROJECT:-GhosttyTabs.xcodeproj}" | ||
| SCHEME="${CMUX_RUN_TESTS_SCHEME:-cmux-unit}" | ||
| CONFIGURATION="${CMUX_RUN_TESTS_CONFIGURATION:-Debug}" | ||
| DESTINATION="${CMUX_RUN_TESTS_DESTINATION:-platform=macOS,arch=arm64}" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n scripts/run_tests.sh | head -65Repository: manaflow-ai/cmux
Length of output: 2765
🏁 Script executed:
# Check if there are other scripts or CI workflows that use this path simultaneously
rg -i "cmux-run-tests-derived|CMUX_RUN_TESTS_DERIVED_DATA_PATH" --type sh --type yaml --type yml
# Also check for any documentation about macOS destination format
rg -i "destination.*platform=macos" --type sh --type swift -A 2 -B 2 | head -40Repository: manaflow-ai/cmux
Length of output: 1978
🏁 Script executed:
# Check if run_tests.sh is called from CI workflows that might run concurrently
rg -i "run_tests\.sh" --type yaml --type sh -B 2 -A 2
# Check for any locking mechanism or timestamping in the script
grep -n "lock\|timestamp\|mktemp\|unique" scripts/run_tests.sh
# Look for how other test scripts handle derived data
rg "DERIVED_DATA_PATH|derivedDataPath" scripts/ -A 1 -B 1Repository: manaflow-ai/cmux
Length of output: 3130
Don't hard-code the default test destination to arm64.
platform=macOS,arch=arm64 makes the pre-push gate fail on Intel Macs. Align with other test scripts in the repo (test-unit.sh, run-tests-v1.sh, run-tests-v2.sh, reload.sh) which use platform=macOS to let xcodebuild select the native architecture.
Suggested fix
-DESTINATION="${CMUX_RUN_TESTS_DESTINATION:-platform=macOS,arch=arm64}"
+DESTINATION="${CMUX_RUN_TESTS_DESTINATION:-platform=macOS}"Also applies to: 45-53
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@scripts/run_tests.sh` at line 13, The default DESTINATION value is hard-coded
to "platform=macOS,arch=arm64", which forces arm64 and breaks Intel Macs; update
the DESTINATION assignment in scripts/run_tests.sh (the DESTINATION variable) to
use only "platform=macOS" as the default (like other scripts), and apply the
same change for the other occurrences around lines 45-53 so xcodebuild can pick
the native architecture.
| DERIVED_DATA_PATH="${CMUX_RUN_TESTS_DERIVED_DATA_PATH:-/tmp/cmux-run-tests-derived}" | ||
| BUILT_APP_PATH="$DERIVED_DATA_PATH/Build/Products/$CONFIGURATION/cmux DEV.app" | ||
| FIXTURE_PATH="$REPO_ROOT/tests/fixtures/rapid_spawn_kill.sh" | ||
| LEAK_TEST_PATH="$REPO_ROOT/tests/regression/test_iosurface_leak.sh" | ||
| IOSURFACE_LIMIT_MB="${CMUX_RAPID_SPAWN_KILL_IOSURFACE_LIMIT_MB:-50}" |
There was a problem hiding this comment.
Use a unique DerivedData directory per run.
/tmp/cmux-run-tests-derived is shared across invocations. If a prior or concurrent run leaves a cmux DEV.app behind, the leak stage can end up exercising stale artifacts instead of the build produced by this invocation.
Suggested fix
-DERIVED_DATA_PATH="${CMUX_RUN_TESTS_DERIVED_DATA_PATH:-/tmp/cmux-run-tests-derived}"
+DERIVED_DATA_PATH_DEFAULT=0
+if [ -n "${CMUX_RUN_TESTS_DERIVED_DATA_PATH:-}" ]; then
+ DERIVED_DATA_PATH="$CMUX_RUN_TESTS_DERIVED_DATA_PATH"
+else
+ DERIVED_DATA_PATH="$(mktemp -d "${TMPDIR:-/tmp}/cmux-run-tests-derived.XXXXXX")"
+ DERIVED_DATA_PATH_DEFAULT=1
+fi
BUILT_APP_PATH="$DERIVED_DATA_PATH/Build/Products/$CONFIGURATION/cmux DEV.app"
+
+cleanup() {
+ if [ "$DERIVED_DATA_PATH_DEFAULT" -eq 1 ]; then
+ rm -rf "$DERIVED_DATA_PATH"
+ fi
+}
+trap cleanup EXITAlso applies to: 57-63, 88-98
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@scripts/run_tests.sh` around lines 14 - 18, DERIVED_DATA_PATH is currently a
fixed shared directory causing stale artifacts; change its default to create a
unique per-run temp directory (e.g., use mktemp -d with a
/tmp/cmux-run-tests-derived.XXXX template) and make BUILT_APP_PATH,
FIXTURE_PATH, LEAK_TEST_PATH and any other uses (the other blocks around the
indicated ranges) reference that variable so each invocation uses its own
DerivedData; ensure any setup/cleanup logic that assumes the old path is updated
to handle the dynamic DERIVED_DATA_PATH.
| guard let headerStr = String(data: headerData, encoding: .utf8), | ||
| let contentLength = Int(headerStr.trimmingCharacters(in: .whitespaces)), | ||
| contentLength >= 0 else { | ||
| return .notMCP | ||
| } | ||
|
|
||
| // Check if we have the full body | ||
| let bodyStart = separatorRange.upperBound | ||
| let totalNeeded = bodyStart + contentLength | ||
| guard buffer.count >= totalNeeded else { |
There was a problem hiding this comment.
Cap accepted frame sizes before returning .needMore.
Right now any non-negative Content-Length is accepted, so a peer can advertise a huge body and make this connection buffer grow without bound. Please reject lengths above a sane maximum here instead of waiting indefinitely for the full payload.
Proposed fix
enum MCPFraming {
+ private static let maxMessageBytes = 1 << 20
+
/// Result of attempting to parse one MCP message from a buffer.
enum ParseResult {
@@
- guard let headerStr = String(data: headerData, encoding: .utf8),
- let contentLength = Int(headerStr.trimmingCharacters(in: .whitespaces)),
- contentLength >= 0 else {
+ guard let headerStr = String(data: headerData, encoding: .utf8),
+ let contentLength = Int(headerStr.trimmingCharacters(in: .whitespaces)),
+ contentLength >= 0,
+ contentLength <= maxMessageBytes else {
return .notMCP
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/MCPServer.swift` around lines 47 - 56, The parser currently accepts
any non-negative Content-Length which allows a peer to advertise arbitrarily
large frames; add a sanity cap (e.g. MAX_CONTENT_LENGTH or mcpMaxContentLength)
and check it immediately after parsing contentLength in the MCPServer.swift
block (the guard that binds headerStr and contentLength and before computing
bodyStart/totalNeeded); if contentLength is greater than the cap, reject the
frame (return the appropriate failure like .notMCP or an explicit invalid-frame
result used elsewhere) instead of falling through to the .needMore path so the
connection cannot force unbounded buffering.
| case .ok(let payload): | ||
| let text: String | ||
| if let dict = payload as? [String: Any], | ||
| let jsonData = try? JSONSerialization.data(withJSONObject: dict, options: []), | ||
| let jsonStr = String(data: jsonData, encoding: .utf8) { | ||
| text = jsonStr | ||
| } else { | ||
| text = String(describing: payload) | ||
| } |
There was a problem hiding this comment.
Serialize any JSON payload, not just dictionaries.
This only emits JSON text for [String: Any]. If v2Execute returns an array or other JSON-compatible top-level value, it falls back to String(describing:), which is not stable JSON and is much harder for MCP clients to consume.
Proposed fix
- if let dict = payload as? [String: Any],
- let jsonData = try? JSONSerialization.data(withJSONObject: dict, options: []),
+ if JSONSerialization.isValidJSONObject(payload),
+ let jsonData = try? JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]),
let jsonStr = String(data: jsonData, encoding: .utf8) {
text = jsonStr
} else {
text = String(describing: payload)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| case .ok(let payload): | |
| let text: String | |
| if let dict = payload as? [String: Any], | |
| let jsonData = try? JSONSerialization.data(withJSONObject: dict, options: []), | |
| let jsonStr = String(data: jsonData, encoding: .utf8) { | |
| text = jsonStr | |
| } else { | |
| text = String(describing: payload) | |
| } | |
| case .ok(let payload): | |
| let text: String | |
| if JSONSerialization.isValidJSONObject(payload), | |
| let jsonData = try? JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]), | |
| let jsonStr = String(data: jsonData, encoding: .utf8) { | |
| text = jsonStr | |
| } else { | |
| text = String(describing: payload) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/MCPServer.swift` around lines 181 - 189, The current case .ok
handling only JSON-serializes [String: Any]; update it to emit stable JSON for
any JSON-compatible top-level value: first check
JSONSerialization.isValidJSONObject(payload) and if true serialize with
JSONSerialization.data(withJSONObject:payload), otherwise detect primitive JSON
types (String, Int, Double, Bool, NSNull/Optional.none) and serialize those by
casting and encoding each with JSONEncoder (e.g., encode the
String/Int/Double/Bool value) to produce proper JSON text; if all serialization
attempts fail, fall back to String(describing: payload). Ensure these changes
replace the logic that used only a [String: Any] cast so v2Execute payloads that
are arrays or primitives are emitted as valid JSON.
| // On first read, detect MCP Content-Length framing vs NDJSON/V1. | ||
| if !mcpDetected && mcpHandler == nil { | ||
| let firstBytes = Data(buffer[0..<min(bytesRead, 16)]) | ||
| if firstBytes.starts(with: "Content-Length:".data(using: .utf8)!) { | ||
| // Reject MCP connections when password auth is required. | ||
| // MCP clients (socat) cannot perform the password handshake. | ||
| if accessMode.requiresPasswordAuth { | ||
| let errMsg = "ERROR: MCP connections not supported in password auth mode\n" | ||
| errMsg.withCString { ptr in _ = write(socket, ptr, strlen(ptr)) } | ||
| return | ||
| } | ||
| mcpDetected = true | ||
| mcpHandler = MCPHandler { [weak self] method, params in | ||
| guard let self else { return .error(code: "internal", message: "Server unavailable") } | ||
| return self.mcpExecuteV2(method: method, params: params) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| while let newlineIndex = pending.firstIndex(of: "\n") { | ||
| let line = String(pending[..<newlineIndex]) | ||
| pending = String(pending[pending.index(after: newlineIndex)...]) | ||
| let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| guard !trimmed.isEmpty else { continue } | ||
| if mcpDetected, let handler = mcpHandler { | ||
| // MCP mode: Content-Length framed messages | ||
| mcpBuffer.append(contentsOf: buffer[0..<bytesRead]) | ||
|
|
||
| if let authResponse = authResponseIfNeeded(for: trimmed, authenticated: &authenticated) { | ||
| writeSocketResponse(authResponse, to: socket) | ||
| continue | ||
| while true { | ||
| let result = MCPFraming.parse(mcpBuffer) | ||
| switch result { | ||
| case .message(let messageData, let consumed): | ||
| mcpBuffer = mcpBuffer.subdata(in: consumed..<mcpBuffer.count) | ||
| if let response = handler.handleRequest(messageData) { | ||
| let framed = MCPFraming.encodeResponse(response) | ||
| writeSocketData(framed, to: socket) | ||
| } | ||
| continue // Try parsing next message from buffer | ||
| case .needMore: | ||
| break | ||
| case .notMCP: | ||
| // Corrupted data after MCP detection — close connection. | ||
| mcpBuffer.removeAll() | ||
| return | ||
| } | ||
| break // Only reached from .needMore | ||
| } |
There was a problem hiding this comment.
Buffer enough bytes before locking into MCP mode.
This decides the protocol from a single read(). If the initial Content-Length: header is split across packets, the first chunk falls through to the newline parser, emits an unknown-command error for the partial header, and the connection is permanently desynchronized. Keep an undecided preamble buffer until you've either matched the full MCP prefix or seen enough data to rule it out.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/TerminalController.swift` around lines 1603 - 1644, The code
currently decides MCP vs NDJSON from a single read by checking only the first
chunk (firstBytes) which can split the "Content-Length:" header; change this to
accumulate an undecided preamble buffer (e.g., mcpPreamble) by appending
buffer[0..<bytesRead] while mcpDetected == false && mcpHandler == nil, and only
decide MCP when either the preamble contains the full "Content-Length:" prefix
(check using the full prefix length) or the preamble grows past a safe threshold
(e.g., 64 bytes) or contains a newline that proves it is NDJSON; when the prefix
is detected set mcpDetected = true and create mcpHandler and move any remaining
bytes from mcpPreamble into mcpBuffer, otherwise once ruled out stop
accumulating and fall back to the newline/NDJSON parser using the preserved
preamble content (do not emit errors for partial headers). Ensure all references
use the existing symbols mcpDetected, mcpHandler, mcpBuffer and replace
firstBytes logic with the new mcpPreamble accumulation and checks.
| case "report_meta": | ||
| // Format: report_meta key value [--icon=X] [--color=#RRGGBB] [--surface=S] | ||
| guard let key = params["key"] as? String, let value = params["value"] as? String else { | ||
| return .error(code: "invalid_params", message: "report_meta requires key and value") | ||
| } | ||
| args = "report_meta \(mcpQuoteV1(key)) \(mcpQuoteV1(value))" | ||
| if let icon = params["icon"] as? String { args += " --icon=\(mcpQuoteV1(icon))" } | ||
| if let color = params["color"] as? String { args += " --color=\(mcpQuoteV1(color))" } | ||
| if let surface = params["surface"] as? String { args += " --surface=\(mcpQuoteV1(surface))" } | ||
| case "set_progress": | ||
| // Format: set_progress value [--label=TEXT] [--surface=S] | ||
| if let value = params["value"] as? Double { | ||
| args = "set_progress \(value)" | ||
| } else if let value = params["value"] as? Int { | ||
| args = "set_progress \(value)" | ||
| } | ||
| if let label = params["label"] as? String { args += " --label=\(mcpQuoteV1(label))" } | ||
| if let surface = params["surface"] as? String { args += " --surface=\(mcpQuoteV1(surface))" } |
There was a problem hiding this comment.
Don't silently ignore surface targeting in the V1 MCP bridge.
report_meta and set_progress only route by tab/workspace, but this bridge accepts surface and forwards --surface=... anyway. That returns success while mutating whichever tab is currently resolved, not the surface's owner. Either reject surface here or resolve it to the owning workspace and pass --tab=<workspace_id> instead.
♻️ Suggested fix
case "report_meta":
// Format: report_meta key value [--icon=X] [--color=#RRGGBB] [--surface=S]
guard let key = params["key"] as? String, let value = params["value"] as? String else {
return .error(code: "invalid_params", message: "report_meta requires key and value")
}
args = "report_meta \(mcpQuoteV1(key)) \(mcpQuoteV1(value))"
if let icon = params["icon"] as? String { args += " --icon=\(mcpQuoteV1(icon))" }
if let color = params["color"] as? String { args += " --color=\(mcpQuoteV1(color))" }
- if let surface = params["surface"] as? String { args += " --surface=\(mcpQuoteV1(surface))" }
+ if let surface = params["surface"] as? String {
+ guard let surfaceId = UUID(uuidString: surface) else {
+ return .error(code: "invalid_params", message: "report_meta surface must be a UUID")
+ }
+ guard let located = v2MainSync({ AppDelegate.shared?.locateSurface(surfaceId: surfaceId) }) else {
+ return .error(code: "not_found", message: "Surface not found")
+ }
+ args += " --tab=\(located.workspaceId.uuidString)"
+ }
case "set_progress":
// Format: set_progress value [--label=TEXT] [--surface=S]
if let value = params["value"] as? Double {
args = "set_progress \(value)"
} else if let value = params["value"] as? Int {
args = "set_progress \(value)"
}
if let label = params["label"] as? String { args += " --label=\(mcpQuoteV1(label))" }
- if let surface = params["surface"] as? String { args += " --surface=\(mcpQuoteV1(surface))" }
+ if let surface = params["surface"] as? String {
+ guard let surfaceId = UUID(uuidString: surface) else {
+ return .error(code: "invalid_params", message: "set_progress surface must be a UUID")
+ }
+ guard let located = v2MainSync({ AppDelegate.shared?.locateSurface(surfaceId: surfaceId) }) else {
+ return .error(code: "not_found", message: "Surface not found")
+ }
+ args += " --tab=\(located.workspaceId.uuidString)"
+ }Based on learnings: for panel_id-only routes in Sources/TerminalController.swift, resolve the real owner across windows instead of relying on the active window to avoid active-window bias.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/TerminalController.swift` around lines 2068 - 2085, The V1 MCP
handlers for "report_meta" and "set_progress" currently accept a
params["surface"] and append "--surface=..." which silently targets the active
tab instead of the surface's owning workspace; change the logic in the case
"report_meta" and case "set_progress" blocks to either reject surface (return
.error(code: "invalid_params", message: "surface not supported for v1")) or
resolve the surface to its owning workspace ID and pass "--tab=<workspace_id>"
instead of "--surface=..."; implement this by checking params["surface"] early
in each case, calling your existing workspace/surface lookup routine (or add a
helper like resolveOwnerWorkspace(forSurface:)) to get the workspace id, return
an error if resolution fails, and append "--tab=\(workspaceId)" (or error out)
rather than appending "--surface=\(…)".
| /// Windows positioned below this Y coordinate are treated as off-screen | ||
| /// SwiftUI system windows (settings, about, etc.) and skipped during layout | ||
| /// flushes to avoid triggering Auto Layout constraint cycles. | ||
| private static let offscreenLayoutSkipYThreshold: CGFloat = -2000 |
There was a problem hiding this comment.
Hardcoded off-screen Y threshold can skip real visible windows
Line 8060 uses window.frame.origin.y >= -2000 as an off-screen filter. On vertically stacked multi-display setups, visible windows can legitimately be below -2000, so they’ll be skipped incorrectly.
Suggested fix
- /// Windows positioned below this Y coordinate are treated as off-screen
- /// SwiftUI system windows (settings, about, etc.) and skipped during layout
- /// flushes to avoid triggering Auto Layout constraint cycles.
- private static let offscreenLayoutSkipYThreshold: CGFloat = -2000
-
private func flushWorkspaceWindowLayouts() {
for window in NSApp.windows {
- // Skip windows that are not visible or are off-screen SwiftUI
- // system windows (settings, about, etc.). Calling
- // layoutSubtreeIfNeeded on those can trigger an infinite
- // constraint-update cycle (NSGenericException) when their
- // SwiftUI layout has not converged.
- guard window.isVisible, window.frame.origin.y >= Self.offscreenLayoutSkipYThreshold else { continue }
+ // Skip windows that are not visible or not on any connected screen.
+ // This avoids forcing layout on detached/system utility windows while
+ // keeping visible multi-display windows eligible for layout flush.
+ guard window.isVisible else { continue }
+ let isOnAnyScreen = NSScreen.screens.contains { $0.frame.intersects(window.frame) }
+ guard isOnAnyScreen else { continue }
window.contentView?.layoutSubtreeIfNeeded()
window.contentView?.displayIfNeeded()
}
}Also applies to: 8055-8061
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/Workspace.swift` around lines 8048 - 8051, The hardcoded offscreen Y
threshold (offscreenLayoutSkipYThreshold and the check using
window.frame.origin.y >= -2000) can incorrectly skip visible windows on
tall/multi-display setups; replace that heuristic with a proper on-screen test:
determine whether the window's frame intersects any connected NSScreen frame (or
use window.screen and convert the window rect to screen coordinates) and only
treat it as off-screen when it does not intersect any NSScreen bounds. Update
the logic in Workspace where offscreenLayoutSkipYThreshold is referenced (the
window/frame check block) to use this intersection test and remove or deprecate
the -2000 constant. Ensure the change handles nil window.screen and considers
converted screen coordinates so visible windows on stacked displays are not
skipped.
| log "PASS IOSurface footprint stayed within threshold" | ||
| } | ||
|
|
||
| main "$@" |
There was a problem hiding this comment.
Missing explicit exit 0 on success path — the same bug this PR fixes elsewhere.
The script logs "PASS" but falls off the end without an explicit exit 0. This is the identical "fall-off-end bug" pattern described in the PR objectives for the pre-push hook. The last command executed would be log "PASS ...", and while log (a printf) typically exits 0, relying on implicit exit status is fragile and inconsistent with the explicit exit 1 / exit "$status" patterns used elsewhere in this script.
Proposed fix
log "PASS IOSurface footprint stayed within threshold"
+ exit 0
}
main "$@"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| log "PASS IOSurface footprint stayed within threshold" | |
| } | |
| main "$@" | |
| log "PASS IOSurface footprint stayed within threshold" | |
| exit 0 | |
| } | |
| main "$@" |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tests/regression/test_iosurface_leak.sh` around lines 263 - 266, The success
path in the test_iosurface_leak.sh script currently logs a PASS but falls off
the end of the main function without an explicit exit code; add an explicit exit
0 on the successful branch (i.e., immediately after the log "PASS IOSurface
footprint stayed within threshold" in the main function) so the script returns a
deterministic success status consistent with the existing exit "$status"/exit 1
patterns.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 707a05e321
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let firstBytes = Data(buffer[0..<min(bytesRead, 16)]) | ||
| if firstBytes.starts(with: "Content-Length:".data(using: .utf8)!) { | ||
| // Reject MCP connections when password auth is required. |
There was a problem hiding this comment.
Detect MCP mode using buffered prefix matching
MCP detection currently checks only the current read() chunk with firstBytes.starts(with: "Content-Length:"). On stream sockets, the first read can legally contain only part of that header (for example Content-Len), which makes this branch miss MCP permanently and route the connection into newline mode; the MCP request then never gets parsed or answered. Detection needs to use accumulated bytes (or partial-prefix matching) to avoid intermittent MCP hangs.
Useful? React with 👍 / 👎.
| args = "report_meta \(mcpQuoteV1(key)) \(mcpQuoteV1(value))" | ||
| if let icon = params["icon"] as? String { args += " --icon=\(mcpQuoteV1(icon))" } | ||
| if let color = params["color"] as? String { args += " --color=\(mcpQuoteV1(color))" } | ||
| if let surface = params["surface"] as? String { args += " --surface=\(mcpQuoteV1(surface))" } |
There was a problem hiding this comment.
Honor workspace when routing MCP set_status calls
This builder never maps the MCP workspace argument onto the V1 --tab option, so set_status writes metadata to the currently selected workspace instead of the workspace requested by the client. In multi-workspace sessions this causes status updates to land in the wrong workspace even when workspace is explicitly provided.
Useful? React with 👍 / 👎.
| // layoutSubtreeIfNeeded on those can trigger an infinite | ||
| // constraint-update cycle (NSGenericException) when their | ||
| // SwiftUI layout has not converged. | ||
| guard window.isVisible, window.frame.origin.y >= Self.offscreenLayoutSkipYThreshold else { continue } |
There was a problem hiding this comment.
Remove hardcoded Y cutoff for visible window layout flush
The new guard skips any visible window whose frame origin is below -2000, but real user windows can have that coordinate on setups with a monitor positioned below the primary display (for example a 2160px-tall lower screen). In that case layout/display flushing is skipped for an active window, which can leave workspace UI geometry stale on that display.
Useful? React with 👍 / 👎.
Sister PR to voicelayer #182. cmux fork hook had same fall-off-end bug. Source: brainbar-85cb1121-307.
Summary by cubic
Fixes the pre-push git hook to exit 0 on success and adds a regression gate that runs a new test harness before pushes. Also introduces native MCP protocol support on the socket server and fixes a launch crash caused by Auto Layout cycles.
New Features
.githooks/pre-pushrunsscripts/run_tests.sh; setup documented in CONTRIBUTING.Content-Lengthframing, implementsinitialize,tools/list,tools/call, and maps 20 tools to V2 methods; covered by unit tests.tests/fixtures/rapid_spawn_kill.sh,tests/regression/test_iosurface_leak.sh, and an Xcode-driven fixture test integrated viascripts/run_tests.sh.Bug Fixes
*_id, parse multiple frames per read, close on malformed frames, and quote V1 args from MCP routes.Written for commit 707a05e. Summary will update on new commits. Review in cubic
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes
Tests & Infrastructure