---
title: Multi-select on mobile (long-press a row, act on many)
---

# Mobile Multi-Select (long-press sessions)

## 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](../../.claude/memory/contracts/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 in `paused` / `archived` / `ended` / `error` are 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:

1. Tap the **Cancel** button on the action bar.
2. Tap the **last** selected row (deselect everything → count drops to 0 → action bar swaps back to the tab bar).
3. 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 menu
- `user-select: none` (with `-webkit-user-select: none`) — suppresses text selection during the hold
- `touch-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](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](../../.claude/memory/contracts/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](../../src/renderer/src/hooks/useLongPress.ts)
- Mobile session row that wires the hook + applies `mobile-press-target`: [src/renderer/src/features/dashboard/MobileSessionRow.tsx](../../src/renderer/src/features/dashboard/MobileSessionRow.tsx)
- Action bar that replaces the tab bar: [src/renderer/src/components/ui/MobileSelectionActionBar.tsx](../../src/renderer/src/components/ui/MobileSelectionActionBar.tsx)
- Conditional swap (action bar vs tab bar) lives in [src/renderer/src/App.tsx](../../src/renderer/src/App.tsx) gated on `selectedSessionIds.size + selectedInboxKeys.size > 0` (mobile only via `useIsMobile()`).
- Selection store (sessions + inbox keys, cross-project / cross-group reset semantics, `getSelectedSessionIds()` union, `clearAllSessionSelections()`): [src/renderer/src/stores/selection-store.ts](../../src/renderer/src/stores/selection-store.ts)
- Centralized `mobile-press-target` on inbox-shaped rows: `INBOX_ROW_CONTAINER_BASE` in [src/renderer/src/lib/styles.ts](../../src/renderer/src/lib/styles.ts) (UnifiedInboxRow + InboxCronHealCard inherit it).
- Row highlight in selection mode (active row yields, selection is the only accent): `inboxRowStateClass`'s `isSelectionMode` branch in [src/renderer/src/lib/styles.ts](../../src/renderer/src/lib/styles.ts), passed by [MobileSessionRow.tsx](../../src/renderer/src/features/dashboard/MobileSessionRow.tsx) (always) and [UnifiedInboxRow.tsx](../../src/renderer/src/features/dashboard/UnifiedInboxRow.tsx) (gated on `useIsMobile()`). Locked by [tests/unit/lib/styles.test.ts](../../tests/unit/lib/styles.test.ts).
- CSS utility definition: [src/renderer/src/styles/globals.css](../../src/renderer/src/styles/globals.css) — `.mobile-press-target` block.
- Lint test that pins all three CSS properties + both consumers in place: [tests/unit/lint/mobile-row-touch-callout.test.ts](../../tests/unit/lint/mobile-row-touch-callout.test.ts)

## Related

- [bulk-select-sidebar.md](bulk-select-sidebar.md) — the same bulk actions driven from a desktop keyboard instead.
- [bulk-stop-restart-sessions.md](bulk-stop-restart-sessions.md) — what the Stop and Restart bulk actions do.
- [snooze-a-session.md](snooze-a-session.md) — the Snooze action in the selection bar.

