Repository navigation
iOS: launch agent workspaces from the task composer - #7670
Conversation
A bottom-right floating plus button on the workspace list opens a New Task
composer: type a prompt, pick a task template (Claude/Codex/Shell seeded,
user-editable with SF Symbol or emoji icons), pick the target Mac, and set
the working directory. Submit creates a workspace on that Mac via the
existing workspace.create RPC, now sending title, working_directory,
initial_command, and initial_env from iOS.
Prompt injection is a pure composer: {prompt} placeholder or appended
shell-quoted argument, plus CMUX_TASK_PROMPT in the environment for
multi-line scripts. Templates persist device-locally in a UserDefaults
JSON store behind MobileTaskTemplateStoring. The Mac now expands a
leading ~ in working_directory.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds mobile task composer support with template models, persistence, prompt-based command composition, workspace-create routing, SwiftUI UI, localization, and app wiring. Separately updates terminal workspace creation to normalize and expand working-directory inputs. ChangesMobile Task Composer feature
Working directory tilde expansion fix
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant TaskComposerSheet
participant MobileShellComposite
participant RemoteMac
User->>TaskComposerSheet: Select template, Mac, prompt, and directory
TaskComposerSheet->>MobileShellComposite: submitTaskComposer(macDeviceID, spec)
MobileShellComposite->>MobileShellComposite: Switch and validate pinned Mac context
MobileShellComposite->>RemoteMac: workspace.create
RemoteMac-->>MobileShellComposite: Create result
MobileShellComposite-->>TaskComposerSheet: Success or mapped failure
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (5 errors, 2 warnings)
✅ Passed checks (18 passed)
✨ Finishing Touches🧪 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 |
…with TaskTemplateIcon view
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.swift`:
- Around line 717-719: The create-workspace in-flight state is split across
three separately mutated optionals, which makes it easy for
`createWorkspaceTask`, `createWorkspaceTaskGroupID`, and
`createWorkspaceTaskSpec` to drift out of sync. Refactor the related state in
`MobileShellComposite` into a single model (for example, an `enum` or `struct`
that represents idle vs in-flight) and update the call sites in
`createWorkspaceRequest`, `createWorkspace`, `cancelRemoteOperationTasks`, and
`clearCreateWorkspaceTask` to read/write that one source of truth. Ensure the
new type carries the task, group ID, and spec together so partial updates are no
longer representable.
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileTaskTemplateStore.swift`:
- Around line 31-38: The UserDefaultsMobileTaskTemplateStore is annotated with
`@Observable` but has no tracked mutable state, so SwiftUI won’t observe CRUD
changes. Update the store to either expose and mutate a private(set) templates
property from addTemplate, updateTemplate, and deleteTemplate, or remove
`@Observable` if listTemplates/refreshTemplates remains the intended refresh path.
Keep the fix centered on UserDefaultsMobileTaskTemplateStore and its CRUD
methods so the observation behavior matches the implementation.
In
`@Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileTaskCommandComposer.swift`:
- Around line 32-34: The shellQuoted helper on MobileTaskCommandComposer is
exposed too broadly as public even though it is only an internal implementation
detail. Change shellQuoted to internal so it remains available to `@testable`
imports and tests, but does not widen the public API surface of
CmuxMobileShellModel.
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskTemplateIconPicker.swift`:
- Around line 5-9: The picker’s local state is not initialized from an existing
custom-emoji selection, so reopening it can show a blank Custom emoji field and
no highlighted symbol even when TaskTemplateIconPicker.selection already
contains a custom emoji. Update TaskTemplateIconPicker so emojiInput is seeded
from the incoming selection when the view appears or when selection changes, and
make sure the current selection is still reflected in the grid/highlight logic
even when it is not one of the static symbols.
- Around line 64-74: The file-scope `taskTemplateIcon` helper is being used as a
cross-file API, so it should no longer live as a free function. Move this
rendering logic behind `MacAvatarIcon` (for example as a view-producing property
or method on the existing enum/type) and update `TaskComposerSheet.swift` and
`TaskTemplateEditorView.swift` to call through
`MacAvatarIcon.resolve(custom:defaultSymbol:)` instead. Keep the accessibility
and symbol/emoji rendering behavior the same, but make the API owned by
`MacAvatarIcon` instead of the top-level `taskTemplateIcon` function.
🪄 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: a8855535-d7e5-402f-8deb-0a5323a6aaeb
📒 Files selected for processing (21)
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TaskComposer.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+WorkspaceCreateRequest.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileTaskTemplateStore.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ComposerSubmitRoutingTestSupport.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileTaskComposerSubmitTests.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileTaskTemplateStoreTests.swiftPackages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileTaskCommandComposer.swiftPackages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileTaskTemplate.swiftPackages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileTaskCommandComposerTests.swiftPackages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileTaskTemplateTests.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerButton.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerSheet.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskTemplateEditorView.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskTemplateIconPicker.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceShellView.swiftSources/TerminalController+WorkspaceCreate.swiftcmux.xcodeproj/project.pbxprojcmuxTests/WorkspaceCreateWorkingDirectoryTests.swiftios/cmux/Resources/Localizable.xcstringsios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift
Greptile SummaryThis PR adds a floating "New Task" composer to the iOS workspace list, allowing users to create workspaces from agent templates (Claude, Codex, OpenCode, or plain shell) with a selected Mac, working directory, and prompt. Workspace creation is made idempotent via stable
Confidence Score: 5/5This PR is safe to merge. The idempotency, connection-pinning, and session-generation guards are all correctly layered, and the previously flagged issues from earlier rounds have been resolved. The workspace creation path was thoroughly reworked: idempotency tombstones are written durably before the workspace constructor runs, pinned-context dispositions cover every cancellation/reconnect race, and session-generation checks isolate each account's draft from the next. Actor isolation is maintained throughout — the cache and template store are accessed exclusively from @mainactor contexts. All new user-facing strings have both en and ja translations. The submission state machine correctly prevents double-submission and handles mid-flight dismissal. No correctness bugs were found in the changed paths. No files require special attention. The most complex files — WorkspaceCreatePinnedContext, WorkspaceCreateIdempotencyCache, and MobileShellComposite+WorkspaceCreateRequest — all have corresponding tests that exercise their critical paths. Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant UI as TaskComposerSheet (iOS)
participant Store as MobileShellComposite
participant RPC as MobileCoreRPCSession
participant Mac as TerminalController (Mac)
participant Cache as WorkspaceCreateIdempotencyCache
UI->>Store: submitTaskComposer(macDeviceID, spec, willStartCreate)
Store->>Store: switchToMac(macDeviceID) if needed
Store->>Store: captureWorkspaceCreateContext() → pinnedContext
Store->>Store: createWorkspaceRequest(spec, pinnedContext)
Store->>Store: "willStartCreate() → submissionPhase = .committed"
Store->>RPC: sendRequest(workspace.create, params+operation_id)
RPC->>Mac: workspace.create RPC
Mac->>Cache: containsCompletedOperation(operationID)?
alt Already completed
Cache-->>Mac: true → return workspace list (idempotent)
else First time
Mac->>Mac: v2ValidateMobileWorkingDirectory(path)
Mac->>Cache: accept(operationID) [durable]
Mac->>Mac: addWorkspace(title, command, env, dir)
Mac->>Cache: associate(operationID, workspaceID)
Mac-->>RPC: workspace list + createdWorkspaceID
end
RPC-->>Store: Result
Store->>Store: postResponseDisposition → .apply / .failClosed / .preserveSuccess
alt .apply (current context, not cancelled)
Store->>Store: applyRemoteWorkspaceList + setSelectedWorkspaceID
Store-->>UI: .success(())
UI->>Store: completeTaskComposerSubmission(snapshot)
UI->>UI: dismiss()
else .failClosed (cancelled or stale connection)
Store-->>UI: .failure(.notConnected)
UI->>Store: persistTaskComposerDraft(snapshot.draft)
UI->>UI: show error, keep draft
end
%%{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 UI as TaskComposerSheet (iOS)
participant Store as MobileShellComposite
participant RPC as MobileCoreRPCSession
participant Mac as TerminalController (Mac)
participant Cache as WorkspaceCreateIdempotencyCache
UI->>Store: submitTaskComposer(macDeviceID, spec, willStartCreate)
Store->>Store: switchToMac(macDeviceID) if needed
Store->>Store: captureWorkspaceCreateContext() → pinnedContext
Store->>Store: createWorkspaceRequest(spec, pinnedContext)
Store->>Store: "willStartCreate() → submissionPhase = .committed"
Store->>RPC: sendRequest(workspace.create, params+operation_id)
RPC->>Mac: workspace.create RPC
Mac->>Cache: containsCompletedOperation(operationID)?
alt Already completed
Cache-->>Mac: true → return workspace list (idempotent)
else First time
Mac->>Mac: v2ValidateMobileWorkingDirectory(path)
Mac->>Cache: accept(operationID) [durable]
Mac->>Mac: addWorkspace(title, command, env, dir)
Mac->>Cache: associate(operationID, workspaceID)
Mac-->>RPC: workspace list + createdWorkspaceID
end
RPC-->>Store: Result
Store->>Store: postResponseDisposition → .apply / .failClosed / .preserveSuccess
alt .apply (current context, not cancelled)
Store->>Store: applyRemoteWorkspaceList + setSelectedWorkspaceID
Store-->>UI: .success(())
UI->>Store: completeTaskComposerSubmission(snapshot)
UI->>UI: dismiss()
else .failClosed (cancelled or stale connection)
Store-->>UI: .failure(.notConnected)
UI->>Store: persistTaskComposerDraft(snapshot.draft)
UI->>UI: show error, keep draft
end
Reviews (35): Last reviewed commit: "Cancel owned Mac switch with composer ta..." | Re-trigger Greptile |
…e selection, test-support builders, and task composer types into own files - Move route-selection statics out of MobileShellComposite.swift into MobileShellComposite+RouteSelection.swift and ratchet the budget entry down to 7669 (the +9 task-composer lines are stored properties that cannot live in an extension). - Split ComposerSubmitRoutingTestSupport.swift (509 > 500 threshold) into support fixtures + ComposerSubmitRoutingStoreBuilders.swift. - One major type per file: MobileWorkspaceCreateSpec, MobileTaskTemplateStoring, MobileTaskComposition, TaskTemplateFormView, TaskTemplateIcon each get their own file.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerSheet.swift (2)
266-287: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDirectory is not recalculated when template selection changes via the editor.
refreshTemplates()updatesselectedTemplateIDwhen the current template is deleted, but never recalculatesdirectory. Similarly,addTemplate()selects a new template without resettingdidEditDirectoryor updating the directory. If the user deletes the selected template (falling back to a different one) or adds a new template with a different default directory, the directory field still shows the old value. This can lead to workspaces created with an unexpected working directory.🐛 Proposed fix: recalculate directory in refreshTemplates and reset didEditDirectory in addTemplate
private func addTemplate(_ template: MobileTaskTemplate) { store.taskTemplateStore?.addTemplate(template) selectedTemplateID = template.id + didEditDirectory = false } private func refreshTemplates() { templates = store.taskTemplateStore?.listTemplates() ?? [] if let selectedTemplateID, templates.contains(where: { $0.id == selectedTemplateID }) { - return + if !didEditDirectory { + directory = Self.suggestedDirectory( + template: selectedTemplate, + macDeviceID: selectedMacDeviceID, + templateStore: store.taskTemplateStore + ) + } + return } selectedTemplateID = templates.first?.id + if !didEditDirectory { + directory = Self.suggestedDirectory( + template: selectedTemplate, + macDeviceID: selectedMacDeviceID, + templateStore: store.taskTemplateStore + ) + } }🤖 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/TaskComposer/TaskComposerSheet.swift` around lines 266 - 287, The template selection flow in addTemplate() and refreshTemplates() leaves directory stale when the selected template changes. Update addTemplate(_:), refreshTemplates(), and the directory-related state so that selecting a newly added template resets didEditDirectory and recalculates directory from the current template, and when refreshTemplates() falls back to a different selectedTemplateID it also recomputes directory before returning.
81-82: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winStore and cancel the submission Task instead of fire-and-forget.
The
Task { await submit() }in the Create button action has meaningful lifecycle (network call + state mutations) but is not stored or cancelled. If the user dismisses the sheet mid-submission, the task continues orphaned. As per coding guidelines, fire-and-forget Tasks with real lifecycle should be stored and tied to a caller-owned operation.♻️ Proposed fix: store the Task and cancel on disappear
+ `@State` private var submitTask: Task<Void, Never>? // In the button action: - Task { await submit() } + submitTask = Task { await submit() } // Add cancellation on disappear: + .onDisappear { + submitTask?.cancel() + }🤖 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/TaskComposer/TaskComposerSheet.swift` around lines 81 - 82, The Create button in TaskComposerSheet currently launches submit() with a fire-and-forget Task, which leaves the submission running without ownership. Store the created Task in a sheet-owned property in TaskComposerSheet, reuse it for the button action instead of creating an untracked task, and cancel it when the sheet disappears or is dismissed. Make the lifecycle explicit around the submit() path so the task is tied to the view’s lifetime.Source: Coding guidelines
♻️ Duplicate comments (1)
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskTemplateIconPicker.swift (1)
5-9: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
emojiInputis still not initialized from the incomingselection.This was flagged in a previous review: when the picker opens with a custom emoji already in
selection, the emoji TextField is blank and no grid cell is highlighted. The current code still initializesemojiInput = ""without seeding fromselection.🐛 Proposed fix: seed emojiInput from the incoming selection
struct TaskTemplateIconPicker: View { `@Binding` var selection: String - `@State` private var emojiInput = "" + `@State` private var emojiInput: String + + init(selection: Binding<String>) { + self._selection = selection + let current = selection.wrappedValue + self._emojiInput = State(initialValue: Self.symbols.contains(current) ? "" : current) + }🤖 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/TaskComposer/TaskTemplateIconPicker.swift` around lines 5 - 9, TaskTemplateIconPicker still starts with an empty emojiInput instead of reflecting the incoming selection, so the TextField and highlighted symbol state are out of sync when a custom emoji is already selected. Update TaskTemplateIconPicker’s state setup so emojiInput is seeded from selection when the view is created, and keep the binding/selection logic in sync in the initializer or equivalent setup for the picker.
🤖 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.
Outside diff comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerSheet.swift`:
- Around line 266-287: The template selection flow in addTemplate() and
refreshTemplates() leaves directory stale when the selected template changes.
Update addTemplate(_:), refreshTemplates(), and the directory-related state so
that selecting a newly added template resets didEditDirectory and recalculates
directory from the current template, and when refreshTemplates() falls back to a
different selectedTemplateID it also recomputes directory before returning.
- Around line 81-82: The Create button in TaskComposerSheet currently launches
submit() with a fire-and-forget Task, which leaves the submission running
without ownership. Store the created Task in a sheet-owned property in
TaskComposerSheet, reuse it for the button action instead of creating an
untracked task, and cancel it when the sheet disappears or is dismissed. Make
the lifecycle explicit around the submit() path so the task is tied to the
view’s lifetime.
---
Duplicate comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskTemplateIconPicker.swift`:
- Around line 5-9: TaskTemplateIconPicker still starts with an empty emojiInput
instead of reflecting the incoming selection, so the TextField and highlighted
symbol state are out of sync when a custom emoji is already selected. Update
TaskTemplateIconPicker’s state setup so emojiInput is seeded from selection when
the view is created, and keep the binding/selection logic in sync in the
initializer or equivalent setup for the picker.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 5b4d4ac5-14e2-4374-a829-0f558fbcd8de
📒 Files selected for processing (3)
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerSheet.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskTemplateEditorView.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskTemplateIconPicker.swift
- Guard submit() against re-entry and hold the submit Task in state; Cancel and drag-to-dismiss now cancel it, and a cancelled submit drops its result instead of persisting last-used defaults (Codex P2 + Greptile P1). - Seed the custom-emoji field from an existing emoji selection so reopening the icon picker shows the current icon (CodeRabbit). - Narrow shellQuoted to internal; tests use @testable (CodeRabbit). - Drop inert @observable from UserDefaultsMobileTaskTemplateStore: it has no tracked stored state, views re-read after mutations (CodeRabbit).
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerSheet.swift (2)
108-113: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winDisable Cancel button during submission.
The Cancel button is not disabled when
isSubmittingis true, so the user can dismiss the sheet while the fire-and-forgetTask { await submit() }is still running. The workspace creation continues after dismissal, which may surprise the user.🔧 Proposed fix
ToolbarItem(placement: .cancellationAction) { Button(L10n.string("mobile.common.cancel", defaultValue: "Cancel")) { dismiss() } + .disabled(isSubmitting) }🤖 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/TaskComposer/TaskComposerSheet.swift` around lines 108 - 113, Disable the Cancel action in TaskComposerSheet while submission is in progress. Update the Button inside the toolbar’s .cancellationAction to respect isSubmitting so the sheet cannot be dismissed during the Task { await submit() } flow, and keep the state tied to the existing submit lifecycle in TaskComposerSheet/submit(). Use the same isSubmitting flag already used by the form controls to gate the Cancel button’s enabled state.
266-287: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDirectory not recalculated after template add or edit.
addTemplateselects a new template but doesn't updatedirectoryor resetdidEditDirectory, so the directory field won't reflect the new template'sdefaultDirectory. Similarly,refreshTemplatesreloads templates after the editor is dismissed but never recalculates the directory when!didEditDirectory, so editing the selected template'sdefaultDirectoryin the editor leaves the field stale.Compare with
templateChip(line 207-228), which correctly resetsdidEditDirectory = falseand recalculates the directory on selection.addTemplateshould follow the same pattern, andrefreshTemplatesshould recalculate when the user hasn't manually edited.🔧 Proposed fix
private func addTemplate(_ template: MobileTaskTemplate) { store.taskTemplateStore?.addTemplate(template) selectedTemplateID = template.id + failureText = nil + didEditDirectory = false + directory = Self.suggestedDirectory( + template: template, + macDeviceID: selectedMacDeviceID, + templateStore: store.taskTemplateStore + ) } private func updateTemplate(_ template: MobileTaskTemplate) { store.taskTemplateStore?.updateTemplate(template) } private func deleteTemplates(_ offsets: IndexSet) { for index in offsets { store.taskTemplateStore?.deleteTemplate(id: templates[index].id) } } private func refreshTemplates() { templates = store.taskTemplateStore?.listTemplates() ?? [] if let selectedTemplateID, templates.contains(where: { $0.id == selectedTemplateID }) { + if !didEditDirectory { + directory = Self.suggestedDirectory( + template: selectedTemplate, + macDeviceID: selectedMacDeviceID, + templateStore: store.taskTemplateStore + ) + } return } selectedTemplateID = templates.first?.id + if !didEditDirectory { + directory = Self.suggestedDirectory( + template: selectedTemplate, + macDeviceID: selectedMacDeviceID, + templateStore: store.taskTemplateStore + ) + } }🤖 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/TaskComposer/TaskComposerSheet.swift` around lines 266 - 287, `TaskComposerSheet` is not keeping the directory state in sync after template changes. Update `addTemplate(_:)` so it also resets `didEditDirectory` and recalculates `directory` from the newly added template’s `defaultDirectory`, mirroring the selection flow used by `templateChip`. Also update `refreshTemplates()` to recompute `directory` from the currently selected template whenever `didEditDirectory` is false, so edits to a template’s `defaultDirectory` are reflected after the editor closes.
♻️ Duplicate comments (1)
Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileTaskCommandComposer.swift (1)
11-13: 📐 Maintainability & Code Quality | 🟠 Major | 💤 Low value
shellQuotedremainspublic— still should beinternal.This was previously flagged: tests use
@testable import CmuxMobileShellModel, sointernalaccess is sufficient. Exposing it aspublicunnecessarily widens the package's API surface.♻️ Proposed fix
- public func shellQuoted(_ value: String) -> String { + func shellQuoted(_ value: String) -> String {🤖 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/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileTaskCommandComposer.swift` around lines 11 - 13, The shellQuoted helper in MobileTaskCommandComposer is still exposing more API surface than needed; change its access level from public to internal since the test target already uses `@testable` import and can still access it. Update the declaration of shellQuoted in MobileTaskCommandComposer to keep it package-internal while preserving its current quoting behavior.
🤖 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`+RouteSelection.swift:
- Around line 13-19: Mark the pure route-selection helpers on
MobileShellComposite as nonisolated static so they do not inherit `@MainActor`
isolation; update prefersNonLoopbackRoutes and the related static helpers
firstReconnectHostPortRoute and isIPLiteralHost in
MobileShellComposite+RouteSelection to remain value-only helpers without
unnecessary actor hops.
---
Outside diff comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerSheet.swift`:
- Around line 108-113: Disable the Cancel action in TaskComposerSheet while
submission is in progress. Update the Button inside the toolbar’s
.cancellationAction to respect isSubmitting so the sheet cannot be dismissed
during the Task { await submit() } flow, and keep the state tied to the existing
submit lifecycle in TaskComposerSheet/submit(). Use the same isSubmitting flag
already used by the form controls to gate the Cancel button’s enabled state.
- Around line 266-287: `TaskComposerSheet` is not keeping the directory state in
sync after template changes. Update `addTemplate(_:)` so it also resets
`didEditDirectory` and recalculates `directory` from the newly added template’s
`defaultDirectory`, mirroring the selection flow used by `templateChip`. Also
update `refreshTemplates()` to recompute `directory` from the currently selected
template whenever `didEditDirectory` is false, so edits to a template’s
`defaultDirectory` are reflected after the editor closes.
---
Duplicate comments:
In
`@Packages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileTaskCommandComposer.swift`:
- Around line 11-13: The shellQuoted helper in MobileTaskCommandComposer is
still exposing more API surface than needed; change its access level from public
to internal since the test target already uses `@testable` import and can still
access it. Update the declaration of shellQuoted in MobileTaskCommandComposer to
keep it package-internal while preserving its current quoting behavior.
🪄 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: 3e5dc5f6-eeac-4ed7-82c4-aed1314bf735
⛔ Files ignored due to path filters (1)
.github/swift-file-length-budget.tsvis excluded by!**/*.tsv
📒 Files selected for processing (15)
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+RouteSelection.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+WorkspaceCreateRequest.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileTaskTemplateStore.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileTaskTemplateStoring.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileWorkspaceCreateSpec.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ComposerSubmitRoutingStoreBuilders.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/ComposerSubmitRoutingTestSupport.swiftPackages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileTaskCommandComposer.swiftPackages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileTaskComposition.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerSheet.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskTemplateEditorView.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskTemplateFormView.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskTemplateIcon.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskTemplateIconPicker.swift
💤 Files with no reviewable changes (5)
- Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskTemplateIconPicker.swift
- Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileTaskTemplateStore.swift
- Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskTemplateEditorView.swift
- Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
- Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+WorkspaceCreateRequest.swift
Seed names are user-facing and persisted; a Japanese fresh install kept the English 'Shell' label. MobileTaskTemplate.seedDefaults now takes the display names, the UserDefaults store passes L10n-resolved values, and the catalog gains mobile.taskComposer.template.seed.* (en/ja).
…with template changes - A sent workspace.create cannot be recalled, so Cancel is disabled and interactive dismissal is blocked while a submit is in flight; the bounded RPC always reports success/failure in the sheet instead of a cancelled sheet silently creating a remote workspace (Codex P1). - Directory field now recomputes through one syncSuggestedDirectory() path when templates are added, deleted, or edited, not just on chip taps, so Create cannot send a deleted/previous template's path (Cursor Bugbot). Chip taps and Mac switches share the same helper.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskTemplateIconPicker.swift (1)
56-69: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winTapping a symbol leaves the custom-emoji field showing a stale value.
The new
init(lines 9-15) can now seedemojiInputwith a non-empty custom emoji. If the user then taps a symbol iniconButton,selectionupdates butemojiInputis never cleared, so the text field still displays the old emoji even though a symbol is now selected — a newly-reachable inconsistent UI state introduced by the init fix (previouslyemojiInputalways started empty, so this couldn't happen).🐛 Proposed fix: clear the field when a symbol is chosen
private func iconButton(value: String) -> some View { let isSelected = selection == value Button { selection = value + if !emojiInput.isEmpty { + emojiInput = "" + } } label: {🤖 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/TaskComposer/TaskTemplateIconPicker.swift` around lines 56 - 69, In TaskTemplateIconPicker, selecting a symbol via iconButton currently updates selection but leaves emojiInput unchanged, which can keep the custom-emoji field stale after the new init seeds it with a value. Update the Button action inside iconButton(value:) so it also clears emojiInput when a symbol is chosen, keeping the selection state and text field in sync.
🤖 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.
Outside diff comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskTemplateIconPicker.swift`:
- Around line 56-69: In TaskTemplateIconPicker, selecting a symbol via
iconButton currently updates selection but leaves emojiInput unchanged, which
can keep the custom-emoji field stale after the new init seeds it with a value.
Update the Button action inside iconButton(value:) so it also clears emojiInput
when a symbol is chosen, keeping the selection state and text field in sync.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 14831cc8-c19e-40b7-a74d-b6653ec58c74
📒 Files selected for processing (7)
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileTaskTemplateStore.swiftPackages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileTaskCommandComposer.swiftPackages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileTaskTemplate.swiftPackages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileTaskTemplateTests.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskComposerSheet.swiftPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TaskComposer/TaskTemplateIconPicker.swiftios/cmux/Resources/Localizable.xcstrings
Restored lastMacDeviceID is now validated against displayPairedMacs before use, mirroring the template-id validation, so a forgotten Mac cannot become the default target (Cursor Bugbot).
Dogfood feedback: the floating button sat too high (bottom padding 20 -> 6) and the seeded templates used generic SF Symbols. Claude/Codex/OpenCode now render their brand images (AgentIcons imagesets copied from the macOS catalog into CmuxMobileShellUI resources, agent: icon values), OpenCode is seeded as a fourth default, and the icon picker offers the brand icons ahead of the symbol grid. Template storage keys bump to v2 (feature is unshipped; dogfood-only v1 data is dropped and cleaned up).
Namespaced (AgentIcons/...) image lookups fail at runtime inside the SwiftPM resource bundle: CoreUI reports 'No image named ... found in asset catalog' even though assetutil shows the namespaced entries in the compiled Assets.car. Root-level names resolve.
Dev reloads pass PRODUCT_BUNDLE_IDENTIFIER as a global xcodebuild override, which stamps every SwiftPM resource bundle with the app's own identifier. CoreUI registers asset catalogs per bundle identifier, so the package catalog loses to the app catalog and Image(named:) fails for every entry (namespaced or flat). Ship the PNGs as .copy resources and load @3x files by explicit URL with a dark-variant pick for Codex.
A workspace.create that throws after the connection generation changed mid-flight is mapped to .success, so the composer dismisses and persists last-used defaults for a task that was never created (Cursor bot finding on PR 7670). Red commit; fix follows.
A workspace.create that throws after the connection generation changed mid-flight was mapped to .success, so the composer dismissed and persisted last-used defaults for a task that was never created. Only a cancelled request (whose result the sheet drops) still maps to success; a stale failure now returns its mapped failure while skipping the connection-state side effects that belong to the new connection.
submitTaskComposer skipped switchToMac whenever the selected Mac id matched foregroundMacDeviceID, but a dropped connection leaves that id in place with remoteClient nil, so submit failed as not-connected without attempting a reconnect. Also switch when remoteClient is nil; switchToMac already short-circuits when the foreground connection is genuinely live.
Bugbot is paused — on-demand spend limit reachedBugbot 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. |
# Conflicts: # Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCSession.swift # Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+WorkspaceCreateRequest.swift # Sources/TerminalController.swift
# Conflicts: # Packages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCSession.swift
# Conflicts: # ios/cmuxPackage/Sources/cmuxFeature/CMUXMobileRootScene.swift
Summary
Verification
Evidence
Checklist
Dogfood approval is required before merge.