Skip to content

Fix iOS workspace swipe-delete confirmation crash - #6051

Merged
azooz2003-bit merged 5 commits into
mainfrom
fix-ios-delete-swipe-crash
Jun 13, 2026
Merged

azooz2003-bit merged 5 commits into
mainfrom
fix-ios-delete-swipe-crash

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jun 13, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • move iOS workspace delete confirmation state from reusable rows to WorkspaceListView
  • keep destructive delete as reveal-and-tap instead of full-swipe auto-presentation

Repro fixed

Swipe-delete one workspace, then swipe-delete the row above it. The confirmation dialog is now owned by the list instead of a recycled row.

Verification

  • git diff --check HEAD~1..HEAD
  • ./scripts/lint-ios-package-conventions.sh
  • python3 scripts/swift_file_length_budget.py --budget .github/swift-file-length-budget.tsv
  • ./scripts/reload-cloud.sh --tag wdel (cloud unavailable, local fallback succeeded)
  • ios/scripts/reload.sh --tag wdel

Localization

No new user-facing strings. Existing localized delete confirmation strings were moved from row-level presentation to list-level presentation.


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


Summary by cubic

Fixes an iOS crash when swipe-deleting workspaces by moving delete-confirmation state to WorkspaceListView and presenting the native confirmation dialog from the swiped row. Full-swipe now opens the confirm dialog (no auto-delete); recycled cells no longer own presentation, and swipe and context-menu deletes share the same flow.

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

Review in cubic

Summary by CodeRabbit

  • New Features

    • Centralized confirmation flow for closing workspaces: rows now request and reflect a single pending close state.
    • Confirmation actions are conditionally enabled so destructive close only proceeds when confirmation is available.
  • Bug Fixes

    • Dismissing or canceling confirmation reliably clears the pending close state.
    • Swipe/delete and context-menu actions consistently follow the confirm-or-immediate-close behavior; selection/tap no longer interferes.

@vercel

vercel Bot commented Jun 13, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 13, 2026 7:14pm
cmux-staging Building Building Preview, Comment Jun 13, 2026 7:14pm

@coderabbitai

coderabbitai Bot commented Jun 13, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

List-level state tracks a pending workspace close; rows request closure via list helpers and receive an external isConfirmingClose binding and optional confirmCloseWorkspace callback. Swipe and context delete actions call closeWorkspace directly; confirmation dialog is presented via the list-managed binding and confirmed via the list's confirm helper.

Changes

Workspace Close Confirmation Flow

Layer / File(s) Summary
List: pending-close state and helpers
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swift
Adds @State workspacePendingCloseID, requestWorkspaceClose, closeConfirmationBinding(for:), and confirmCloseWorkspace() to track and manage pending close confirmations.
List → Row wiring changes
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swift
Updates workspaceRow to pass closeWorkspace: requestWorkspaceClose, isConfirmingClose: closeConfirmationBinding(for:), and conditional confirmCloseWorkspace closure.
Row API and confirmation UI externalization
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceNavigationRow.swift
Replaces row-local @State confirmation with var isConfirmingClose: Binding<Bool> = .constant(false) and optional var confirmCloseWorkspace: ((MobileWorkspacePreview.ID) -> Void)? = nil; trailing swipe and context-menu delete now call closeWorkspace directly and the confirmation dialog is driven by the external binding.

Possibly related PRs

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰
I nudge a row to whisper "close",
You hold the seed until it grows.
A swipe, a tray, a cautious cheer,
Confirmed — the workspace hops from here.
🥕

