Skip to content

Send sidebar links through the external-open rules - #7397

Merged
teamleaderleo merged 8 commits into
manaflow-ai:mainfrom
ejc3:browser-external-open-host-patterns
Oct 4, 2026
Merged

teamleaderleo merged 8 commits into
manaflow-ai:mainfrom
ejc3:browser-external-open-host-patterns

Conversation

@ejc3

@ejc3 ejc3 commented Jul 5, 2026 •

Copy link
Copy Markdown
Contributor

browser.urlsToAlwaysOpenExternally sends a matching link to the system browser instead of cmux's embedded one. Terminal links, the intercepted open command and link clicks inside the embedded browser already follow it. Pull-request and port links in the sidebar do not: they open embedded even when a rule matches, so a site that only works in a managed browser dead-ends there.

Repro: add a rule such as github.com, then click a pull-request link in the sidebar. It opens in the embedded browser.

What changes

The sidebar's pull-request and port links, and the action that opens every pull request of a workspace, now ask BrowserExternalNavigationHandler.sidebarLinkDestination(for:prefersEmbeddedBrowser:) where to open. A link that matches a rule goes to the system browser even when "open sidebar links in the cmux browser" is on, because a rule-listed site cannot work in the embedded web view at all. Only web schemes are checked against the rules. Other schemes keep following the preference.

This PR used to carry a second half that changed when a link inside a page may leave for the system browser. The in-page routing has landed separately in #10634, and the user-activation check here was wrong (it read NSApp.currentEvent at decision time, which can be a later mouse move), so that half is removed. The diff is now three files.

Tests

BrowserLinkOpenSettingsTests adds four: a sidebar link with no matching rule follows the preference, a pull-request link that matches a rule goes to the system browser, a port link that matches a rule does too while localhost stays embedded, and a non-web scheme follows the preference even when a rule's text matches it.

Against a sidebarLinkDestination that only follows the preference, three of the four fail, each reporting embeddedBrowser where systemBrowser is expected. With the rule check in place, all 27 tests in the suite pass.

Main's test target does not compile at 00f182fc0fa (RightSidebarTabCustomizationTests.swift, which #17211 repairs), so I ran both commits with that file and CloudMachineOrderingTests.swift removed locally. Neither removal is in this branch.

Changelog

Fixed: Sidebar pull request and port links follow the “always open externally” URL rules

@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

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

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds link escape routing for browser navigations, new-tab and popup opens, sidebar/pull-request link handling, and end-to-end UI test coverage with updated fixture text and locale instructions.

Changes

External link escape routing

