Skip to content

Add browser profile targeting to CLI pane creation - #8874

Merged
austinywang merged 13 commits into
mainfrom
issue-2720-browser-profile-flag
Jul 25, 2026
Merged

austinywang merged 13 commits into
mainfrom
issue-2720-browser-profile-flag

Conversation

@austinywang

@austinywang austinywang commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Closes #2720

Issue: #2720

Summary

  • Resolve explicit browser profile selectors in the CmuxBrowser domain, preferring an existing UUID before exact case-insensitive display-name matching.
  • Thread the validated profile UUID through browser.open_split and pane.create, including Dock pane creation, while leaving an omitted selector as nil so the existing source/workspace/last-used fallback chain is unchanged.
  • Return structured invalid_params errors for unknown and ambiguous selectors; ambiguous errors include every candidate display name and UUID.
  • Add --profile <name-or-uuid> plumbing and help for browser open, browser open-split, and new-pane --type browser.
  • Reuse the existing browser profiles socket API in the CLI and label its effective current profile as last used.
  • Localize all new and changed terminal-facing text in English and Japanese.

Acceptance criteria

  • cmux browser open <url> --profile <name-or-uuid> forwards and applies the selected profile.
  • cmux browser open-split <url> --profile <name-or-uuid> forwards and applies the selected profile.
  • cmux new-pane --type browser --url <url> --profile <name-or-uuid> forwards and applies the selected profile.
  • cmux browser profiles lists profile display names and UUIDs and marks the last-used profile.
  • UUID selection takes precedence, then falls back to case-insensitive display-name matching.
  • Unknown selectors return a clear error instead of falling back.
  • Ambiguous display names return a clear error listing candidate names and UUIDs.
  • Omitting --profile preserves the existing profile fallback behavior.

Verification

  • swift test --package-path Packages/macOS/CmuxBrowser --filter BrowserProfileRepositoryTests (22 tests passed under native arm64)
  • swift test --package-path Packages/macOS/CmuxControlSocket (278 tests passed under native arm64)
  • ./scripts/lint-pbxproj-test-wiring.sh
  • ./scripts/check-pbxproj.sh
  • python3 scripts/check-package-resolved-policy.py
  • python3 scripts/check-workspace-package-groups.py --check
  • python3 scripts/lint-feature-flags.py
  • Parsed Resources/Localizable.xcstrings with duplicate-key detection and verified English/Japanese coverage for every changed key.
  • App builds and Xcode tests were intentionally not run locally per task constraints; the PR's app/socket behavior tests run in CI.

Summary by CodeRabbit

  • New Features
    • Added --profile <name|uuid> support for browser pane creation, browser open/split flows, and split/tab creation with a resolved “preferred profile” carried through.
  • Bug Fixes
    • Improved profile selector handling: trims input, resolves UUIDs first, then case-insensitive display names; ambiguity and not-found now return clearer invalid_params responses (including candidate details when applicable).
    • Updated “last used” marker text/label and refined localized CLI help/error messaging.
  • Tests
    • Added unit/integration coverage for selector resolution behavior and CLI socket plumbing; registered new Swift tests and extended CI regression runs.

@coderabbitai

coderabbitai Bot commented Jul 24, 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

Changes

The PR adds browser profile selector resolution, CLI and socket plumbing, profile-ID propagation into browser creation, localized errors/help, and Swift/Python integration coverage for valid, missing, and ambiguous selectors.

Browser profile selection

Layer / File(s) Summary
Profile resolution contract
Packages/macOS/CmuxBrowser/Sources/..., Sources/Panels/BrowserAutomation.swift, Packages/macOS/CmuxBrowser/Tests/...
Resolves selectors by UUID or case-insensitive display name and reports matched, missing, or ambiguous results with candidate details.
CLI and pane input handling
CLI/cmux.swift, Packages/macOS/CmuxControlSocket/Sources/..., Resources/Localizable.xcstrings
Parses and validates --profile, accepts profile aliases in pane creation, and adds localized help and error messages.
Browser creation propagation
Sources/TerminalController*.swift, Sources/DockSplitStore.swift, Sources/TerminalController.swift
Forwards resolved profile IDs through dock, workspace, browser surface, and split creation paths.
Integration validation
cmuxTests/BrowserProfileSocketTests.swift, tests/test_browser_profile_cli.py, cmux.xcodeproj/project.pbxproj, .github/workflows/ci.yml
Tests socket selection and structured errors, CLI forwarding and listing output, project registration, and CI execution.

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

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant TerminalController
  participant BrowserProfileStore
  participant Workspace
  CLI->>TerminalController: send profile selector
  TerminalController->>BrowserProfileStore: resolveProfileSelection(selector)
  BrowserProfileStore-->>TerminalController: matched ID or invalid result
  TerminalController->>Workspace: create browser surface or split with preferredProfileID
