Skip to content

iOS: compose a prompt and launch an agent workspace - #8314

Closed
azooz2003-bit wants to merge 5 commits into
mainfrom
feat-ios-spark
Closed

azooz2003-bit wants to merge 5 commits into
mainfrom
feat-ios-spark

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 17, 2026 •

Copy link
Copy Markdown
Collaborator

Compose a prompt on iPhone, tap Launch, and land in a new workspace where the agent is already working on it. Until now the phone could only create an empty workspace; running an agent meant typing claude "…" into a terminal keyboard.

How it works

New Mac capability workspace.launch_agent.v1 with two RPCs:

  • mobile.agent.launch_options reports which coding agents resolve on the Mac (claude, codex, via AgentExecutableResolver) and suggests working directories (the plain-create inherit directory first, then open workspaces' directories, deduped).
  • mobile.workspace.launch_agent creates the workspace through the shared v2WorkspaceCreate path with focus=false, eager_load_terminal=true, so the pty spawns in the background immediately, and types '<resolved-executable>' '<prompt>' at the fresh login shell via Ghostty initial_input (the agent inherits the user's shell environment exactly as if typed by hand). Single-line commands within the same 900-byte budget as the fork/resume path go inline; multiline or oversized prompts run through a staged /bin/zsh launcher script (AgentPromptWorkspaceLaunch in CMUXAgentLaunch, unit-tested). The workspace is titled from the prompt and the prompt is recorded via handlePromptSubmit, so the sidebar and iOS rows immediately show what the agent is doing. The response is the same workspace-list payload as workspace.create with created_workspace_id.

workspace.create gains an initial_input param alongside initial_command (typed at the shell rather than replacing it), which the CLI/control socket get for free.

On iOS the workspace list grows a bottom "Ask an agent…" glass bar and a "New Agent Task" item in the "+" menu. Both open a full-screen composer: auto-focused prompt editor, directory and agent chips (fed by launch_options, advisory with safe fallbacks), and a Launch button with haptics. MobileShellComposite.launchAgentWorkspace mirrors createRemoteWorkspace's apply/select path, so a successful launch selects the created workspace and dismissing the composer reveals the live terminal. Failures render inline with the shared mutation-failure copy and never lose the draft. Old Macs without the capability hide every entry point, and the update advisor names the feature (workspace.launch_agent.v1 registered with firstReleasedMacVersion: nil).

Ticket authorization treats mobile.workspace.launch_agent exactly like workspace.create (Mac-wide tickets required for group placement; created workspace recorded for follow-up scoped calls), and mobile.agent.launch_options like the read-only workspace list.

Tests

  • CMUXAgentLaunchTests/AgentPromptWorkspaceLaunchTests: quoting, inline-vs-script budget boundary, multiline routing, title derivation (10 tests).
  • CmuxMobileRPCTests/MobileAgentLaunchOptionsResponseTests: wire decode incl. sparse payloads (3 tests).
  • cmuxTests/MobileHostAuthorizationTests: attach-ticket acceptance for both new methods, group-placement rejection for workspace-scoped tickets.
  • Verified live on simulator + tagged Mac build (screenshots in PR comments).

Localization: all new strings in ios/cmux/Resources/Localizable.xcstrings with en + ja entries (11 keys); Mac side has no new user-facing strings.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Compose a prompt on iPhone, tap Launch, and land in a new workspace with the coding agent already running. Adds workspace.launch_agent.v1 and a full-screen iOS composer so you can start agent tasks without a desktop terminal.

  • New Features

    • Mac: workspace.launch_agent.v1 with mobile.agent.launch_options (installed agents + suggested directories) and mobile.workspace.launch_agent (creates a workspace and types '<executable>' '<prompt>' at the login shell via Ghostty initial_input; inline within budget or via a staged script). Titles from the prompt and returns the usual workspace-list payload with created_workspace_id. workspace.create also accepts initial_input.
    • iOS: bottom “Ask an agent…” bar and “New Agent Task” in “+” open a full-screen composer (prompt field, agent and directory chips, Launch with haptics). Success selects the new workspace; dismiss reveals the live terminal. Errors render inline and keep the draft. Button label no longer wraps; home directories render as “~”.
    • Auth & gating: mobile.workspace.launch_agent authorized like workspace.create (group placement still needs a Mac-wide ticket). mobile.agent.launch_options is read-only. Client gates only on the capability; old Macs hide entry points and the update hint names the feature. Tests and localization updated.
  • Bug Fixes

    • Fix Swift 6 capture error in the sidebar PR status icon view by explicitly capturing the tint color in NSImage’s drawing handler.

Written for commit b1ccdb0. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added an iOS “Agent Launch” flow with a prompt composer, agent/directory selection, and an “Ask an agent…” bar.
    • Enabled capability-gated agent-task creation from the workspace list (with Mac update hint support).
    • Added macOS support for launching installed coding agents in new workspaces, including safe shell startup command generation and derived workspace titles.
  • Bug Fixes
    • Improved mobile authorization and workspace tracking for agent launches.
    • Improved display of agent-launch failure reasons and added initial terminal input support for mobile workspace creation.
  • Tests
    • Added coverage for decoding launch options, command construction, and authorization behavior.

cmux reload-cloud and others added 3 commits July 16, 2026 21:55
New capability workspace.launch_agent.v1: mobile.agent.launch_options
reports installed agents + suggested working directories, and
mobile.workspace.launch_agent creates a workspace, types the agent
command with the composed prompt at the fresh login shell (Ghostty
initial_input, inline within the 900-byte fork budget or via a staged
launcher script), titles the workspace from the prompt, and records the
prompt as the workspace's submitted message. workspace.create gains an
initial_input param alongside initial_command.

On iOS the workspace list grows a bottom "Ask an agent…" glass bar and
a "New Agent Task" item in the + menu, both opening a full-screen
composer (prompt editor, directory + agent chips, Launch). A successful
launch selects the created workspace so dismissing the composer reveals
the agent's live terminal; failures show inline and keep the draft.
Old Macs without the capability hide the entry points and the update
advisor names the feature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Mac authorizes mobile.workspace.launch_agent for workspace-scoped
attach tickets exactly like workspace.create (group placement still
requires a Mac-wide ticket), so the client gate must not borrow the
group-mutation ticket policy: it hid the composer on connections where
the launch itself is fully authorized.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Launch label no longer wraps (fixedSize, chips truncate instead) and a
macOS home directory renders as "~" in the directory chip rather than
the username.

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

coderabbitai Bot commented Jul 17, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds end-to-end mobile agent launching: Mac RPC handlers discover agents and create workspaces, iOS provides prompt composition with agent and directory selection, and capability, authorization, error handling, testing, project integration, and localization are updated.

Changes

Agent launch RPC and Mac implementation

Layer / File(s) Summary
RPC contracts and authorization
Packages/iOS/CmuxMobileRPC/..., Sources/Mobile/..., Sources/TerminalController.swift, cmuxTests/...
Adds typed launch-options decoding, capability advertisement, RPC dispatch and fallback handling, method-specific authorization, created-resource tracking, and decoding/authorization tests.
Mac launch backend and startup input
Sources/TerminalController+MobileAgentLaunch.swift, Sources/TerminalController+WorkspaceCreate.swift, Packages/macOS/CMUXAgentLaunch/..., cmux.xcodeproj/project.pbxproj
Adds agent discovery and workspace-launch RPCs, inline or staged startup scripts, prompt-derived titles, initial terminal input, and project integration with launch-input tests.

iOS agent launch flow

Layer / File(s) Summary
Mobile shell orchestration
Packages/iOS/CmuxMobileShell/..., Packages/iOS/CmuxMobileShellUI/.../WorkspaceShellView*
Adds capability-gated option fetching and workspace launching, including workspace selection, terminal synchronization, failure mapping, stale-operation handling, and compact-stack navigation state.
Prompt composer and workspace integration
Packages/iOS/CmuxMobileShellUI/.../AgentLaunch*, WorkspaceListView*, MobileWorkspaceMutationFailure+Copy.swift
Adds the launch bar and full-screen composer with prompt validation, agent and directory menus, launch progress and errors, workspace-list presentation, accessibility identifiers, and localized failure text.
Localization
ios/cmux/Resources/Localizable.xcstrings
Adds English and Japanese agent-launch strings and feature hints, while reformatting existing localization entries without changing their content.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant AgentLaunchComposerView
  participant MobileShellComposite
  participant MobileHostService
  participant TerminalController
  participant WorkspaceManager
  User->>AgentLaunchComposerView: enter prompt and choose options
  AgentLaunchComposerView->>MobileShellComposite: launchAgentWorkspace
  MobileShellComposite->>MobileHostService: mobile.workspace.launch_agent
  MobileHostService->>TerminalController: authorize and dispatch request
  TerminalController->>WorkspaceManager: create workspace with agent startup input
  WorkspaceManager-->>TerminalController: return workspace list
  TerminalController-->>MobileHostService: return launch result
  MobileHostService-->>MobileShellComposite: return workspace response
  MobileShellComposite-->>AgentLaunchComposerView: select created workspace
Loading

Possibly related PRs

Suggested reviewers: lawrencecchen


Important

Pre-merge checks failed

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

❌ Failed checks (4 errors, 1 warning)

Check name Status Explanation Resolution
Cmux Swift Blocking Runtime ❌ Error New v2MainSync hops were added in TerminalController+MobileAgentLaunch.swift on the socket/terminal path, introducing blocking main-thread synchronization in production code. Replace the main-sync hops with async MainActor access or an explicit callback/state transition for workspace metadata and prompt-submit updates.
Cmux Expensive Synchronous Load ❌ Error @MainActor mobileHostHandleRPC directly calls v2MobileAgentLaunchOptions, which synchronously runs AgentExecutableResolver.resolve() (PATH and ~/.nvm/fnm scans) on the socket path. Move launch-option resolution off-main via a detached/background actor or cached accessor; keep MainActor work limited to UI/process-launch handoff.
Cmux Swift @Concurrent ❌ Error fetchAgentLaunchOptions does network+decode on @MainActor and is awaited from SwiftUI .task with no hop, violating the UI-heavy async rule. Move the RPC/JSON work off MainActor (e.g. a nonisolated helper or detached task) and keep only UI state updates on the actor; don’t add @concurrent to @MainActor methods.
Cmux No Ambient Global State ❌ Error Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentPromptWorkspaceLaunch.swift:8 adds a caseless enum namespace with only static helpers, which the rule forbids. Move the prompt-launch helpers onto an instantiable service (e.g. AgentPromptWorkspaceLaunchService) and inject it at the RPC/app seam instead of exposing a static helper namespace.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (20 passed)
Check name Status Explanation
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 No new actor-isolation regression: the added models are plain value types, and the UI/store code stays on MainActor by design.
Cmux Browser Automation Off-Main ✅ Passed The commit only changes an AppKit sidebar view; no browser.* routing, worker-lane, or WebKit-wait code was touched.
Cmux Cache Substitution Correctness ✅ Passed launch_options is advisory UI data with nil/static fallbacks, and launchAgentWorkspace uses a fresh RPC response plus generation/cancel stale-op checks—no persisted cache swap.
Cmux No Hacky Sleeps ✅ Passed Diff only touches a Swift file; no covered non-Swift runtime code or sleep/timer/polling patterns were introduced.
Cmux Algorithmic Complexity ✅ Passed HEAD diff is a one-line capture-list change in an AppKit draw closure; no new collection scans, sorting/filtering, or batch rescans were introduced.
Cmux Swift Concurrency ✅ Passed Diff uses async/await plus SwiftUI view-action bridges only; no new DispatchQueue, Combine, or completion-handler patterns were introduced.
Cmux Swift Package Boundaries ✅ Passed Reusable launch-shell and RPC models were extracted to CMUXAgentLaunch/CMuxMobileRPC; app-target edits are host-service and UI glue, not standalone domain logic.
Cmux Swiftpm Lockfiles ✅ Passed The only Package.swift edit adds resources; there are no Package.resolved, .gitignore, or SwiftPM package-reference diffs.
Cmux Swift Logging ✅ Passed Reviewed the touched Swift files and found no added print/debugPrint/dump/NSLog or Logger declarations; the new agent-launch code is non-logging.
Cmux User-Facing Error Privacy ✅ Passed New agent-launch error copy is generic (“Couldn't launch…”) and uses sanitized reasonText; no vendor/internal details are exposed.
Cmux Full Internationalization ✅ Passed All new Swift UI strings use L10n/String(localized:) and the added xcstrings keys are translated for both supported locales (en, ja).
Cmux Swiftui State Layout ✅ Passed New agent-launch UI uses @State/@binding only; List rows stay snapshot+closure-based, and no GeometryReader/ObservableObject regression appears.
Cmux Architecture Rethink ✅ Passed The agent-launch flow uses one shared launch path and explicit list-owned composer state; no sleeps, polling, locks, or split lifecycle ownership were introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No new standalone NSWindow/WindowGroup code was added; the iOS composer is a fullScreenCover/View and no cmux.* window IDs or owner registrations changed.
Cmux Source Artifacts ✅ Passed All changed paths are intentional source/config/test/localization files; none are logs, screenshots, temp folders, caches, or artifact dirs forbidden by the rule.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No new test/debug-only seam appears in changed Sources; the widened helper is used by a new production caller, not a test wrapper.
Title check ✅ Passed The title is concise, specific, and accurately summarizes the main change: composing and launching an agent workspace on iOS.
Description check ✅ Passed It covers the summary and testing sections well, but omits the Demo Video, Review Trigger, and Checklist sections from the template.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-ios-spark

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.

@greptile-apps

greptile-apps Bot commented Jul 17, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds end-to-end agent-launch support: compose a prompt on iPhone, tap Launch, and land in a new workspace where the agent is already running. The Mac gains two new RPCs (mobile.agent.launch_options, mobile.workspace.launch_agent) behind the workspace.launch_agent.v1 capability, and iOS gains a full-screen composer with agent/directory chips, inline errors, and draft persistence.

  • Mac side: TerminalController+MobileAgentLaunch creates the workspace via the shared v2WorkspaceCreate path with eager_load_terminal=true, types the shell command at the fresh login shell via Ghostty initial_input (inline within a 900-byte budget, otherwise via a staged /bin/zsh script), records the prompt through handlePromptSubmit, and returns the standard workspace-list payload with created_workspace_id.
  • iOS side: AgentLaunchComposerView provides the prompt editor, agent/directory selection chips, haptic feedback, and inline failure display; MobileShellComposite+AgentLaunch mirrors the createRemoteWorkspace apply/select path so a successful launch navigates straight into the live terminal; MobileWorkspaceMutationFailure+Copy consolidates the failure-reason strings previously duplicated in the workspace-action toast.
  • Auth & gating: mobile.workspace.launch_agent is authorized identically to workspace.create (group-placement guard for workspace-scoped tickets); mobile.agent.launch_options is read-only; capability absence hides all entry points on old Macs.

Confidence Score: 4/5

Safe to merge after addressing the blank-fullScreenCover trap in WorkspaceListView.

The Mac-side RPC logic, ticket authorization, shell-command quoting, and the iOS composite layer are all well-structured and consistent with existing patterns. The one concrete defect is in WorkspaceListView: when the Mac disconnects while the agent-launch composer is open, launchAgent becomes nil, the fullScreenCover body renders empty, and iOS fullScreenCover does not support swipe-to-dismiss by default — leaving the user with no escape short of force-quitting. Everything else in the PR — auth, localization, error copy, the script staging path, state management in the composer — looks correct.

WorkspaceListView.swift — the fullScreenCover needs an .onChange guard to dismiss when launchAgent becomes nil.

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swift Adds the 'Ask an agent…' compose bar, fullScreenCover for the agent-launch composer, and the launchAgent/fetchAgentLaunchOptions closure props. The fullScreenCover body conditionally renders AgentLaunchComposerView — if launchAgent becomes nil after the cover is presented (Mac disconnect), the cover renders empty with no dismiss affordance.
Sources/TerminalController+MobileAgentLaunch.swift New Mac-side RPCs: v2MobileAgentLaunchOptions (resolves installed agents + suggests dirs) and v2MobileWorkspaceLaunchAgent (creates workspace with eager terminal and types the shell command via initial_input, with inline/script routing, prompt-derived title, and handlePromptSubmit). File I/O for script staging runs on the socket worker thread, not main actor. Logic looks correct.
Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentPromptWorkspaceLaunch.swift Pure shell-command and title-derivation helpers (quoting, inline/script budget routing, title truncation). Well-tested. The caseless-enum namespace pattern was flagged in a previous thread; all logic is otherwise correct.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentLaunchComposerView.swift Full-screen prompt composer with auto-focus, agent/directory chips, inline failure display, haptics, and draft preservation via @binding. State management is clean — no ObservableObject, no render-time mutations, no broad invalidation patterns.
Sources/Mobile/MobileHostService+TicketAuthorization.swift Extends ticket authorization to mobile.workspace.launch_agent (same group-placement guard as workspace.create) and mobile.agent.launch_options (read-only, no extra guard). Authorization semantics correctly mirror the described access model.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentLaunch.swift iOS composite layer: fetchAgentLaunchOptions (advisory, nil on error) and launchAgentWorkspace (mirrors createRemoteWorkspace's apply/select path). Generation checks and cancellation guards are consistent with existing workspace-create patterns.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView+WorkspaceActions.swift Adds launchAgentInCompactStackClosure, launchAgentIfConnectedClosure, and fetchAgentLaunchOptionsClosure computed properties alongside the existing create-workspace closure pattern. workspaceMutationFailure widened from private to internal — legitimate production caller, not a test seam.
Sources/TerminalController+WorkspaceCreate.swift Adds initial_input param (type-at-shell, not trimmed to preserve the trailing newline) alongside the existing initial_command. Conditional is correct and consistent with the initialCommand pattern.
ios/cmux/Resources/Localizable.xcstrings Adds 11 new string keys, all with en and ja translations matching every locale already in the catalog. No existing strings modified.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileWorkspaceMutationFailure+Copy.swift Extracts the failure-reason text logic from WorkspaceShellView+WorkspaceActionToast into a shared extension so the agent-launch composer can reuse the same localized strings. The refactor is clean; no duplicate copy exists after the change.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant iOS as iOS Composer
    participant Shell as MobileShellComposite
    participant RPC as MobileCoreRPCClient
    participant Mac as TerminalController (Mac)
    participant TC as v2WorkspaceCreate
    participant Ghostty as Ghostty PTY

    iOS->>Shell: fetchAgentLaunchOptions()
    Shell->>RPC: mobile.agent.launch_options
    RPC->>Mac: v2MobileAgentLaunchOptions
    Mac-->>RPC: "{agents, directories, default_directory}"
    RPC-->>Shell: MobileAgentLaunchOptionsResponse
    Shell-->>iOS: MobileAgentLaunchOptions (agents + dirs)

    iOS->>Shell: launchAgentWorkspace(prompt, agentID, dir)
    Shell->>RPC: mobile.workspace.launch_agent
    RPC->>Mac: v2MobileWorkspaceLaunchAgent
    Mac->>Mac: AgentPromptWorkspaceLaunch.shellCommand()
    alt prompt fits inline budget
        Mac->>TC: "v2WorkspaceCreate(initial_input=cmd+newline)"
    else multiline or oversized
        Mac->>Mac: writeMobileAgentLaunchScript(body)
        Mac->>TC: "v2WorkspaceCreate(initial_input=script invocation)"
    end
    TC->>Ghostty: spawn PTY with initial_input
    Ghostty->>Ghostty: types command at login shell
    TC-->>Mac: ok(workspace_id)
    Mac->>Mac: handlePromptSubmit(workspaceId, prompt)
    Mac->>Mac: v2MobileWorkspaceList(createdWorkspaceID)
    Mac-->>RPC: workspace-list payload + created_workspace_id
    RPC-->>Shell: MobileSyncWorkspaceListResponse
    Shell->>Shell: applyRemoteWorkspaceList + setSelectedWorkspaceID
    Shell-->>iOS: .success
    iOS->>iOS: dismiss() reveals live terminal
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"}}}%%
sequenceDiagram
    participant iOS as iOS Composer
    participant Shell as MobileShellComposite
    participant RPC as MobileCoreRPCClient
    participant Mac as TerminalController (Mac)
    participant TC as v2WorkspaceCreate
    participant Ghostty as Ghostty PTY

    iOS->>Shell: fetchAgentLaunchOptions()
    Shell->>RPC: mobile.agent.launch_options
    RPC->>Mac: v2MobileAgentLaunchOptions
    Mac-->>RPC: "{agents, directories, default_directory}"
    RPC-->>Shell: MobileAgentLaunchOptionsResponse
    Shell-->>iOS: MobileAgentLaunchOptions (agents + dirs)

    iOS->>Shell: launchAgentWorkspace(prompt, agentID, dir)
    Shell->>RPC: mobile.workspace.launch_agent
    RPC->>Mac: v2MobileWorkspaceLaunchAgent
    Mac->>Mac: AgentPromptWorkspaceLaunch.shellCommand()
    alt prompt fits inline budget
        Mac->>TC: "v2WorkspaceCreate(initial_input=cmd+newline)"
    else multiline or oversized
        Mac->>Mac: writeMobileAgentLaunchScript(body)
        Mac->>TC: "v2WorkspaceCreate(initial_input=script invocation)"
    end
    TC->>Ghostty: spawn PTY with initial_input
    Ghostty->>Ghostty: types command at login shell
    TC-->>Mac: ok(workspace_id)
    Mac->>Mac: handlePromptSubmit(workspaceId, prompt)
    Mac->>Mac: v2MobileWorkspaceList(createdWorkspaceID)
    Mac-->>RPC: workspace-list payload + created_workspace_id
    RPC-->>Shell: MobileSyncWorkspaceListResponse
    Shell->>Shell: applyRemoteWorkspaceList + setSelectedWorkspaceID
    Shell-->>iOS: .success
    iOS->>iOS: dismiss() reveals live terminal
Loading

Reviews (3): Last reviewed commit: "Fix Swift 6 capture error in sidebar PR ..." | Re-trigger Greptile

Comment on lines +8 to +66
public enum AgentPromptWorkspaceLaunch {
/// The startup text a fresh terminal should type at its shell prompt.
public enum StartupInput: Equatable, Sendable {
/// The composed command fits the inline budget; type it directly
/// (trailing newline included).
case inline(String)
/// The command is too large or multiline to type inline; the caller
/// writes `body` to a script file and types `/bin/zsh '<path>'`.
case script(body: String)
}

/// Same inline budget as the agent fork/resume startup-input path: typed
/// lines beyond this go through a launcher script instead of the pty.
public static let maxInlineBytes = 900

/// `<executable> '<prompt>'` with both sides single-quoted for POSIX shells.
public static func shellCommand(executablePath: String, prompt: String) -> String {
"\(singleQuoted(executablePath)) \(singleQuoted(prompt))"
}

/// Inline when the command is a single line within budget; script otherwise.
/// Multiline prompts always take the script path so the typed input never
/// depends on the shell's quote-continuation behavior.
public static func startupInput(command: String, maxInlineBytes: Int = maxInlineBytes) -> StartupInput {
let inline = command + "\n"
if !command.contains(where: \.isNewline), inline.utf8.count <= maxInlineBytes {
return .inline(inline)
}
return .script(body: "#!/bin/zsh\nexec \(command)\n")
}

/// The typed line that runs a written launcher script.
public static func scriptInvocation(scriptPath: String) -> String {
"/bin/zsh \(singleQuoted(scriptPath))\n"
}

/// A concise workspace title derived from the prompt: first line, collapsed
/// whitespace, cut at a word boundary. `nil` when the prompt is blank.
public static func derivedWorkspaceTitle(prompt: String, maxLength: Int = 48) -> String? {
let firstLine = prompt
.trimmingCharacters(in: .whitespacesAndNewlines)
.split(whereSeparator: \.isNewline)
.first
.map(String.init) ?? ""
let collapsed = firstLine
.split(whereSeparator: { $0.isWhitespace })
.joined(separator: " ")
guard !collapsed.isEmpty else { return nil }
guard collapsed.count > maxLength else { return collapsed }
let prefix = String(collapsed.prefix(maxLength))
let cut = prefix.lastIndex(where: { $0 == " " }).map { String(prefix[..<$0]) } ?? prefix
return cut + "…"
}

/// POSIX single-quoting: wraps in `'…'`, escaping embedded single quotes.
public static func singleQuoted(_ value: String) -> String {
"'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'"
}
}

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 Caseless enum used as a static-method namespace

AgentPromptWorkspaceLaunch is a public enum with no cases whose entire API surface is static methods and a single static constant. This is the caseless-enum namespace pattern that violates the cmux-no-ambient-global-state rule.

The functions are pure and stateless, so the natural fix is a struct that takes maxInlineBytes as an init parameter (or property), turning shellCommand, startupInput, scriptInvocation, derivedWorkspaceTitle, and singleQuoted into regular instance methods. Call sites in TerminalController+MobileAgentLaunch.swift already construct the launch plan in one place, so plumbing a let launcher = AgentPromptWorkspaceLaunch(...) there is minimal churn.

Rule Used: Flag new ambient global state in production Swift:... (source)

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!

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

🤖 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/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+AgentLaunch.swift:
- Around line 101-103: Update the stale/cancelled early returns in the agent
launch flow around isCurrentRemoteOperation and Task.isCancelled so they do not
return .success(()). Return an explicit superseded/cancelled outcome or an
appropriate failure that prevents AgentLaunchComposerView from clearing the
draft and dismissing when workspace application or selection is skipped.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentLaunchComposerView.swift`:
- Around line 232-238: Update resolvedAgentID to return nil when no agent has
installed == true, rather than falling back to the first unavailable agent.
Adjust canLaunch to require a valid installed resolved agent, so launch remains
disabled when all reported agents are unavailable; preserve existing validation
for other launch requirements.
- Around line 282-290: Update pathBasename(_:) so it replaces the path with "~"
only when the /Users/<name> path matches the current Mac user's authoritative
home directory; otherwise return the actual basename, including for
/Users/Shared and other users' directories. Reuse the platform home-directory
source rather than treating every two-component /Users path as the home
directory.

In `@Sources/TerminalController`+MobileAgentLaunch.swift:
- Around line 26-34: Update the mobile RPC handlers containing the
AgentExecutableResolver discovery and script staging logic, including the
handlers around the referenced provider-list, staging, and cleanup sections, to
be async and move all executable lookup, directory enumeration, metadata access,
file writes, chmod, and deletion behind a dedicated actor or `@concurrent` async
helper. Keep only TabManager snapshots and mutations on MainActor, and await the
background filesystem operations from mobileHostHandleRPC.
- Around line 91-99: Update the staged launch-script flow around
writeMobileAgentLaunchScript, scriptInvocation, and the workspace-creation
failure path so each script deletes itself on launcher exit and is explicitly
removed when workspace creation fails. Retain the script URL for cleanup, and
limit pruning to recovering scripts left behind by crashes rather than normal
successful launches.
🪄 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: 464ad84d-a2e1-4f6e-83c2-8a8f1a2d873e

📥 Commits

Reviewing files that changed from the base of the PR and between b7bd901 and 9a74dfb.

📒 Files selected for processing (27)
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileAgentLaunchOptionsResponse.swift
  • Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swift
  • Packages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileAgentLaunchOptionsResponseTests.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileMacUpdateCapabilityRequirement.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileMacUpdateFeature.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentLaunch.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+WorkspaceActions.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentLaunchComposeBar.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentLaunchComposerView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileMacUpdateHint+Display.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileWorkspaceMutationFailure+Copy.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView+Actions.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView+WorkspaceActionToast.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView+WorkspaceActions.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swift
  • Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentPromptWorkspaceLaunch.swift
  • Packages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentPromptWorkspaceLaunchTests.swift
  • Sources/Mobile/MobileHostService+Capabilities.swift
  • Sources/Mobile/MobileHostService+TicketAuthorization.swift
  • Sources/Mobile/MobileHostService.swift
  • Sources/TerminalController+MobileAgentLaunch.swift
  • Sources/TerminalController+WorkspaceCreate.swift
  • Sources/TerminalController.swift
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/MobileHostAuthorizationTests.swift
  • ios/cmux/Resources/Localizable.xcstrings

Comment on lines +101 to +103
guard isCurrentRemoteOperation(client: client, generation: generation), !Task.isCancelled else {
return .success(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not report superseded launches as successful.

Both stale/cancelled branches return .success(()), so AgentLaunchComposerView clears the draft and dismisses even though this method skipped applying/selecting the created workspace. Add an explicit superseded/cancelled outcome, or return a failure that preserves the draft.

Also applies to: 122-122

🤖 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
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+AgentLaunch.swift
around lines 101 - 103, Update the stale/cancelled early returns in the agent
launch flow around isCurrentRemoteOperation and Task.isCancelled so they do not
return .success(()). Return an explicit superseded/cancelled outcome or an
appropriate failure that prevents AgentLaunchComposerView from clearing the
draft and dismissing when workspace application or selection is skipped.

Source: Coding guidelines

Comment on lines +232 to +238
private var resolvedAgentID: String? {
if let selectedAgentID,
resolvedAgents.contains(where: { $0.id == selectedAgentID && $0.installed }) {
return selectedAgentID
}
return resolvedAgents.first(where: \.installed)?.id ?? resolvedAgents.first?.id
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Disable launch when no reported agent is installed.

Line 237 selects the first agent even if every option has installed == false, while canLaunch ignores agent availability. The composer therefore enables a launch that the Mac is guaranteed to reject.

Proposed fix
 private var resolvedAgentID: String? {
     if let selectedAgentID,
        resolvedAgents.contains(where: { $0.id == selectedAgentID && $0.installed }) {
         return selectedAgentID
     }
-    return resolvedAgents.first(where: \.installed)?.id ?? resolvedAgents.first?.id
+    return resolvedAgents.first(where: \.installed)?.id
 }

 private var canLaunch: Bool {
-    !trimmedPrompt.isEmpty && !isLaunching
+    !trimmedPrompt.isEmpty && resolvedAgentID != nil && !isLaunching
 }

Also applies to: 298-300

🤖 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
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentLaunchComposerView.swift`
around lines 232 - 238, Update resolvedAgentID to return nil when no agent has
installed == true, rather than falling back to the first unavailable agent.
Adjust canLaunch to require a valid installed resolved agent, so launch remains
disabled when all reported agents are unavailable; preserve existing validation
for other launch requirements.

Comment on lines +282 to +290
private static func pathBasename(_ path: String) -> String? {
// A macOS home directory reads better as "~" than as the username.
let components = path.split(separator: "/", omittingEmptySubsequences: true)
if components.count == 2, components[0] == "Users" {
return "~"
}
let basename = (path as NSString).lastPathComponent
return basename.isEmpty ? nil : basename
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not label every /Users/<name> path as the home directory.

This also converts /Users/Shared or another user's directory to ~, misleading the user about where the agent will run. Without the Mac's authoritative home path, retain the actual basename instead.

Proposed fix
 private static func pathBasename(_ path: String) -> String? {
-    // A macOS home directory reads better as "~" than as the username.
-    let components = path.split(separator: "/", omittingEmptySubsequences: true)
-    if components.count == 2, components[0] == "Users" {
-        return "~"
-    }
     let basename = (path as NSString).lastPathComponent
     return basename.isEmpty ? nil : basename
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static func pathBasename(_ path: String) -> String? {
// A macOS home directory reads better as "~" than as the username.
let components = path.split(separator: "/", omittingEmptySubsequences: true)
if components.count == 2, components[0] == "Users" {
return "~"
}
let basename = (path as NSString).lastPathComponent
return basename.isEmpty ? nil : basename
}
private static func pathBasename(_ path: String) -> String? {
let basename = (path as NSString).lastPathComponent
return basename.isEmpty ? nil : basename
}
🤖 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
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentLaunchComposerView.swift`
around lines 282 - 290, Update pathBasename(_:) so it replaces the path with "~"
only when the /Users/<name> path matches the current Mac user's authoritative
home directory; otherwise return the actual basename, including for
/Users/Shared and other users' directories. Reuse the platform home-directory
source rather than treating every two-component /Users path as the home
directory.

Comment on lines +26 to +34
let resolver = AgentExecutableResolver(
configuredExecutablePaths: AgentExecutableResolver.cmuxConfiguredExecutablePaths()
)
let agents: [[String: Any]] = Self.mobilePromptLaunchableProviders.map { provider in
[
"id": provider.rawValue,
"name": provider.displayName,
"installed": (try? resolver.resolve(provider)) != nil,
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Move executable discovery and script staging off the main actor.

These synchronous handlers run from @MainActor mobileHostHandleRPC, so executable lookup, directory enumeration, metadata reads, writes, chmod, and deletion can block the UI. Make the RPC handlers async and perform this filesystem work through an actor or @concurrent async helper, hopping to MainActor only for TabManager snapshots and mutations.

As per coding guidelines, “flag changed CPU-heavy, file-I/O-heavy, parsing-heavy, or network-heavy async helpers called from UI isolation without an explicit actor hop or @concurrent boundary.”

Also applies to: 75-98, 135-165

🤖 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`+MobileAgentLaunch.swift around lines 26 - 34,
Update the mobile RPC handlers containing the AgentExecutableResolver discovery
and script staging logic, including the handlers around the referenced
provider-list, staging, and cleanup sections, to be async and move all
executable lookup, directory enumeration, metadata access, file writes, chmod,
and deletion behind a dedicated actor or `@concurrent` async helper. Keep only
TabManager snapshots and mutations on MainActor, and await the background
filesystem operations from mobileHostHandleRPC.

Source: Coding guidelines

Comment on lines +91 to +99
switch AgentPromptWorkspaceLaunch.startupInput(command: command) {
case let .inline(line):
startupInput = line
case let .script(body):
guard let scriptURL = Self.writeMobileAgentLaunchScript(body: body) else {
return .err(code: "internal_error", message: "Could not stage the launch script", data: nil)
}
startupInput = AgentPromptWorkspaceLaunch.scriptInvocation(scriptPath: scriptURL.path)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Delete staged prompt scripts deterministically.

Pruning runs only during a later staged launch. After a successful launch—or if workspace creation fails—the script containing the user's prompt can remain indefinitely. Have the launcher self-delete on exit and explicitly remove it when workspace creation fails; opportunistic pruning should only recover crash leftovers.

Also applies to: 132-165

🤖 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`+MobileAgentLaunch.swift around lines 91 - 99,
Update the staged launch-script flow around writeMobileAgentLaunchScript,
scriptInvocation, and the workspace-creation failure path so each script deletes
itself on launcher exit and is explicitly removed when workspace creation fails.
Retain the script URL for cleanup, and limit pruning to recovering scripts left
behind by crashes rather than normal successful launches.

cmux reload-cloud and others added 2 commits July 17, 2026 00:05
NSImage's escaping drawing handler referenced the view's color property
without explicit capture semantics, which is a hard compile error; main
merged #8270 with PR CI off, so
tagged app builds from current main fail. Capture the color by value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines 370 to +377
filter.pruneMachinesForFilterMenu(visibleMacSelection: selection)
}
#if os(iOS)
.fullScreenCover(isPresented: $isPresentingAgentLaunchComposer) {
if let launchAgent {
AgentLaunchComposerView(
draft: $agentLaunchDraft,
fetchOptions: fetchAgentLaunchOptions ?? { nil },

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 User trapped in blank fullScreenCover on Mac disconnect

When the Mac disconnects while the composer is open, launchAgent becomes nil (since launchAgentIfConnectedClosure returns nil when store.supportsAgentLaunch is false). SwiftUI re-renders the fullScreenCover body with an empty view — isPresentingAgentLaunchComposer stays true, but there is no Cancel button, no NavigationStack, and no dismiss() call site. On iOS, fullScreenCover does not support interactive swipe-to-dismiss by default, so the user has no escape other than force-quitting the app.

Fix: add an .onChange guard that dismisses the cover when the capability disappears:

.onChange(of: launchAgent == nil) { _, isNil in
    if isNil && isPresentingAgentLaunchComposer {
        isPresentingAgentLaunchComposer = false
    }
}

Attach this alongside the existing .fullScreenCover.

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Merged origin/main through 01cf217 (PR 8299, iOS reconnect/build-isolation fixes) per Aziz. Current main fails to compile the macOS app (Swift 6 capture error in SidebarWorkspaceRowSlotViews.swift from #8270, merged with PR CI off), so this branch carries the one-line fix b1ccdb0 to stay buildable; the same fix is up standalone as #8326 — if that merges first, the duplicate here resolves cleanly. Re-verified on the merged head: tagged macOS + iOS sim rebuilt, composer → launch → live agent terminal smoke passed.

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants