Repository navigation
iOS: compose a prompt and launch an agent workspace - #8314
azooz2003-bit wants to merge 5 commits into
Conversation
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>
📝 WalkthroughWalkthroughAdds 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. ChangesAgent launch RPC and Mac implementation
iOS agent launch flow
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
Possibly related PRs
Suggested reviewers: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (4 errors, 1 warning)
✅ Passed checks (20 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adds 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 (
Confidence Score: 4/5Safe 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
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
%%{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
Reviews (3): Last reviewed commit: "Fix Swift 6 capture error in sidebar PR ..." | Re-trigger Greptile |
| 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: "'\\''") + "'" | ||
| } | ||
| } |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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
📒 Files selected for processing (27)
Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileAgentLaunchOptionsResponse.swiftPackages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCClient.swiftPackages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileAgentLaunchOptionsResponseTests.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileMacUpdateCapabilityRequirement.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileMacUpdateFeature.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+AgentLaunch.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+WorkspaceActions.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentLaunchComposeBar.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentLaunchComposerView.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileMacUpdateHint+Display.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MobileWorkspaceMutationFailure+Copy.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView+Actions.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView+WorkspaceActionToast.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView+WorkspaceActions.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swiftPackages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/AgentPromptWorkspaceLaunch.swiftPackages/macOS/CMUXAgentLaunch/Tests/CMUXAgentLaunchTests/AgentPromptWorkspaceLaunchTests.swiftSources/Mobile/MobileHostService+Capabilities.swiftSources/Mobile/MobileHostService+TicketAuthorization.swiftSources/Mobile/MobileHostService.swiftSources/TerminalController+MobileAgentLaunch.swiftSources/TerminalController+WorkspaceCreate.swiftSources/TerminalController.swiftcmux.xcodeproj/project.pbxprojcmuxTests/MobileHostAuthorizationTests.swiftios/cmux/Resources/Localizable.xcstrings
| guard isCurrentRemoteOperation(client: client, generation: generation), !Task.isCancelled else { | ||
| return .success(()) | ||
| } |
There was a problem hiding this comment.
🎯 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
| 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 | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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 | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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, | ||
| ] |
There was a problem hiding this comment.
🚀 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
| 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) | ||
| } |
There was a problem hiding this comment.
🔒 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.
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>
| filter.pruneMachinesForFilterMenu(visibleMacSelection: selection) | ||
| } | ||
| #if os(iOS) | ||
| .fullScreenCover(isPresented: $isPresentingAgentLaunchComposer) { | ||
| if let launchAgent { | ||
| AgentLaunchComposerView( | ||
| draft: $agentLaunchDraft, | ||
| fetchOptions: fetchAgentLaunchOptions ?? { nil }, |
There was a problem hiding this comment.
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.
|
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. |
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.v1with two RPCs:mobile.agent.launch_optionsreports which coding agents resolve on the Mac (claude,codex, viaAgentExecutableResolver) and suggests working directories (the plain-create inherit directory first, then open workspaces' directories, deduped).mobile.workspace.launch_agentcreates the workspace through the sharedv2WorkspaceCreatepath withfocus=false, eager_load_terminal=true, so the pty spawns in the background immediately, and types'<resolved-executable>' '<prompt>'at the fresh login shell via Ghosttyinitial_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/zshlauncher script (AgentPromptWorkspaceLaunchinCMUXAgentLaunch, unit-tested). The workspace is titled from the prompt and the prompt is recorded viahandlePromptSubmit, so the sidebar and iOS rows immediately show what the agent is doing. The response is the same workspace-list payload asworkspace.createwithcreated_workspace_id.workspace.creategains aninitial_inputparam alongsideinitial_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.launchAgentWorkspacemirrorscreateRemoteWorkspace'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.v1registered withfirstReleasedMacVersion: nil).Ticket authorization treats
mobile.workspace.launch_agentexactly likeworkspace.create(Mac-wide tickets required for group placement; created workspace recorded for follow-up scoped calls), andmobile.agent.launch_optionslike 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.Localization: all new strings in
ios/cmux/Resources/Localizable.xcstringswith en + ja entries (11 keys); Mac side has no new user-facing strings.🤖 Generated with Claude Code
Need help on this PR? Tag
/codesmithwith 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.v1and a full-screen iOS composer so you can start agent tasks without a desktop terminal.New Features
workspace.launch_agent.v1withmobile.agent.launch_options(installed agents + suggested directories) andmobile.workspace.launch_agent(creates a workspace and types'<executable>' '<prompt>'at the login shell via Ghosttyinitial_input; inline within budget or via a staged script). Titles from the prompt and returns the usual workspace-list payload withcreated_workspace_id.workspace.createalso acceptsinitial_input.mobile.workspace.launch_agentauthorized likeworkspace.create(group placement still needs a Mac-wide ticket).mobile.agent.launch_optionsis 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
Written for commit b1ccdb0. Summary will update on new commits.
Summary by CodeRabbit