Loading

Suggested reviewers: lawrencecchen


Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Cmux Full Internationalization ❌ Error Resources/Localizable.xcstrings supports 20 locales, but every new/changed browser-profile key only has en/ja entries. Add translations for all locales already present in Resources/Localizable.xcstrings for each new/changed key, or update the supported-locale set consistently.
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Cmux No Test Or Debug Seam In Production Source ❓ Inconclusive placeholder2 pending
✅ Passed checks (22 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding browser profile targeting to CLI pane creation.
Description check ✅ Passed The description covers the summary, acceptance criteria, and verification, with only non-critical template sections left incomplete.
Linked Issues check ✅ Passed The changes satisfy #2720 by adding profile selection, browser profiles listing, UUID precedence, clear errors, and preserved fallback behavior.
Out of Scope Changes check ✅ Passed The added tests, localization, CI wiring, and project file updates are all directly supporting the browser profile targeting work.
Cmux Swift Actor Isolation ✅ Passed New browser-profile types are value-only Sendable, and the BrowserProfileStore/Repository and caller paths remain explicitly @MainActor.
Cmux Swift Blocking Runtime ✅ Passed New profile-selector code only resolves/threads UUIDs; no added waits, sleeps, semaphores, or locks appear in the changed production paths. Test scaffolding is deterministic.
Cmux Browser Automation Off-Main ✅ Passed PASS: browser.open_split stays on the main V2 path, and the new profile lookup runs inside @MainActor code; no new worker-lane WebKit-wait browser command was added.
Cmux Expensive Synchronous Load ✅ Passed Diff adds only in-memory browser profile selector resolution and UUID plumbing; no agent-history, large JSON, or other expensive sync loads moved onto interactive paths.
Cmux Cache Substitution Correctness ✅ Passed No fresh-vs-cache substitution in a persistence/history/undo/snapshot path; the new profile-resolution code only uses the repository’s authoritative in-memory profile list.
Cmux No Hacky Sleeps ✅ Passed No added fixed sleeps/timers/polling in touched non-Swift files; the Python test only uses subprocess/thread timeouts, and CI YAML adds no waits.
Cmux Algorithmic Complexity ✅ Passed New profile selection uses single-pass lookups over the profile list and tiny fixed flag sets; no nested rescans or superlinear hot-path work were introduced.
Cmux Swift Concurrency ✅ Passed Touched Swift changes are synchronous plumbing; no new DispatchQueue, completion-handler, Combine, or fire-and-forget Task patterns were introduced.
Cmux Swift @Concurrent ✅ Passed PASS: The touched Swift files add no @concurrent/nonisolated async declarations; async browser helpers hop to MainActor where needed, and the new tests are actor-bound.
Cmux Swift Package Boundaries ✅ Passed Core profile resolution moved into CmuxBrowser/CmuxControlSocket packages; app-target changes are thin CLI/socket/AppKit glue and wiring.
Cmux Swiftpm Lockfiles ✅ Passed No Package.resolved, .gitignore, or Package.swift changes; the Xcode project diff only adds test-file wiring and no SwiftPM package-reference edits.
Cmux Swift Logging ✅ Passed Touched runtime code adds no new print/NSLog/Logger usage; new prints are CLI user output, and no file-scoped Logger changes were introduced.
Cmux User-Facing Error Privacy ✅ Passed New user-facing errors only mention profile names/UUIDs and CLI flags; no vendor, internal provider, secret, or raw upstream data is exposed.
Cmux Swiftui State Layout ✅ Passed PASS: The patch only adds a forwarding method to an existing ObservableObject store and non-view plumbing; no new GeometryReader, lazy-row store refs, or render-time state writes appear.
Cmux Architecture Rethink ✅ Passed The diff adds direct selector plumbing and tests; BrowserProfileRepository remains the sole resolver, with no sleeps, observers, locks, or duplicate lifecycle ownership introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: The patch only adds a test-only NSWindow fixture and attachment-rendering logic; no new standalone cmux-owned window/controller or cmuxAuxiliaryWindowIdentifiers changes.
Cmux Source Artifacts ✅ Passed Changed paths are intentional source/test/config files and a project file; none are logs, caches, build output, or scratch artifacts.
Cmux No Ambient Global State ✅ Passed Production diff adds only member methods/local vars and enum cases; no new file-scope API, mutable globals, or singleton state was introduced.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-2720-browser-profile-flag

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.

@austinywang
austinywang marked this pull request as ready for review July 24, 2026 19:04
@cursor

cursor Bot commented Jul 24, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@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: 1

🤖 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 `@cmuxTests/BrowserProfileSocketTests.swift`:
- Around line 12-33: Register cleanup before the throwing profile-creation calls
in browserCreationCommandsHonorExplicitProfilesAndRejectInvalidSelectors, using
a mutable collection of created profile IDs that cleanup can update as each
creation succeeds. Ensure the defer restores BrowserAvailabilitySettings and
effectiveLastUsedProfileID and deletes every partially or fully created profile,
including when any try `#require` call fails.
🪄 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 Plus

Run ID: 0f447d20-32da-4f4a-9ae2-b5a65b941c81

📥 Commits

Reviewing files that changed from the base of the PR and between 5188223 and 60c59bc.

📒 Files selected for processing (4)
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Profiles/BrowserProfileRepositoryTests.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/BrowserProfileSocketTests.swift
  • tests/test_browser_profile_cli.py

Comment thread cmuxTests/BrowserProfileSocketTests.swift
@greptile-apps

greptile-apps Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds --profile <name|uuid> targeting to browser open, browser open-split, and new-pane --type browser, threading a validated preferredProfileID through both the browser.open_split and pane.create socket paths — including Dock pane creation — while leaving an omitted selector as nil so the existing last-used fallback is undisturbed.

  • Resolves selectors in BrowserProfileRepository via UUID-first lookup then case-insensitive display-name matching, returning a typed BrowserProfileSelectionResolution (matched/notFound/ambiguous) that the socket handlers convert to structured invalid_params errors with candidate UUIDs for the ambiguous case.
  • Adds @MainActor to omitsRemoteMirrorOnlyWindow and converts the key-path reference to a closure to satisfy Swift 6 actor isolation requirements for isRemoteTmuxMirror.
  • All new user-facing strings are fully localized in English and Japanese in Localizable.xcstrings; a new BrowserProfileSocketTests suite and Python CLI integration test cover the full selector/error matrix.

Confidence Score: 5/5

Safe to merge; the profile-selection and socket-plumbing changes are well-scoped and fully tested.

The implementation covers all acceptance criteria with matching unit and integration tests. Profile resolution is correctly isolated to the MainActor through BrowserProfileRepository, actor isolation on omitsRemoteMirrorOnlyWindow is properly fixed, and all new user-facing strings have English and Japanese translations. The one observable behavioural asymmetry — profile-not-found fires before browser-disabled when the browser is disabled — is intentional and pinned by the BrowserProfileSocketTests assertions. No correctness bugs, blocking primitives, or test seams in production source were found.

Files Needing Attention: Sources/TerminalController+ControlPaneContext.swift and Sources/TerminalController.swift share the same profile-resolution-before-availability ordering; worth revisiting if the UX of disabled-browser + unresolved selector comes up in user feedback.

Important Files Changed

Filename Overview
CLI/cmux.swift Adds parseBrowserProfileOption helper and threads --profile through browser open, open-split, and new-pane; validates that profile is only allowed with --type browser before sending to socket; localized help text added; no new global state or blocking primitives.
Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Profiles/Repository/BrowserProfileRepository.swift Adds resolveProfileSelection: UUID match first, then case-insensitive display-name filter returning matched/notFound/ambiguous. Logic is clean; profiles list is small and fixed-allocation so O(n) scan is acceptable.
Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Profiles/Values/BrowserProfileSelectionResolution.swift New Sendable+Equatable enum with three cases; clean value type with no isolation concerns.
Sources/TerminalController+ControlPaneContext.swift Profile validation block added before browser-disabled check; preferredBrowserProfileID threaded through dock and workspace split paths. Profile resolution order (before availability check) leads to inconsistent error messages when browser is disabled — see inline comment.
Sources/TerminalController.swift browser.open_split path gains the same profile validation/resolution block; same ordering issue (profileNotFound fires before browserDisabled) as the pane.create path.
Sources/AppDelegate+WindowDock.swift Adds @mainactor to omitsRemoteMirrorOnlyWindow and switches key-path to closure to satisfy Swift 6 actor isolation for the isRemoteTmuxMirror access — correct fix.
Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Pane/ControlCommandCoordinator+Pane.swift Extends ControlPaneCreateInputs construction with profileRaw, hasInvalidProfileParam, hasMultipleProfileParams, and maps invalidBrowserProfile resolution to an invalid_params error with candidate data.
Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Pane/ControlPaneCreateInputs.swift Three new optional fields (profileRaw, hasInvalidProfileParam, hasMultipleProfileParams) defaulting to nil/false so existing callers need no changes.
Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Pane/ControlPaneCreateResolution.swift Adds invalidBrowserProfile case carrying selector, message, and candidates array — fits cleanly into the existing resolution enum pattern.
Sources/Panels/BrowserAutomation.swift Adds five new BrowserProfileAutomationError cases with localized descriptions; extends ambiguousProfile to carry the candidate list for richer error output.
Sources/Panels/BrowserPanel.swift Adds resolveProfileSelection forwarding method to BrowserProfileStore; called from production socket handlers, not a test seam.
Resources/Localizable.xcstrings All new and changed keys carry both English and Japanese translations; ambiguousProfile format string updated consistently in both locales.
cmuxTests/BrowserProfileSocketTests.swift New test suite covering name-based open_split, UUID-based pane.create, fallback (no profile), unknown selector, disabled-browser with unknown/valid selectors, malformed types, whitespace-only, multiple selectors, non-browser pane, ambiguous selector, and remote-workspace rejection.
tests/test_browser_profile_cli.py Python integration test verifies CLI argument forwarding via a fake socket server; covers browser open, open-split, new-pane, non-browser profile rejection, missing value error, surface-qualified open rejection, and browser profiles list output.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["CLI --profile selector"] --> B["parseBrowserProfileOption"]
    B --> C{pane type?}
    C -->|not browser| D["CLIError: profileRequiresBrowserPane"]
    C -->|browser| E["Send profile= to socket"]
    E --> F{Multiple profile keys?}
    F -->|yes| G["invalid_params: multipleProfileSelectors"]
    F -->|no| H{Value is non-string?}
    H -->|yes| I["invalid_params: invalidProfileSelector"]
    H -->|no| J{selector present?}
    J -->|no| K["preferredProfileID = nil (existing fallback)"]
    J -->|yes| L["BrowserProfileStore.resolveProfileSelection"]
    L -->|matched| M["preferredProfileID = profile.id"]
    L -->|notFound| N["invalid_params: profileNotFound"]
    L -->|ambiguous| O["invalid_params: ambiguousProfile + candidate UUIDs"]
    M --> P{Browser disabled?}
    K --> P
    P -->|yes + profile given| Q["invalid_params: browserDisabled"]
    P -->|yes, no profile| R["legacy disabled flow"]
    P -->|no| S{Remote workspace?}
    S -->|yes + profile given| T["invalid_params: profileUnavailableInRemoteWorkspace"]
    S -->|no| U["newBrowserSplit / DockSplitStore with preferredProfileID"]
Loading

Reviews (7): Last reviewed commit: "test: import remote workspace configurat..." | Re-trigger Greptile

let first = repo.createProfile(named: "Shared")!
let second = repo.createProfile(named: "shared")!

#expect(repo.resolveProfileSelection("SHARED") == .ambiguous([first, second]))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Order-sensitive ambiguity assertion will fail if implementation uses unordered storage

The assertion == .ambiguous([first, second]) depends on the implementation returning candidates in insertion/creation order. If resolveProfileSelection builds its candidate list from a Set, Dictionary, or any other unordered structure, this test will fail spuriously even when the result is semantically correct. The downstream socket test (BrowserProfileSocketTests) correctly avoids this by using Set-based comparison. Consider extracting IDs and comparing as a Set here too, or pinning the order expectation to whatever the production implementation guarantees (e.g. by name).

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 242422d: the test now compares the ambiguous candidate UUIDs as a Set, so it asserts membership without inventing an ordering contract.

— Claude Code

Comment on lines +12 to +170
@Test func browserCreationCommandsHonorExplicitProfilesAndRejectInvalidSelectors() throws {
let defaults = UserDefaults.standard
let wasBrowserDisabled = BrowserAvailabilitySettings.isDisabled(defaults: defaults)
let store = BrowserProfileStore.shared
let previousLastUsedProfileID = store.effectiveLastUsedProfileID
let suffix = UUID().uuidString
let target = try #require(store.createProfile(named: "Issue 2720 Target \(suffix)"))
let fallback = try #require(store.createProfile(named: "Issue 2720 Fallback \(suffix)"))
let ambiguousName = "Issue 2720 Shared \(suffix)"
let ambiguousFirst = try #require(store.createProfile(named: ambiguousName))
let ambiguousSecond = try #require(store.createProfile(named: ambiguousName.lowercased()))
let createdProfileIDs = [target.id, fallback.id, ambiguousFirst.id, ambiguousSecond.id]

BrowserAvailabilitySettings.setDisabled(false, defaults: defaults)
defer {
TerminalController.shared.setActiveTabManager(nil)
for profileID in createdProfileIDs {
_ = store.deleteProfile(id: profileID)
}
store.noteUsed(previousLastUsedProfileID)
BrowserAvailabilitySettings.setDisabled(wasBrowserDisabled, defaults: defaults)
}

store.noteUsed(fallback.id)
let openManager = TabManager()
defer { openManager.tabs.forEach { $0.teardownAllPanels() } }
let openWorkspace = try #require(openManager.selectedWorkspace)
let openSourceID = try #require(openWorkspace.focusedPanelId)
TerminalController.shared.setActiveTabManager(openManager)

let openResponse = try call(
method: "browser.open_split",
params: [
"workspace_id": openWorkspace.id.uuidString,
"surface_id": openSourceID.uuidString,
"url": "about:blank",
"profile": target.displayName.lowercased(),
"focus": false,
]
)
let openResult = try successfulResult(openResponse)
let openedSurfaceID = try #require(
(openResult["surface_id"] as? String).flatMap(UUID.init(uuidString:))
)
let openedPanel = try #require(openWorkspace.panels[openedSurfaceID] as? BrowserPanel)
#expect(openedPanel.profileID == target.id)

store.noteUsed(fallback.id)
let paneManager = TabManager()
defer { paneManager.tabs.forEach { $0.teardownAllPanels() } }
let paneWorkspace = try #require(paneManager.selectedWorkspace)
let paneSourceID = try #require(paneWorkspace.focusedPanelId)
TerminalController.shared.setActiveTabManager(paneManager)

let paneResponse = try call(
method: "pane.create",
params: [
"workspace_id": paneWorkspace.id.uuidString,
"surface_id": paneSourceID.uuidString,
"direction": "right",
"type": "browser",
"url": "about:blank",
"profile": target.id.uuidString,
"focus": false,
]
)
let paneResult = try successfulResult(paneResponse)
let paneSurfaceID = try #require(
(paneResult["surface_id"] as? String).flatMap(UUID.init(uuidString:))
)
let panePanel = try #require(paneWorkspace.panels[paneSurfaceID] as? BrowserPanel)
#expect(panePanel.profileID == target.id)