Layer / File(s) Summary
escape predicate and pattern parsing
Sources/Panels/BrowserPanel.swift
Adds BrowserLinkOpenSettings.linkEscapesToSystemBrowser(_:), hardens external-open regex parsing with guarded try? and DEBUG logging, and changes browserOpenExternalNavigationURL from private to internal.
WebKit navigation and popup interception
Sources/Panels/BrowserNavigationDelegate.swift, Sources/Panels/BrowserPanel.swift
decidePolicyFor cancels link-activated navigations matching the escape check and routes them externally; openLinkInNewTab and createWebViewWith similarly route matching URLs to browserOpenExternalNavigationURL instead of embedded tab/popup creation.
Sidebar and pull-request link opening
Sources/ContentView.swift
Pull-request link opening and openSidebarLink/openPullRequestLink/openPortLink now check linkEscapesToSystemBrowser to choose between embedded tabManager.openBrowser and NSWorkspace.shared.open fallback.
UI-test socket and fixture harness
cmuxUITests/BrowserFixtureInteractionUITests.swift, cmux.xcodeproj/project.pbxproj
Adds launch/configuration hooks, socket fallback paths, readiness polling, logging, and project wiring for the new UI test source.
External-open routing UI tests and fixtures
cmuxUITests/BrowserExternalOpenRoutingUITests.swift, cmuxUITests/BrowserFixtures/*
Adds capture-based UI tests plus HTML fixtures that exercise matched and unmatched external-open cases, popup opens, and form POST routing.
Unit tests and locale wording
cmuxTests/BrowserConfigTests.swift, Resources/Localizable.xcstrings
New tests validate domain pattern matching and escape behavior; locale strings update the link-opening instructions for embedded browser and sidebar links.

Estimated code review effort: 4 (Complex) | ~45 minutes


Important

Pre-merge checks failed

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

❌ Failed checks (3 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Full Internationalization ❌ Error Edited settings.browser.externalPatterns and its subtitle cover 19 locales but omit supported km, violating the catalog's full-i18n rule. Add km translations for both settings.browser.externalPatterns and settings.browser.externalPatterns.subtitle in Resources/Localizable.xcstrings and keep every supported locale filled.
Cmux No Test Or Debug Seam In Production Source ❌ Error BrowserPanel.swift adds a DEBUG-only UI-test capture sink in browserOpenExternalNavigationURL, a production-source test seam with no product caller. Move the capture/observation hook into the test target or a dedicated debug-only file; keep production code internal and use @testable import for tests.
Cmux No Ambient Global State ❌ Error Sources/Panels/BrowserPanel.swift:1223 widens browserOpenExternalNavigationURL from private to module-internal file-scope API, violating the no-ambient-global-state rule. Move the open-routing behavior onto BrowserPanel (or an injected router/service) and keep file-scope helpers private/fileprivate; don’t expose a module-global free function.
Description check ⚠️ Warning The description gives a clear problem, implementation summary, test results, and changelog. However, it says the diff is three files and that in-page routing was removed, while the supplied change sum… Reconcile the description with the final diff and objectives. Clarify which browser-routing and localization changes are included, and update the testing details accordingly. Add the applicable localization-audit result, a demo video or scr…
✅ Passed checks (21 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed The new routing helpers are top-level pure utilities; call sites stay inside @MainActor BrowserPanel/BrowserNavigationDelegate/ContentView, with no new Sendable/shared-mutable types.
Cmux Swift Blocking Runtime ✅ Passed PASS: the only new waits/polling are in BrowserFixtureInteractionUITests scaffolding; the production changes add no new blocking primitives.
Cmux Browser Automation Off-Main ✅ Passed Wait-heavy browser verbs stay on the socket worker, use explicit main hops, and have policy-test coverage; this PR only adds link-routing tests/helpers.
Cmux Expensive Synchronous Load ✅ Passed Touched browser-routing paths add no new synchronous agent-history loads; no RestorableAgentSessionIndex/JSONL/Data(contentsOf:) work was introduced on main/interactive paths.
Cmux Cache Substitution Correctness ✅ Passed No production diff swaps authoritative loads for cached values in persistence/history/undo/snapshot paths; the changes only add link-routing predicates and UI-test capture plumbing.
Cmux No Hacky Sleeps ✅ Passed No changed JS/TS/shell/build runtime files are in scope, and the new polling/wait helpers are Swift test scaffolding, which this check excludes.
Cmux Algorithmic Complexity ✅ Passed No new nested scans or per-record rescans on scalable collections; added iteration is over small settings/pattern lists or test fixtures, and hot paths stay linear.
Cmux Swift Concurrency ✅ Passed App-side link-routing changes are synchronous; the only new DispatchQueue is XCTest pipe-drain plumbing, which is a permitted test boundary.
Cmux Swift @Concurrent ✅ Passed Only BrowserFixtureInteractionUITests changed; the added CLI-sink helper is synchronous, with no new nonisolated async work or @concurrent misuse.
Cmux Swift File And Package Boundaries ✅ Passed PASS: Production changes are small glue in already-oversized app files; no file grows >250 lines, and the larger additions are tests/fixtures, which are exempt.
Cmux Swiftpm Lockfiles ✅ Passed Diff only adds a UITest file to cmux.xcodeproj; no Package.swift, Package.resolved, .gitignore, or SwiftPM package-reference changes are present.
Cmux Swift Logging ✅ Passed Added production logs are DEBUG-guarded cmuxDebugLog calls; remaining NSLog usage is preexisting or confined to UI tests.
Cmux User-Facing Error Privacy ✅ Passed Changed user-facing copy stays generic: the external-open alert says cmux could not open the link, and the settings subtitle only documents the public config key.
Cmux Swiftui State Layout ✅ Passed Diff only adds link-routing helpers/checks in existing views; no new ObservableObject/@published, GeometryReader layout changes, row store refs, or render-time state writes.
Cmux Architecture Rethink ✅ Passed PASS: the new polling/capture code is UI-test-only, and production routing uses explicit WebKit callback bridges into one shared external-open helper with clear ownership.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR only changes browser-routing logic and test fixtures; it adds no standalone NSWindow/NSPanel/WindowGroup code or cmuxAuxiliaryWindowIdentifiers entries.
Cmux Source Artifacts ✅ Passed Changed paths are source/config/localization/tests/fixtures only; no logs, caches, temp dirs, build output, or other artifact paths appear in the diff.
Title check ✅ Passed The title clearly identifies the sidebar-link change and its use of external-open rules.
Full details: Description check

Explanation

The description gives a clear problem, implementation summary, test results, and changelog. However, it says the diff is three files and that in-page routing was removed, while the supplied change summary and objectives include broader browser-routing, localization, and UI-test changes.

Resolution

Reconcile the description with the final diff and objectives. Clarify which browser-routing and localization changes are included, and update the testing details accordingly. Add the applicable localization-audit result, a demo video or screenshot for the behavior change, and the relevant checklist items.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@ejc3
ejc3 force-pushed the browser-external-open-host-patterns branch from 4662c3e to c4e49bb Compare July 5, 2026 19:38
@ejc3 ejc3 changed the title Open configured hosts in the system browser instead of the embedded browser Apply the external-open URL rules to links opened anywhere in cmux Jul 5, 2026
@ejc3
ejc3 force-pushed the browser-external-open-host-patterns branch 3 times, most recently from ecfc729 to b18f1d0 Compare July 5, 2026 20:35
@ejc3
ejc3 marked this pull request as ready for review July 5, 2026 20:50
@greptile-apps

greptile-apps Bot commented Jul 5, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends the browser.urlsToAlwaysOpenExternally escape rules — previously consulted only for terminal link clicks and intercepted open calls — to apply wherever cmux opens an http/https link: main-frame and target=_blank link activations inside the embedded browser, context-menu "Open Link in New Tab", popup-window navigations, and both sidebar link-open paths. A new linkEscapesToSystemBrowser gate wraps the existing substring/regex matcher with a web-scheme guard. The existing UITestCaptureSink DEBUG capture pattern intercepts escapes in CI so no real browser is launched during tests.

  • BrowserNavigationDelegate and BrowserUIDelegate each gain an early escape check for .linkActivated navigations that match the rules; form POSTs, downloads, and scripted window.open are intentionally excluded.
  • BrowserPopupWindowController mirrors both delegate escape paths so popup-window link clicks obey the same policy.
  • ContentView.swift sidebar PR-link and port-link paths add a !linkEscapesToSystemBrowser(url) guard ahead of the embedded-open attempt, falling through to the pre-existing NSWorkspace.shared.open fallback.
  • Localization strings are updated in all 19 supported locales, and new unit + socket UI tests cover matched/unmatched links, scripted window.open, and form-POST routing.

Confidence Score: 5/5

Safe to merge. All six link-open entry points are updated consistently, matching semantics are unchanged, and the escape gate is restricted to http/https with a web-scheme guard.

Every new escape check follows the same .linkActivated + non-download guard used by the existing checkout-escape path. Form POSTs, scripted window.open, and downloads are deliberately excluded and verified by the new UI test suite. The UITestCaptureSink #if DEBUG interception is the established pattern already present in GhosttyTerminalView, WindowDragHandleView, and other production files. Localization is complete across all 19 locales. No new global state, no blocking primitives, and no actor-isolation issues were introduced.

No files require special attention.

Important Files Changed

Filename Overview
Sources/Panels/BrowserNavigationDelegate.swift Adds escape check before the insecure-HTTP gate; correctly restricted to .linkActivated, main-frame-or-nil target, non-download navigations. Uses injectable presentAlert and conditionally reports terminal cancellation only on success.
Sources/Panels/BrowserPanel.swift Adds linkEscapesToSystemBrowser static helper (web-scheme guard + existing matcher), escape check in openLinkInNewTab (context-menu path), escape check in BrowserUIDelegate.createWebViewWith for target=_blank, and UITestCaptureSink interception under #if DEBUG in browserOpenExternalNavigationURL — follows the established capture-sink pattern used by GhosttyTerminalView and others.
Sources/Panels/BrowserPopupWindowController.swift Mirrors the main-browser escape policy for both PopupNavigationDelegate (link-activated, non-download, pre-insecure-HTTP-gate) and PopupUIDelegate (target=_blank path). No presentAlert injection on the popup nav path (uses default browserPresentAlert), consistent with popup context.
Sources/ContentView.swift Three sidebar link-open sites gain !BrowserLinkOpenSettings.linkEscapesToSystemBrowser(url) guards; matched URLs fall through to the pre-existing NSWorkspace.shared.open path — correct for a context that has no WKWebView to anchor an alert.
cmuxUITests/BrowserExternalOpenRoutingUITests.swift New end-to-end suite exercising matched link escape, unmatched embedded stay, scripted window.open non-escape, and form-POST non-escape through real WebKit link activations; capture-file sink avoids launching a real browser in CI.
cmuxUITests/BrowserFixtureInteractionUITests.swift Hardens BrowserFixtureSocketTestCase with nc and bundled-CLI fallback transports, hidden-webview discard env var, lastWorkspaceID tracking, waitForCondition/waitForSelector/waitForEmbeddedTab polling helpers, and openTabURLs helper — all test-only scaffolding improvements.
Resources/Localizable.xcstrings Settings subtitle for 'URLs to Always Open Externally' updated in all 19 supported locales to describe full link-open coverage.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Link activation event] --> B{Source?}
    B -->|Terminal link click| C[Existing path: TerminalController]
    B -->|intercepted open call| D[Existing path: open handler]
    B -->|Main-frame click in embedded browser| E[BrowserNavigationDelegate decidePolicyFor]
    B -->|target=_blank click in embedded browser| F[BrowserUIDelegate createWebViewWith]
    B -->|Context menu Open Link in New Tab| G[BrowserPanel openLinkInNewTab]
    B -->|Link click in popup window| H[PopupNavigationDelegate + PopupUIDelegate]
    B -->|Sidebar PR/port link| I[ContentView sidebarLink handlers]
    E --> J{linkEscapesToSystemBrowser?}
    F --> J
    G --> J
    H --> J
    I --> K{linkEscapesToSystemBrowser?}
    J -->|Yes: http/https + rule matches| L[browserOpenExternalNavigationURL]
    J -->|No| M[Embedded browser normal flow]
    K -->|Yes| N[NSWorkspace.shared.open]
    K -->|No| O[tabManager.openBrowser embedded tab]
    L --> P{DEBUG + CMUX_UI_TEST_CAPTURE_EXTERNAL_OPEN_PATH set?}
    P -->|Yes| Q[Write to capture file — no real browser]
    P -->|No| R[NSWorkspace.shared.open in system browser]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Link activation event] --> B{Source?}
    B -->|Terminal link click| C[Existing path: TerminalController]
    B -->|intercepted open call| D[Existing path: open handler]
    B -->|Main-frame click in embedded browser| E[BrowserNavigationDelegate decidePolicyFor]
    B -->|target=_blank click in embedded browser| F[BrowserUIDelegate createWebViewWith]
    B -->|Context menu Open Link in New Tab| G[BrowserPanel openLinkInNewTab]
    B -->|Link click in popup window| H[PopupNavigationDelegate + PopupUIDelegate]
    B -->|Sidebar PR/port link| I[ContentView sidebarLink handlers]
    E --> J{linkEscapesToSystemBrowser?}
    F --> J
    G --> J
    H --> J
    I --> K{linkEscapesToSystemBrowser?}
    J -->|Yes: http/https + rule matches| L[browserOpenExternalNavigationURL]
    J -->|No| M[Embedded browser normal flow]
    K -->|Yes| N[NSWorkspace.shared.open]
    K -->|No| O[tabManager.openBrowser embedded tab]
    L --> P{DEBUG + CMUX_UI_TEST_CAPTURE_EXTERNAL_OPEN_PATH set?}
    P -->|Yes| Q[Write to capture file — no real browser]
    P -->|No| R[NSWorkspace.shared.open in system browser]
Loading

Reviews (11): Last reviewed commit: "browser: close the escape-rule gaps revi..." | Re-trigger Greptile

Comment thread Sources/Panels/BrowserPanel.swift Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Panels/BrowserNavigationDelegate.swift`:
- Around line 272-294: The debug log in BrowserNavigationDelegate’s
external-link escape path uses raw url.absoluteString instead of the same
redacted helper used elsewhere. Update the cmuxDebugLog call in the escape
branch to use browserNavigationDebugURL(url) (or the shared URL formatting
helper) so BrowserNavigationDelegate keeps debug URL logging consistent and
avoids leaking full URLs.

In `@Sources/Panels/BrowserPanel.swift`:
- Around line 8797-8815: The escape-path handling in BrowserPanel’s
createWebView logic is fine, but the new debug log should match the existing
redaction pattern used elsewhere in this method. Update the `cmuxDebugLog` call
inside the `.linkActivated` /
`BrowserLinkOpenSettings.linkEscapesToSystemBrowser` branch to use
`browserNavigationDebugURL(...)` instead of `url.absoluteString`, so the
`browserOpenExternalNavigationURL` path keeps debug output consistent and avoids
exposing raw URLs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2c9693b4-f49a-4007-9731-a62939d42682

📥 Commits

Reviewing files that changed from the base of the PR and between c1e4b26 and b18f1d0.

📒 Files selected for processing (5)
  • Resources/Localizable.xcstrings
  • Sources/ContentView.swift
  • Sources/Panels/BrowserNavigationDelegate.swift
  • Sources/Panels/BrowserPanel.swift
  • cmuxTests/BrowserConfigTests.swift

Comment thread Sources/Panels/BrowserNavigationDelegate.swift Outdated
Comment thread Sources/Panels/BrowserPanel.swift Outdated
@ejc3
ejc3 force-pushed the browser-external-open-host-patterns branch from 99bee55 to f6d78f3 Compare July 6, 2026 00:02

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxUITests/BrowserFixtureInteractionUITests.swift`:
- Around line 332-361: `waitForEmbeddedTab` currently lets a single transient
`socketResult`/`openTabURLs` failure abort the whole poll instead of retrying
until `timeout`. Update `waitForEmbeddedTab` to mirror the retry-tolerant
pattern used by `waitForCondition` and `waitForSelector`: catch per-attempt
errors inside the loop, ignore transient failures, and keep polling
`openTabURLs` until `browser.tab.list` succeeds and a matching URL is found or
the deadline expires. Preserve the existing `waitForEmbeddedTab` signature and
use its `openTabURLs` helper so the fix stays localized.
- Around line 128-173: In socketEnvelopeViaBundledCLI, the Process is waiting on
exit without draining standardError, and stdout is only read after
waitUntilExit(), which can deadlock if cmux rpc writes enough output. Update
this fallback path to consume both stdout and stderr concurrently (or otherwise
continuously) before waiting, while preserving the existing success-envelope
behavior and timeout handling in socketEnvelopeViaBundledCLI.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0951cec8-6e64-43ad-8ac1-31d24768d5cb

📥 Commits

Reviewing files that changed from the base of the PR and between 99bee55 and f6d78f3.

📒 Files selected for processing (6)
  • Sources/Panels/BrowserPanel.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxUITests/BrowserExternalOpenRoutingUITests.swift
  • cmuxUITests/BrowserFixtureInteractionUITests.swift
  • cmuxUITests/BrowserFixtures/external-open-routing.html
  • cmuxUITests/BrowserFixtures/external-open-target.html

Comment thread cmuxUITests/BrowserFixtureInteractionUITests.swift Outdated
Comment thread cmuxUITests/BrowserFixtureInteractionUITests.swift Outdated
@ejc3
ejc3 marked this pull request as draft July 7, 2026 20:01
@ejc3
ejc3 force-pushed the browser-external-open-host-patterns branch from fc03d53 to dcab3b8 Compare July 7, 2026 20:05
@ejc3
ejc3 marked this pull request as ready for review July 7, 2026 20:11
@ejc3
ejc3 force-pushed the browser-external-open-host-patterns branch from dcab3b8 to c181e79 Compare July 11, 2026 19:09
Comment thread Sources/Panels/BrowserNavigationDelegate.swift Outdated
@ejc3
ejc3 force-pushed the browser-external-open-host-patterns branch 4 times, most recently from 47d297f to 03f5815 Compare July 22, 2026 01:54
@ejc3
ejc3 marked this pull request as draft July 25, 2026 05:58
@ejc3
ejc3 force-pushed the browser-external-open-host-patterns branch 2 times, most recently from ab34d06 to 46596ae Compare July 31, 2026 06:02
@ejc3
ejc3 force-pushed the browser-external-open-host-patterns branch from 46596ae to fbadd36 Compare August 9, 2026 06:30
@ejc3
ejc3 marked this pull request as ready for review August 9, 2026 15:57

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d6f1c4e. Configure here.

Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment thread Sources/Panels/BrowserNavigationPopupPolicy.swift Outdated
@ejc3
ejc3 force-pushed the browser-external-open-host-patterns branch from d6f1c4e to ee7fdc9 Compare August 16, 2026 04:01
@ejc3
ejc3 force-pushed the browser-external-open-host-patterns branch from df31ff6 to 422c4ec Compare August 28, 2026 15:59
ejc3 added a commit to ejc3/cmux that referenced this pull request Sep 26, 2026
@ejc3 ejc3 changed the title Apply the external-open URL rules to links opened anywhere in cmux Send sidebar links through the external-open rules, and escape only on a real user event Sep 28, 2026
ejc3 added a commit to ejc3/cmux that referenced this pull request Sep 28, 2026
ejc3 added a commit to ejc3/cmux that referenced this pull request Sep 28, 2026
ejc3 added a commit to ejc3/cmux that referenced this pull request Sep 28, 2026
A link now leaves for the system browser only while a real input event
is in flight, and the socket browser.click runs JavaScript, so the matched
and unmatched link cases click through accessibility the way a person
does. The scripted popup and form cases keep the socket click, since a
scripted action is what they test.
ejc3 added a commit to ejc3/cmux that referenced this pull request Sep 29, 2026
@teamleaderleo teamleaderleo added S3: minor Wrong behavior with a workaround area: browser The embedded browser, web surfaces, inline VS Code labels Sep 30, 2026
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

CI failure attribution

CI passes on 24301818f0 (run 37204211567 attempt 2).

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's.

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

CI fast guards passes on 24301818f0 (https://github.com/manaflow-ai/cmux/actions/runs/37204211324).

@teamleaderleo

Copy link
Copy Markdown
Collaborator

Taking this: checking the remaining sidebar-link slice against the browser routing that already landed.

  • OrchardSpoon g1 🌀

@teamleaderleo teamleaderleo left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the sidebar routing work. That part is still useful, but the in-browser half already landed in #10634 and this branch still changes its user-activation policy.

The new default hasUserActivation: browserNavigationHasSimpleUserActivation() samples NSApp.currentEvent when WebKit asks for a navigation decision. A later mouse-move event can replace the original click, so a real click on a rule-matched link can remain in the embedded browser. This needs event tracking rather than another check of the current event.

Could you trim and refresh this PR to the sidebar matcher, its three open paths, and focused sidebar-routing tests, as suggested in the earlier review? That will let us validate and land the useful sidebar slice while the browser gesture change gets its own fix.

  • OrchardSpoon g1 🌀

ejc3 added 3 commits October 3, 2026 17:11
Catch-up merge by scripts/ci/catch_up_pr.py (RFC manaflow-ai#14631).
Merged by scripts/merge-main.sh: origin/main at 00f182f.

Catch-up-previous-head: 28adbe1
Catch-up-base: 00f182f
The in-page half of the external-open rules has landed separately. What remains here is the sidebar: pull-request and port links follow the rules, with tests for the matcher.
ejc3 added a commit to ejc3/cmux that referenced this pull request Oct 4, 2026
ejc3 added a commit to ejc3/cmux that referenced this pull request Oct 4, 2026
@ejc3 ejc3 changed the title Send sidebar links through the external-open rules, and escape only on a real user event Send sidebar links through the external-open rules Oct 4, 2026
@ejc3

ejc3 commented Oct 4, 2026

Copy link
Copy Markdown
Contributor Author

Trimmed as you asked. The branch is now the sidebar matcher, its three open paths and four tests in BrowserLinkOpenSettingsTests: three files, 80 lines added. The in-page activation change and its UI tests are gone.

Co-authored-by: ejc3 <ejc3@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo
teamleaderleo merged commit a6f1af5 into manaflow-ai:main Oct 4, 2026
64 checks passed
@teamleaderleo

Copy link
Copy Markdown
Collaborator

Merged, thank you @ejc3! PR and port links in the sidebar now follow your external-open rules.

@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

Merge receipt for 24301818f0: every check was green at merge (15 verified; 21 skipped by policy). Full suite runs on main after merge.

rustybret pushed a commit to rustybret/bmux that referenced this pull request Oct 4, 2026
0589e2c ci: fall back to ancestor evidence for --main-fix (manaflow-ai#17277)
0cad11f ci: require merge checks only when their workflows exist (manaflow-ai#17284)
a6f1af5 Send sidebar links through the external-open rules (manaflow-ai#7397)
f8db895 Clear restored agent notifications once the agent is gone (manaflow-ai#17067)
93585c1 Expose the workspace task-status lane to custom sidebars (manaflow-ai#17245)
austinywang added a commit that referenced this pull request Oct 5, 2026
…t screens (#17230)

* cloud welcome: introducing cmux cloud window, shown once, five layouts to compare from help (wip)

* cloud onboarding: glass welcome window, lowercase and machine focus layouts, sidebar intro with banner and reasons, enablement view takes plain values (wip)

* cloud onboarding: one welcome design (machine focus, all lowercase), sidebar intro titles by plan, 6pt buttons, drop the comparison layouts

* cloud onboarding: review fixes (glass behind compiler guard, welcome considered once per launch, size after hosting, no return shortcut, comments, orphaned string)

* cloud welcome: ignore the titlebar safe area (fixes a layout-loop crash on open), three reasons

* cloud tab intro: title first, no icon

* cloud tab intro: app icon banner back, lock badge while the plan needs pro

* cloud tab intro: dark app icon in the banner

* fix(cloud): wait for display helper readiness (#17132)

* fix(cloud): explain unavailable display actions

Show the existing Cloud failure message when New Display is unavailable, and explain the machine ownership restriction when a display is opened into another Cloud workspace.

— unregistered

* fix(cloud): explain unavailable display actions

Show the existing Cloud failure message when New Display is unavailable, and explain the machine ownership restriction when a display is opened into another Cloud workspace.

— unregistered

* fix(cloud): reject cross-machine display opens before projection

* fix(cloud): probe legacy desktop VMs for displays

* fix(cloud): initialize wallpapers for new displays

* fix(cloud): fail closed before display double-click opens

* fix(cloud): replay repeated display ownership hints

* test(cloud): cover display helper refresh

* fix(cloud): refresh installed display helper

* test(cloud): require display service readiness probe

* fix(cloud): wait for display helper readiness

* fix(cloud): keep display creation retryable

* fix(cloud): authorize dynamic display ports

* fix(cloud): let new display retry guest discovery

* Revert "fix(cloud): let new display retry guest discovery"

This reverts commit 0fe008ed3f6fd78338be9e9a2f8161d37984cc0f.

* test(cloud): sidebar visibility filter must keep display creation state

Moves applyingDeviceVisibility next to SurfaceCatalogSnapshot in
CmuxSurfaceCatalogModel (no behavior change) so it is testable, and adds
a failing regression test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): keep display creation state through the sidebar filter

applyingDeviceVisibility rebuilt SurfaceCatalogSnapshot from scratch and
dropped displayCreationMachines, staleMachineIDs and display memberships,
so every desktop VM's New Display row reported additional displays as
unavailable. Filter a copy instead so all per-machine state survives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(surfaces): a split with no room opens as a tab in the target pane

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): optimistic display row; fall back to a tab when a split has no room

New Display now shows a "Starting display…" row from the click until the
guest answers, driven by the catalog's in-flight creation set.

Opening a sidebar resource splits the focused pane; once split admission
had no room left, the open failed with "Could not create the pane:
noSpace". SurfacePaneFactory now opens it as a tab in the pane that would
have been split.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(surfaces): refused split is typed; sidebar gestures open a tab

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(surfaces): scope the no-room tab fallback to sidebar gestures

The factory now throws a typed noSpace instead of opening a tab, so the
layout replay and socket open verbs keep a truthful refused split. Sidebar
opens and New Display use openPreferringSplit, which retries as a tab in
the pane that would have been split. The empty Displays row no longer
shows beside the optimistic Starting display row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): display creation needs no discovery round trip

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(cloud): open the display pane at the click and skip pre-create discovery

New Display paid two VM exec round trips (list, then create, about 2s
each) before any pane appeared. The create reply is already the full guest
catalog, so the list is dropped. The pane now opens at the click with a
native Starting display state and is adopted by the projection once the
guest assigns the display; creation failure closes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): harden the reserved display pane

Drop a second click before it opens a pane; bind the reservation to the
created display id so no other projection adopts it; keep creating when
the pane cannot open; leave the display in the pool when the person closed
the pane; show the failure on a pane the socket refuses to close.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): additional displays serve noVNC beside the primary desktop

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): make additional displays reachable on the VM private address

Displays 2+ ran websockify on 127.0.0.1 while display 1 listens on [::].
The client reaches every display through the private address, so each new
display's first noVNC connection was refused and only recovered after a
timeout. Bind like display 1 (Xvnc stays -localhost), replace proxies an
older helper left on loopback, and drop the open-port call added for
display ports: it changed no routing and cost a control-plane request,
plus a desktop heal exec for display 1, on every open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): stale proxy match covers only this display's websockify

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): match stale display proxies by argv, not pgrep regex

pgrep's ERE has no (?:...) groups, so the stale websockify match failed,
the old loopback listener kept the port, and the rebound proxy exited.
Read /proc argv for this user's websockify on this display's ports.
Verified on a live VM: displays 2 and 3 now accept the noVNC websocket on
the private address, one proxy each, X sessions untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): a failed first desktop connection retries before failing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): retry a display's first connection; warm the carrier at create

noVNC stops at Connect after an initial connect failure; its reconnect only
follows a session that once connected. Restored and new displays could lose
that race while the proxy or guest listener started, leaving "Failed to
connect to server". Retry the route's first connection three times
(0.5s/1s/2s on the injected clock) before showing the failure card.

The first display open on a machine spent 12-18s starting its browser
carrier after the guest exec. Start it alongside the create request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): an explicit Retry gets its own quiet first-connection retries

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): run the guest display script test on the main actor

CloudGuestDisplayScript is @MainActor; the test called it from a
nonisolated context and broke the CmuxCloud package test build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): per-runner build roots only on fleet Macs without glaeda (#17239)

* test(ci): Blacksmith keeps the shared root; reruns keep a per-runner root (red)

Blacksmith macOS runners report RUNNER_ENVIRONMENT=self-hosted, so they got
per-runner roots: every build started cold and no seed could be adopted
(job 111334766861). take-product-canonical-root.sh also refused a product
built at a per-runner root on a fleet Mac without glaeda.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): per-runner roots only on fleet Macs without glaeda

canonical-build-root.sh derived /private/tmp/cmux-ci-<runner> for every
self-hosted runner without the glaeda helper, which included Blacksmith's
ephemeral macOS runners: their builds started cold and their seeds were
never adopted. Derive it only when the fleet directory exists. Let
take-product-canonical-root.sh accept such a root when glaeda isn't there
to hold it, so app-host test reruns build where the product was compiled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): repair main's guards, localization parity and cmuxTests compile (#17207)

* fix(ci): align owned-build-state guards with per-runner canonical roots

#17168 (496195b) derives a canonical root per self-hosted runner, exports
CMUX_CI_CANONICAL_ROOT from the build-slot step and drops the shared
/private/tmp/cmux-ci fallback in test-e2e's owned-state step. Three tests in
tests/test_ci_owned_build_state.py still assumed the old contract, turning
"CI fast guards" red on main.

Update them to the new contract: the slot step exports the root, a
per-runner root reads its own store, and a missing root reads nothing. The
owned-state step now fails with a clear message when the root is missing, and
rejects a root nested under a cmux-ci-* prefix (e.g. cmux-ci-a/../x), so the
looser glob cannot map the store outside the Mac's package directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(l10n): translate the New Machine plan loading strings

#17135 added machines.new.plan.loading, .retry and .error with the English
text copied into every locale, so `localization_catalog.py check` reports 24
parity errors and "Fast static checks" fails on every PR. Translate them for
all catalog locales; Retry reuses common.retry's wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(l10n): match the catalog's plan terminology in de, ko and zh-Hant

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tests): repair cmuxTests compile after the sidebar reorder change

#17070 moved RightSidebarModeBarDragLayout into the CmuxSidebar package,
but RightSidebarTabCustomizationTests still relied on the app module
exporting it, and CloudMachineOrderingTests put `try` calls inside an `&&`
in #expect, which the macro rejects ("operator can throw but expression is
not marked with 'try'"). Import CmuxSidebar and hoist the throwing lookups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tests): hoist the second throwing lookup out of #expect in CloudMachineOrderingTests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): satisfy dogfood-build runner guards and the Swift warning budget

#17206 turned dogfood-build into a macOS build, which tripped three guards:
no pinned Xcode, no fork branch in runs-on, and no static-preflight
dependency. Add `scripts/select-ci-xcode.sh`, the standard owner and fork-PR
branches ahead of the existing selector, and `needs: static-preflight`. The
job's `if` already excludes forks, so manaflow-ai PRs keep the same runner.

#17070's `RightSidebarModeBarDragController.coordinateSpace` is read from
a Sendable geometry closure and broke the zero-warning budget; it is a plain
String constant, so mark it `nonisolated`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ci): a root the glaeda hook exported wins over the per-runner root

Fails on main: canonical-build-root.sh treats an exported /private/tmp/cmux-ci
as unset and derives /private/tmp/cmux-ci-<runner>, so main-compile-probe
refuses the root glaeda placed it in (runs 37157423969, 37155786981).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): honor the canonical root the glaeda hook exported

#17168 (496195b) derives /private/tmp/cmux-ci-<runner> on every self-hosted
runner unless CMUX_CI_CANONICAL_ROOT names a non-default root. glaeda's
runner hook holds root 1 (/private/tmp/cmux-ci) for a compile job and
exports exactly that, so the job then built somewhere glaeda does not hold,
and main-compile-probe refused it: "glaeda placed this job in
/private/tmp/cmux-ci, not /private/tmp/cmux-ci-cmux13s-mac-mini-glaeda".

Let any exported root win. A self-hosted job with no exported root still
gets its per-runner root, so #17168's isolation for unplaced runners stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ci): an unplaced self-hosted job keeps the shared seeded root

Fails on the branch: canonical-build-root.sh derives /private/tmp/cmux-ci-<runner>,
whose fingerprint never matches main's seeds, so new aws runners
(aws-m4pro-9-glaeda-3, aws-m4pro-8-glaeda-4) compiled cold and timed out at
35 minutes in compile admission (run 37158561950, both attempts).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): build unplaced self-hosted jobs at the shared seeded root

#17168 gave every self-hosted job without an exported root its own
/private/tmp/cmux-ci-<runner>. That root is part of the cache fingerprint,
so main's DerivedData seeds and the owned build state never match it, and the
prepare step clears it each job: every new aws runner compiled cold and hit
the 35-minute admission limit (aws-m4pro-9-glaeda-3, aws-m4pro-8-glaeda-4 on
run 37158561950).

Go back to /private/tmp/cmux-ci when no root is exported. glaeda's hook still
exports root N for the jobs it places, so those stay isolated. Unplaced
concurrent runners on one Mac share the root again, as before #17168, until
glaeda places those jobs too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ci): the pool picker never picks an owned label it was not configured with

Fails on main: simple_pool_picker adds every glaeda-<class>-xcode-* label it
sees on a runner, and glaeda-std-xcode-26.3 (ten aws EC2 runners, five per
Mac) sorts before the minis' 26.6, so compile admission ran there and timed
out at 35 minutes while the minis were idle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): pick only configured owned pools, not any owned-looking label

simple_pool_picker added every glaeda-<class>-xcode-* label it saw on an
online runner to the configured CI_OWNED_POOL_SLOTS pools, then tried them
in string order. The aws EC2 Macs carry glaeda-std-xcode-26.3 (ten runners,
five per Mac), which sorts before the minis' glaeda-std-xcode-26.6, so PR
compile admission landed on contended EC2 runners and timed out at 35
minutes while the minis sat idle. Use only the configured pools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ci): the pool picker reads every page of organization runners

Fails on main: LiveState.runners reads one page of 100, but manaflow-ai has
509 runners and the first page holds the aws Macs and no idle mini, so the
picker never saw the mini fleet (run 37166910798 fell back to Blacksmith with
29 idle minis online).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): read every page of runners in the pool picker

LiveState.runners read only the first page of 100 organization runners.
manaflow-ai has about 500, and the first page holds the aws Macs but no idle
mini, so the picker never saw the mini fleet: it took the aws 26.3 label
while that was discoverable, and fell back to Blacksmith once it was not.
Read up to ten pages. Against live data the picker now sees 38 online
glaeda-std-xcode-26.6 minis (33 free) and picks them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: clear the Cloud sidebar warnings that broke the Swift warning budget

Compile admission on a mini (run 37167089830) built cleanly but failed the
zero-warning budget on three warnings from the Cloud sidebar work:
`SurfaceCatalog.shared` used as a default argument of two @MainActor
CloudWorkspaceSidebarPresentation entry points (evaluated nonisolated), and an
implicit `self` in CloudTreeOutlineView's machine-lift reopen closure.
Resolve `.shared` inside the main-actor body and make the capture explicit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): preserve canonical root and translation wording

* test(ci): align canonical root guard with main behavior

* test: use isolated catalogs in Cloud sidebar fixtures

Pass each test fixture catalog through the sidebar snapshot factory and direct presentation assertion so Cloud machine metadata is read from the catalog the test populated.

Co-authored-by: Austin Wang <austinwang115@gmail.com>

* test: model loading Cloud sidebar state

Keep device projections visible while their catalog rows are restoring, and assert that a reserved loading card shows machine identity before its directory appears after adoption.

Co-authored-by: Austin Wang <austinwang115@gmail.com>

* test: use a terminal panel for cloud directory settings

The sidebar detail test supplies a reported directory, so give it a real terminal panel instead of a loading card that correctly suppresses directory metadata.

Co-authored-by: Austin Wang <austinwang115@gmail.com>

* fix(cloud): restore device directory fallback lost in main merge

The merge of main took #17107's presentation file and dropped the
fallback from 27a8d804fdf, so a device projection whose catalog row is
still restoring rendered no directory and
SidebarCloudWorkspaceBadgeTests.deviceNameIsVisibleBesideItsDirectory
failed on b6ea6632cb1 (the regression run is that CI failure).

Co-authored-by: Austin Wang <austinwang115@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Austin Wang <austinwang115@gmail.com>

* ci: require a written merge override for red CI (#17217)

* ci: require merge-gate before merges

* Add API-only merge gate for pull requests

* test: cover merge-gate override decisions

* ci: wire merge-gate tests and push timing

* ci: pin merge gate workflow source

* Reject read-only override authors

* ci: harden merge gate runner and author checks

* ci: keep merge gate on hosted capacity

* ci: fail closed when collaborator lookup fails

* ci: reject stale successes during reruns

* ci: fail closed for untimestamped queued checks

* ci: fail closed for untimestamped pending statuses

* ci: verify matching override failure evidence

* test: cover mismatched override failure evidence

* ci: choose newest merge gate check run

* ci: require compile evidence for main-fix merges (#16993)

* test: reproduce main-fix merging without compile evidence

The installed helper bypasses all checks under --main-fix. The regression records that it merges with no compile checks present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: require build evidence for main fixes

gh-merge-green --main-fix now requires successful Release, Debug and Swift test target build steps on the exact PR head. Existing Swift test failures are waivable only when their parsed issue records match the same Swift test step on the exact base SHA; the audit comment records each match and unrelated failures remain fatal.

CPU, memory and disk checks: the validator uses one bounded 90-second GitHub request per call, caps captured output at 32 MiB, writes audit bodies to temporary files, and does not retain logs, caches or stores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* ci: harden merge gate freshness and evidence

* ci: require per-check override reasons

* ci: complete merge gate review fixes

* docs(ci): document merge-gate rollout order

* ci: make merge helper rollout repairable

* ci: point merge helper refusals to repair runbook

* ci: resolve merge helper symlink for main fixes

* docs: remove stale main-fix PR reference

* fix: avoid pipefail false negatives in merge helper

* fix: keep merge gate events fresh per pull request

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Share one vCPU and memory pool across a plan's Cloud VMs (#17238)

* test(vms): reproduce missing shared Cloud VM resource pool

* feat(vms): share one vCPU and memory pool across a plan's Cloud VMs

Pro, Team (per paid seat), and Founder's Edition get up to 5 active VMs
sharing 20 vCPUs and 40 GB RAM, with machines up to the 32 GB xl row.
Max gets 80 vCPUs and 160 GB RAM with machines up to the 64 GB 2xl row.
The repository enforces the pool next to the active-VM count, under the
same transaction and billing lock, on create, Base open/reset, paused
resume, CPU/memory resize, and fork. Pricing, docs, app, and iOS copy
now describe pooled resources.

* Pooled VMs: Pro overrides stop at 32 GB; drop unused iOS pool strings

* test(vms): reproduce leaked compute claim and stale-plan resume pool

* fix(vms): give back unused compute claims and resume against the caller's pool

* docs(pricing): describe the Team pool per paid seat and localize pool strings in every locale

* Cloud VM: snapshot create honors Idempotency-Key (#17244)

* test(cloud-vm): snapshot create dedups by idempotency key (red)

A retry of POST /api/vm/:id/snapshot with the same Idempotency-Key must return
the first snapshot and take no second one; a retry while the first runs is
refused as in progress; the same key with another name is a conflict; a failed
attempt frees the key.

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

* fix(cloud-vm): snapshot create dedups by Idempotency-Key

POST /api/vm/:id/snapshot now reads Idempotency-Key. A new ledger table,
cloud_vm_snapshot_requests (one row per machine and key, additive migration
20261004120000), records a pending attempt before the provider call and the
provider snapshot after it. A retry with the same key returns the first
snapshot and records no second usage event; a retry while the first runs gets
409 vm_snapshot_in_progress (retryable); the same key with another name gets
409 vm_snapshot_idempotency_conflict; a failed attempt frees the key; a pending
row older than 15 minutes (route budget 600 s) is taken over by a retry.
Requests without a key behave as before.

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

* chore(cloud-vm): smoke --snapshot-check proves snapshot idempotency

With --create, takes one snapshot of the throwaway smoke machine twice with the
same Idempotency-Key, requires the same snapshotId, deletes that snapshot,
then destroys the machine as before. No fixed VM id and no printed token: the
smoke mints its own throwaway user session.

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

---------

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

* Preserve selected tab when closing another surface (#16645)

* Preserve selected tab when closing another surface

* Only select the closing pane's tab when that pane is focused

BonsplitController.selectTab also focuses the pane, so calling it for an
unfocused pane moved focus into the pane where a tab closed, the focus theft
this change is meant to stop. Bonsplit already keeps the selection when an
unselected tab closes; the shouldCloseTab change is the actual fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* docs: detail tmux help options (#16780)

Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* test: guard managed contributor difficulty labels (#16448)

* test: guard managed contributor difficulty labels

* test: reject invalid difficulty descriptions

* test: exercise difficulty label sync requests

---------

Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): repair main's Cloud app-host failures from #17132 and #17103 (#17250)

* test(cloud): expect the display helper readiness loop

#17132 replaced the one-line `list || exit 1` probe with a bounded
retry loop and updated the package test, but cmuxTests still asserted
the old line, so CloudDisplayCatalogTests.guestCommandShape failed on
main (run 37186487936, shard 4/7).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): make the restored-display retry test a desktop

#17132's initialDesktopFailureRetries configured port 6902 without a
resource ID, so CloudBrowserAccessState.isDesktop was false and
desktopConnectionDidChange ignored the failure: no quiet retry, and the
following navigations[1] trapped and crashed the shard 5 app host on
main (run 37186487936). Give it the .display identity additional
displays carry and require the retry before indexing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): a foreign display click shows one synchronous hint

#17103 made a display click on another machine's workspace reject
synchronously through showDisplayOpenHint, so no tree operation starts
and waitForOpen() waited out the 60 s suite limit on main (run
37186487936, shards 2 and 3). Assert no operation ran and that the
rejection is shown once; this stays red until the double-click stops
repeating the hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): show a foreign display's ownership hint once per double-click

AppKit delivers a double-click's first click to handleSingleClick,
which already shows the ownership hint, and #17103 also showed it from
handleDoubleClick, so the user got the same rejection twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): never show upstream tree failures in the Machines status row

#17103 started presenting treeError verbatim so its ownership hints
would survive, but treeError also carries raw upstream failures
(LocalizedError descriptions, create output), so a URL with query
parameters reached the toolbar text, hover help and copy menu.
MachinesCloudStatusTests.emptyStatusHasNoProgressPresentation caught it
on main (run 37186487936, shard 7).

Hints now travel through their own onHint sink (falling back to
onFailure for other callers), the panel records them as trusted copy,
and the status row shows the tree error verbatim only when it is that
hint; everything else gets the safe recovery message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): keep merge-gate diagnostics on the exact PR (#17248)

* fix: keep merge-gate diagnostics on the exact PR

* fix: print repair guidance for gate diagnostics

* fix: allow merge-gate PR comments when permitted

* fix: reject mismatched merge-gate event identities

* fix: keep merge-gate alive on comment errors

* fix: keep the cmux-cua credential out of the Codex argv (#17252)

* test: codex wrapper must not put the cmux-cua credential in argv

The Codex wrapper passes the cmux-cua socket credential as
-c mcp_servers.cmux-cua.env.CMUX_CUA_SOCKET_AUTH_TOKEN=<value>, so any
local user can read it with ps. These assertions require the value to be
absent from argv, Codex to receive it in its environment, and the MCP
server to receive it through env_vars. The fake Codex now builds the MCP
server environment the way Codex does (allow-list + env_vars + env).

* fix: keep the cmux-cua credential out of the Codex argv

The wrapper passed the socket credential as a Codex -c env override, which
put the value in the codex process argv where any local user can read it
with ps. The wrapper now exports CMUX_CUA_SOCKET_AUTH_TOKEN in the parent
shell before exec and emits mcp_servers.cmux-cua.env_vars so Codex forwards
the variable by name to the MCP server (Codex env_vars, openai/codex#5246).
Codex's default shell_environment_policy excludes *TOKEN* names from agent
shell commands. Attachment stays fail-closed when no credential resolves.

* fix: keep codex wrapper overrides when the subcommand gets -c (#17257)

* test: codex wrapper overrides must survive subcommand -c flags

Codex declares -c/--config, --enable, and --disable as clap global
arguments. When a subcommand such as exec or resume also receives one,
Codex 0.159.3 keeps only the subcommand-level values, so the wrapper's
cmux-cua MCP config, hook config, and --disable computer_use placed
before the subcommand are dropped. The fake Codex now applies that rule,
and new cases cover exec, resume, mixed root and subcommand -c, and a
literal prompt after --.

* fix: keep codex wrapper overrides when the subcommand gets -c

Codex declares -c/--config, --enable, and --disable as clap global
arguments and keeps only the subcommand-level values when a subcommand
also receives one. The wrapper put its cmux-cua MCP config, hook config,
and --disable computer_use before the user's argv, so codex exec -c ...
or codex resume ... -c ... dropped all of them. The wrapper now moves the
user's subcommand-level global arguments, in order, in front of the
subcommand. Codex reads one root-level list with the same precedence as
before. Tokens after -- stay in place, and interactive launches without a
subcommand are unchanged.

* Add cmux browser repl: a Playwright-shaped browser REPL for agents (#17256)

`cmux browser repl` is a persistent JavaScript REPL that agents use to drive
cmux browser panes: Playwright page, locator, keyboard and mouse semantics with
native trusted input, budgeted accessibility snapshots with diffs, tabs,
cookie-bearing fetch, a sandboxed fs, named sessions, an MCP server mode and
site tools. Guards live outside agent code: fill-only secrets with redaction
and capture masking, a domain policy over every frame and fetch hop, a per-tab
clipboard for session-created tabs, private per-session temp directories and
bounded cells, fetches and timers. Agent work never moves the user's focus,
hibernated tabs wake on use, and crashed tabs report how to recover.

Squashed from https://github.com/manaflow-ai/cmux/pull/15570 (392 commits; the
CLA action cannot read more than 250 commits of one pull request). Same tree as
that branch's head.

* Cloud: VM file operation routes (port of #16936) (#17254)

* Cloud: VM file operation routes (port of #16936), missing path answers 404

Ports the web part of https://github.com/manaflow-ai/cmux/pull/16936
(feat-cmux-next) to main: /api/vm/[id]/fs/[operation] (list, read, stat,
write, mkdir, remove) with the Freestyle driver and gateway methods.

Includes the fix from feat-cmux-next: Freestyle removes a missing path with
success, so removeVmFile stats first and answers a missing file with
404 vm_file_not_found (any other stat failure, a missing VM included, stays a
provider failure).

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

* test(cloud): stat/read/dir of a missing VM path must answer vm_file_not_found

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

* fix(cloud): stat/read/dir of a missing VM path answer 404 vm_file_not_found

The staging rehearsal of #17254 showed stat of a removed file answering 502
vm_cloud_service_unavailable. Map the guest ENOENT on every file read, as
remove already did, and title the error 'File not found'.

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

---------

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

* Stop calling the legacy Subrouter during account deletion (#17273)

* test: account deletion must not call the retired legacy Subrouter

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

* Stop calling the legacy Subrouter during account deletion

The legacy Subrouter at subrouter.cmux.dev is being retired. Account
deletion now skips the legacy revoke phase and needs no legacy env vars.
Local mapping rows are still deleted, and a legacy_delete_pending
tombstone from an older deployment resumes at the hosted checkpoint.

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

* Split the account DELETE handler under the complexity limit

DELETE delegates to deleteAccount and named phase helpers that share one
progress record, so its complexity drops from 46 to under 20 and its
grandfathered baseline entry is removed. Behavior is unchanged.

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

---------

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

* ci: remove merge gate and restore exact-head merging (#17275)

* Cloud: private network routes (port of #16948) (#17255)

* Cloud: private network routes (port of #16948), missing firewall rule answers 404

Ports the web part of https://github.com/manaflow-ai/cmux/pull/16948
(feat-cmux-next) to main: /api/vm/firewall (list, get, create, delete),
/api/vm/network and /api/vm/tunnel/network/[operation], with the Freestyle
driver, gateway and private-network workflow pieces.

Includes the fix from feat-cmux-next: a firewall rule that is not in the
caller's network answers 404 vm_firewall_rule_not_found (a provider 404 race
on delete too); a missing VM endpoint stays vm_not_found.

Stacked on the file-routes port (#17254).

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

* refactor(cloud): move firewall endpoint parsing into services/vms/firewallEndpoint

No behavior change; makes the parser testable without the route.

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

* test(cloud): firewall must accept normal CIDR prefixes and team-owned VM endpoints

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

* fix(cloud): firewall accepts normal CIDR prefixes and team-owned VM endpoints

The staging rehearsal of #17255 found two defects. validCidr compared the
prefix with net.isIP(), which returns the family (4 or 6), so any IPv4
prefix above /4 was refused; it now uses canonicalCidr. The vmId ownership
check looked the VM up in the personal scope, but every new VM is
team-owned, so vmId endpoints were vm_not_found; the route now resolves the
account scope when a vmId is named, and the VM must be the caller's own
(the firewall edits the caller's network). The provider now gets only the
rule fields: the Freestyle driver spreads its input into the request body,
so userId and provider were sent to Freestyle.

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

* test(cloud): firewall rules must name a caller resource as destination; get/delete must find VM rules

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

* fix(cloud): decide firewall rule ownership on the shared provider account

The Freestyle account is shared by every cmux user, so the API decides
whose a rule is. Reading and deleting: the rule names at least one resource
and every resource it names is the caller's. get and delete now read the
rule by id; they searched only the network listing, so a rule that named a
VM and a CIDR was created (201) and then could not be found or deleted. The
list merges the network listing with one listing per caller VM. Creating:
the destination must be a caller resource (400 vm_invalid_firewall_rule),
because a destination of only an address range or the public Internet would
reach other tenants' machines. Every firewall call resolves the account
scope like the other VM routes.

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

* test(cloud): firewall refuses unknown endpoint fields as owned and sends canonical CIDRs

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

* fix(cloud): unknown firewall endpoint fields are never owned; send canonical CIDRs

Security review P2: the provider adds selectors as new optional fields, and
an unknown one could name another tenant's resource, so a rule with an
unknown endpoint field is not the caller's. Review P3: send the canonical
range so a valid non-canonical CIDR does not fail at the provider.

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

* fix(cloud): title vm_firewall_rule_not_found 'Firewall rule not found'

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

* test(cloud): firewall needs a 100-rule cap, a bounded list, and a per-user rate limit

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

* fix(cloud): cap firewall rules at 100, bound the unfiltered list, rate-limit mutations

These routes are new on a provider account shared by every cmux user.
Create refuses at 100 owned rules (409 vm_firewall_rule_limit). An
unfiltered list reads the network plus at most the 10 newest live VMs, one
provider call each; older VMs list with ?vmId. Create and delete are
throttled per user with the Vercel firewall rule CMUX_VM_FIREWALL_RATE_LIMIT_ID
(no other VM mutation route has a limiter, so this follows the team-invite
limiter: fail closed when the firewall is unavailable, fail open and report
when the rule is unset or removed).

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

---------

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

* remote-tmux: stop a torn-down control stream from feeding the reconnected one (#16897)

* remote-tmux: failing test for a torn-down stream feeding the next one

A control stream torn down for a reconnect keeps delivering what its reader
had already buffered. The test holds the main actor while a first client
writes 560 KB, starts a reconnect, and expects none of those bytes to reach
the connection.

* remote-tmux: stop a torn-down stream from feeding the next one

Cancelling the task that reads a control client's stdout does not empty the
reader's buffer, so chunks the old client had already written were still
ingested after the teardown. Once the reconnect had respawned, a leftover
command result was taken for the new client's attach reply. The connection
then never asked for windows and the mirror stayed blank for good.

Each read loop now stops as soon as its process generation is no longer the
current one, and closes its reader.

---------

Co-authored-by: ejc3 <ejc3@users.noreply.github.com>

* remote-tmux: keep a window whose Dock has panels when its mirrors move out (#17237)

* remote-tmux: failing test for a docked terminal closed when its window's mirrors move

* remote-tmux: keep a window whose Dock has panels when its mirrors move out

---------

Co-authored-by: ejc3 <ejc3@users.noreply.github.com>

* Expose the workspace task-status lane to custom sidebars (#17245)

Custom sidebars could not read a workspace's task-status lane. cmux already
resolves one per workspace and the control socket can pin it, but the
interpreter data context carried no field for it, so a sidebar had no way to
group or colour rows by whether a workspace needs attention.

`workspaces[i].status` now carries the resolved lane as its raw wire value:
todo, working, needs-attention, review or done. The snapshot takes it as a
required parameter so a dropped wiring breaks the build rather than reporting
a silent "todo".


Claude-Session: https://claude.ai/code/session_0113SqtxGQwjHjzFw8mkSgwU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Clear restored agent notifications once the agent is gone (#17067)

* test: prune read notifications of agents that died with the previous app

An agent alive at quit dies without SessionEnd, so its last "Completed in"
notification is restored on every launch and shown as the workspace's
latest sidebar summary even though no agent is running. The stale-agent
sweep must drop it once the pane has no agent again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: drop restored agent notifications once the agent does not return

Notifications persist across relaunch so an unseen agent result is not
lost, but an agent that was alive when cmux quit dies without SessionEnd.
Nothing clears its notification afterwards: agent PIDs are not restored,
so the 30s stale-PID sweep has no dead PID to catch.

Restore now records the notifications of local panes that hosted an agent
(resume binding or restorable agent snapshot). The stale-agent sweep
removes the read ones when the pane has no agent PID again; unread ones
survive until read, and a pane the agent resumes into is handed back to
its hooks. Remote terminals are skipped since their agent can outlive the
app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: keep restored notifications while the resume is in flight

Covers a read notification posted after restore (never tracked) and a
pane whose restored resume has not reported an agent PID yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: defer restored notification prune while the resume is in flight

The 30-second sweep can run before an auto-resumed agent reports its PID.
Skip panes whose restored command is still in flight, using the
coordinator's existing ownsInFlightRestoredCommand contract. Also look up
tracked notifications by id instead of scanning the store per panel, and
mark the value-only snapshot helper nonisolated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: keep read non-agent notifications on a restored agent pane

The restore tracks every notification persisted on a pane that hosted an
agent, so a read `cmux notify` banner on that pane is pruned with the
agent's result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: track only agent-produced notifications on restored agent panes

Use the persisted `isAgentEvent` provenance instead of panel ownership so
a `cmux notify` banner on a pane that hosted an agent is not retired with
the agent's result. Unknown provenance restores as agent-produced, matching
TerminalNotificationStore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Send sidebar links through the external-open rules (#7397)

* browser: apply external-open rules to sidebar links

The sidebar's pull-request and port links (SwiftUI and AppKit rows, and
the open-all-pull-requests action) opened in the embedded browser whenever
that preference was on, without consulting the URL rules that route a site
to the system browser. Sites listed there cannot work in the embedded web
view at all, so a rule now wins over the embedded preference on those
paths, the same way it does for a click inside a page.

* browser: require a user event before a link escapes to the system browser

WebKit reports a script calling click() on an anchor as .linkActivated,
the same as a real click, so the external-open rules on their own let a
page hand itself a system-browser open at a moment of its choosing. The
navigation-typed escape now also requires an AppKit event in flight (a
key, left-mouse, or middle-mouse event) and never intercepts a download,
on every path that consults the rules: the main navigation delegate, the
target=_blank UI delegate, and both popup delegates. The context menu's
Open Link in New Tab is a gesture by construction and says so.

The event check is a bound rather than a proof: NSApp.currentEvent says
an event is being dispatched, not that this navigation is the thing the
user asked for. Middle-clicks arrive as otherMouse events and count.

* browser: e2e coverage for external-open link routing

BrowserExternalOpenRoutingUITests drives real WebKit link activations
through the socket browser.click against a local fixture and asserts
routing at the delegate layer, where popup-vs-link-activation behavior
actually diverges and unit tests cannot reach. Escapes are captured to a
file through the existing DEBUG-only UI-test sink
(CMUX_UI_TEST_CAPTURE_EXTERNAL_OPEN_PATH) from the external-navigation
handler's default opener, so CI never opens Safari. Four cases: a matched
link click escapes while the embedded page stays put; an unmatched click
navigates embedded; a scripted window.open to a matched host never
escapes; a target=_blank form POST to a matched host stays embedded.

The shared BrowserFixtureSocketTestCase gains subclass hooks for launch
arguments and environment, falls back from the in-process socket client
to nc -U and then the bundled cmux CLI, disables hidden-webview discarding
for the backgrounded UI-test host, and polls browser.wait through the
cold-start content-process transient.

* browser: click links for real in the external-open UI tests

A link now leaves for the system browser only while a real input event
is in flight, and the socket browser.click runs JavaScript, so the matched
and unmatched link cases click through accessibility the way a person
does. The scripted popup and form cases keep the socket click, since a
scripted action is what they test.

* browser: keep only the sidebar links, drop the in-page activation change

The in-page half of the external-open rules has landed separately. What remains here is the sidebar: pull-request and port links follow the rules, with tests for the matcher.

* browser: use a rule the pattern safety check accepts in the port-link test

---------

Co-authored-by: ejc3 <ejc3@users.noreply.github.com>
Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* ci: require merge checks only when their workflows exist (#17284)

* test: cover merging repos without aggregate CI workflow

* fix: make merge checks conditional on base workflows

* ci: fall back to ancestor evidence for --main-fix (#17277)

* ci: use nearest ancestor for main-fix evidence

* ci: constrain ancestor evidence to path-filtered changes

* ci: inspect renamed paths in ancestor evidence

* ci: bound ancestor evidence traversal

* ci: reject incomplete ancestor path comparisons

* fix(session): sweep stale scrollback replay files (#16056)

* test(session): cover replay sweep and permissions

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): sweep stale scrollback replay files

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): sweep stale replay files before restore

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): remove synchronous replay sweep from app init

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): gate restore on off-main replay cleanup

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(app): keep replay sweep off startup critical path

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): skip replay sweep under XCTest

* fix(session): harden retained replay files during stale sweep

* docs(session): keep crash-recovery gate semantics accurate

* test(session): cover legacy replay permissions and sweep filters

---------

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>
Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Cover dotted Claude project dir in session directory search (#4939)

* docs: clarify Claude project dir decode asymmetry

* test: cover dotted Claude project dir in session directory scope

* test: scope Claude session roots per task instead of process env

Swift Testing runs suites in parallel, so setting CLAUDE_CONFIG_DIR process-wide could leak into other tests. A DEBUG-only TaskLocal override keeps the fixture root local to the test's task tree.

* test: cover Claude cwd-filter lookup without a DEBUG seam

Widen the Claude candidate enumerator and its two types to internal so the test reaches them through @testable import, per the no-test-debug-seam review rule. The test checks .claude/worktrees and .worktrees cwds and that dot-preserving and other project dirs are excluded.

---------

Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Answer the tmux session commands Claude Code calls (#13632)

* Answer the tmux session commands Claude Code calls

Claude Code's tmux backend tears down and reattaches its agent panes with
kill-session, switch-client, new-session -A and show-options -g prefix. The
compatibility layer rejected all four, so a Teams session failed with
"Unsupported tmux compatibility command" once it got past creating panes.

A tmux session is a cmux workspace, which has-session and new-session already
assume, so kill-session closes that workspace, switch-client selects it, and
new-session -A attaches to it when it exists instead of creating a duplicate.
show-options now answers from a table, and prefix reports the C-b that a
default tmux client would.

The sequence test drives all four through the real shim. Its fake socket also
now unwraps the capability envelope that the shell integration adds inside a
cmux terminal, so the test reports the behavior it checks rather than a JSON
decode error; that unwrapping moved into the shared helper.

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

* Reject a socket payload that is not a request

The fake servers indexed request["method"] straight off json.loads, so a
payload that decoded to null, a list, or an object with a non-string method
raised inside the handler thread and surfaced as an unrelated CLI error. They
now answer those with an error line, which names the real problem.

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

* test: reject empty fake socket request methods

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: reject a failed tmux session lookup and kill-session -a

A workspace.list error must not become a new session, and kill-session -a
must not close the caller.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Keep a failed tmux session lookup from creating a workspace

new-session -A treated every resolution error as a missing session, and
kill-session ignored -a and closed the caller. A missing session still
creates; a lookup failure and an unsupported flag now fail first.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix custom-sidebar nil-comparison so optional-guarded views render (#7943) (#7974)

* Add failing test: sidebar nil-comparison yields nothing (#7943)

In a custom sidebar, `x != nil` / `x == nil` against a bound optional field
does not evaluate to true/false — it evaluates to nothing. Interpolation
renders empty, ternaries always take the else branch, and `if x != nil`
guards are never taken, so optional-guarded views never render.

This commit adds only the regression test (no fix) so CI shows it red.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix sidebar nil-comparison to evaluate to a Bool (#7943)

The interpreter's value model had no null case and no `nil`-literal
evaluator branch, so `nil` evaluated to a host `SwiftValue?` of `nil` — the
same value that means "expression unsupported / no value". `evalInfix` then
bailed on the comparison, so `x != nil` / `x == nil` produced nothing:
interpolation rendered empty, ternaries always took the else branch, and
`if x != nil` guards were never taken. Optional-guarded views (including the
shipped status-board.swift / finder.swift examples) silently drew nothing.

- Add `SwiftValue.null` for the `nil` literal and for comparing an absent
  optional field against `nil` (distinct from host `nil` = "no value").
- Evaluate `NilLiteralExprSyntax` to `.null`.
- Handle `==` / `!=` before the operand guards, coalescing an absent operand
  (host `nil`) and the `nil` literal to `.null`, so the comparison yields a
  Bool that is true/false when present and false/true when absent.

Fixes #7943

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: preserve nil comparison evaluation failures

* fix: preserve nested nil comparison misses

* fix: preserve parenthesized nil comparison misses

* fix: handle nil optional binding and equality budget

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Leo Li <cheerleaderleo@outlook.com>

* fix: refresh merge helper and honor neutral checks (#17294)

* test: cover neutral checks and helper checkout refresh

* test: tolerate absent git diagnostics

* fix: refresh clean main checkout before merging

* test: cover cloud welcome close shortcut ownership

* fix: route cloud welcome close shortcut to its window

---------

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>
Co-authored-by: Austin Wang <austinwang115@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Leo <cheerleaderleo@outlook.com>
Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
Co-authored-by: BlueRaddish <jeeholife2@gmail.com>
Co-authored-by: EJ <ej@campbell.name>
Co-authored-by: ejc3 <ejc3@users.noreply.github.com>
Co-authored-by: Philipp Mochine <philipp@mochine.de>
Co-authored-by: mys <wowpotato@naver.com>
Co-authored-by: Alejandro Florez <soyeladice@gmail.com>
Co-authored-by: Sungho Park <relilau00@gmail.com>
Co-authored-by: Darío Kondratiuk <dariokondratiuk@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mark Xian <mark-xian@foxmail.com>
Co-authored-by: Austin Wang <38676809+austinywang@users.noreply.github.com>
austinywang added a commit that referenced this pull request Oct 5, 2026
…visible (#17127)

* cloud: enable cloud for pro, upgrade for free, keep the button when the panel rebuilds

the cloud tab and settings > cloud now show enable cloud only when the plan
includes cloud and upgrade for free accounts. the plan answer lives on the
account flow (per account) instead of the panel's view state, so switching
sidebar modes no longer drops the button back to "checking your cmux plan…".

* cloud: stop waiting on a slow plan check, title enable cloud machines, debug plan override

* cloud: cloud.fill for the pro-required screen, settings shows only the enable row until cloud is on, enable cloud machines title

* cloud: drop the debug plan override

* cloud: keep prominent cloud buttons visible in an inactive window, keep a known plan through failed or cancelled checks, recheck settings on account change

* cloud gate: newest plan check wins, drop answers for a switched account

* fix cloud billing plan concurrency and state ownership

* fix cloud plan localization parity

* fix cloud entitlement detection for team plans

* avoid stale cloud entitlements across team changes

* report billing refresh success to settings

* preserve account flow billing refresh contract

* cloud onboarding: introducing cmux cloud welcome, and cloud enablement screens (#17230)

* cloud welcome: introducing cmux cloud window, shown once, five layouts to compare from help (wip)

* cloud onboarding: glass welcome window, lowercase and machine focus layouts, sidebar intro with banner and reasons, enablement view takes plain values (wip)

* cloud onboarding: one welcome design (machine focus, all lowercase), sidebar intro titles by plan, 6pt buttons, drop the comparison layouts

* cloud onboarding: review fixes (glass behind compiler guard, welcome considered once per launch, size after hosting, no return shortcut, comments, orphaned string)

* cloud welcome: ignore the titlebar safe area (fixes a layout-loop crash on open), three reasons

* cloud tab intro: title first, no icon

* cloud tab intro: app icon banner back, lock badge while the plan needs pro

* cloud tab intro: dark app icon in the banner

* fix(cloud): wait for display helper readiness (#17132)

* fix(cloud): explain unavailable display actions

Show the existing Cloud failure message when New Display is unavailable, and explain the machine ownership restriction when a display is opened into another Cloud workspace.

— unregistered

* fix(cloud): explain unavailable display actions

Show the existing Cloud failure message when New Display is unavailable, and explain the machine ownership restriction when a display is opened into another Cloud workspace.

— unregistered

* fix(cloud): reject cross-machine display opens before projection

* fix(cloud): probe legacy desktop VMs for displays

* fix(cloud): initialize wallpapers for new displays

* fix(cloud): fail closed before display double-click opens

* fix(cloud): replay repeated display ownership hints

* test(cloud): cover display helper refresh

* fix(cloud): refresh installed display helper

* test(cloud): require display service readiness probe

* fix(cloud): wait for display helper readiness

* fix(cloud): keep display creation retryable

* fix(cloud): authorize dynamic display ports

* fix(cloud): let new display retry guest discovery

* Revert "fix(cloud): let new display retry guest discovery"

This reverts commit 0fe008ed3f6fd78338be9e9a2f8161d37984cc0f.

* test(cloud): sidebar visibility filter must keep display creation state

Moves applyingDeviceVisibility next to SurfaceCatalogSnapshot in
CmuxSurfaceCatalogModel (no behavior change) so it is testable, and adds
a failing regression test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): keep display creation state through the sidebar filter

applyingDeviceVisibility rebuilt SurfaceCatalogSnapshot from scratch and
dropped displayCreationMachines, staleMachineIDs and display memberships,
so every desktop VM's New Display row reported additional displays as
unavailable. Filter a copy instead so all per-machine state survives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(surfaces): a split with no room opens as a tab in the target pane

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): optimistic display row; fall back to a tab when a split has no room

New Display now shows a "Starting display…" row from the click until the
guest answers, driven by the catalog's in-flight creation set.

Opening a sidebar resource splits the focused pane; once split admission
had no room left, the open failed with "Could not create the pane:
noSpace". SurfacePaneFactory now opens it as a tab in the pane that would
have been split.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(surfaces): refused split is typed; sidebar gestures open a tab

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(surfaces): scope the no-room tab fallback to sidebar gestures

The factory now throws a typed noSpace instead of opening a tab, so the
layout replay and socket open verbs keep a truthful refused split. Sidebar
opens and New Display use openPreferringSplit, which retries as a tab in
the pane that would have been split. The empty Displays row no longer
shows beside the optimistic Starting display row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): display creation needs no discovery round trip

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(cloud): open the display pane at the click and skip pre-create discovery

New Display paid two VM exec round trips (list, then create, about 2s
each) before any pane appeared. The create reply is already the full guest
catalog, so the list is dropped. The pane now opens at the click with a
native Starting display state and is adopted by the projection once the
guest assigns the display; creation failure closes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): harden the reserved display pane

Drop a second click before it opens a pane; bind the reservation to the
created display id so no other projection adopts it; keep creating when
the pane cannot open; leave the display in the pool when the person closed
the pane; show the failure on a pane the socket refuses to close.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): additional displays serve noVNC beside the primary desktop

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): make additional displays reachable on the VM private address

Displays 2+ ran websockify on 127.0.0.1 while display 1 listens on [::].
The client reaches every display through the private address, so each new
display's first noVNC connection was refused and only recovered after a
timeout. Bind like display 1 (Xvnc stays -localhost), replace proxies an
older helper left on loopback, and drop the open-port call added for
display ports: it changed no routing and cost a control-plane request,
plus a desktop heal exec for display 1, on every open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): stale proxy match covers only this display's websockify

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): match stale display proxies by argv, not pgrep regex

pgrep's ERE has no (?:...) groups, so the stale websockify match failed,
the old loopback listener kept the port, and the rebound proxy exited.
Read /proc argv for this user's websockify on this display's ports.
Verified on a live VM: displays 2 and 3 now accept the noVNC websocket on
the private address, one proxy each, X sessions untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): a failed first desktop connection retries before failing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): retry a display's first connection; warm the carrier at create

noVNC stops at Connect after an initial connect failure; its reconnect only
follows a session that once connected. Restored and new displays could lose
that race while the proxy or guest listener started, leaving "Failed to
connect to server". Retry the route's first connection three times
(0.5s/1s/2s on the injected clock) before showing the failure card.

The first display open on a machine spent 12-18s starting its browser
carrier after the guest exec. Start it alongside the create request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): an explicit Retry gets its own quiet first-connection retries

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): run the guest display script test on the main actor

CloudGuestDisplayScript is @MainActor; the test called it from a
nonisolated context and broke the CmuxCloud package test build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): per-runner build roots only on fleet Macs without glaeda (#17239)

* test(ci): Blacksmith keeps the shared root; reruns keep a per-runner root (red)

Blacksmith macOS runners report RUNNER_ENVIRONMENT=self-hosted, so they got
per-runner roots: every build started cold and no seed could be adopted
(job 111334766861). take-product-canonical-root.sh also refused a product
built at a per-runner root on a fleet Mac without glaeda.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): per-runner roots only on fleet Macs without glaeda

canonical-build-root.sh derived /private/tmp/cmux-ci-<runner> for every
self-hosted runner without the glaeda helper, which included Blacksmith's
ephemeral macOS runners: their builds started cold and their seeds were
never adopted. Derive it only when the fleet directory exists. Let
take-product-canonical-root.sh accept such a root when glaeda isn't there
to hold it, so app-host test reruns build where the product was compiled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): repair main's guards, localization parity and cmuxTests compile (#17207)

* fix(ci): align owned-build-state guards with per-runner canonical roots

#17168 (496195b) derives a canonical root per self-hosted runner, exports
CMUX_CI_CANONICAL_ROOT from the build-slot step and drops the shared
/private/tmp/cmux-ci fallback in test-e2e's owned-state step. Three tests in
tests/test_ci_owned_build_state.py still assumed the old contract, turning
"CI fast guards" red on main.

Update them to the new contract: the slot step exports the root, a
per-runner root reads its own store, and a missing root reads nothing. The
owned-state step now fails with a clear message when the root is missing, and
rejects a root nested under a cmux-ci-* prefix (e.g. cmux-ci-a/../x), so the
looser glob cannot map the store outside the Mac's package directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(l10n): translate the New Machine plan loading strings

#17135 added machines.new.plan.loading, .retry and .error with the English
text copied into every locale, so `localization_catalog.py check` reports 24
parity errors and "Fast static checks" fails on every PR. Translate them for
all catalog locales; Retry reuses common.retry's wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(l10n): match the catalog's plan terminology in de, ko and zh-Hant

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tests): repair cmuxTests compile after the sidebar reorder change

#17070 moved RightSidebarModeBarDragLayout into the CmuxSidebar package,
but RightSidebarTabCustomizationTests still relied on the app module
exporting it, and CloudMachineOrderingTests put `try` calls inside an `&&`
in #expect, which the macro rejects ("operator can throw but expression is
not marked with 'try'"). Import CmuxSidebar and hoist the throwing lookups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tests): hoist the second throwing lookup out of #expect in CloudMachineOrderingTests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): satisfy dogfood-build runner guards and the Swift warning budget

#17206 turned dogfood-build into a macOS build, which tripped three guards:
no pinned Xcode, no fork branch in runs-on, and no static-preflight
dependency. Add `scripts/select-ci-xcode.sh`, the standard owner and fork-PR
branches ahead of the existing selector, and `needs: static-preflight`. The
job's `if` already excludes forks, so manaflow-ai PRs keep the same runner.

#17070's `RightSidebarModeBarDragController.coordinateSpace` is read from
a Sendable geometry closure and broke the zero-warning budget; it is a plain
String constant, so mark it `nonisolated`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ci): a root the glaeda hook exported wins over the per-runner root

Fails on main: canonical-build-root.sh treats an exported /private/tmp/cmux-ci
as unset and derives /private/tmp/cmux-ci-<runner>, so main-compile-probe
refuses the root glaeda placed it in (runs 37157423969, 37155786981).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): honor the canonical root the glaeda hook exported

#17168 (496195b) derives /private/tmp/cmux-ci-<runner> on every self-hosted
runner unless CMUX_CI_CANONICAL_ROOT names a non-default root. glaeda's
runner hook holds root 1 (/private/tmp/cmux-ci) for a compile job and
exports exactly that, so the job then built somewhere glaeda does not hold,
and main-compile-probe refused it: "glaeda placed this job in
/private/tmp/cmux-ci, not /private/tmp/cmux-ci-cmux13s-mac-mini-glaeda".

Let any exported root win. A self-hosted job with no exported root still
gets its per-runner root, so #17168's isolation for unplaced runners stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ci): an unplaced self-hosted job keeps the shared seeded root

Fails on the branch: canonical-build-root.sh derives /private/tmp/cmux-ci-<runner>,
whose fingerprint never matches main's seeds, so new aws runners
(aws-m4pro-9-glaeda-3, aws-m4pro-8-glaeda-4) compiled cold and timed out at
35 minutes in compile admission (run 37158561950, both attempts).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): build unplaced self-hosted jobs at the shared seeded root

#17168 gave every self-hosted job without an exported root its own
/private/tmp/cmux-ci-<runner>. That root is part of the cache fingerprint,
so main's DerivedData seeds and the owned build state never match it, and the
prepare step clears it each job: every new aws runner compiled cold and hit
the 35-minute admission limit (aws-m4pro-9-glaeda-3, aws-m4pro-8-glaeda-4 on
run 37158561950).

Go back to /private/tmp/cmux-ci when no root is exported. glaeda's hook still
exports root N for the jobs it places, so those stay isolated. Unplaced
concurrent runners on one Mac share the root again, as before #17168, until
glaeda places those jobs too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ci): the pool picker never picks an owned label it was not configured with

Fails on main: simple_pool_picker adds every glaeda-<class>-xcode-* label it
sees on a runner, and glaeda-std-xcode-26.3 (ten aws EC2 runners, five per
Mac) sorts before the minis' 26.6, so compile admission ran there and timed
out at 35 minutes while the minis were idle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): pick only configured owned pools, not any owned-looking label

simple_pool_picker added every glaeda-<class>-xcode-* label it saw on an
online runner to the configured CI_OWNED_POOL_SLOTS pools, then tried them
in string order. The aws EC2 Macs carry glaeda-std-xcode-26.3 (ten runners,
five per Mac), which sorts before the minis' glaeda-std-xcode-26.6, so PR
compile admission landed on contended EC2 runners and timed out at 35
minutes while the minis sat idle. Use only the configured pools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ci): the pool picker reads every page of organization runners

Fails on main: LiveState.runners reads one page of 100, but manaflow-ai has
509 runners and the first page holds the aws Macs and no idle mini, so the
picker never saw the mini fleet (run 37166910798 fell back to Blacksmith with
29 idle minis online).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): read every page of runners in the pool picker

LiveState.runners read only the first page of 100 organization runners.
manaflow-ai has about 500, and the first page holds the aws Macs but no idle
mini, so the picker never saw the mini fleet: it took the aws 26.3 label
while that was discoverable, and fell back to Blacksmith once it was not.
Read up to ten pages. Against live data the picker now sees 38 online
glaeda-std-xcode-26.6 minis (33 free) and picks them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: clear the Cloud sidebar warnings that broke the Swift warning budget

Compile admission on a mini (run 37167089830) built cleanly but failed the
zero-warning budget on three warnings from the Cloud sidebar work:
`SurfaceCatalog.shared` used as a default argument of two @MainActor
CloudWorkspaceSidebarPresentation entry points (evaluated nonisolated), and an
implicit `self` in CloudTreeOutlineView's machine-lift reopen closure.
Resolve `.shared` inside the main-actor body and make the capture explicit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): preserve canonical root and translation wording

* test(ci): align canonical root guard with main behavior

* test: use isolated catalogs in Cloud sidebar fixtures

Pass each test fixture catalog through the sidebar snapshot factory and direct presentation assertion so Cloud machine metadata is read from the catalog the test populated.

Co-authored-by: Austin Wang <austinwang115@gmail.com>

* test: model loading Cloud sidebar state

Keep device projections visible while their catalog rows are restoring, and assert that a reserved loading card shows machine identity before its directory appears after adoption.

Co-authored-by: Austin Wang <austinwang115@gmail.com>

* test: use a terminal panel for cloud directory settings

The sidebar detail test supplies a reported directory, so give it a real terminal panel instead of a loading card that correctly suppresses directory metadata.

Co-authored-by: Austin Wang <austinwang115@gmail.com>

* fix(cloud): restore device directory fallback lost in main merge

The merge of main took #17107's presentation file and dropped the
fallback from 27a8d804fdf, so a device projection whose catalog row is
still restoring rendered no directory and
SidebarCloudWorkspaceBadgeTests.deviceNameIsVisibleBesideItsDirectory
failed on b6ea6632cb1 (the regression run is that CI failure).

Co-authored-by: Austin Wang <austinwang115@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Austin Wang <austinwang115@gmail.com>

* ci: require a written merge override for red CI (#17217)

* ci: require merge-gate before merges

* Add API-only merge gate for pull requests

* test: cover merge-gate override decisions

* ci: wire merge-gate tests and push timing

* ci: pin merge gate workflow source

* Reject read-only override authors

* ci: harden merge gate runner and author checks

* ci: keep merge gate on hosted capacity

* ci: fail closed when collaborator lookup fails

* ci: reject stale successes during reruns

* ci: fail closed for untimestamped queued checks

* ci: fail closed for untimestamped pending statuses

* ci: verify matching override failure evidence

* test: cover mismatched override failure evidence

* ci: choose newest merge gate check run

* ci: require compile evidence for main-fix merges (#16993)

* test: reproduce main-fix merging without compile evidence

The installed helper bypasses all checks under --main-fix. The regression records that it merges with no compile checks present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: require build evidence for main fixes

gh-merge-green --main-fix now requires successful Release, Debug and Swift test target build steps on the exact PR head. Existing Swift test failures are waivable only when their parsed issue records match the same Swift test step on the exact base SHA; the audit comment records each match and unrelated failures remain fatal.

CPU, memory and disk checks: the validator uses one bounded 90-second GitHub request per call, caps captured output at 32 MiB, writes audit bodies to temporary files, and does not retain logs, caches or stores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* ci: harden merge gate freshness and evidence

* ci: require per-check override reasons

* ci: complete merge gate review fixes

* docs(ci): document merge-gate rollout order

* ci: make merge helper rollout repairable

* ci: point merge helper refusals to repair runbook

* ci: resolve merge helper symlink for main fixes

* docs: remove stale main-fix PR reference

* fix: avoid pipefail false negatives in merge helper

* fix: keep merge gate events fresh per pull request

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Share one vCPU and memory pool across a plan's Cloud VMs (#17238)

* test(vms): reproduce missing shared Cloud VM resource pool

* feat(vms): share one vCPU and memory pool across a plan's Cloud VMs

Pro, Team (per paid seat), and Founder's Edition get up to 5 active VMs
sharing 20 vCPUs and 40 GB RAM, with machines up to the 32 GB xl row.
Max gets 80 vCPUs and 160 GB RAM with machines up to the 64 GB 2xl row.
The repository enforces the pool next to the active-VM count, under the
same transaction and billing lock, on create, Base open/reset, paused
resume, CPU/memory resize, and fork. Pricing, docs, app, and iOS copy
now describe pooled resources.

* Pooled VMs: Pro overrides stop at 32 GB; drop unused iOS pool strings

* test(vms): reproduce leaked compute claim and stale-plan resume pool

* fix(vms): give back unused compute claims and resume against the caller's pool

* docs(pricing): describe the Team pool per paid seat and localize pool strings in every locale

* Cloud VM: snapshot create honors Idempotency-Key (#17244)

* test(cloud-vm): snapshot create dedups by idempotency key (red)

A retry of POST /api/vm/:id/snapshot with the same Idempotency-Key must return
the first snapshot and take no second one; a retry while the first runs is
refused as in progress; the same key with another name is a conflict; a failed
attempt frees the key.

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

* fix(cloud-vm): snapshot create dedups by Idempotency-Key

POST /api/vm/:id/snapshot now reads Idempotency-Key. A new ledger table,
cloud_vm_snapshot_requests (one row per machine and key, additive migration
20261004120000), records a pending attempt before the provider call and the
provider snapshot after it. A retry with the same key returns the first
snapshot and records no second usage event; a retry while the first runs gets
409 vm_snapshot_in_progress (retryable); the same key with another name gets
409 vm_snapshot_idempotency_conflict; a failed attempt frees the key; a pending
row older than 15 minutes (route budget 600 s) is taken over by a retry.
Requests without a key behave as before.

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

* chore(cloud-vm): smoke --snapshot-check proves snapshot idempotency

With --create, takes one snapshot of the throwaway smoke machine twice with the
same Idempotency-Key, requires the same snapshotId, deletes that snapshot,
then destroys the machine as before. No fixed VM id and no printed token: the
smoke mints its own throwaway user session.

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

---------

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

* Preserve selected tab when closing another surface (#16645)

* Preserve selected tab when closing another surface

* Only select the closing pane's tab when that pane is focused

BonsplitController.selectTab also focuses the pane, so calling it for an
unfocused pane moved focus into the pane where a tab closed, the focus theft
this change is meant to stop. Bonsplit already keeps the selection when an
unselected tab closes; the shouldCloseTab change is the actual fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* docs: detail tmux help options (#16780)

Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* test: guard managed contributor difficulty labels (#16448)

* test: guard managed contributor difficulty labels

* test: reject invalid difficulty descriptions

* test: exercise difficulty label sync requests

---------

Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): repair main's Cloud app-host failures from #17132 and #17103 (#17250)

* test(cloud): expect the display helper readiness loop

#17132 replaced the one-line `list || exit 1` probe with a bounded
retry loop and updated the package test, but cmuxTests still asserted
the old line, so CloudDisplayCatalogTests.guestCommandShape failed on
main (run 37186487936, shard 4/7).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): make the restored-display retry test a desktop

#17132's initialDesktopFailureRetries configured port 6902 without a
resource ID, so CloudBrowserAccessState.isDesktop was false and
desktopConnectionDidChange ignored the failure: no quiet retry, and the
following navigations[1] trapped and crashed the shard 5 app host on
main (run 37186487936). Give it the .display identity additional
displays carry and require the retry before indexing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): a foreign display click shows one synchronous hint

#17103 made a display click on another machine's workspace reject
synchronously through showDisplayOpenHint, so no tree operation starts
and waitForOpen() waited out the 60 s suite limit on main (run
37186487936, shards 2 and 3). Assert no operation ran and that the
rejection is shown once; this stays red until the double-click stops
repeating the hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): show a foreign display's ownership hint once per double-click

AppKit delivers a double-click's first click to handleSingleClick,
which already shows the ownership hint, and #17103 also showed it from
handleDoubleClick, so the user got the same rejection twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): never show upstream tree failures in the Machines status row

#17103 started presenting treeError verbatim so its ownership hints
would survive, but treeError also carries raw upstream failures
(LocalizedError descriptions, create output), so a URL with query
parameters reached the toolbar text, hover help and copy menu.
MachinesCloudStatusTests.emptyStatusHasNoProgressPresentation caught it
on main (run 37186487936, shard 7).

Hints now travel through their own onHint sink (falling back to
onFailure for other callers), the panel records them as trusted copy,
and the status row shows the tree error verbatim only when it is that
hint; everything else gets the safe recovery message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): keep merge-gate diagnostics on the exact PR (#17248)

* fix: keep merge-gate diagnostics on the exact PR

* fix: print repair guidance for gate diagnostics

* fix: allow merge-gate PR comments when permitted

* fix: reject mismatched merge-gate event identities

* fix: keep merge-gate alive on comment errors

* fix: keep the cmux-cua credential out of the Codex argv (#17252)

* test: codex wrapper must not put the cmux-cua credential in argv

The Codex wrapper passes the cmux-cua socket credential as
-c mcp_servers.cmux-cua.env.CMUX_CUA_SOCKET_AUTH_TOKEN=<value>, so any
local user can read it with ps. These assertions require the value to be
absent from argv, Codex to receive it in its environment, and the MCP
server to receive it through env_vars. The fake Codex now builds the MCP
server environment the way Codex does (allow-list + env_vars + env).

* fix: keep the cmux-cua credential out of the Codex argv

The wrapper passed the socket credential as a Codex -c env override, which
put the value in the codex process argv where any local user can read it
with ps. The wrapper now exports CMUX_CUA_SOCKET_AUTH_TOKEN in the parent
shell before exec and emits mcp_servers.cmux-cua.env_vars so Codex forwards
the variable by name to the MCP server (Codex env_vars, openai/codex#5246).
Codex's default shell_environment_policy excludes *TOKEN* names from agent
shell commands. Attachment stays fail-closed when no credential resolves.

* fix: keep codex wrapper overrides when the subcommand gets -c (#17257)

* test: codex wrapper overrides must survive subcommand -c flags

Codex declares -c/--config, --enable, and --disable as clap global
arguments. When a subcommand such as exec or resume also receives one,
Codex 0.159.3 keeps only the subcommand-level values, so the wrapper's
cmux-cua MCP config, hook config, and --disable computer_use placed
before the subcommand are dropped. The fake Codex now applies that rule,
and new cases cover exec, resume, mixed root and subcommand -c, and a
literal prompt after --.

* fix: keep codex wrapper overrides when the subcommand gets -c

Codex declares -c/--config, --enable, and --disable as clap global
arguments and keeps only the subcommand-level values when a subcommand
also receives one. The wrapper put its cmux-cua MCP config, hook config,
and --disable computer_use before the user's argv, so codex exec -c ...
or codex resume ... -c ... dropped all of them. The wrapper now moves the
user's subcommand-level global arguments, in order, in front of the
subcommand. Codex reads one root-level list with the same precedence as
before. Tokens after -- stay in place, and interactive launches without a
subcommand are unchanged.

* Add cmux browser repl: a Playwright-shaped browser REPL for agents (#17256)

`cmux browser repl` is a persistent JavaScript REPL that agents use to drive
cmux browser panes: Playwright page, locator, keyboard and mouse semantics with
native trusted input, budgeted accessibility snapshots with diffs, tabs,
cookie-bearing fetch, a sandboxed fs, named sessions, an MCP server mode and
site tools. Guards live outside agent code: fill-only secrets with redaction
and capture masking, a domain policy over every frame and fetch hop, a per-tab
clipboard for session-created tabs, private per-session temp directories and
bounded cells, fetches and timers. Agent work never moves the user's focus,
hibernated tabs wake on use, and crashed tabs report how to recover.

Squashed from https://github.com/manaflow-ai/cmux/pull/15570 (392 commits; the
CLA action cannot read more than 250 commits of one pull request). Same tree as
that branch's head.

* Cloud: VM file operation routes (port of #16936) (#17254)

* Cloud: VM file operation routes (port of #16936), missing path answers 404

Ports the web part of https://github.com/manaflow-ai/cmux/pull/16936
(feat-cmux-next) to main: /api/vm/[id]/fs/[operation] (list, read, stat,
write, mkdir, remove) with the Freestyle driver and gateway methods.

Includes the fix from feat-cmux-next: Freestyle removes a missing path with
success, so removeVmFile stats first and answers a missing file with
404 vm_file_not_found (any other stat failure, a missing VM included, stays a
provider failure).

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

* test(cloud): stat/read/dir of a missing VM path must answer vm_file_not_found

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

* fix(cloud): stat/read/dir of a missing VM path answer 404 vm_file_not_found

The staging rehearsal of #17254 showed stat of a removed file answering 502
vm_cloud_service_unavailable. Map the guest ENOENT on every file read, as
remove already did, and title the error 'File not found'.

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

---------

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

* Stop calling the legacy Subrouter during account deletion (#17273)

* test: account deletion must not call the retired legacy Subrouter

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

* Stop calling the legacy Subrouter during account deletion

The legacy Subrouter at subrouter.cmux.dev is being retired. Account
deletion now skips the legacy revoke phase and needs no legacy env vars.
Local mapping rows are still deleted, and a legacy_delete_pending
tombstone from an older deployment resumes at the hosted checkpoint.

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

* Split the account DELETE handler under the complexity limit

DELETE delegates to deleteAccount and named phase helpers that share one
progress record, so its complexity drops from 46 to under 20 and its
grandfathered baseline entry is removed. Behavior is unchanged.

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

---------

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

* ci: remove merge gate and restore exact-head merging (#17275)

* Cloud: private network routes (port of #16948) (#17255)

* Cloud: private network routes (port of #16948), missing firewall rule answers 404

Ports the web part of https://github.com/manaflow-ai/cmux/pull/16948
(feat-cmux-next) to main: /api/vm/firewall (list, get, create, delete),
/api/vm/network and /api/vm/tunnel/network/[operation], with the Freestyle
driver, gateway and private-network workflow pieces.

Includes the fix from feat-cmux-next: a firewall rule that is not in the
caller's network answers 404 vm_firewall_rule_not_found (a provider 404 race
on delete too); a missing VM endpoint stays vm_not_found.

Stacked on the file-routes port (#17254).

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

* refactor(cloud): move firewall endpoint parsing into services/vms/firewallEndpoint

No behavior change; makes the parser testable without the route.

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

* test(cloud): firewall must accept normal CIDR prefixes and team-owned VM endpoints

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

* fix(cloud): firewall accepts normal CIDR prefixes and team-owned VM endpoints

The staging rehearsal of #17255 found two defects. validCidr compared the
prefix with net.isIP(), which returns the family (4 or 6), so any IPv4
prefix above /4 was refused; it now uses canonicalCidr. The vmId ownership
check looked the VM up in the personal scope, but every new VM is
team-owned, so vmId endpoints were vm_not_found; the route now resolves the
account scope when a vmId is named, and the VM must be the caller's own
(the firewall edits the caller's network). The provider now gets only the
rule fields: the Freestyle driver spreads its input into the request body,
so userId and provider were sent to Freestyle.

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

* test(cloud): firewall rules must name a caller resource as destination; get/delete must find VM rules

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

* fix(cloud): decide firewall rule ownership on the shared provider account

The Freestyle account is shared by every cmux user, so the API decides
whose a rule is. Reading and deleting: the rule names at least one resource
and every resource it names is the caller's. get and delete now read the
rule by id; they searched only the network listing, so a rule that named a
VM and a CIDR was created (201) and then could not be found or deleted. The
list merges the network listing with one listing per caller VM. Creating:
the destination must be a caller resource (400 vm_invalid_firewall_rule),
because a destination of only an address range or the public Internet would
reach other tenants' machines. Every firewall call resolves the account
scope like the other VM routes.

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

* test(cloud): firewall refuses unknown endpoint fields as owned and sends canonical CIDRs

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

* fix(cloud): unknown firewall endpoint fields are never owned; send canonical CIDRs

Security review P2: the provider adds selectors as new optional fields, and
an unknown one could name another tenant's resource, so a rule with an
unknown endpoint field is not the caller's. Review P3: send the canonical
range so a valid non-canonical CIDR does not fail at the provider.

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

* fix(cloud): title vm_firewall_rule_not_found 'Firewall rule not found'

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

* test(cloud): firewall needs a 100-rule cap, a bounded list, and a per-user rate limit

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

* fix(cloud): cap firewall rules at 100, bound the unfiltered list, rate-limit mutations

These routes are new on a provider account shared by every cmux user.
Create refuses at 100 owned rules (409 vm_firewall_rule_limit). An
unfiltered list reads the network plus at most the 10 newest live VMs, one
provider call each; older VMs list with ?vmId. Create and delete are
throttled per user with the Vercel firewall rule CMUX_VM_FIREWALL_RATE_LIMIT_ID
(no other VM mutation route has a limiter, so this follows the team-invite
limiter: fail closed when the firewall is unavailable, fail open and report
when the rule is unset or removed).

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

---------

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

* remote-tmux: stop a torn-down control stream from feeding the reconnected one (#16897)

* remote-tmux: failing test for a torn-down stream feeding the next one

A control stream torn down for a reconnect keeps delivering what its reader
had already buffered. The test holds the main actor while a first client
writes 560 KB, starts a reconnect, and expects none of those bytes to reach
the connection.

* remote-tmux: stop a torn-down stream from feeding the next one

Cancelling the task that reads a control client's stdout does not empty the
reader's buffer, so chunks the old client had already written were still
ingested after the teardown. Once the reconnect had respawned, a leftover
command result was taken for the new client's attach reply. The connection
then never asked for windows and the mirror stayed blank for good.

Each read loop now stops as soon as its process generation is no longer the
current one, and closes its reader.

---------

Co-authored-by: ejc3 <ejc3@users.noreply.github.com>

* remote-tmux: keep a window whose Dock has panels when its mirrors move out (#17237)

* remote-tmux: failing test for a docked terminal closed when its window's mirrors move

* remote-tmux: keep a window whose Dock has panels when its mirrors move out

---------

Co-authored-by: ejc3 <ejc3@users.noreply.github.com>

* Expose the workspace task-status lane to custom sidebars (#17245)

Custom sidebars could not read a workspace's task-status lane. cmux already
resolves one per workspace and the control socket can pin it, but the
interpreter data context carried no field for it, so a sidebar had no way to
group or colour rows by whether a workspace needs attention.

`workspaces[i].status` now carries the resolved lane as its raw wire value:
todo, working, needs-attention, review or done. The snapshot takes it as a
required parameter so a dropped wiring breaks the build rather than reporting
a silent "todo".


Claude-Session: https://claude.ai/code/session_0113SqtxGQwjHjzFw8mkSgwU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Clear restored agent notifications once the agent is gone (#17067)

* test: prune read notifications of agents that died with the previous app

An agent alive at quit dies without SessionEnd, so its last "Completed in"
notification is restored on every launch and shown as the workspace's
latest sidebar summary even though no agent is running. The stale-agent
sweep must drop it once the pane has no agent again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: drop restored agent notifications once the agent does not return

Notifications persist across relaunch so an unseen agent result is not
lost, but an agent that was alive when cmux quit dies without SessionEnd.
Nothing clears its notification afterwards: agent PIDs are not restored,
so the 30s stale-PID sweep has no dead PID to catch.

Restore now records the notifications of local panes that hosted an agent
(resume binding or restorable agent snapshot). The stale-agent sweep
removes the read ones when the pane has no agent PID again; unread ones
survive until read, and a pane the agent resumes into is handed back to
its hooks. Remote terminals are skipped since their agent can outlive the
app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: keep restored notifications while the resume is in flight

Covers a read notification posted after restore (never tracked) and a
pane whose restored resume has not reported an agent PID yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: defer restored notification prune while the resume is in flight

The 30-second sweep can run before an auto-resumed agent reports its PID.
Skip panes whose restored command is still in flight, using the
coordinator's existing ownsInFlightRestoredCommand contract. Also look up
tracked notifications by id instead of scanning the store per panel, and
mark the value-only snapshot helper nonisolated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: keep read non-agent notifications on a restored agent pane

The restore tracks every notification persisted on a pane that hosted an
agent, so a read `cmux notify` banner on that pane is pruned with the
agent's result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: track only agent-produced notifications on restored agent panes

Use the persisted `isAgentEvent` provenance instead of panel ownership so
a `cmux notify` banner on a pane that hosted an agent is not retired with
the agent's result. Unknown provenance restores as agent-produced, matching
TerminalNotificationStore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Send sidebar links through the external-open rules (#7397)

* browser: apply external-open rules to sidebar links

The sidebar's pull-request and port links (SwiftUI and AppKit rows, and
the open-all-pull-requests action) opened in the embedded browser whenever
that preference was on, without consulting the URL rules that route a site
to the system browser. Sites listed there cannot work in the embedded web
view at all, so a rule now wins over the embedded preference on those
paths, the same way it does for a click inside a page.

* browser: require a user event before a link escapes to the system browser

WebKit reports a script calling click() on an anchor as .linkActivated,
the same as a real click, so the external-open rules on their own let a
page hand itself a system-browser open at a moment of its choosing. The
navigation-typed escape now also requires an AppKit event in flight (a
key, left-mouse, or middle-mouse event) and never intercepts a download,
on every path that consults the rules: the main navigation delegate, the
target=_blank UI delegate, and both popup delegates. The context menu's
Open Link in New Tab is a gesture by construction and says so.

The event check is a bound rather than a proof: NSApp.currentEvent says
an event is being dispatched, not that this navigation is the thing the
user asked for. Middle-clicks arrive as otherMouse events and count.

* browser: e2e coverage for external-open link routing

BrowserExternalOpenRoutingUITests drives real WebKit link activations
through the socket browser.click against a local fixture and asserts
routing at the delegate layer, where popup-vs-link-activation behavior
actually diverges and unit tests cannot reach. Escapes are captured to a
file through the existing DEBUG-only UI-test sink
(CMUX_UI_TEST_CAPTURE_EXTERNAL_OPEN_PATH) from the external-navigation
handler's default opener, so CI never opens Safari. Four cases: a matched
link click escapes while the embedded page stays put; an unmatched click
navigates embedded; a scripted window.open to a matched host never
escapes; a target=_blank form POST to a matched host stays embedded.

The shared BrowserFixtureSocketTestCase gains subclass hooks for launch
arguments and environment, falls back from the in-process socket client
to nc -U and then the bundled cmux CLI, disables hidden-webview discarding
for the backgrounded UI-test host, and polls browser.wait through the
cold-start content-process transient.

* browser: click links for real in the external-open UI tests

A link now leaves for the system browser only while a real input event
is in flight, and the socket browser.click runs JavaScript, so the matched
and unmatched link cases click through accessibility the way a person
does. The scripted popup and form cases keep the socket click, since a
scripted action is what they test.

* browser: keep only the sidebar links, drop the in-page activation change

The in-page half of the external-open rules has landed separately. What remains here is the sidebar: pull-request and port links follow the rules, with tests for the matcher.

* browser: use a rule the pattern safety check accepts in the port-link test

---------

Co-authored-by: ejc3 <ejc3@users.noreply.github.com>
Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* ci: require merge checks only when their workflows exist (#17284)

* test: cover merging repos without aggregate CI workflow

* fix: make merge checks conditional on base workflows

* ci: fall back to ancestor evidence for --main-fix (#17277)

* ci: use nearest ancestor for main-fix evidence

* ci: constrain ancestor evidence to path-filtered changes

* ci: inspect renamed paths in ancestor evidence

* ci: bound ancestor evidence traversal

* ci: reject incomplete ancestor path comparisons

* fix(session): sweep stale scrollback replay files (#16056)

* test(session): cover replay sweep and permissions

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): sweep stale scrollback replay files

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): sweep stale replay files before restore

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): remove synchronous replay sweep from app init

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): gate restore on off-main replay cleanup

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(app): keep replay sweep off startup critical path

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): skip replay sweep under XCTest

* fix(session): harden retained replay files during stale sweep

* docs(session): keep crash-recovery gate semantics accurate

* test(session): cover legacy replay permissions and sweep filters

---------

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>
Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Cover dotted Claude project dir in session directory search (#4939)

* docs: clarify Claude project dir decode asymmetry

* test: cover dotted Claude project dir in session directory scope

* test: scope Claude session roots per task instead of process env

Swift Testing runs suites in parallel, so setting CLAUDE_CONFIG_DIR process-wide could leak into other tests. A DEBUG-only TaskLocal override keeps the fixture root local to the test's task tree.

* test: cover Claude cwd-filter lookup without a DEBUG seam

Widen the Claude candidate enumerator and its two types to internal so the test reaches them through @testable import, per the no-test-debug-seam review rule. The test checks .claude/worktrees and .worktrees cwds and that dot-preserving and other project dirs are excluded.

---------

Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Answer the tmux session commands Claude Code calls (#13632)

* Answer the tmux session commands Claude Code calls

Claude Code's tmux backend tears down and reattaches its agent panes with
kill-session, switch-client, new-session -A and show-options -g prefix. The
compatibility layer rejected all four, so a Teams session failed with
"Unsupported tmux compatibility command" once it got past creating panes.

A tmux session is a cmux workspace, which has-session and new-session already
assume, so kill-session closes that workspace, switch-client selects it, and
new-session -A attaches to it when it exists instead of creating a duplicate.
show-options now answers from a table, and prefix reports the C-b that a
default tmux client would.

The sequence test drives all four through the real shim. Its fake socket also
now unwraps the capability envelope that the shell integration adds inside a
cmux terminal, so the test reports the behavior it checks rather than a JSON
decode error; that unwrapping moved into the shared helper.

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

* Reject a socket payload that is not a request

The fake servers indexed request["method"] straight off json.loads, so a
payload that decoded to null, a list, or an object with a non-string method
raised inside the handler thread and surfaced as an unrelated CLI error. They
now answer those with an error line, which names the real problem.

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

* test: reject empty fake socket request methods

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: reject a failed tmux session lookup and kill-session -a

A workspace.list error must not become a new session, and kill-session -a
must not close the caller.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Keep a failed tmux session lookup from creating a workspace

new-session -A treated every resolution error as a missing session, and
kill-session ignored -a and closed the caller. A missing session still
creates; a lookup failure and an unsupported flag now fail first.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix custom-sidebar nil-comparison so optional-guarded views render (#7943) (#7974)

* Add failing test: sidebar nil-comparison yields nothing (#7943)

In a custom sidebar, `x != nil` / `x == nil` against a bound optional field
does not evaluate to true/false — it evaluates to nothing. Interpolation
renders empty, ternaries always take the else branch, and `if x != nil`
guards are never taken, so optional-guarded views never render.

This commit adds only the regression test (no fix) so CI shows it red.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix sidebar nil-comparison to evaluate to a Bool (#7943)

The interpreter's value model had no null case and no `nil`-literal
evaluator branch, so `nil` evaluated to a host `SwiftValue?` of `nil` — the
same value that means "expression unsupported / no value". `evalInfix` then
bailed on the comparison, so `x != nil` / `x == nil` produced nothing:
interpolation rendered empty, ternaries always took the else branch, and
`if x != nil` guards were never taken. Optional-guarded views (including the
shipped status-board.swift / finder.swift examples) silently drew nothing.

- Add `SwiftValue.null` for the `nil` literal and for comparing an absent
  optional field against `nil` (distinct from host `nil` = "no value").
- Evaluate `NilLiteralExprSyntax` to `.null`.
- Handle `==` / `!=` before the operand guards, coalescing an absent operand
  (host `nil`) and the `nil` literal to `.null`, so the comparison yields a
  Bool that is true/false when present and false/true when absent.

Fixes #7943

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: preserve nil comparison evaluation failures

* fix: preserve nested nil comparison misses

* fix: preserve parenthesized nil comparison misses

* fix: handle nil optional binding and equality budget

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Leo Li <cheerleaderleo@outlook.com>

* fix: refresh merge helper and honor neutral checks (#17294)

* test: cover neutral checks and helper checkout refresh

* test: tolerate absent git diagnostics

* fix: refresh clean main checkout before merging

* test: cover cloud welcome close shortcut ownership

* fix: route cloud welcome close shortcut to its window

---------

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>
Co-authored-by: Austin Wang <austinwang115@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Leo <cheerleaderleo@outlook.com>
Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
Co-authored-by: BlueRaddish <jeeholife2@gmail.com>
Co-authored-by: EJ <ej@campbell.name>
Co-authored-by: ejc3 <ejc3@users.noreply.github.com>
Co-authored-by: Philipp Mochine <philipp@mochine.de>
Co-authored-by: mys <wowpotato@naver.com>
Co-authored-by: Alejandro Florez <soyeladice@gmail.com>
Co-authored-by: Sungho Park <relilau00@gmail.com>
Co-authored-by: Darío Kondratiuk <dariokondratiuk@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mark Xian <mark-xian@foxmail.com>
Co-authored-by: Austin Wang <38676809+austinywang@users.noreply.github.com>

---------

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>
Co-authored-by: Austin Wang <38676809+austinywang@users.noreply.github.com>
Co-authored-by: Austin Wang <austinwang115@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Leo <cheerleaderleo@outlook.com>
Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
Co-authored-by: BlueRaddish <jeeholife2@gmail.com>
Co-authored-by: EJ <ej@campbell.name>
Co-authored-by: ejc3 <ejc3@users.noreply.github.com>
Co-authored-by: Philipp Mochine <philipp@mochine.de>
Co-authored-by: mys <wowpotato@naver.com>
Co-authored-by: Alejandro Florez <soyeladice@gmail.com>
Co-authored-by: Sungho Park <relilau00@gmail.com>
Co-authored-by: Darío Kondratiuk <dariokondratiuk@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mark Xian <mark-xian@foxmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: browser The embedded browser, web surfaces, inline VS Code S3: minor Wrong behavior with a workaround

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants