Skip to content

feat: native MCP protocol support on socket server - #1

Merged
EtanHey merged 62 commits into
mainfrom
feat/mcp-native-server
Mar 17, 2026
Merged

EtanHey merged 62 commits into
mainfrom
feat/mcp-native-server

Conversation

@EtanHey

@EtanHey EtanHey commented Mar 17, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds MCP (Model Context Protocol) Content-Length framing support directly to the cmux socket server
  • Enables AI tools (Claude Code, Cursor, Codex) to connect via socat STDIO UNIX-CONNECT:/tmp/cmux.sock — zero external processes needed
  • Protocol auto-detection on first read: Content-Length: → MCP, { → V2, else V1 — all three coexist on the same socket
  • 20 MCP tools that route 1:1 to existing V2 socket methods (list_surfaces, read_screen, send_input, send_key, new_split, etc.)
  • V1 sidebar commands (set_status, set_progress) handled via dedicated V1 bridge

Architecture

New file: Sources/MCPServer.swift (~400 lines)

  • MCPFraming: Content-Length framing parser/encoder (LSP-style Content-Length: N\r\n\r\n{json})
  • MCPHandler: JSON-RPC 2.0 handler for initialize, tools/list, tools/call
  • MCPToolSchemas: 20 tool definitions + routing table mapping MCP tool names to V2 methods

Modified: Sources/TerminalController.swift (+142 lines)

  • handleClient(): Detect MCP framing on first read, branch to MCP or V1/V2 read loops
  • mcpExecuteV2(): Bridge MCP tool calls → V2 processV2Command() dispatch
  • mcpExecuteV1(): Bridge MCP sidebar tools → V1 processCommand() dispatch

Test plan

  • MCPFramingTests: 8 tests — parse complete/partial/multi-message, notMCP detection, zero-length body, encode round-trip
  • MCPHandlerTests: 10 tests — initialize response, tools/list (20 tools), tools/call routing, error handling, notifications, ping
  • Routing tests: 13 tests — all 20 MCP tools verified to route to correct V2/V1 methods
  • CI compilation (requires GhosttyKit xcframework)
  • Socat end-to-end test with running cmux instance

MCP .mcp.json config (after merge)

{
  "mcpServers": {
    "cmux": {
      "command": "socat",
      "args": ["STDIO", "UNIX-CONNECT:/tmp/cmux.sock"]
    }
  }
}

Part of Three-Daemon Sprint

Phase 4 of the Three-Daemon Sprint — replacing 26 duplicate MCP processes (1.6GB RAM) with 3 singleton daemons on Unix sockets.

🤖 Generated with Claude Code

lawrencecchen and others added 28 commits February 22, 2026 15:50
…1-ssh-remote-port-proxying"

This reverts commit f7cbbad.
# Conflicts:
#	.github/workflows/ci.yml
#	CLI/cmux.swift
#	Sources/GhosttyTerminalView.swift
#	Sources/SocketControlSettings.swift
#	Sources/TabManager.swift
#	Sources/TerminalController.swift
#	Sources/Workspace.swift
#	ghostty
#	scripts/reload.sh
@EtanHey

EtanHey commented Mar 17, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@EtanHey

EtanHey commented Mar 17, 2026

Copy link
Copy Markdown
Owner Author

Self-Review: Round 1 Findings & Fixes (41231ba)

Critical fixes:

  1. Test initialization bug — All tools/list and tools/call tests were broken. XCTest creates fresh instances per test method, so initialized was always false. Added initialize call in setUp() and a dedicated testToolsListBeforeInitializeReturnsError test.

  2. MCP message loop only processed one message per read — The while true loop broke after handling one .message. Multiple messages arriving in a single read() call were deferred until the next read. Fixed with continue after .message.

Security fix:

  1. V1 command string injection — mcpExecuteV1 interpolated user-provided values directly into V1 command strings. Added mcpQuoteV1() helper that wraps values in double-quotes with proper escaping.

Bug fix:

  1. press_enter param not forwarded — Declared in send_input schema but not routed to V2.

Improvements:

  1. writeSocketData helper — Handles partial writes and EINTR instead of silently discarding data.
  2. encodeResponse fallback — Uses try? with minimal error JSON instead of try! crash.
  3. Password auth guard — Rejects MCP connections when password auth is required (MCP clients can't do the handshake).
  4. .notMCP after detection — Closes connection on corrupted data instead of infinite byte-dropping loop.

@coderabbitai The MCP server auto-detects protocol on first socket read (Content-Length framing = MCP, otherwise V1/V2 NDJSON). This is intentional — both protocols share the same Unix domain socket. The initialized handshake guard on tools/list and tools/call follows the MCP 2024-11-05 spec. V1 commands like report_meta and set_progress are bridged through mcpExecuteV1 which builds quoted V1 command strings. Please learn this for future reviews.

lawrencecchen and others added 15 commits March 17, 2026 00:40
* 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>
…-build-failure

Fix nightly release browser portal compile
…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>
…anaflow-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>
…p-review-fixes

Fix SSH remote CLI wrapper and proxy follow-ups
@coderabbitai

coderabbitai Bot commented Mar 17, 2026

Copy link
Copy Markdown

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

CodeRabbit commands

These commands are invoked using PR/Issue comments.

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai evaluate custom pre-merge check --instructions <custom-checks-instructions> --name <custom-checks-title> [--mode <error|warning>] to validate the custom pre-merge checks instructions. Defaults to error when --mode is omitted.
    • @coderabbitai ignore pre-merge checks to override pre-merge checks and get an approval on PR.
    • @coderabbitai run pre-merge checks to run pre-merge checks on the pull request.
    • @coderabbitai plan to generate a coding prompt for all unresolved human review comments in this PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai fix merge conflict to automatically resolve merge conflicts.
  • @coderabbitai autofix to automatically fix issues identified in unresolved review comments.
  • @coderabbitai resolve to resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai generate configuration to create a PR that adds the current resolved configuration as .coderabbit.yaml (or show it if already present).
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit configuration file (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, support, documentation and community

  • Visit our status page to check the current availability of CodeRabbit.
  • Create a ticket on our support page for assistance with any issues or questions.
  • Visit our documentation site for detailed information on how to use CodeRabbit.
  • Join our Discord community to connect with other users and get help from the community.
  • Follow us on X/Twitter for updates and announcements.

@EtanHey

EtanHey commented Mar 17, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai help

@coderabbitai

coderabbitai Bot commented Mar 17, 2026

Copy link
Copy Markdown

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

CodeRabbit commands

These commands are invoked using PR/Issue comments.

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai evaluate custom pre-merge check --instructions <custom-checks-instructions> --name <custom-checks-title> [--mode <error|warning>] to validate the custom pre-merge checks instructions. Defaults to error when --mode is omitted.
    • @coderabbitai ignore pre-merge checks to override pre-merge checks and get an approval on PR.
    • @coderabbitai run pre-merge checks to run pre-merge checks on the pull request.
    • @coderabbitai plan to generate a coding prompt for all unresolved human review comments in this PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai fix merge conflict to automatically resolve merge conflicts.
  • @coderabbitai autofix to automatically fix issues identified in unresolved review comments.
  • @coderabbitai resolve to resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai generate configuration to create a PR that adds the current resolved configuration as .coderabbit.yaml (or show it if already present).
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit configuration file (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, support, documentation and community

  • Visit our status page to check the current availability of CodeRabbit.
  • Create a ticket on our support page for assistance with any issues or questions.
  • Visit our documentation site for detailed information on how to use CodeRabbit.
  • Join our Discord community to connect with other users and get help from the community.
  • Follow us on X/Twitter for updates and announcements.

@EtanHey

EtanHey commented Mar 17, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Full review and also review the comments that were left for you and all the reviews that were done, and then conduct a fuller review with all of it in mind.

@coderabbitai

coderabbitai Bot commented Mar 17, 2026

Copy link
Copy Markdown

@EtanHey, noted — I've recorded the architectural details for future reviews and will now conduct a full review incorporating all prior round 1 and round 2 findings.


✏️ Learnings added
Learnt from: EtanHey
URL: https://github.com/EtanHey/cmux/pull/1

Timestamp: 2026-03-17T10:34:22.155Z
Learning: In the cmux repository (Swift), the MCP socket server auto-detects the protocol on the first socket read: `Content-Length:` prefix indicates MCP (LSP-style framing), `{` indicates V2 NDJSON, and anything else is treated as V1. This is intentional — all three protocols coexist on the same Unix domain socket (/tmp/cmux.sock).

Learnt from: EtanHey
URL: https://github.com/EtanHey/cmux/pull/1

Timestamp: 2026-03-17T10:34:22.155Z
Learning: In the cmux repository (Swift), the `initialized` handshake guard on `tools/list` and `tools/call` in `MCPHandler` follows the MCP 2024-11-05 spec. This guard is intentional and correct — requests before `initialize` should return an error.

Learnt from: EtanHey
URL: https://github.com/EtanHey/cmux/pull/1

Timestamp: 2026-03-17T10:34:22.155Z
Learning: In the cmux repository (Swift), V1 sidebar commands (`report_meta`, `set_progress`) are bridged through `mcpExecuteV1()` in `TerminalController.swift`, which builds quoted V1 command strings using the `mcpQuoteV1()` helper to prevent command string injection from user-provided values.

Learnt from: EtanHey
URL: https://github.com/EtanHey/cmux/pull/1

Timestamp: 2026-03-17T10:34:22.155Z
Learning: In the cmux repository (Swift), MCP fragmented first-read detection is acknowledged as low risk because Unix domain sockets deliver the first read atomically in practice. This is a deliberate design trade-off.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Actions performed

Full review triggered.

@coderabbitai

coderabbitai Bot commented Mar 17, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@EtanHey has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 20 minutes and 34 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b6b8570c-7d67-4308-bab1-561239ba8048

📥 Commits

Reviewing files that changed from the base of the PR and between 3b507d3 and 001508d.

📒 Files selected for processing (110)
  • .github/workflows/build-ghosttykit.yml
  • .github/workflows/ci-macos-compat.yml
  • .github/workflows/ci.yml
  • .github/workflows/nightly.yml
  • .github/workflows/release.yml
  • .github/workflows/test-depot.yml
  • .github/workflows/test-e2e.yml
  • CLAUDE.md
  • CLI/cmux.swift
  • GhosttyTabs.xcodeproj/project.pbxproj
  • Resources/Info.plist
  • Resources/Localizable.xcstrings
  • Resources/shell-integration/cmux-bash-integration.bash
  • Resources/shell-integration/cmux-zsh-integration.zsh
  • Sources/AppDelegate.swift
  • Sources/BrowserWindowPortal.swift
  • Sources/ContentView.swift
  • Sources/GhosttyTerminalView.swift
  • Sources/MCPServer.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/BrowserPanelView.swift
  • Sources/Panels/TerminalPanel.swift
  • Sources/SessionPersistence.swift
  • Sources/SocketControlSettings.swift
  • Sources/TabManager.swift
  • Sources/TerminalController.swift
  • Sources/TerminalWindowPortal.swift
  • Sources/Update/UpdateController.swift
  • Sources/Update/UpdateDelegate.swift
  • Sources/Update/UpdateDriver.swift
  • Sources/Update/UpdateTestSupport.swift
  • Sources/Update/UpdateViewModel.swift
  • Sources/WindowToolbarController.swift
  • Sources/Workspace.swift
  • Sources/cmuxApp.swift
  • TODO.md
  • cmuxTests/AppDelegateShortcutRoutingTests.swift
  • cmuxTests/BrowserImportMappingTests.swift
  • cmuxTests/CLIProcessRunnerTests.swift
  • cmuxTests/CmuxWebViewKeyEquivalentTests.swift
  • cmuxTests/GhosttyConfigTests.swift
  • cmuxTests/MCPServerTests.swift
  • cmuxTests/SessionPersistenceTests.swift
  • cmuxTests/TabManagerSessionSnapshotTests.swift
  • cmuxTests/TerminalControllerSocketSecurityTests.swift
  • cmuxTests/WorkspaceRemoteConnectionTests.swift
  • cmuxUITests/AutomationSocketUITests.swift
  • cmuxUITests/BrowserImportProfilesUITests.swift
  • cmuxUITests/BrowserOmnibarSuggestionsUITests.swift
  • cmuxUITests/BrowserPaneNavigationKeybindUITests.swift
  • cmuxUITests/CloseWindowConfirmDialogUITests.swift
  • cmuxUITests/CloseWorkspaceCmdDUITests.swift
  • cmuxUITests/CloseWorkspaceConfirmDialogUITests.swift
  • cmuxUITests/CloseWorkspacesConfirmDialogUITests.swift
  • cmuxUITests/JumpToUnreadUITests.swift
  • cmuxUITests/MenuKeyEquivalentRoutingUITests.swift
  • cmuxUITests/MultiWindowNotificationsUITests.swift
  • cmuxUITests/SidebarResizeUITests.swift
  • cmuxUITests/UpdatePillUITests.swift
  • daemon/remote/README.md
  • daemon/remote/cmd/cmuxd-remote/cli.go
  • daemon/remote/cmd/cmuxd-remote/cli_test.go
  • daemon/remote/cmd/cmuxd-remote/main.go
  • daemon/remote/cmd/cmuxd-remote/main_test.go
  • daemon/remote/go.mod
  • docs/remote-daemon-spec.md
  • scripts/build-ghostty-cli-helper.sh
  • scripts/build_remote_daemon_release_assets.sh
  • scripts/ghosttykit-checksums.txt
  • scripts/release_asset_guard.js
  • scripts/release_asset_guard.test.js
  • scripts/reload.sh
  • tests/fixtures/ssh-remote/Dockerfile
  • tests/fixtures/ssh-remote/run.sh
  • tests/fixtures/ssh-remote/sshd_config
  • tests/fixtures/ssh-remote/ws_echo.py
  • tests/test_ci_self_hosted_guard.sh
  • tests/test_cli_version_flag.py
  • tests/test_cli_version_memory_guard.py
  • tests/test_issue_1138_sidebar_pr_polling.py
  • tests/test_remote_daemon_release_assets.sh
  • tests/test_sidebar_copy_ssh_error_context_menu.py
  • tests_v2/pane_resize_test_support.py
  • tests_v2/test_cli_browser_console_errors_text.py
  • tests_v2/test_cli_global_flags_and_v1_error_contract.py
  • tests_v2/test_cli_sidebar_metadata_commands.py
  • tests_v2/test_pane_break_swap_preserve_focus.py
  • tests_v2/test_pane_resize_preserves_ls_scrollback.py
  • tests_v2/test_pane_resize_preserves_visible_content.py
  • tests_v2/test_rename_tab_cli_parity.py
  • tests_v2/test_ssh_remote_browser_favicon_uses_proxy.py
  • tests_v2/test_ssh_remote_browser_move_rebinds_proxy.py
  • tests_v2/test_ssh_remote_cli_metadata.py
  • tests_v2/test_ssh_remote_cli_relay.py
  • tests_v2/test_ssh_remote_daemon_resize_stdio.py
  • tests_v2/test_ssh_remote_docker_bootstrap_nonlogin_shell.py
  • tests_v2/test_ssh_remote_docker_forwarding.py
  • tests_v2/test_ssh_remote_docker_reconnect.py
  • tests_v2/test_ssh_remote_interactive_cmux_command_regression.py
  • tests_v2/test_ssh_remote_last_surface_clears_remote_state.py
  • tests_v2/test_ssh_remote_proxy_bind_conflict.py
  • tests_v2/test_ssh_remote_resize_scrollback_regression.py
  • tests_v2/test_ssh_remote_second_session_mux_regression.py
  • tests_v2/test_ssh_remote_shell_integration.py
  • tests_v2/test_ssh_remote_shortcuts_stay_remote.py
  • tests_v2/test_surface_list_custom_titles.py
  • tests_v2/test_v1_panel_creation_preserves_focus.py
  • tests_v2/test_workspace_create_background_starts_terminal.py
  • tests_v2/test_workspace_create_initial_env.py
  • vendor/bonsplit
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/mcp-native-server
📝 Coding Plan
  • Generate coding plan for human review comments

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

❤️ Share

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

EtanHey and others added 2 commits March 17, 2026 12:48
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 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>
@EtanHey
EtanHey force-pushed the feat/mcp-native-server branch from 41231ba to 001508d Compare March 17, 2026 10:49
@EtanHey
EtanHey merged commit 85a75e9 into main Mar 17, 2026
1 check passed
@EtanHey
EtanHey deleted the feat/mcp-native-server branch March 17, 2026 11:25
EtanHey added a commit that referenced this pull request Mar 17, 2026
* 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>
EtanHey added a commit that referenced this pull request Mar 17, 2026
* 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>
EtanHey added a commit that referenced this pull request Mar 17, 2026
* 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>
EtanHey pushed a commit that referenced this pull request Oct 1, 2026
* Add CmuxMobileSSH core and direct SSH PRD

SwiftNIO SSH over Network.framework: connect with host key policy,
key/password auth, exec, PTY shells with resize, direct-tcpip channels,
jump hosts, subsystem channels. OpenSSH private key parsing for Ed25519
and ECDSA. Live integration tests against a local sshd lab.

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

* Add SSH port forwarding, key/host stores, key installer, encrypted key import

Local forwards over direct-tcpip for the in-app browser, Secure Enclave and
imported keys in the Keychain, local host records with known-hosts pinning,
password-once authorized_keys install, and bcrypt_pbkdf + AES decryption for
passphrase-protected OpenSSH keys.

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

* Add SFTP v3 client for the SSH file browser

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

* Add SSH computers runtime to the mobile shell

SSH hosts publish workspace rows through workspacesByMac like the
demonstration computer. Demo-only branch points become locally-served
checks so SSH surfaces route input, replay, viewport, and composer paste to
the SSH runtime instead of a Mac. Plain, tmux, and cmux-tui persistence
providers; cmux-tui client with bytes-mode attach and phone geometry;
npm-verified cmux-tui upload; TOFU and changed-key prompts.

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

* Expose SSH file, forwarding, and lookup API for the UI

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

* Add SSH terminal fidelity, file browser, port forwarding, image paste

The phone's emulator answers terminal queries for plain/tmux SSH surfaces
and filters its replies for cmux-tui (whose server already answers), SSH
surfaces get local pixel scrolling and mouse clicks, sign-out keeps SSH
rows, the app injects a persistent SSH runtime, and SSH workspaces gain an
SFTP file browser, port forwarding into the native browser, and image paste
over SFTP.

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

* Add SSH computers UI: Computers section, host editor, keys, prompts, signed-out use

SSH hosts get their own section in Computers and open their workspace list;
add/edit form with key picker, jump host, persistence, idle close, and
password-once key install; SSH key management (Secure Enclave generate,
OpenSSH import); root-level trust/changed-key/persistence prompts; and SSH
without a cmux account.

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

* Register CmuxMobileSSH in workspaces and pin SwiftNIO SSH dependencies

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

* cmux-tui: per-terminal idle-close policy (terminal-idle-close-v1)

set-terminal-idle-policy stores an idle close time per hosted terminal in
the registry; an owner-side reaper closes terminals with no attached views
past their deadline through the normal close path. Reattach resets the
clock; null clears the policy.

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

* Add cmux-tui browser surfaces over SSH, idle policy client, installer lab test, translations

cmux-tui browser tabs stream into the existing browser stream pane with
tap/scroll/keys/navigation; the phone applies the host's idle-close policy
when the server supports terminal-idle-close-v1; the cmux-tui upload is
lab-tested; all SSH strings are localized in the 9 catalog languages.

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

* reload-build: cap Xcode selection at the pinned toolchain major

The hosted Blacksmith macos-26 image now carries the Xcode 27 RC and
select-ci-xcode.sh ranks by newest macOS SDK, so every dispatched iOS
dev-build archive switched to the 27 SDK and fails compiling main's
SwiftUI (toolbarMinimizeBehavior:
https://github.com/manaflow-ai/cmux/actions/runs/35783022784 and
https://github.com/manaflow-ai/cmux/actions/runs/35786136840). Feed the
selector's existing CMUX_CI_MAX_MACOS_SDK_MAJOR ceiling from
.xcode-version's major so the ranking keeps the newest pinned-major
Xcode and skips unvalidated newer SDKs, with the older-runner fallback
unchanged.

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

* cmux-tui: rustfmt idle-close files

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

* Accept smart-punctuation-mangled OpenSSH key armor

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

* reload-build: sign the simulator leg to run locally so Keychain works

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

* cmux-tui: address seeded idle-close test terminals by stable id

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

* cmux-tui: satisfy clippy in idle-close reaper

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

* Fix SSH issues found in simulator verification

- Handshake deadline pauses while the host key prompt is open; a declined
  key reports hostKeyRejected (including behind a jump host), not a timeout.
- Replayed history and cmux-tui snapshots strip terminal query requests, so
  the phone never answers stale queries into the PTY (Mac manaflow-ai#10332 class).
- Replacements fully reset the local terminal before replaying history.
- Selected SSH host auto-connects on launch/foreground; explicit disconnect
  or a declined prompt stays manual.
- Signed-out SSH mode skips Mac-centric What's New/pairing sheets.
- Literal text entry (no autocorrect) on SSH host and key fields.

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

* PRD: record verification status

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

* SSH round 2: tmux control mode, browser modes, UI polish

- tmux via control mode: session = workspace, pane = tab, New Terminal
  opens a window, phone attaches through its own grouped session.
- cmux-tui New Terminal; plain workspaces have no terminal tabs.
- cmux-tui: re-snapshot when a full-screen app exits so shell history
  behind it returns.
- Browser: shared bottom chrome for streamed and native browsers, a
  Streamed / On iPhone mode picker remembered per browser, SSH On iPhone
  routed through a SOCKS5 proxy over SSH plus a loopback port mirror.
- Files chip opens SFTP at the shell's directory; title-menu SSH items
  and the open-port sheet removed.
- Sign-in 'Use with SSH only', mode-aware empty states with Mac-parity
  status, + chooses a computer under All Computers, declined identity
  pauses auto-connect across relaunch, key origin on every key row,
  friendly Face ID errors, no dev-tag suffix on SSH hosts.

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

* Fix tmux server crash on abrupt phone disconnect and SSH list bugs

- Phone grouped sessions no longer use destroy-unattached (tmux 3.7c
  frees a session another exiting client still references and
  segfaults); the phone kills its grouped session on close and collects
  stale ones on connect. Repro: 8/10 crashes before, 0/20 after.
- Paired-Mac reconcile, team switches, and Mac outage handling leave SSH
  computers alone; newest listing wins; lists refresh on appear/active.
- SSH workspace ids resolve through the row's RPC id, so New Terminal
  works when several computers are live.
- Strip screen-style title sequences (ESC k ... ST) from tmux pane output.
- A successful refresh after a failure reports the host connected.

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

* PRD: round 2 decisions

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

* SSH round 3: mixed session kinds per host

One SSH connection per host serves cmux-tui workspaces, tmux sessions,
and plain shells side by side. Each workspace row is one kind's top-level
primitive (subtitle shows the kind); + offers a menu of kinds; existing
server sessions and cmux-tui workspaces from any cmux-tui session are
discovered; the tab switcher groups tmux windows and cmux-tui screens with
New Window / Split Pane / New Screen / New Tab. Per-host persistence mode
and the first-connect prompt are removed. The phone claims cmux-tui
geometry only while a terminal is on screen.

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

* Confirm before ending tmux sessions and cmux-tui workspaces over SSH

Every close entrypoint asks through one store decision: tmux and cmux-tui
rows outlive the phone, so ending one explains what stops on the
computer; plain shells close in one tap.

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

* cmux-tui: test that a displaced terminal geometry owner reclaims when the new owner leaves

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

* cmux-tui: hand terminal geometry back to the displaced owner

When a phone claimed a shared terminal's geometry and then released its
viewport or disconnected, the grid froze at the phone's size and the laptop
client that it displaced stayed non-authoritative until its user focused a
pane. Each terminal runtime now remembers the owners a claim displaced. When
the current owner releases, disables its sizing, or disconnects, the most
recent displaced owner that still reports a viewport for a view of that
terminal becomes the owner again and the PTY resizes to its report. Departed
clients are forgotten, and an explicit use-all-sizes release still freezes.

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

* Test SSH version line race and shared trust prompts

Two failing regressions found in simulator verification of password-once
key install:

- A server version line that arrives before the caller resumes from the
  TCP connect is dropped, so the handshake stalls until the timeout (every
  app-launch auto-connect timed out in the simulator).
- Saving a new computer auto-connects it while Install with Password logs
  in; the second trust question cancelled the first, failing the install
  with "server identity not trusted" before the user answered.

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

* Install the SSH handler before connect; share identical trust prompts

SSHConnection.connect added NIOSSHHandler to the pipeline only after the
awaited TCP connect resumed. Servers send their version line on accept, so
when the caller's resumption was delayed (a busy cooperative pool at app
launch) the line hit an empty pipeline and was discarded, and the client
never sent KEXINIT. The handler and handshake observer are now installed
by the bootstrap's channel initializer (and the jump channel's
initializer), so no inbound byte can precede them. The handshake budget
now also covers the TCP connect, matching its documentation.

MobileSSHComputers.ask cancelled any pending waiter with the same prompt
id. An identical question (same host, address, and keys) now joins the
pending prompt and receives the same answer; a question about a different
key still replaces the stale one with a cancel.

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

* SSH: reconnect after key install; plain wording for connect failures

A key install that succeeds clears the refusal left by a connect that ran
before the key existed and connects with the key. Refused, timed-out,
unresolvable and unreachable connects read as sentences instead of
POSIXErrorCode values.

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

* Test that a stale Cancel after Trust does not pause the host

When the trust sheet goes away after Trust and Connect, SwiftUI writes nil
through the sheet binding and the presenter answers the last rendered
prompt with Cancel. That pauses the host's automatic connects, so a newly
added computer never reconnects at app launch.

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

* Ignore answers for SSH prompts that are no longer pending

After Trust and Connect, the dismissing prompt sheet writes nil through
its item binding and the presenter answers the prompt it last rendered
with Cancel. answer() treated that as a decline and persisted
autoConnectPaused, so a newly added computer never auto-connected again.
answer() now acts only on a prompt that is still pending with the same
question; a stale or superseded answer is a no-op.

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

* Conform the SSH packages to the iOS package conventions

The iOS package-conventions lint flagged 28 errors in code this branch
added. Each namespace enum becomes a value or moves onto the type it
serves:

- Copy (L10nSSH, SSHCopy, SSHKeyErrorCopy, MobileSSHBiometryErrorCopy)
  becomes instantiable structs, like MobilePairingCopy.
- Parsers and codecs become initializers on their output:
  SSHParsedPrivateKey(openSSH:passphrase:), SSHHostKeyVerdict(presented:
  pinned:), MobileSSHTmuxLayout(_:).leaves, CmuxTUIBrowserEventWire(line:)
  .surfaceEvent, Decodable.init(cmuxTUILine:), Data(cmuxTUIBase64:).
- Configured workers become values: BcryptPBKDF(rounds:),
  SSHPrivateKeyDecryption(cipher:kdfOptions:), SSHKeyInstaller(
  sshDirectory:), MobileSSHCmuxTUIInstaller(binDirectory:).
- SSH ids become a MobileSSHIdentifier value over the raw string.
- Wire constants become typed values (SFTPStatusCode, SFTPAttributeFlags
  as an OptionSet).
- Shell quoting and the terminal query-reply filter move onto String and
  Data.

The two NIO auth delegates drop OSAllocatedUnfairLock and become actors.
NIO completes both callbacks through promises, so the actor hop only
delays the promise; the host-key delegate still pauses the handshake
deadline synchronously on the event loop before hopping.

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

* SSH: four regression-pass UX fixes

- SSH-only mode no longer reads "Mac update required" on the Computers
  button. The toolbar label treated SSH computer ids as Macs; with no
  version on record the Mac floor called them outdated. The label now
  scopes its warning to paired-Mac ids.
- Saving a new SSH host, or new connection details, connects it through
  MobileSSHComputers.autoConnect (saveHostAndConnect). Changed details
  close the stale connection and clear the old failure. The password
  install keeps its own connect after the key lands.
- The terminal keeps the keyboard after the system "Allow Paste" alert.
  The alert makes the app inactive, and the input-session reducer forgot
  the focused owner on every resign. It now restores the owner a
  foreground interruption took, and forgets it on background or any newer
  intent. Shared by SSH and paired-Mac terminals.
- The SSH Files chip shows when the terminal opens. It is the only entry
  to the server's files, so it no longer waits for a scroll reveal
  (TerminalFilesChipReveal.always); the Mac chip stays scroll-revealed.

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

* PRD: round 4 decisions and verification

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

* SSH: await queued auto-connect pause writes instead of polling

The pause flag is persisted from an unstructured Task, and the tests
waited for it with a 1000-yield poll that the test-determinism gate
rejects (yield-count-poll). Chain the writes through one stored task so a
pause and a later resume always land on disk in order, and let tests
await that task directly.

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

* cmux-tui SDKs: count set-terminal-idle-policy in the command inventory tests

The idle-close change added set-terminal-idle-policy to the generated
protocol (113 commands) but the per-language coverage tests still pinned
112, so every SDK package job failed on the exact count.

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

* SSH: only the newest pause write restores the in-memory flag

Each pause-flag write re-applied its own value to the in-memory host after
landing, to undo a reload that read the old flag. When a resume followed a
pause (opening a host behind a declined jump host), the older pause write
landed afterwards and set the flag back to paused until the resume's write
caught up, which aDeclinedJumpHostPausesTheHostsBehindIt caught under
full-suite load. Writes now carry a per-host generation and only the
newest one touches memory.

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

* SSH: creating on a selected SSH computer does not need the Mac

WorkspaceMacSelectionScope.canCreateWorkspace checked the foreground Mac's
create gate before the locally served (SSH) case, so with no Mac
connected the SSH computer's create action was disabled even though it
creates on its own connection. sshComputerIsSelectableAndCreatableWithout
WorkspacesOrMac covers this and failed in the iOS simulator lanes. The
locally served case now returns before that gate; a pending computer
switch still blocks it.

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

* SSH: keep the Devices toolbar label's ID filter off the main actor

MobileDevicesToolbarLabel.macPairingIDs is a static helper on a SwiftUI
View, so its filter closure inherited main-actor isolation and trapped
the whole xctest process when sshComputersNeverCountAsMacsForTheWarning
called it from a nonisolated test. In the iOS simulator lanes that crash
took down hundreds of unrelated tests per launch (xcresult: "Crash:
xctest at closure #1 in static MobileDevicesToolbarLabel.macPairingIDs").
The pure helpers are now nonisolated, and the test that builds the view
runs on the main actor.

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

* cmux-tui SSH: list hashed session sockets, add split, learn session from identify

Sessions whose names are too long for a socket path live at
cmux-tui-hashed-<uid>/<sha256>.sock. Discovery now lists them next to
named sockets (first runtime directory per session wins) and matches a
session to a hashed socket by SHA-256 digest. The control's session is
the name the owner reports in identify, so a hashed socket learns it.
Socket names follow the server's validate_session_name (spaces, Unicode,
long names are valid); server ensure keeps its stricter rule.

Adds CmuxTUIControl.split(pane:direction:) for Split Pane and
SSHConnection.isOpen. Lab-free wire tests drive a scripted peer over an
in-memory channel: identify-reported session, subscribe routing of
tree-changed/surface-exited, split params and errors.

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

* SSH: cmux-tui rows follow remote topology; Split Pane per screen

Topology: each cmux-tui provider subscribes on its control connection.
tree-changed, surface-exited, empty, overflow, daemon shutdown, and an
owner that went away (control dropped while SSH lives) ask the runtime
to relist through the existing onTopologyChange path. A gate coalesces
a burst to one request per listing; titles, sizes, and bells never
relist. No timers.

Hashed sessions: the registry keys a hashed socket's provider by the
name its owner reports and finds cached providers by digest.

Split Pane (D32): a cmux-tui screen section offers New Tab, then Split
Pane, which splits the screen's active pane to the right (split). tmux
windows keep Split Pane only. Section actions are one enum shared by the
store, the registry, and the picker; the label reuses the localized
mobile.ssh.tabs.splitPane string.

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

* cmux-tui: test that another client's creations keep a frontend's view

A phone creating a screen, a tab, or a split through new-screen, new-tab,
or split moves the shared tree's active fields only; an attached
frontend keeps the screen, pane, and tab it shows (preserve_client_view,
present since before 0.13.4). Test-only.

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

* PRD: D40-D43 for the deferred cmux-tui items

Hashed sockets listed, rows follow remote topology through subscribe,
cmux-tui Split Pane, and phone creations keep the laptop's view.

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

* SSH: plain shells report their folder to Files through OSC 7

The Files chip in a plain SSH shell always opened the remote home folder:
MobileSSHPlainProvider did not conform to MobileSSHCurrentDirectoryProviding,
so currentDirectory(surfaceID:) returned nil for every shell. tmux and
cmux-tui can be asked for a pane's folder; a plain login shell cannot.

Terminals learn a shell's folder from the OSC 7 report (ESC ] 7 ; file://host/path)
the shell prints before each prompt, which fish sends by default and zsh/bash
send with terminal shell integration. MobileSSHWorkingDirectoryReport reads
those reports passively from the channel's output (split chunks joined, BEL
or ST terminated, percent-decoded, bounded), and the plain provider keeps the
newest one per shell and answers currentDirectory with it. Nothing is sent to
the shell and no dotfile is touched; a shell that never reports keeps the
home-folder fallback.

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

* SSH: a relaunch lands on the SSH computer used last

The signed-out SSH shell cannot show "All Computers" (its empty states are
Mac-oriented), so on launch selectSSHComputerForSignedOutShellIfNeeded
scopes the list to an SSH computer whenever the saved scope is not one. It
always picked hosts.first, the oldest host, so after using another host
under "All Computers" (or deleting the selected host) every relaunch went
back to the first computer instead of the one in use.

A paired Mac is restored from its persisted active flag, written when the
user switches to it. SSH computers now keep the same fact:
MobileSSHComputers.open(hostID:), the one path every SSH selection goes
through (title picker, Computers screen, new host, row switch), records the
host in SSHHostStore (ssh-last-used-host.json, cleared when the host is
deleted), reload() restores it before publishing hosts, and preferredHost
(last used, else oldest) is what the signed-out shell selects.

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

* SSH: name the switcher's browser section by computer kind

The terminal switcher listed an SSH computer's browser tabs under "Mac
Browsers": TerminalPickerMenu hard-coded that section title for every
workspace, and the tabs of a cmux-tui host are not on a Mac.
TerminalPickerMenuValue now carries whether the workspace belongs to an SSH
computer and titles the section "Browsers" there (new key
mobile.ssh.browserStream.menuTitle, all nine languages); cmux Mac
workspaces keep "Mac Browsers". The kind is part of the menu value, so the
native menu rebuilds when it changes. HIG Menus: a section title names its
group.

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

* SSH: Browse Files in the workspace title menu

Browsing an SSH terminal's files was only reachable from the Files chip on
the terminal, a control the user is deciding whether to keep always visible.
HIG Toolbars places whole-document commands in the document menu next to
the title, and HIG Menus asks for verb labels, so the workspace title menu
now offers "Browse Files" (folder symbol, own section above the workspace
actions) whenever an SSH terminal is showing. It calls the chip's own
presentSSHFiles(terminalID:), so both open the same SFTP browser at the
shell's current folder. The chip is unchanged. New key
mobile.ssh.files.menuItem in all nine languages.

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

* SSH tmux: pin the control-mode seed order (B6 investigation)

B6 reports a tmux pane that is sometimes blank on first open and renders
after reopening. This Mac has no free PTYs, so tmux cannot start a pane
here; instead the seed path is pinned with an in-memory tmux -C stream.
MobileSSHTmuxControlClient now takes its byte pipe through a small
MobileSSHTmuxControlTransport protocol (SSHSessionChannel conforms; tests
pass a pipe), with no behavior change.

MobileSSHTmuxSeedOrderTests drives the provider's attach order and checks
that refresh-client -C precedes the pause/capture/state/continue batch in
one ordered stream, a flag-0 startup reply block is not taken as the reply
to the phone's first command, %output before the capture reply is dropped
(the capture has it), and the pane gets the grid, then the snapshot, then
output that followed the capture. It passes on the current code, so the
blank first open does not come from the client's seed ordering; the
remaining suspects are on the phone side (see
artifacts/dssh/v3/night/results.md).

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

* SSH: pin seed delivery across late teardown and before the first grid (failing)

Two of the three new tests fail on the current code: a late teardown of
the previous view retires the new view's viewport negotiation and the SSH
attach never starts, and a replay before any grid attaches at 80x24.

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

* SSH: attach at the phone's grid as soon as the view subscribes (B6)

The SSH attach was triggered by the Mac cold-replay deferral, which waits
for the viewport acknowledgement. A late teardown of the previous view
(clearTerminalViewport after the new view subscribed) retires that
negotiation, so the replay never ran: no attach, no tmux capture-pane or
cmux-tui vt-state seed, blank until a reopen. And a replay with no grid
yet attached at a placeholder 80x24 and resized afterwards.

The phone owns SSH geometry, so the grid is recorded when the viewport is
prepared (before the sink registers), SSH sinks replay at registration
without waiting for the negotiation, and an attach with no known grid
waits for the first one (input typed meanwhile is queued) instead of
seeding at 80x24. Output that arrives with no subscriber was already kept
in the surface's replay buffer and repainted on subscription; the new
test pins that too.

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

* SSH: offer Reconnect only when the host or the shown session is down

The workspace title menu used the Mac rule for every row, so a connected
SSH shell offered Reconnect, and choosing it ran the Mac redial path
(switchToMac with an SSH id, then reconnectOrRefresh of the foreground
Mac) instead of touching the SSH host.

MobileSSHComputers now owns both halves: canReconnect(hostID:surfaceID:)
is true when the host is idle or failed, or the shown terminal's session
ended (tracked in endedSurfaces), false while connecting and for a live
terminal on a connected host; reconnect(hostID:surfaceID:) opens the host
and reattaches the shown terminal when it is not live (an ended shell
opens a new one). Mac rows keep canReconnectFromTitleMenu.

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

* SSH: a refused terminal request reads as a sentence

channelRequestRejected("pty-req") reached the terminal as a raw enum
dump. A refused pty-req or shell now reads "This computer refused to open
a terminal. Try again."; any other refused request reads "This computer
refused the request (<reason>)." keeping tmux's own reason. Nine
languages.

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

* SSH: New Workspace follows a host switch

The + menu is Equatable on its value alone, and the value held only the
kinds, which are the same on every host. After switching SSH hosts
SwiftUI kept the old menu and its create closure, so the first New Shell
opened on the previous host: in the night pass that host was a real sshd
out of PTYs, which refused pty-req (channelRequestRejected), and the
server being watched logged no session. A retry worked because the menu
had re-rendered by then.

The value now carries the SSH target host, so a host switch invalidates
the menu and its action.

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

* PRD: overnight changelog

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

* cmux-tui: test that an empty workspace create reaches a subscriber

`workspace create --empty` goes through the resource router's pure
creation path, which commits the registry patch without emitting a
coarse MuxEvent, so `subscribe` clients (phones, native attach
frontends) never see a tree-changed push for it; the row appears only
when the next real change flushes. Seen live in the direct-SSH v4 pass
(artifacts/dssh/v4/pty-live/results.md, check 5). This commit adds the
failing regression; the fix follows.

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

* cmux-tui: push tree-changed when an empty workspace is created

The resource router's pure creation path
(resource_create_empty_workspace_selected) committed the registry patch
and published only the journal event, never a MuxEvent, so subscribe
clients learned about `workspace create --empty` only when the next
real change flushed. Emit the same WorkspaceAdded TreeDelta the legacy
create-workspace path emits, after the patch applies, with the entity
snapshot and workspace revision the delta contract requires.

Server-side only: SSH hosts see it once a cmux-tui release past the
pinned 0.13.4 ships (PRD changelog updated).

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

* SSH: test that a dead tmux server asks for a relist

v4 pty-live finding: when the tmux server ends, its control-mode exec
channel dies while the SSH connection stays up, and sessions on the
next server never appear until a manual pull-to-refresh. Adds the
failing regression (an unexpected control death on a live host must
request one relist and forget the per-server grouped-session collection
pass) plus the behavior-preserving seams it drives: a nil-connection
test path like MobileSSHPlainProvider's, the control wiring extracted
into adopt(_:session:), an injectable hostConnectionIsOpen, and a pump
completion await on the control client. Scripted in-memory tmux -C
pipe, no PTY, no sleeps. The fix follows.

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

* SSH: regression test for a deleted key reading as a raw error

A host whose key was deleted keeps a dangling keyID; a connect then loads
a key whose secret is gone and throws SSHKeyStoreError.missingSecret, which
MobileSSHComputers.describe(_:) rendered as the raw enum case name in the
terminal and the row status. This test pins that it must read as the
'choose a key' sentence instead. Fails on the current code; the next commit
fixes it.

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

* SSH: a deleted key reads as 'choose a key', not a raw error

MobileSSHComputers.describe(_:) had no case for SSHKeyStoreError, so a
connect on a host whose key was deleted (the host keeps a dangling keyID,
and privateKey(for:) throws missingSecret) fell to String(describing:) and
showed the literal 'missingSecret' in the terminal and the row status.

Map SSHKeyStoreError.missingSecret to the existing localized noKey copy
('Choose a key for this computer first.'), the same guidance the noKey path
gives and the exact recovery the user needs (re-pick a key in the editor).
Covers every missing-key-secret cause (deleted key, keychain eviction,
restore) with no new string.

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

* SSH: relist once when the tmux server dies under a live connection

A control client that ends while still registered was closed by tmux,
not the phone (phone-initiated closes remove it from the registry
first). When the SSH connection is still open that means the server
(or this session) ended: forget the per-server stale-grouped-session
collection pass, which belonged to the dead server, and fire the
existing onTopologyChange relist path once, so a restarted server's
sessions appear without pull-to-refresh. The relist reuses
refreshWorkspaces' generation coalescing and failure handling; no
timers, one relist per death. Connection teardown stays on the
runtime's own close path.

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

* SSH: test that a failed Reconnect answers in the terminal

Tapping Reconnect in the title menu while the computer is still down
changes nothing on screen: the title already read Disconnected, the row
already carried the failure sentence, and nothing new reaches the
terminal, so the tap looks ignored (v4 edges pass, scenario 1).

Expected to fail until the next commit delivers the failure sentence to
the shown surface.

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

* SSH: a failed Reconnect prints its failure sentence in the terminal

Reconnect against a still-down computer already walks
connecting -> failed, but a refused loopback connect resolves in
milliseconds and the failed state renders the exact chrome (red
Disconnected) shown before the tap, so nothing visibly happens.
reconnect(hostID:surfaceID:) now delivers the failure sentence to the
shown surface after a failed open, with the same red-notice rendering a
failed attach uses (shared errorNotice helper). A declined identity
prompt leaves the status idle, so cancelling stays quiet; the
still-visible Connecting/Reconnecting state during a slow connect is
unchanged.

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

* SSH: All Computers empty state stops pitching Mac pairing to SSH-only users

The aggregated (All Computers) empty state always rendered the
Mac-pairing copy ("Enable iOS pairing in cmux Settings > Mobile on your
Mac..."), which describes a Mac an SSH-only user does not have; per-host
SSH empty states were already right (v4 edges pass, secondary
observation; PRD D29 mode-aware empty states).

WorkspaceListEmptyGuidance decides from what exists: SSH computers with
no paired Mac get SSH guidance (the per-host "No Workspaces" title, a
terminal icon, and "Choose a computer from the menu at the top, or add
one from the Computers screen."), and any paired Mac keeps the Mac copy.
The SSH variant also drops Retry and See Docs, which drive the Mac
workspace-list recovery and the Mac pairing docs. The guidance rides the
table snapshot into the empty row model, so a change re-renders the row.
New string localized in all nine app languages.

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

* SSH: regression that a dropped transport leaves the terminal with no 'Session ended' line

connectionClosed removes the host's attachments silently and relies on each
child channel's own close event to write the '[Session ended]' line, so a
transport drop whose channel close lags leaves the shown terminal with nothing.
MobileSSHConnectionDropTests.droppedTransportEndsTheShownTerminal drives the
connection-close path alone and fails: no notice, endedSurfaces empty.

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

* SSH: a dropped transport ends its terminals deterministically

A whole-connection drop (server death) now routes through the same idempotent
per-surface end path a child channel's close uses, so the '[Session ended]' line
and the endedSurfaces mark land on the transport close instead of waiting on each
channel's own close event, which can lag arbitrarily under load. It also drops
stale attachAwaitingGrid entries so a reconnect re-seeds through the ordinary
subscribe path. handle(.ended) and connectionClosed share endTerminalSurface,
which is idempotent through endedSurfaces so the two paths never double-print.

MobileSSHConnectionDropTests.droppedTransportEndsTheShownTerminal now passes;
channelCloseEndsTheShownTerminal and reconnectWhileSubscribedRepaints guard the
channel-close line and the reconnect repaint.

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

* PRD: overnight 2 changelog

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

* SSH: the Files chip is scroll-revealed again, exactly like a Mac's

Round 4 made the SSH Files chip always visible (TerminalFilesChipReveal
.always) because it is the only entry to the server's files, but a chip
that never fades sits over the first terminal rows. Aziz: it should look
and behave exactly like Files on a paired Mac. The reveal special case is
reverted: every terminal's chip is hidden at rest, shown while scrolling,
and faded after the same linger, with the same assistive-technology
bypass, through the same component. Browse Files in the workspace title
menu (D28 follow-up) remains the always-visible entry point, so
discoverability does not regress. TerminalFilesChipReveal and its tests
are deleted as dead code; the SSH chip's persistent mount (no artifact
count to gate it) is unchanged (PRD D44).

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

* SSH: Split Right and Split Down replace the single Split Pane

The grouped tab switcher's one "Split Pane" always split one way (tmux
stacked below, cmux-tui to the right), with no way to pick the other
orientation. It is now the two directional actions the cmux macOS app
has, with the same names, translations (all nine app languages, copied
from the macOS catalog), and SF Symbols: Split Right puts the new pane
side by side (tmux `split-window -h`, cmux-tui `split dir:"right"`)
and Split Down stacks it (`-v` / `dir:"down"`), on both tmux windows
and cmux-tui screens, still detached so no attached client's view moves
(PRD D45, superseding D32/D42's single action; HIG Menus: one item per
action). MobileSSHSectionAction carries its direction; the tmux flag
mapping is a pure helper with a unit test, and the cmux-tui wire tests
already pin `dir` for both values.

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

* SSH: empty workspace lists render the paired-Mac empty state

An SSH host's "No Workspaces" overlay was a bare ContentUnavailableView
and the SSH-variant All Computers row used its own terminal icon and
title, so SSH empty states looked like a different app from a paired
Mac's. The paired-Mac empty row's visual scaffold (macbook.and.iphone
icon at 44pt, "No workspaces yet" title2.bold, secondary message,
spacing, width cap) is now one shared view,
WorkspaceListEmptyStateScaffold, used by the table row and the per-host
SSH overlay, so every empty state is pixel-identical. Only what must
differ differs: SSH messages carry the host's status line (per host,
still inside the pull-to-refresh scroll view with auto-connect) or the
choose/add-computer line (All Computers), and the Mac-only Retry and See
Docs buttons stay on the Mac variant, which drives Mac workspace-list
recovery and pairing docs (PRD D46). The mobile.ssh.empty.title key is
now unused and removed.

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

* iOS: remove the SSH-only entry; every user signs in

The sign-in screen's quiet "Use with SSH only" entry, the signed-out SSH
shell it opened (SSHOnlyWelcomeView, MobileSSHOnlyPreference, the
mobileSSHOnlyEntry environment action), and the audience machinery that
suppressed Mac notices for it (MobileWhatsNewAudience) are removed:
MobileRootAuthGate.shouldShowSignIn no longer takes a bypass, so a
signed-out launch always lands on sign-in and every user signs in before
using the app (PRD D47, superseding D5's no-account entry; deliberate
deviation from HIG Managing accounts, since every cmux surface is
account-backed). The SSH computers feature itself is untouched for
signed-in users: hosts and keys stay on-device, the Computers screen and
pairing's "Connect with SSH Instead" still add hosts, and
WorkspaceListEmptyGuidance still keys on has-SSH-computers /
no-paired-Mac, which a signed-in account before its first pairing hits.
An attach-ticket session (no Stack account) keeps its settings
sign-in row. Localization keys for the removed strings are deleted.

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

* PRD: round 5 (D44-D47)

Files chip back to the Mac scroll reveal, Split Right/Split Down, one
empty-state scaffold, and required sign-in; D5/D29/D42 and the round-4
chip note updated to point at their supersessions; changelog line.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

3 participants