store.noteUsed(fallback.id)
let fallbackManager = TabManager()
defer { fallbackManager.tabs.forEach { $0.teardownAllPanels() } }
let fallbackWorkspace = try #require(fallbackManager.selectedWorkspace)
let fallbackSourceID = try #require(fallbackWorkspace.focusedPanelId)
TerminalController.shared.setActiveTabManager(fallbackManager)

let fallbackResponse = try call(
method: "browser.open_split",
params: [
"workspace_id": fallbackWorkspace.id.uuidString,
"surface_id": fallbackSourceID.uuidString,
"url": "about:blank",
"focus": false,
]
)
let fallbackResult = try successfulResult(fallbackResponse)
let fallbackSurfaceID = try #require(
(fallbackResult["surface_id"] as? String).flatMap(UUID.init(uuidString:))
)
let fallbackPanel = try #require(fallbackWorkspace.panels[fallbackSurfaceID] as? BrowserPanel)
#expect(fallbackPanel.profileID == fallback.id)

let unknownSelector = "Issue 2720 Missing \(suffix)"
let unknownResponse = try call(
method: "browser.open_split",
params: [
"workspace_id": fallbackWorkspace.id.uuidString,
"surface_id": fallbackSourceID.uuidString,
"profile": unknownSelector,
]
)
let unknownError = try errorPayload(unknownResponse)
#expect(unknownError["code"] as? String == "invalid_params")
#expect((unknownError["message"] as? String)?.contains(unknownSelector) == true)
#expect((unknownError["data"] as? [String: Any])?["profile"] as? String == unknownSelector)

let ambiguousResponse = try call(
method: "pane.create",
params: [
"workspace_id": fallbackWorkspace.id.uuidString,
"surface_id": fallbackSourceID.uuidString,
"direction": "right",
"type": "browser",
"profile": ambiguousName.uppercased(),
]
)
let ambiguousError = try errorPayload(ambiguousResponse)
let ambiguousMessage = try #require(ambiguousError["message"] as? String)
let ambiguousCandidates = try #require(
(ambiguousError["data"] as? [String: Any])?["candidates"] as? [[String: Any]]
)
#expect(ambiguousError["code"] as? String == "invalid_params")
#expect(ambiguousMessage.contains(ambiguousFirst.id.uuidString))
#expect(ambiguousMessage.contains(ambiguousSecond.id.uuidString))
#expect(Set(ambiguousCandidates.compactMap { $0["id"] as? String }) == [
ambiguousFirst.id.uuidString,
ambiguousSecond.id.uuidString,
])
}

private func call(method: String, params: [String: Any]) throws -> [String: Any] {
let request: [String: Any] = [
"jsonrpc": "2.0",
"id": UUID().uuidString,
"method": method,
"params": params,
]
let data = try JSONSerialization.data(withJSONObject: request)
let line = try #require(String(data: data, encoding: .utf8))
let response = TerminalController.shared.handleSocketLine(line)
return try #require(
JSONSerialization.jsonObject(with: Data(response.utf8)) as? [String: Any]
)
}

