fix(mobile): workspace row long-press opens only the menu; delete shows progress and tolerates teardown failures

Merged
SA
3mo ago
+310-138

Summary

  • Long-pressing a home workspace row on iOS opened the native context menu and navigated into the workspace. Root cause is in expo-router ≤ 56.2.18 (it never cancels react-native's in-flight touch when the menu opens); upstream's 56.2.19 Swift fix is carried here as a bun patch of our pinned version.
  • The delete flow gets a visible in-flight state (dimmed row, spinner, disabled tap/"+"), a real error message on failure, and stops letting a failing teardown script block or loop the delete. Same teardown fix applied to the workspace-screen sheet.

Why / Context

Reported: "pressing and holding does the normal click (navigates)" and "the delete flow has no signal and then it fails". Three distinct defects were under that:

  1. expo-router's Link.Menu puts a UIContextMenuInteraction on the trigger, but RN's RCTSurfaceTouchHandler can't be prevented by a descendant's gesture, so lifting the finger after the menu presents still delivers onPress. Upstream fixed it in 56.2.19 (cancelReactNativeTouches) — released 2026-08-17, blocked by bun's 3-day minimumReleaseAge and paired with a new expo-modules-core (bumping one module alone risks a launch crash, see patches/README.md).
  2. The teardown-failed retry sent force but not skipTeardown, so the script ran and failed again — an endless "Force delete" prompt.
  3. Relay caps a request at 30s while a teardown may run 60s; slow deletes surfaced as "Delete failed" while the host was still deleting.

How It Works

  • patches/expo-router@56.2.18.patch: verbatim upstream Swift — when UIKit asks for the menu configuration, find the nearest RCTSurfaceTouchHandler and toggle isEnabled so the press is delivered as touchesCancelled. Native change: dev clients need a rebuild to pick it up. Guard test reads the installed Swift file, so it keeps passing after the eventual bump; drop the patch then.
  • useWorkspaceRowActions (new, under WorkspaceRow/hooks/) owns rename/delete/copy/share + isDeleting; WorkspaceRowMenu is now purely the native Link.Menu (Delete marked destructive).
  • Delete: dirty worktree → "Delete anyway" (force, teardown still runs); teardown failed → retried once with skipTeardown automatically, no prompt (per Satya: the toast was annoying, and the script already ran); other errors alert with the host's message; on any other failure the host list is refetched and, because the host archives the row before slow work, a missing row means the delete committed (covers the relay timeout).
  • useHostWorkspaces cache ops gain refetchHost for that.

Manual QA Checklist

Verified on the iPhone 16e simulator with a native build of this branch, against a live host over the relay (standalone host-service, scratch project with two worktrees, one with a 6s teardown, one dirty with a teardown that exits 1). Screenshots + notes: https://claude.ai/code/artifact/eb182eda-203d-4622-8520-506ac8f09208

  • Long-press a row → native menu (Rename / Delete in red / Copy ID / Share); dismiss → still on home, no navigation
  • Delete a clean workspace → confirm alert → row dims with spinner, "+" disabled, for the whole teardown → row gone
  • Delete a dirty workspace with a failing teardown → "Worktree has uncommitted changes → Delete anyway" → deleted (filmed before the teardown prompt was removed; the retry now runs without asking — JS-only change, typechecked)
  • Not exercised: a >30s teardown (relay timeout path); code path is refetch-and-decide

Testing

  • bun run typecheck (mobile, after build:types)
  • bun run lint
  • bun test in apps/mobile (incl. the new patch guard)
  • Native build with the patch compiles (xcodebuild, Debug simulator)

Known Limitations

  • Old dev clients keep the old long-press behaviour until rebuilt (JS reload isn't enough).
  • The "before" claim rests on the mechanism + upstream's changelog; I did not re-film an unpatched binary side by side.

Follow-ups

  • Next Expo SDK 56 batch bump (expo-router ≥ 56.2.19 with its same-day expo-modules-core): delete the patch and its patchedDependencies entry.

Summary by cubic

Fixes iOS long-press on a workspace row so it opens only the native context menu (no navigation), and makes delete show progress while tolerating teardown failures and relay timeouts. Previously, long-press also fired the row’s onPress; delete had no in-flight UI and could loop or misreport failures.

Carries upstream Swift fix as a bun patch to expo-router@56.2.18 so the menu cancels the in-flight touch. While delete runs the row dims, a spinner replaces the icon, and tap and the "+" button are disabled. A teardown failure is retried once with skipTeardown; dirty worktrees prompt “Delete anyway”; other errors show the host’s message; after a relay timeout the host list is refetched and a missing row counts as committed.

  • Workspace header actions adopt the same delete retry/teardown and timeout handling.
  • useHostWorkspaces adds refetchHost to confirm commits after relay timeouts.

Rollout notes

  • Rebuild mobile dev clients to pick up the native expo-router patch.
  • When bumping to expo-router ≥ 56.2.19, remove patches/expo-router@56.2.18.patch and its patchedDependencies entry; the guard test continues to pass.

Reviewer notes

  • Adds the Swift patch and a guard test; updates package.json/bun.lock.
  • WorkspaceRowMenu is now a thin Link.Menu; actions moved into useWorkspaceRowActions with isDeleting wired into WorkspaceRow.