Multi-select on mobile (long-press a row, act on many)
On a phone, press and hold a session row to start selecting, tap more rows to add or remove them, then run one bulk action from the bar that replaces the tab bar. Covers the 500 ms long-press, how the selection is dismissed, and why the gesture occasionally does not fire on iOS Safari.
What it is
Multi-select sessions on mobile by long-pressing a row, then tap more rows to add or remove them, then run a bulk action from the tab-bar slot at the bottom of the screen.
Where to find it
A session row on a phone — in a project's list or the unified inbox. The actions appear in the selection bar at the bottom of the screen, in the slot the tab bar normally occupies.
How it behaves
Entering selection mode
- Press and hold any session row in the project sidebar or the unified inbox for 500 ms. The row enters the selection on release.
- Long-press is ADD-only. A second long-press on a row that is already selected is a no-op — the row stays selected, selection mode stays engaged. (To remove a row from the selection use a tap instead; see "Adding and removing rows" below.) This protects you from the common case of long-pressing the row you actually want as the seed of a multi-select while it happens to be the currently active session — pre-2026-05-23 that second long-press would silently exit selection mode.
- The session you have open comes along. Starting a fresh multi-select by long-pressing another row also folds in the session you currently have open (the one highlighted "by default"), so a bulk action acts on it too — matching desktop Ctrl+Click. It is folded in only on the first long-press of a fresh selection, only when it is in the same project, and never when the open session is the row you pressed. Don't want it included? Tap it to drop it from the selection.
- Sliding your finger up or down during the hold (vertical scroll) cancels the timer — scrolling never fires a selection.
- In the project sidebar, long-press selects session rows. In the unified inbox, you can also long-press any row (SMS, daily digest, approvals, alerts, …) into the selection — it goes into a separate inbox-keys selection. Bulk Snooze and Archive then act on every selected row whatever its type; Pause applies only to the sessions in the selection (a non-session row has no process to pause).
- Mobile only. On desktop the same rows use middle-click to archive — long-press does nothing.
Adding and removing rows
Once one row is selected, the UI is in selection mode and a single tap on any session row toggles it in or out — no second long-press needed.
- Tap a not-selected row → adds it to the selection
- Tap a selected row → removes it from the selection
- Tap the last selected row → exits selection mode (count goes to 0)
In selection mode the selected rows are the only tinted ones — so what's highlighted is exactly what's selected, and the tint count matches the N selected count. Every selected row shows the accent tint; every unselected row — including the session you have open — shows no tint. When the open session is folded into a fresh selection (see "Entering selection mode") it simply shows the selected tint like any other member; when it is not part of the selection it stays untinted rather than keeping its "currently open" highlight, so it can never be mistaken for a selected row. (Outside selection mode the open row keeps its usual active tint — that yields only while you are multi-selecting, and only on mobile; desktop keeps the active-wins highlight. See frontend-inbox-row-contract.md § "Mobile long-press multi-select contract" rule 5.)
The action bar (replaces the tab bar)
While at least one row is selected, the bottom mobile tab bar is replaced in place by a four-button selection action bar of the same height — the swap does not reflow the layout. Buttons left to right:
- Cancel (back-arrow icon) — clears the selection and returns to the tab bar. The button label shows the live count:
3 selected. The bar carries a soft accent tint so it reads as a distinct selection-mode toolbar. - Snooze (alarm-clock icon) — opens the snooze palette pre-configured for the whole selection. The date / time picker applies to every selected row at once, whatever its type (sessions, SMS, digests, approvals, alerts, …) — not just sessions.
- Pause (pause icon) — pauses every selected session that is currently in a running-ish state (
running,needs_you,stalled,starting,ready). Sessions already inpaused/archived/ended/errorare skipped silently. If no session matches, an info toast says "No running sessions to pause" and the selection clears. A success toast offers Undo (restores all paused sessions back to their previous status). - Archive (archive icon) — archives / dismisses every selected row, whatever its type (not just sessions). Selected sessions soft-archive, capturing the pre-archive status of any in the attention bucket (
needs_you,error,stalled) so Undo restores them to that exact status; every other row (alerts, SMS, digests, approvals, …) is dismissed the same way it would be one at a time.
While an action is in flight all four buttons are disabled.
Exiting selection mode
Three ways to exit:
- Tap the Cancel button on the action bar.
- Tap the last selected row (deselect everything → count drops to 0 → action bar swaps back to the tab bar).
- Run Snooze, Pause, or Archive — the bar clears the selection automatically when the action completes.
Note that switching the active project via the project pill on the projects tab also clears the selection (selection is project-scoped — the store auto-clears on activeProjectId change).
Why the long-press doesn't fire on iOS Safari sometimes
iOS Safari has three native gestures that can pre-empt a 500 ms timer: the long-press share / copy / lookup callout, native text selection during the hold, and the 300 ms double-tap-to-zoom delay before any tap event fires. The session row container opts into a CSS utility (.mobile-press-target) that disables all three:
-webkit-touch-callout: none— suppresses the long-press menuuser-select: none(with-webkit-user-select: none) — suppresses text selection during the holdtouch-action: manipulation— disables the 300 ms double-tap-to-zoom delay
Without those three, you'd see the iOS share menu pop up at the 500 ms mark instead of selection mode entering. If you build a new row component that wires useLongPress, applying .mobile-press-target (directly, or via the centralized inbox-row base class) is mandatory.
Relationship to desktop bulk-select
Desktop has its own keyboard-driven multi-select on the same selection-store: Shift+J / K extends, Ctrl+Click toggles, Esc clears, then E / Ctrl+W / P / H run the bulk action. See bulk-select-sidebar.md. Mobile multi-select shares the same store and the same shared helpers — Archive / Snooze route through lib/bulk-inbox-actions.ts (every row type), Pause through bulkPauseSessions (session-only) — the difference is purely the entry / exit UX. Both now also fold the currently-active item into a fresh selection — desktop on the first Ctrl+Click, mobile on the first long-press — for sessions and for the unified inbox's session rows alike (the inbox long-press passes its visible list + active row to the same store seed; see mobile-multiselect-active-seed-contract.md). A row selected on desktop and then loaded on mobile would still appear selected.
For agents
Code references
- Long-press hook with 500 ms timer + scroll-cancel + click-suppression: src/renderer/src/hooks/useLongPress.ts
- Mobile session row that wires the hook + applies
mobile-press-target: src/renderer/src/features/dashboard/MobileSessionRow.tsx - Action bar that replaces the tab bar: src/renderer/src/components/ui/MobileSelectionActionBar.tsx
- Conditional swap (action bar vs tab bar) lives in src/renderer/src/App.tsx gated on
selectedSessionIds.size + selectedInboxKeys.size > 0(mobile only viauseIsMobile()). - Selection store (sessions + inbox keys, cross-project / cross-group reset semantics,
getSelectedSessionIds()union,clearAllSessionSelections()): src/renderer/src/stores/selection-store.ts - Centralized
mobile-press-targeton inbox-shaped rows:INBOX_ROW_CONTAINER_BASEin src/renderer/src/lib/styles.ts (UnifiedInboxRow + InboxCronHealCard inherit it). - Row highlight in selection mode (active row yields, selection is the only accent):
inboxRowStateClass'sisSelectionModebranch in src/renderer/src/lib/styles.ts, passed by MobileSessionRow.tsx (always) and UnifiedInboxRow.tsx (gated onuseIsMobile()). Locked by tests/unit/lib/styles.test.ts. - CSS utility definition: src/renderer/src/styles/globals.css —
.mobile-press-targetblock. - Lint test that pins all three CSS properties + both consumers in place: tests/unit/lint/mobile-row-touch-callout.test.ts
Related
- bulk-select-sidebar.md — the same bulk actions driven from a desktop keyboard instead.
- bulk-stop-restart-sessions.md — what the Stop and Restart bulk actions do.
- snooze-a-session.md — the Snooze action in the selection bar.
Last verified 2026-09-23