private func successfulResult(_ response: [String: Any]) throws -> [String: Any] {
#expect(response["ok"] as? Bool == true)
return try #require(response["result"] as? [String: Any])
}

private func errorPayload(_ response: [String: Any]) throws -> [String: Any] {
#expect(response["ok"] as? Bool == false)
return try #require(response["error"] as? [String: Any])
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Five independent scenarios fused into one @Test function hides partial failures

All five scenarios — name-based open_split, UUID-based pane.create, no-profile fallback, unknown selector, and ambiguous selector — share a single @Test function. Because the function uses try #require and try successfulResult(...), the first failing call short-circuits the rest. When the unknown-selector path breaks, the ambiguous-selector coverage silently disappears, and vice versa. The sibling unit tests in BrowserProfileRepositoryTests correctly use one @Test func per scenario.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Keeping this as one serialized behavior flow is intentional: it shares one BrowserProfileStore lifecycle and verifies the complete socket contract (name, UUID, fallback, unknown, ambiguous) without multiplying mutation of persisted global profile state. Each scenario has localized #expect/#require diagnostics, while repository edge cases remain split into isolated unit tests.

— Claude Code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Fair point — the shared profile lifecycle and the layered "each subsequent scenario builds on the previous state" structure are real constraints that would complicate splitting. The distinction between socket-level behavioral flow tests and isolated unit tests is a reasonable boundary to draw here.

The short-circuit risk from try #require is still present, but given that all five scenarios share a single profile store setup and teardown, the consolidation tradeoff makes sense. Acknowledged.

Comment on lines +135 to +171
["browser", "open-split", "https://example.com", "--profile", PROFILE_ID],
)
assert_last_call(state, "browser.open_split", PROFILE_ID)