🚥 Pre-merge checks | ✅ 19 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description includes a clear Summary section explaining what changed and why, comprehensive Verification section with specific commands run, and Localization notes. However, the required Testing, Demo Video, Review Trigger, and Checklist sections from the template are missing or incomplete. Complete the Testing section with explicit manual verification steps, add a Demo Video URL if UI changes warrant it, include the Review Trigger block, and fill out the Checklist items to match the template structure.
✅ Passed checks (19 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and specifically describes the primary fix: resolving an iOS workspace swipe-delete confirmation crash by moving state ownership to the list view.
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 workspace close confirmation moved via SwiftUI @State/Binding in WorkspaceListView.swift/WorkspaceNavigationRow.swift; no new async/service protocols, Sendable ref types, or background UI-store acc...
Cmux Swift Blocking Runtime ✅ Passed Checked PR #6051 patch against swift-blocking-runtime rules; no added primitives like Task.sleep, DispatchSemaphore/Group.wait, asyncAfter, Timer, DispatchQueue.main.sync, or NSLock found.
Cmux Expensive Synchronous Load ✅ Passed In the updated WorkspaceListView.swift/WorkspaceNavigationRow.swift, I found no RestorableAgentSessionIndex.load/sysctl or other sync heavy loader calls; changes only manage delete-confirmation sta...
Cmux Cache Substitution Correctness ✅ Passed PR only refactors Swift UI state/bindings for workspace delete confirmation (list-owned pending ID). No diff swaps fresh authoritative reads for cached values in persistence/history/undo/snapshot p...
Cmux No Hacky Sleeps ✅ Passed PR changes are confined to Swift UI rows (workspacePendingCloseID/confirmCloseWorkspace only appear in WorkspaceListView.swift & WorkspaceNavigationRow.swift); no non-Swift runtime scripts with hac...
Cmux Algorithmic Complexity ✅ Passed PR diff only adds confirmation state bindings/wrappers in WorkspaceListView & WorkspaceNavigationRow; no new filter/sort/contains(where:) or nested full-collection scans were introduced.
Cmux Swift Concurrency ✅ Passed Checked WorkspaceListView.swift and WorkspaceNavigationRow.swift for legacy concurrency markers (DispatchQueue/DispatchGroup/Combine/Task {}/sink/Future/continuations); none found in current diff c...
Cmux Swift @Concurrent ✅ Passed Modified Swift files contain no @concurrent, nonisolated async, or UI-called Task/await helpers; close-confirmation flow is synchronous via @State/Binding callbacks.
Cmux Swift File And Package Boundaries ✅ Passed PR only refactors iOS delete-confirmation UI/state in existing views (WorkspaceListView 368 lines, WorkspaceNavigationRow 125; +39/+17 diff) and doesn’t add mixed responsibilities or oversized-file...
Cmux Swift Logging ✅ Passed PR diff (commit fe1e44e) for WorkspaceListView.swift/WorkspaceNavigationRow.swift adds no print/debugPrint/dump/NSLog or new Logger constants, per swift-logging rules.
Cmux User-Facing Error Privacy ✅ Passed Inspected WorkspaceListView.swift and WorkspaceNavigationRow.swift: user-facing text is generic delete confirmation (“Delete Workspace?”, “Cancel”, “Mac”); no upstream/provider names, raw payloads,...
Cmux Full Internationalization ✅ Passed PR i18n: user-facing Swift text in WorkspaceNavigationRow uses L10n.string (e.g., mobile.workspace.delete.confirmTitle) and keys exist in ios/cmux/Resources/Localizable.xcstrings for en+ja; no web...
Cmux Swiftui State Layout ✅ Passed PR changes only add list-owned @State workspacePendingCloseID and Binding-based confirmation wiring; no ObservableObject/@Published/@StateObject/@EnvironmentObject, GeometryReader measurement, list...
Cmux Architecture Rethink ✅ Passed WorkspaceListView now owns delete-confirm state via workspacePendingCloseID and passes isConfirmingClose/confirmCloseWorkspace to rows; no sleep/asyncAfter/poll/locks/observers/side-channel pattern...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR diff only updates workspace delete confirmation UI; it contains no NSWindow/NSPanel/SwiftUI Window/WindowGroup or cmuxAuxiliaryWindowIdentifiers, so the auxiliary close-shortcut rule isn’t trigg...
Cmux Source Artifacts ✅ Passed git diff shows only one added file (Sources/GitPollClock.swift, ~1KB) and no added paths matching temp/cache/DerivedData/screenshots/recordings/artifact directories per the rules file.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix-ios-delete-swipe-crash

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 and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 13, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes the iOS crash caused by List recycling rows that owned @State private var isConfirmingClose. Confirmation state is moved to WorkspaceListView as a single workspacePendingCloseID optional, threaded down to rows via a computed Binding<Bool> and a separate confirm callback. The swipe-action button drops role: .destructive (replacing it with an explicit .tint(.red)) so a full swipe now triggers the confirmation flow rather than bypassing it.

  • WorkspaceListView gains workspacePendingCloseID: MobileWorkspacePreview.ID? as the single source of truth for pending delete confirmation, with three private helpers managing request, binding creation, and confirmation.
  • WorkspaceNavigationRow replaces @State private var isConfirmingClose with an injected Binding<Bool> prop and a confirmCloseWorkspace callback, decoupling presentation ownership from the reusable cell.
  • Context-menu and swipe-action delete paths both funnel through requestWorkspaceClose, ensuring consistent confirmation behavior across entry points.

Confidence Score: 5/5

The change is safe to merge; the crash repro is addressed and both delete paths (swipe and context menu) route through the same confirmation flow.

The root cause (row-owned @State surviving cell recycling) is correctly eliminated by moving state to the list. Both the swipe-action and context-menu paths funnel through requestWorkspaceClose, so no delete can bypass confirmation. The only observations are a misleading parameter signature on confirmCloseWorkspace — a design inconsistency that carries no runtime risk.

No files require special attention beyond the confirmCloseWorkspace signature cleanup noted in the inline suggestions.

Important Files Changed

Filename Overview
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swift Adds workspacePendingCloseID state at list scope and three helpers to own the delete-confirmation lifecycle; passes computed binding and confirm callback to each row.
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceNavigationRow.swift Removes per-row @State var isConfirmingClose; replaces it with an injected Binding prop and a separate confirmCloseWorkspace callback. The callback ignores its ID parameter, relying on workspacePendingCloseID in the parent.

Sequence Diagram

sequenceDiagram
    participant User
    participant Row as WorkspaceNavigationRow
    participant List as WorkspaceListView
    participant Mac as Mac

    User->>Row: "swipe trailing or context Delete"
    Row->>List: closeWorkspace(workspaceID)
    Note over List: workspacePendingCloseID = workspaceID
    List-->>Row: isConfirmingClose binding becomes true
    Row->>User: confirmationDialog appears

    alt User confirms
        User->>Row: tap Delete confirm
        Row->>List: confirmCloseWorkspace(workspaceID)
        Note over List: workspacePendingCloseID = nil
        List->>Mac: closeWorkspace(workspaceID)
    else User cancels
        User->>Row: tap Cancel or backdrop
        Row->>List: "isConfirmingClose.wrappedValue = false"
        Note over List: workspacePendingCloseID = nil
    end
Loading

Reviews (3): Last reviewed commit: "Keep iOS delete swipe native" | Re-trigger Greptile

Comment on lines +341 to +348
private var requestWorkspaceClose: ((MobileWorkspacePreview.ID) -> Void)? {
guard closeWorkspace != nil else {
return nil
}
return { workspaceID in
workspacePendingCloseID = workspaceID
}
}

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 requestWorkspaceClose computed var causes broad row invalidation on every delete gesture

requestWorkspaceClose is a computed property, so each evaluation of body produces a new closure instance. Because ((MobileWorkspacePreview.ID) -> Void)? does not conform to Equatable, SwiftUI cannot short-circuit the structural comparison for WorkspaceNavigationRow, and every visible row body re-evaluates whenever workspacePendingCloseID changes. Before this PR the confirmation state lived inside each row's own @State, so only the tapped row would re-render. Now, each swipe initiation and each dialog dismissal broadcasts to all rows in the list.

Rule Used: Flag SwiftUI changes that can cause stale state, b... (source)

@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.

Caution

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

⚠️ Outside diff range comments (1)
Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceNavigationRow.swift (1)

61-69: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Turn off full-swipe for the destructive trailing action.

Line 61 still lets a full trailing swipe trigger the delete request immediately, so the confirmation dialog auto-presents instead of preserving the intended reveal-and-tap flow from this PR. Set allowsFullSwipe to false here.

Suggested fix
-        .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+        .swipeActions(edge: .trailing, allowsFullSwipe: false) {
🤖 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/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceNavigationRow.swift`
around lines 61 - 69, The trailing swipe action currently allows a full swipe to
immediately invoke the destructive action; update the swipeActions modifier in
WorkspaceNavigationRow (the block using .swipeActions(edge: .trailing,
allowsFullSwipe: true) with the closeWorkspace Button) to set allowsFullSwipe to
false so the delete is revealed and must be tapped (i.e., change
allowsFullSwipe: true to allowsFullSwipe: false).
🤖 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/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceNavigationRow.swift`:
- Around line 61-69: The trailing swipe action currently allows a full swipe to
immediately invoke the destructive action; update the swipeActions modifier in
WorkspaceNavigationRow (the block using .swipeActions(edge: .trailing,
allowsFullSwipe: true) with the closeWorkspace Button) to set allowsFullSwipe to
false so the delete is revealed and must be tapped (i.e., change
allowsFullSwipe: true to allowsFullSwipe: false).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 34559bca-93f4-4173-a257-e399756070a4

📥 Commits

Reviewing files that changed from the base of the PR and between fe1e44e and d1de918.

📒 Files selected for processing (2)
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceListView.swift
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceNavigationRow.swift

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In
`@Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceNavigationRow.swift`:
- Line 61: The trailing swipe action in WorkspaceNavigationRow.swift currently
uses .swipeActions(edge: .trailing, allowsFullSwipe: true), which enables
full-swipe delete; change allowsFullSwipe to false so the trailing delete uses
reveal-and-tap confirmation (i.e., replace allowsFullSwipe: true with
allowsFullSwipe: false in the .swipeActions call in WorkspaceNavigationRow).
🪄 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: f5f7cbd9-b9ae-42b1-b3db-1a0b51d40b74

📥 Commits

Reviewing files that changed from the base of the PR and between 9d3c3ca and c075ddf.

📒 Files selected for processing (1)
  • Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceNavigationRow.swift

@@ -54,11 +60,12 @@ struct WorkspaceNavigationRow: View {
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Disable full-swipe on trailing delete to preserve reveal-and-tap confirmation flow.

Line 61 sets allowsFullSwipe: true, which allows a full swipe to trigger delete-request presentation directly. That conflicts with the stated behavior for this PR (“no full-swipe auto-presentation”). Set trailing delete swipe to allowsFullSwipe: false.

Proposed diff
-        .swipeActions(edge: .trailing, allowsFullSwipe: true) {
+        .swipeActions(edge: .trailing, allowsFullSwipe: false) {
🤖 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/CmuxMobileShellUI/Sources/CmuxMobileShellUI/WorkspaceNavigationRow.swift`
at line 61, The trailing swipe action in WorkspaceNavigationRow.swift currently
uses .swipeActions(edge: .trailing, allowsFullSwipe: true), which enables
full-swipe delete; change allowsFullSwipe to false so the trailing delete uses
reveal-and-tap confirmation (i.e., replace allowsFullSwipe: true with
allowsFullSwipe: false in the .swipeActions call in WorkspaceNavigationRow).

@azooz2003-bit
azooz2003-bit merged commit e52dfc1 into main Jun 13, 2026
31 of 32 checks passed
@azooz2003-bit
azooz2003-bit deleted the fix-ios-delete-swipe-crash branch June 13, 2026 21:35
hhsw2015 pushed a commit to hhsw2015/cmux that referenced this pull request Jun 14, 2026
PRs included:
- AppDelegate decomposition: CmuxSession session-snapshot repository (manaflow-ai#6030)
- Fix Cmd+T cwd after session restore (manaflow-ai#6055)
- Speed up iOS terminal scroll rendering (manaflow-ai#6035)
- Preserve Pi sessions across workspace restore (manaflow-ai#5607)
- Scope Biome checks to maintained JS sources (manaflow-ai#6008)
- Fix manaflow-ai#5917: restore OSC 11 pane-local backgrounds (manaflow-ai#5997)
- Expose stable window title templates (manaflow-ai#6059)
- Honor macos-option-as-alt left/right
- Fix macOS 27 SF Symbol rasterization crash (manaflow-ai#5999)
- CmuxRemote* family: extract Workspace remote/cloud-VM connectivity
- Fix iOS workspace swipe-delete confirmation crash (manaflow-ai#6051)
- TabManager decomposition Wave 3+4 sub-models
- Sidebar row cleanups: branchless frame anchor
- CmuxIPCService: extract AppDelegate multi-window CLI routing
- CmuxSidebarGit: extract TabManager git-metadata + PR-polling subsystem
- CmuxTerminalCore: extract terminal core leaf

Fork-side adjustments:
- ghostty submodule: cherry-pick mouse-modifier-state fix onto our renderer-realized branch
- Workspace.swift: take theirs (upstream extracted ~7700 lines into CmuxCore.Remote/CmuxRemoteSession packages); restore fork's renameTopLevelLayoutTabContaining/closeTopLevelLayoutTabContaining + surfaceTmuxClientTTYNames + WorkspaceLayoutTab integration
- TabManager.swift: take theirs; re-add static allocatePortOrdinal()
- BrowserPanelView, RenderableSystemSymbol: keep fork's cmuxSymbolPixelSize extension on top of upstream's cmuxSymbolRasterSize
- Add CmuxWorkspaces / CMUXSessionDaemon / CmuxCommandPalette imports to TerminalController, Workspace, SessionPersistence
- Sources/Workspace+P43Stubs.swift: thin shims for SplitEqualizer, WorkspaceRemoteSessionController.PortScanKickReason, WorkspaceGroupNewWorkspacePlacementSettings (legacy types fork TC still calls; replace with package APIs in P44+)
- Sources/GhosttySurfaceSizeDeferralReason.swift: restore fork-only enum (deleted by upstream)
- Sources/StableLayout/SessionBlueprintExportAction.swift: parked debug action (depends on legacy SessionPersistenceStore, gone)
- Sources/GhosttyTerminalView.swift: stub ghostty_surface_select_cursor_line_compat (needs zig 0.15.2 xcframework rebuild)
- pbxproj: keep-both, drop stale ProcessPipeReader/SplitEqualizer/Panels/BrowserProxyEndpoint refs, fix SurfaceHibernationPolicy UUID collision
- Drop fork's WorkspaceRemoteConfiguration.swift + WorkspaceRemoteSSHBatchCommandBuilder.swift (extracted to CmuxCore package)

This branch was successfully deployed

1 active deployment
Preview – cmux — c075ddf6 Deployed Jun 13, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant