---
title: Inbox overview
---

# Inbox overview

## What it is

The **Inbox** is Omniscio's cross-project triage view — one place that surfaces every item across every project that needs your attention right now, regardless of which channel it came from. It sits at the very top of the projects sidebar with an amber inbox icon and an amber count badge showing how many items are waiting. Click it (or trigger your inbox keyboard shortcut) and the middle column switches from "this project's sessions" to "everything that needs you, grouped by source."

The Inbox is **not a separate folder, mailbox, or queue** — it's a live aggregation. Every item in it also exists in its home location (a session in its project, an SMS thread in the SMS virtual project, a cron approval in the queue), so triaging from the Inbox is the same as triaging from each home. The Inbox just collapses them into one list so you don't have to walk every project to find the four sessions that need replies, the two SMS threads with new messages, and the cron job pending approval.

**Inbox attention scoped to a real project also appears in that project's "Needs You" section.** Most inbox items are either session attention (already shown in their project) or virtual-source items (SMS, digest, cron) with no real-project home. But some **non-session** items are tied to a real project — a repo's PR-merge-queue nudge, a project's context-size (doc-token) alert, a project-scoped cron-job approval, a Nighty-Tidy summary, an **agent alert** (for example the Auto-lander's "branches landed" notice), or a held **Bug-Intake "Review"** card. Those surface in **both** places: the global Inbox **and** the owning project's **Needs You** sidebar section (with the project's amber attention badge counting them), on desktop and mobile — so opening the project shows what needs you there, not only in the Inbox. **These notice rows behave exactly like the session rows beside them:** **Tab / J / K** (and mobile tap) reach them in the order they're shown, and clicking or keyboard-activating one opens its detail **in place** in the project's main panel — you stay in the project, and closing it returns you there, not to the global Inbox. One shared membership source drives the row, the amber badge, and the project-view nav so they cannot drift; see [project-needs-you-attention-contract.md](../../.claude/memory/contracts/project-needs-you-attention-contract.md).

The Inbox surfaces **ten different item types** today: sessions in any "needs you" attention status (questions, plan-mode, permission requests, rate-limit prompts, auth issues, API errors, stopped runs), today's daily digest if it hasn't been read, unread SMS conversations, unread Telegram conversations, pending cron-job approvals, pending automation-rule approvals, **CLI Pending Actions** queued by external AIs (settings PATCHes, session lifecycle, project deletes, project bug-intake toggles, away-mode rule changes — see [cli-pending-actions.md](cli-pending-actions.md)), recipe runs awaiting approval gates, recipe authoring approvals, and recipes ready to run. PRD Stack plugin attention items render as a separate group below the unified list.

While items remain, the Inbox keeps one selected for you so you're never staring at a stale empty pane: finishing the current item drops you into the next, and an item that arrives **while you're sitting on "All clear"** opens on its own — a session, a text, an alert. **Approvals are the deliberate exception:** an approval that arrives while you're idle in the inbox waits as a row instead of opening on its own, so a background agent's approval can never take over your screen — you open it when you're ready. (Opening the Inbox yourself still shows the top item, approval or not, and finishing the item before an approval still advances onto it — only a from-behind arrival is held back. See [inbox-navigation-contract.md](../../.claude/memory/contracts/inbox-navigation-contract.md) `approvals-no-passive-auto-open`.) If items remain but none is open (auto-select hasn't landed one yet), the reading pane prompts **"Select an item"** rather than falsely claiming all-clear. The **"All clear"** panel — a green inbox icon, the heading **"All clear"**, and the subtext **"No sessions need your attention right now"** — appears **only** when the Inbox is genuinely empty (the count badge is 0). That's the goal state — empty inbox means triage is done.

## Where to find it

At the very **top of the projects sidebar**, marked with an amber inbox icon and an amber badge counting what is waiting. Clicking it — or pressing your inbox keyboard shortcut — switches the middle column to the grouped list of everything needing you.

## How it behaves

### How to use it

1. **Open the Inbox.** Click the amber **Inbox** entry at the top of the projects sidebar (above the divider that separates Inbox from your pinned and grouped projects). The icon is a mailbox glyph; the badge to its right shows the total count of pending items across every source. The middle column switches from per-project sessions to the unified inbox list, with `aria-label="Inbox, N needing attention"` set for screen-reader callout.
2. **Read the grouped list.** Items are grouped by their source project — a real project's group header shows the project's color bar and icon plus the project name; virtual sources show a Lucide icon (alarm clock for **CRON JOBS**, smartphone for **SMS**, newspaper for **DAILY DIGEST**) plus a tinted label. Every group header — real project or virtual source alike — also carries a parenthesised count of how many items sit in that group (for example **Omniscio (12)**), on desktop and mobile. Inside each group, items are sorted oldest-first so the longest-waiting items are easy to spot. Group order itself is **frozen** so the list doesn't shuffle under your finger as new items arrive — a new group is appended to the end rather than splicing into the middle — and it **stays stable across visits**, changing only as you clear items or new ones arrive (it resets when you restart the app). **Collapse groups to scan by product.** A **Collapse all / Expand all** toggle folds the whole Inbox down to a list of product names, and every group header carries a small **arrow (chevron)** that folds that one group down to just its name and count — so you can shrink the Inbox, then open one product to see only its items. On **both desktop and mobile the toggle sits in the Inbox header row itself** (the same row as the "Inbox" title): on desktop it's an **icon-only** button beside the source-filter icon, with its "Collapse all"/"Expand all" label shown in the hover tooltip; on mobile it's the labeled toggle. On desktop it's hidden while **Focus Mode** is armed (the list is behind the focus overlay, so there's nothing to fold). Collapsing only hides the rows; it never changes what's in the Inbox or the order you navigate. On desktop the group holding the item you're currently reading stays open so keyboard navigation never lands on a hidden row; on **mobile** an explicit fold fully folds (your place is remembered and reopens the moment you tap or swipe). Your collapsed/expanded choices are remembered across restarts and on your phone. Tapping a group **name** still jumps into that project as before — the arrow is the dedicated fold control.
3. **Click a row to act on it.** A session row opens that session in the right pane so you can read the question, choose a plan option, grant or deny a permission, or reply. An SMS row opens the conversation viewer. A daily-digest row opens today's digest. An approval row (cron, automation, CLI Pending, recipe) opens an approval modal showing the action kind, the target, the JSON payload, and Approve / Reject buttons. **All five approval kinds sit together in one "Approvals" section, and approving or rejecting one carries you to the next pending approval — never off into a project's sessions — until the queue is clear.** **Closing an approval (the X) without deciding doesn't lose it** — the approval stays waiting in the Inbox and Omniscio drops you onto the next pending item so you can keep moving through the queue (close the last one and you land back on the list). The amber row colour signals "decision required" so you can scan and prioritize.
4. **Triage with keyboard.** **J** / **K** walk forward and backward through the unified list across groups (the same shortcuts you use inside a project's session list). **Shift+J** / **Shift+K** extend a multi-selection so you can bulk-archive or bulk-snooze. Plain arrows clear any stale multi-selection so a follow-up bulk action operates on what you can see, not on a selection from twenty rows ago. However you move the selection — walking with **J** / **K**, or clearing items so the cursor jumps to the next one — the Inbox keeps the selected row **in view**: if it would land off-screen (for example above the fold after you clear an item near the top), the list scrolls just enough to reveal it, so you never lose your place. See [inbox-navigation-contract.md](../../.claude/memory/contracts/inbox-navigation-contract.md) `active-row-scrolled-into-view`.
5. **Items leave the Inbox automatically as you handle them.** Reply to a session and its row disappears (the session is no longer in attention status). Read the digest and its row clears. Approve a CLI pending action and the row vanishes. An **SMS** thread leaves the Inbox when you dispose of it — tap **Mark as read** on the thread, archive it, or reply (in Omniscio, or straight from your phone, which Omniscio catches on its next background sync within ~30 seconds). Just _opening_ a thread to read it is a peek: it clears the blue unread dot but keeps the thread in the Inbox until you actually deal with it, so an unread text is never stranded outside the Inbox (see [sms-inbox-attention-contract.md](../../.claude/memory/contracts/sms-inbox-attention-contract.md)). Snoozing or archiving from inside a session — or right-clicking the daily-digest row and choosing **Archive** or **Snooze**, or pressing **S** on it — also drops it out of the Inbox immediately. The badge count on the sidebar updates live via push events — no refresh. **And a restart never leaves you a row you can't act on:** an Allow/Deny tool-permission request lives inside the running agent's own process, so it cannot survive Omniscio closing. On the next launch that session moves out of the Inbox into **Interrupted** and — with auto-resume on — restarts and asks again with a live Allow/Deny card, instead of sitting in the Inbox forever with nothing to click. Every other kind of "needs you" (a question, a plan to approve, a finished turn) is written into the transcript, so it survives a restart and correctly stays in the Inbox. See [permission-prompt-lifecycle-contract.md](../../.claude/memory/contracts/permission-prompt-lifecycle-contract.md) P17.
6. **Mobile uses the same view.** On a mobile-width viewport (the phone web UI or a narrow window) the Inbox is just another row in the projects panel; tap it and the sessions panel slides in showing the same unified, grouped list. Swipe-left on a row archives the underlying session with an Undo toast. Clearing the item you're viewing slides you straight into the **next** one — or back to the list when the inbox empties — exactly as it does on desktop (one shared reconcile drives every way you clear, so a phone never gets dropped to the list instead of advancing; see [inbox-navigation-contract.md](../../.claude/memory/contracts/inbox-navigation-contract.md) `mobile-panel-follows-advance`). The Inbox is also the default landing tab on mobile because triage is the primary phone use case — see [mobile-remote-access.md](mobile-remote-access.md).

### Scope and limits

- **Desktop only — no CLI control surface.** The unified channels layer that feeds this inbox — connecting a channel, applying its config, sending a reply, marking an item read, or deleting an item (the `CHANNELS_CONNECT` / `CHANNELS_CONFIG_APPLY` / `CHANNELS_SEND` / `CHANNELS_MARK_READ` / `CHANNELS_DELETE_ITEM` actions across the Slack, RSS, webhook, calendar, Drive, and Sheets adapters) — is driven through the desktop/mobile UI only. There is no localhost CLI route (the `127.0.0.1:19519` control server) that manages channels or sends on their behalf; these mutating actions are the classified `messaging-client` desktop-only exemption in the CLI-parity manifest.

## For agents

### How it works

The Inbox virtual project is identified by the constant `INBOX_ID = '__inbox__'` exported from [session-store.ts](../../src/renderer/src/stores/session-store.ts). Selecting the Inbox sets `activeProjectId === INBOX_ID`, which a derived `isInboxSelected` flag in [Dashboard.tsx](../../src/renderer/src/features/dashboard/Dashboard.tsx) feeds into [SessionsSidebar.tsx](../../src/renderer/src/features/dashboard/SessionsSidebar.tsx) so the middle column flips into inbox-render mode. The sidebar entry itself is rendered in [ProjectsSidebar.tsx](../../src/renderer/src/features/dashboard/ProjectsSidebar.tsx) (look for `data-tour="inbox-button"`) — the amber count badge reads from `useInboxAttentionCount()`.

Item aggregation lives in [stores/inbox-items.ts](../../src/renderer/src/stores/inbox-items.ts). The `useInboxItems()` hook calls one adapter per source — `useSessionInboxItems`, `useDigestInboxItems`, `useSmsInboxItems`, `useTelegramInboxItems`, `useCronApprovalItems`, `useAutomationApprovalItems`, `useCliPendingApprovalItems`, `useRecipeApprovalItems`, `useRecipeAuthoringApprovalItems`, `useRecipeInboxItems` — and concatenates them into a single `UnifiedInboxItem[]` array (`{ integration, id, title, timestamp, projectId, inboxGroupId?, dotColor?, payloadRef }`). `computeUnifiedInboxGroups()` groups by `item.inboxGroupId ?? item.projectId`, sorts items inside each group by ascending timestamp, and sorts the groups themselves by a frozen-order map so ordering doesn't shift mid-triage. **The five approval stores (cron, automation, cli-pending, recipe, recipe-authoring) set `inboxGroupId` to the shared "Approvals" group** (via the one [approval-inbox-group.ts](../../src/renderer/src/stores/approval-inbox-group.ts) chokepoint) so every approve/reject row sits in ONE contiguous queue — resolving one advances to the next **approval**, never an adjacent session — **while keeping its `projectId`** for project-view Needs-You membership + sidebar badges (a cron-failure NOTICE and every non-approval row leave `inboxGroupId` unset and still group by `projectId`). Before this split, `projectId` did double duty (grouping AND project membership), so an approval surfaced in a project scattered out of the Approvals group and a resolve walked onto the adjacent session — see [inbox-navigation-contract.md](../../.claude/memory/contracts/inbox-navigation-contract.md) `approvals-are-one-inbox-group`. `computeUnifiedInboxGroups` is the SOLE author of that map — the sessions-only `computeOrderedInboxGroups` only READS it (seeding a sessions-only subset would evict virtual groups like the `__alerts__` pile, the 2026-07 "alerts jump to the bottom" reflow). The map is an in-memory singleton that persists across inbox visits (`setActiveProject` no longer clears it — Option B) and resets on app restart. The Ask Omniscio visibility gate (`filterVisibleInboxItems`) hides Ask Omniscio sessions from the inbox when that feature toggle is off.

Group header rendering is centralized in [inbox-helpers.ts](../../src/renderer/src/features/dashboard/inbox-helpers.ts): `getIntegrationGroupName`, `getGroupColor`, `getIntegrationGroupIcon`, and `getIntegrationGroupIconColor` map a group id (which is itself a project id, real or virtual) to its display label, vertical color bar, and icon. The header component itself ([SidebarInboxContent.tsx](../../src/renderer/src/features/dashboard/SidebarInboxContent.tsx) on desktop, [MobileInboxGroups.tsx](../../src/renderer/src/features/dashboard/MobileInboxGroups.tsx) on mobile) also renders a parenthesised item count next to the name — `group.items.length`, the exact per-group array the rows below map over, so the count can never disagree with the list. Each item row renders through [UnifiedInboxRow.tsx](../../src/renderer/src/features/dashboard/UnifiedInboxRow.tsx) — the dispatch shim that decides which navigation action runs when the row is clicked. The list-side "All clear" panel is wrapped in `EmptyState` and rendered inline in `SessionsSidebar.tsx` / `MobileSessionsList.tsx` when that rendered list is empty. The right-hand reading pane decides its inbox empty state separately via `inboxMainPaneEmptyKind` ([inbox-main-pane.ts](../../src/renderer/src/features/dashboard/inbox-main-pane.ts)): it shows the "All clear" celebration only when `useInboxIsEmpty()` is true, and "Select an item" when items exist but none is open — never gating "All clear" on "nothing is selected" (the 2026-06-07 badge=1 / pane-says-empty bug). The badge count (`useInboxAttentionCount`), the rendered list (`useInboxItems`), and that empty signal all derive from one `useVisibleInboxItems()` pipeline so they can't drift; see [inbox-count-list-parity-contract.md](../../.claude/memory/contracts/inbox-count-list-parity-contract.md).

**Group collapse/expand** is a render-only accordion layered on top of the same grouping — it changes what's drawn, never the data. A persisted `collapsedInboxGroups` setting (an array of collapsed group ids, which are project ids) is read through the shared [inbox-collapse.ts](../../src/renderer/src/features/dashboard/inbox-collapse.ts) hook, which both [SidebarInboxContent.tsx](../../src/renderer/src/features/dashboard/SidebarInboxContent.tsx) (desktop) and [MobileSessionsList.tsx](../../src/renderer/src/features/dashboard/MobileSessionsList.tsx) / [MobileInboxGroups.tsx](../../src/renderer/src/features/dashboard/MobileInboxGroups.tsx) (mobile) use to draw the per-group chevron, plus the Collapse-all / Expand-all control ([InboxCollapseControls.tsx](../../src/renderer/src/features/dashboard/InboxCollapseControls.tsx)) — which lives in the Inbox header row on both platforms, not the scroll body: on **desktop** an icon-only button ([InboxHeaderCollapseButton.tsx](../../src/renderer/src/features/dashboard/InboxHeaderCollapseButton.tsx)) rendered by [SidebarHeader.tsx](../../src/renderer/src/features/dashboard/SidebarHeader.tsx) beside the source-filter (hidden while Focus Mode is armed), and on **mobile** the labeled toggle via [MobileInboxCollapseToggle.tsx](../../src/renderer/src/features/dashboard/MobileInboxCollapseToggle.tsx) (rendered by [MobileBreadcrumb.tsx](../../src/renderer/src/components/ui/MobileBreadcrumb.tsx)). Collapsing a group only skips RENDERING its rows — it never touches the unified `flatList`, the frozen group order, or the nav resolver — so navigation still walks every item. **Both the desktop and mobile inboxes opt OUT of the active-group auto-reveal** (`activeGroupId = null`), so an explicit collapse folds every group — including the one holding the item you currently have open. Navigation is collapse-blind (it walks the unified flat list, not just the rendered rows), and opening an item shows it in the reading pane, so a folded active group can't strand you — the open item still shows on the right, and "sidebar order IS the navigation order" still holds (folding hides rows, never reorders them). WITH the auto-reveal, "Collapse all" (and a group's own chevron) couldn't fold the active item's section: on mobile a single-group inbox looked dead (2026-07-19), and on desktop the ALERTS section stayed open while everything else folded (2026-07-20). State persists silently through `saveCollapsePreference` (no toast) and rides the mobile bootstrap (`collapsedInboxGroups` is in `WEB_BOOTSTRAP_SETTING_KEYS`) so a phone remembers it across reloads. The render-only invariant and the no-active-reveal rule are locked by [inbox-collapse.test.ts](../../tests/unit/features/dashboard/inbox-collapse.test.ts), the desktop fold case in [SidebarInboxContent.test.tsx](../../tests/unit/features/dashboard/SidebarInboxContent.test.tsx), and the mobile fold case in [MobileSessionsList.test.tsx](../../tests/unit/components/MobileSessionsList.test.tsx); see also [inbox-navigation-contract.md](../../.claude/memory/contracts/inbox-navigation-contract.md) `mobile-collapse-no-active-reveal`.

Auto-selection runs through a single chokepoint, `runInboxAutoSelectIfIdle` in [session-navigation.ts](../../src/renderer/src/stores/session-navigation.ts): when you're in the inbox and nothing is selected, it fills the gap — picking **the newest visible item** on a genuine cold/empty **entry**, but **advancing to the sidebar-ADJACENT row** (never the newest) when the selection went empty because an action removed the row you were on (an approval resolved, a session ejected under you). It remembers the row you were last on — a **recovery anchor** — so acting on a row moves you to the neighbouring item like every other inbox action, instead of teleporting you to the newest item at the very bottom of the list (the 2026-07-04 "approving jumps to the very bottom" regression, structurally un-regressable now). See [inbox-navigation-contract.md](../../.claude/memory/contracts/inbox-navigation-contract.md) `idle-auto-select-recovers-adjacent`. Four event-driven gates call it — entering the inbox, the session fetch-race, `updateSessionStatus`, and a visibility-recovery subscriber. But those four only observe the session + AI-manager stores, while the inbox aggregates ~20 sources, so the first item to land from any _other_ source (an SMS, an approval, an alert) could slip through every gate. A **render-driven backstop** — `useInboxAutoSelectBackstop` ([useInboxAutoSelectBackstop.ts](../../src/renderer/src/features/dashboard/useInboxAutoSelectBackstop.ts)), mounted in [Dashboard.tsx](../../src/renderer/src/features/dashboard/Dashboard.tsx) — closes that gap by re-running the same chokepoint against the EXACT `useInboxItems()` list the inbox renders (which subscribes to all sources), so the first item to arrive is selected regardless of which store changed. **One deliberate exception protects your attention** (2026-07-23): when the chokepoint fires **passively** — a new item landing while you sit idle, or a fetch / visibility recovery — it **skips approvals**, so a background agent's approval waits as a row instead of auto-opening its full-screen pane over you (the "an agent forced my attention" fix). Deliberately opening the Inbox still shows the newest item (approval or not), and the active close-advance after you finish an approval still flows to the next one; **J** / **K** / **Tab** always reach approvals too. See [inbox-navigation-contract.md](../../.claude/memory/contracts/inbox-navigation-contract.md) `approvals-no-passive-auto-open`. **The same backstop also recovers a stranded selection** (2026-07-20, widened 2026-08-27): a row can leave the inbox without you acting on it — a derived card like the **weekly-suggestions** card is pinned to one specific summary, so a newer summary silently re-targets it and the open row leaves the list; a snooze fires; another device or agent resolves the item out from under you. The selection then points at a row that no longer exists, and that is worse than a stranded reading pane: auto-select only fires when NOTHING is selected, so one stale selection **wedges the whole inbox** — it reads as empty, and every item that arrives afterwards is left unselected (the reported "the inbox was empty, something appeared, and it was NOT auto-selected"). The backstop notices the selected row is gone from the live list and advances you to the next live row. Originally this covered only detail cards, which left six kinds — SMS threads, the daily digest, the weekly summary, drip items, scheduled messages and the PR merge queue — with nothing to recover them; it now covers every selection kind except three that another mechanism deliberately owns: a **session** is cleared by the attention-membership eject instead, the mobile new-session composer has no row by design, and an **approval** is left to its own confirm-before-close (an approval row can blink out of the pending list during a reload, and treating that blink as gone was itself a bug). See [inbox-navigation-contract.md](../../.claude/memory/contracts/inbox-navigation-contract.md) `orphan-selection-reconcile`. Conversely, when the _active_ session **stops needing you** on its own — a backend transition, not your reply — `updateSessionStatus` **ejects** it (clears `activeSessionId`) so the next item can take its place rather than the panel sitting on "All clear" with the row unread. **Needs You is a strict membership surface**: the eject fires for _any_ status outside `ATTENTION_STATUSES`, so a session that goes back to running / starting / ready / waiting leaves the Inbox exactly like one that ends or is archived — there is no alive/reviving exemption. An earlier "stay on that session" rule (2026-06-23, broadened 2026-06-27) kept a self-reviving session selected here; it was removed because it left running sessions sitting in Needs You after they had stopped needing you. You can still watch a running session — it lives under its **project**, which is unaffected. `isActiveSessionInboxBlocked` applies the same membership rule, so the reading pane and the list can never disagree about it. The eject is gated to backend transitions only (it never fires for your own replies / nudge / keep-waiting / restart, which navigate off the sender first — restart joined this family on 2026-06-30, so restarting the session you're viewing carries you to the next one instead of stranding you on it). The full gate set, that race-avoidance rule, and the all-source render backstop are locked in [inbox-navigation-contract.md](../../.claude/memory/contracts/inbox-navigation-contract.md) (`active-session-auto-eject` and `empty-inbox-auto-select-backstop`). One refinement on top of "select the newest" (2026-06-19): when you **return** to the Inbox after a detour (Settings, another project, a view switch), Omniscio re-selects the exact item you were last looking at rather than jumping to the newest — it remembers your spot in memory and validates the item still exists, falling back to the newest only if that item is gone. See [inbox-navigation-contract.md](../../.claude/memory/contracts/inbox-navigation-contract.md) `inbox-entry-detour-return`. A second refinement (2026-07-01) governs an explicit **close**: closing an approval pane (the X) does NOT resolve the approval (unlike Approve/Reject, which flip the row terminal), so the row stays `pending` and — being the newest attention item — the auto-select would immediately re-grab it, making the X appear dead ("the same approval reopens"). `closeApprovalPaneAdvancing` records the just-closed approval as a one-row exclusion the chokepoint skips for that idle window, so a close ADVANCES to the next-newest item (or back to the list on mobile when nothing remains) and self-clears the moment something is selected or you leave the inbox. See [inbox-navigation-contract.md](../../.claude/memory/contracts/inbox-navigation-contract.md) `explicit-close-advances-to-next`.

## Related

- [snooze-an-inbox-item.md](snooze-an-inbox-item.md) — every row in the Inbox (all 15 kinds) is snoozable via right-click → Snooze; one unified Snooze Palette, one expiry sweep, one gate helper, counts update live via `INBOX_SNOOZE_CHANGED`
- [cli-pending-actions.md](cli-pending-actions.md) — how external-AI requests land in the inbox as approve/reject rows
- [bulk-select-sidebar.md](bulk-select-sidebar.md) — Shift+J/K multi-select and bulk archive/snooze across inbox groups
- [snooze-a-session.md](snooze-a-session.md) — snoozing a session removes it from the inbox until the snooze expires
- [archive-a-session.md](archive-a-session.md) — archiving from the inbox kills the CLI process and drops the row
- [mobile-remote-access.md](mobile-remote-access.md) — the mobile/phone view defaults to the Inbox as the landing tab
- [daily-digest.md](daily-digest.md) — the daily-digest row that surfaces in the Inbox once a day