run_cli(
cli,
socket_path,
[
"new-pane",
"--type",
"browser",
"--url",
"https://example.com",
"--profile",
"Work Profile",
],
)
assert_last_call(state, "pane.create", "Work Profile")

run_cli(cli, socket_path, ["browser", "open", "https://example.com"])
assert_last_call(state, "browser.open_split", None)

profiles = run_cli(cli, socket_path, ["browser", "profiles"])
if "Work Profile" not in profiles or PROFILE_ID not in profiles:
raise AssertionError(f"profile list omitted name or UUID: {profiles!r}")
if "last used" not in profiles:
raise AssertionError(f"profile list did not mark last-used profile: {profiles!r}")
finally:
server.shutdown()
server.server_close()
thread.join(timeout=5)
print("PASS: browser profile CLI plumbing")
return 0


if __name__ == "__main__":
raise SystemExit(main())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 CLI error-path coverage gap for invalid and ambiguous selectors

The Python tests verify that a correctly plumbed --profile value is forwarded to the socket, but they don't exercise what the CLI prints when the socket returns an invalid_params error for an unknown or ambiguous selector. The acceptance criteria include "unknown and ambiguous selectors return clear errors", but run_cli raises AssertionError on any non-zero exit code without inspecting the error text. As a result, a regression where the CLI swallows the error or prints a raw internal message would not be caught here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The feature-specific unknown/ambiguous error code, selector, candidate IDs, and human-readable message are covered at the real socket handler boundary in BrowserProfileSocketTests. The Python test intentionally owns only CLI argument/output plumbing; making its fake server synthesize the same error would retest the unchanged generic sendV2 error printer rather than profile resolution.

— Claude Code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That's a fair separation of concerns. The socket-level tests own error code correctness and message content; the Python test owns CLI argument forwarding. Retesting the generic error printer through a fake server would add noise without catching profile-resolution regressions.

Acknowledged — no change needed here.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
CLI/cmux.swift (1)

13396-13446: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add multilingual translations for the new profile localization keys.

Localizable.xcstrings only includes en and ja for cli.browser.profile.error.emptySelector, cli.browser.profile.option, cli.newPane.help.profileDescription, and cli.browser.profiles.marker.lastUsed, while InfoPlist.xcstrings supports 18 locales and Localizable.xcstrings supports 20. New Swift localization keys need matching translations for every supported locale in the touched catalog.

🤖 Prompt for 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.

In `@CLI/cmux.swift` around lines 13396 - 13446, Add translations for
cli.browser.profile.error.emptySelector, cli.browser.profile.option,
cli.newPane.help.profileDescription, and cli.browser.profiles.marker.lastUsed in
Localizable.xcstrings for every locale supported by that catalog, not just en
and ja. Preserve the existing English and Japanese entries and match the
catalog’s established localization structure.

Source: Coding guidelines

🤖 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 `@CLI/cmux.swift`:
- Around line 4661-4670: Extract the duplicated profile selector trimming and
empty validation into a shared throwing helper, such as
resolvedBrowserProfileSelector(_ raw: String?) -> String?, preserving the
existing localized error and optional behavior. Replace the inline validation in
both the new-pane flow and the browser open/open-split/new flow with this helper
before assigning params["profile"].

In `@Sources/TerminalController`+ControlPaneContext.swift:
- Around line 170-193: Move the explicit browser profile selector resolution and
validation before the BrowserAvailabilitySettings.isDisabled() external-open
return path in Sources/TerminalController+ControlPaneContext.swift#L170-L193 and
Sources/TerminalController.swift#L6517-L6544. Preserve the existing no-selector
fallback, while ensuring unknown or ambiguous selectors return invalid_params
even when the browser is disabled and a URL is provided.

---

Outside diff comments:
In `@CLI/cmux.swift`:
- Around line 13396-13446: Add translations for
cli.browser.profile.error.emptySelector, cli.browser.profile.option,
cli.newPane.help.profileDescription, and cli.browser.profiles.marker.lastUsed in
Localizable.xcstrings for every locale supported by that catalog, not just en
and ja. Preserve the existing English and Japanese entries and match the
catalog’s established localization structure.
🪄 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 Plus

Run ID: f5bf7fc1-3890-46a6-9efa-174f80855109

📥 Commits

Reviewing files that changed from the base of the PR and between 60c59bc and 59d1b43.

📒 Files selected for processing (15)
  • .github/workflows/ci.yml
  • CLI/cmux.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Profiles/Repository/BrowserProfileRepository.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Profiles/Values/BrowserProfileSelectionResolution.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Pane/ControlCommandCoordinator+Pane.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Pane/ControlPaneBrowserProfileCandidate.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Pane/ControlPaneCreateInputs.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Pane/ControlPaneCreateResolution.swift
  • Resources/Localizable.xcstrings
  • Sources/DockSplitStore.swift
  • Sources/Panels/BrowserAutomation.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/TerminalController+ControlPaneContext.swift
  • Sources/TerminalController+ControlPaneDock.swift
  • Sources/TerminalController.swift

Comment thread CLI/cmux.swift
Comment thread Sources/TerminalController+ControlPaneContext.swift

@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: 1

🤖 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
`@Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Profiles/BrowserProfileRepositoryTests.swift`:
- Line 266: Update the assertion in the profile candidate test to compare Set
values on both sides: wrap the expected first.id and second.id collection in Set
while preserving the existing candidate ID mapping.
🪄 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 Plus

Run ID: 60d2cf1c-851f-4428-9f4c-1f8d73ae8f0a

📥 Commits

Reviewing files that changed from the base of the PR and between 59d1b43 and 242422d.

📒 Files selected for processing (2)
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Profiles/BrowserProfileRepositoryTests.swift
  • cmuxTests/BrowserProfileSocketTests.swift

@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: 1

♻️ Duplicate comments (1)
Sources/TerminalController+ControlPaneContext.swift (1)

171-177: 🎯 Functional Correctness | 🟠 Major

Validate explicit profiles before disabled-browser fallback.

With browser creation disabled and a URL present, both methods return via external open before reaching these blocks. Unknown, ambiguous, or malformed explicit selectors therefore succeed instead of returning invalid_params. Move selector validation before the disabled-browser return, while retaining the omitted-selector fallback; add disabled-browser coverage for invalid selectors.

  • Sources/TerminalController+ControlPaneContext.swift#L171-L177: resolve or reject an explicit selector before browserDisabledCreateResolution(...).
  • Sources/TerminalController.swift#L6517-L6526: resolve or reject an explicit selector before v2BrowserDisabledExternalOpenResult(...).

As per path instructions, an unavailable reliable selector must fail closed rather than fall back.

🤖 Prompt for 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.

In `@Sources/TerminalController`+ControlPaneContext.swift around lines 171 - 177,
Validate explicit browser profile selectors before the disabled-browser
fallback, rejecting unknown, ambiguous, malformed, or otherwise unavailable
selectors with invalid_params rather than opening the URL externally; retain the
omitted-selector fallback. Apply this in
Sources/TerminalController+ControlPaneContext.swift lines 171-177 around browser
profile resolution and in Sources/TerminalController.swift lines 6517-6526
before v2BrowserDisabledExternalOpenResult(...), and add disabled-browser
coverage for invalid selectors.

Source: Path instructions

🤖 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 `@CLI/cmux.swift`:
- Around line 13399-13413: Extract the duplicated `--profile` trimming and
empty-selector validation into a shared helper such as
`resolvedBrowserProfileSelector(_:)`, reusing the existing
`cli.browser.profile.error.emptySelector` error. Update both the `new-pane`
parsing flow and the `focus`/`profile` parsing flow to call the helper and
remove their local validation blocks, preserving the optional selector behavior.

---

Duplicate comments:
In `@Sources/TerminalController`+ControlPaneContext.swift:
- Around line 171-177: Validate explicit browser profile selectors before the
disabled-browser fallback, rejecting unknown, ambiguous, malformed, or otherwise
unavailable selectors with invalid_params rather than opening the URL
externally; retain the omitted-selector fallback. Apply this in
Sources/TerminalController+ControlPaneContext.swift lines 171-177 around browser
profile resolution and in Sources/TerminalController.swift lines 6517-6526
before v2BrowserDisabledExternalOpenResult(...), and add disabled-browser
coverage for invalid selectors.
🪄 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 Plus

Run ID: 24fd40cd-c170-4def-bc0f-a49eafb229ea

📥 Commits

Reviewing files that changed from the base of the PR and between 242422d and 85e5dd5.

📒 Files selected for processing (9)
  • CLI/cmux.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Pane/ControlCommandCoordinator+Pane.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Pane/ControlPaneCreateInputs.swift
  • Resources/Localizable.xcstrings
  • Sources/Panels/BrowserAutomation.swift
  • Sources/TerminalController+ControlPaneContext.swift
  • Sources/TerminalController.swift
  • cmuxTests/BrowserProfileSocketTests.swift
  • tests/test_browser_profile_cli.py

Comment thread CLI/cmux.swift Outdated
@austinywang
austinywang merged commit f8693b0 into main Jul 25, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: --profile option for browser open/new-pane commands

1 participant