---
title: Supermail (part 2)
---
# Supermail (part 2)

## What it is

This is part 2 of the [Supermail](supermail.md) page. It carries the next stretch of the material on that page, moved here because a single page is capped at 40,000 characters.

## Where to find it

Reach this part through [Supermail](supermail.md) — it lists every part and explains where the feature lives in the product. Everything below is reached from the same place.

### Where it is wired in Omniscio

- Sidebar registration: `src/shared/integrations/supermail.ts`
  (`id: 'supermail'`, `featureFlag: 'supermailEnabled'`) +
  `src/renderer/src/integrations/ui-registry.ts` (icon + lazy panel,
  `panelOwnsLayout: true`).
- Virtual-project sentinel: `SUPERMAIL_PLUGIN_PROJECT_ID = '__supermail__'`
  in `src/shared/virtual-project-ids.ts` (the native `__supermail__` form is used so the
  boot-loop seeder doesn't filter it out as a marketplace-plugin shim; the legacy
  `__plugin_supermail__` rows were RETIRED by migration `20260619214747`).
- Sidebar hide-when-off gate: `SIDEBAR_RENDER_GATES` in
  `src/renderer/src/stores/project-visibility.ts`.
- Panel host (Omniscio glue): `src/renderer/src/features/supermail/SupermailPanel.tsx`.
- Settings toggle + search:
  `sections/supermail/SupermailSettings.tsx` (`supermailEnabled` +
  `supermailInboxEnabled`).
- Find Email Quick Launch tab (Supermail mode): when `supermailEnabled`, that tab
  searches Supermail's backend (`SUPERMAIL_SEARCH` → hosted `/search`, JWT stays in
  Main: `src/main/services/supermail/supermail-search-service.ts`) and opens the pick
  in Supermail via `SUPERMAIL_QUICK_FIND_OPEN`, which REUSES the notification-open path
  (foreground the main window + emit `PLUGIN_DEEPLINK` `/thread/<id>` →
  `openSupermailDeepLink`) — no deep-link-router change and no vendored-UI edit.
  Handlers: `src/main/ipc/supermail-quick-find-handlers.ts`. See
  quick-launch-modal.md (the Find Email tab).
- Protocol deep-link (`omniscio://supermail/thread/<id>`): a clickable/shareable link
  that opens a specific email. Unlike the quick-find/notification `PLUGIN_DEEPLINK` path
  above, this one DOES route through the generic deep-link parser + router
  (`open-supermail-thread` → `openSupermailDeepLink`), so it is also a recognized
  destination in `GET /deep-link/resolve`. The `agentmc://supermail/auth` sibling (the
  backend's primary auth scheme; `omniscio://` accepted as an alias) stays a main-process
  OAuth-credential callback, never a navigation (both entry points gate on
  `isSupermailAuthDeepLink`). See [deep-links.md](deep-links.md).

## How it behaves

Everything below is the behaviour, detail and edge cases that belong to this stretch of the Supermail page.

### Inbox rows: soft edges + optional hover actions

Three refinements to the inbox list (the first two apply to the dense single-line rows
shown when the reading pane is off):

- **Soft edge fade instead of a trailing "…".** When a row's text is too long for one
  line it now **fades out softly at the right edge** rather than ending in a "…". The
  old ellipsis inherited the weight of whatever it cut — a bold subject gave heavy dots,
  the thin preview gave light ones — so the trailing dots looked inconsistent from row
  to row, and an essentially-empty row could still show them. The fade is uniform across
  read/unread and a row with no preview text shows nothing. Pure CSS (`.sm-edge-fade`, a
  background-agnostic `mask-image` in `index.css`), with a plain "…" fallback on engines
  without `mask-image` and the fade on the start edge under RTL.
- **"Show quick actions on hover" (Settings → General → Personalization, ON by default).** The
  **Mark Done / Remind Me** buttons that appear on the right of a row on hover can be
  turned off for a cleaner list. Off hides the whole strip — the message **time stays
  visible** on hover (no blank edge) and the **`E` (Done) / `H` (Remind me) keyboard
  shortcuts still work**. It is the single discoverable switch; the finer-grained
  **Settings → Reading → Toolbar buttons → Message rows** controls (Mark done / Snooze) still apply
  on top of it. Device-local (`supermail:row-hover-actions`,
  `features/settings/row-hover-actions-setting.ts`). Invariants:
  `.claude/memory/contracts/supermail-toolbar-controls-contract.md` (`master-row-hover-actions-toggle`/`row-single-line-truncation`).
- **Archiving glides the row out instead of snapping shut.** Mark Done / archive (and
  trash / mute) now **fade the row while collapsing its height**, so the rows below glide
  up to fill the gap — a subtle, quick exit rather than the row sliding hard off-screen and
  the list then snapping shut. Applies to every list row (dense and preview). Pure CSS
  (`grid-template-rows: 1fr → 0fr` + opacity in `thread-list.tsx`), reduced-motion-aware,
  and timed to finish exactly as the store removes the row (`EXIT_ANIMATION_MS`, kept in
  sync between `thread-list.tsx` and `inbox-store-mutators.ts`).
- **Snoozed rows show a clock + "Snoozed until \<time\>".** A snoozed thread used to look
  identical to any other email — you could only tell from the Snoozed folder it listed
  under. Snoozed rows now carry a small clock glyph, with the wake time next to it
  (compact in the dense layout's date slot, full in the preview layout and on hover);
  when the wake time hasn't loaded yet only the clock shows, and once the time passes
  the marker disappears (the existing amber "woke up" reminder dot takes over). Pure
  rendering — the snooze store already tracked the wake time, rows just never read it
  (`thread-list-item-icons.tsx`, `thread-row-format.ts`).

### Toolbar buttons (make each control optional)

Every action button in Supermail's toolbars is **optional**, so you can tighten the UI to
exactly what you use. In **Settings → Reading → Toolbar buttons** each control has a three-way choice:

- **Shown** (default) — the button is visible and its keyboard shortcut / `Ctrl/Cmd+K`
  command work, exactly as today.
- **Keyboard only** — the button is hidden, but its keyboard shortcut and command-palette
  entry still work.
- **Off** — the button is hidden AND its keyboard shortcut + command-palette entry are
  disabled.

Everything defaults to **Shown**, so an untouched install is unchanged; hiding buttons
reflows the toolbars cleanly so the UI genuinely tightens up. The controls are grouped by
where they live:

- **Reading pane** — Reply, Filter like these, Focus mode.
- **Message rows** — Star, Mark done, Snooze. Mark done + Snooze are per-row hover actions; the
  **star** is a persistent **accent-coloured** indicator at the row's **left edge** (fills when
  starred; a hollow outline reveals on hover / keyboard focus). The list is **hotkey-first**: row
  selection is the **`x`** key (there is no hover checkbox), the keyboard-focused row shows a bold
  accent "cursor" highlight (tint + inset ring), and a **`?`** button in the inbox header opens the
  keyboard-shortcuts drawer (the same one the `?` key opens). These same actions — plus Mark read/unread
  and "Filter messages like these" — are also on a **right-click context menu** for any message row;
  the menu respects these same per-action toggles (a control set to **Off** is dropped from the menu too).

A control governs an **action**, so one setting applies everywhere that button appears. The
inbox toolbar's own view prefs (sort · group · unread) are NOT here — they moved to the
always-present **View options** menu (above). And **Compose + Search were removed entirely**
as toolbar buttons (reachable only via `c` / `/` + `Ctrl/Cmd+K`), so they are no longer
listed either. The already-adjustable surfaces are left as-is: the bottom **triage bar**
keeps its own "Customize actions", and the **sidebar / reading pane / contact pane** keep
their own toggles.

Device-local (`supermail:toolbar-controls`), like the other display prefs, so it doesn't
sync to the `mail.jls.dev` web client. Because Supermail's keyboard dispatcher fires an
action's handler without consulting the palette's availability check, **"Off" disables a
shortcut two ways** — the palette entry via each action's `isAvailable`, and the key itself
via a `toolbarActionDisabled()` guard at the shortcut binding sites (never by touching the
shared keyboard core). Store + settings UI:
`features/settings/toolbar-controls-setting.ts` + `features/settings/settings-page.tsx`.
Invariants: `.claude/memory/contracts/supermail-toolbar-controls-contract.md`.

### Sidebar: pinned vs overlay (Superhuman-style)

The Supermail nav has two modes, driven by the in-app **Settings → General →
Personalization → "Show sidebar"** toggle (Supermail's own settings page, not
Omniscio's). The flag is the `hidden` field of `features/layout/sidebar-store.ts`
(`hidden = !pinned`); it persists to `supermail:nav-hidden` and **defaults to
overlay mode** so a fresh install matches Superhuman.

- **Pinned** ("Show sidebar" on, `hidden=false`): the nav is an always-visible
  column rendered in Omniscio's sub-sidebar slot (`PersistentNavSidebar` in
  `AmcSupermailSidebarApp`). Still collapsible to the 44px icon rail.
- **Overlay** ("Show sidebar" off, the default, `hidden=true`): the sub-sidebar
  slot is empty and the mail panel spans full width with a **hamburger**
  top-left. Clicking it sets the transient `overlayOpen` flag, mounting
  `OverlayNavDrawer` (in the panel tree, `AppInner`) — a drawer that slides in
  over a dimmed list. It tucks away on **mouse-leave** (220ms grace), backdrop
  click, or Escape. `overlayOpen` is never persisted (resting state is always
  the hamburger).

Both modes share `NavMenuBody` (one flat, header-less folder list). The account
row at its top opens the **command-palette-style account selector**
(`features/auth/account-switcher.tsx`, opened via `useAccountSwitcherStore` or
the "Switch Account" palette command); it replaces the old dropdown, shows the
active mailbox with a check + `Alt N` hint, and an "Add account" row. Quick-switch
is **Alt+1..9**. "Add account" opens the **chooser** described just below rather than
jumping straight to Google. Below the folders, a **Settings** row (gear glyph → `/settings`)
sits directly in the nav — a visible entry, not tucked away — while the header's ⋯
overflow menu now holds only **Hide sidebar** + **Sign out**.

### Adding a second mailbox — Google _or_ IMAP

All three "Add account" entry points (the account selector, the account menu, and the
`add-account` palette command) open one shared dialog,
`features/auth/add-account-modal.tsx`, held open by `useAddAccountModalStore` and mounted
once in `AppInner`. Step 1 offers **Google** or **Other (IMAP)**; step 2 renders the
_existing_ `<ImapForm>` — the same component the signed-out sign-in screen uses, with its
ISPDB autoconfig — behind a back button. Google still calls `authStore.addAccount()`
unchanged.

The IMAP side is **not** the sign-in route. `authStore.linkImapMailbox()` posts to the
authenticated `POST /auth/mailboxes/imap`, which attaches the mailbox to the user its
bearer token identifies and returns **no session**:

- **`body.email` is mailbox data, never an identity.** It can't resolve, create, or mutate
  another account; the `mailboxes_owner` RLS `WITH CHECK` enforces the same boundary a
  layer down.
- **Adding a mailbox never changes who you are signed in as** — the action writes no
  `user` / `jwt` / `status`. Its sibling `loginWithImap` (public `POST /auth/imap`)
  deliberately _does_ replace the session; reusing it here would have signed the user out
  of their Gmail. Both halves of that contrast are pinned by tests in `auth-store.test.ts`.
- Both hosts pass the shared SSRF guard and a live IMAP **and** SMTP probe before anything
  is written, so a bad credential leaves no half-configured mailbox. The probe code lives
  once in `backend/src/shared/mail/imap/probe.ts`, shared with the sign-in route.
- Re-linking an address already connected **over IMAP** refreshes its credentials in place.
  Re-linking one already connected **through Google** is refused with a **409** — never
  silently converted, which would orphan that mailbox's OAuth tokens. The 409 is
  load-bearing: it is the only one this route emits, and the UI keys on it to show the
  refusal verbatim instead of humanizing it (the message contains "connected", which would
  otherwise be rewritten as unrelated advice about the server address).
- The new mailbox becomes primary and an `imap.poll` first-sync is enqueued
  fire-and-forget, so the UI reports that mail is on its way rather than showing an empty
  inbox with no explanation.

`GET /auth/me` resolves the account `kind` from the **primary** mailbox
(`desc(isPrimary), asc(id)`, limit 1). It previously read an unordered single row, so a
user holding both a Gmail and an IMAP mailbox got whichever the planner happened to
return.

The sidebar **footer** carries the sync-status line, the account/sign-out menu, and — between
them — an **always-visible "Reconnect" button** (`features/sync-status/reconnect-session-button.tsx`
→ `authStore.reconnectMailbox()`, the same re-OAuth the conditional
`sync-status/reconnect-banner.tsx` uses). It is present in **every** signed-in state (not gated on
`needsReconnect()` or `user`), so a stuck/dead session is one click from recovered without hunting
for the account-menu "Sign out" (which is gated on `user` and can vanish exactly when it's needed).

### Reading pane — theming, glass, and Focus mode

The open-conversation reader follows Omniscio's **active theme AND any custom theme**
automatically. Its message cards render on the app's **panel tokens** (`--panel-bg` /
`--panel-border` / `--panel-blur`, bridged into `.supermail-scope` in `index.css`), so
the pane is **glassy under a glass theme** (e.g. Glassmorphism — a one-tap built-in
theme) and flat under a flat one, with no hardcoded per-component colour. There is no
separate colour/theme picker inside Supermail — choose a theme (or a custom theme) in
**Settings → Appearance** and the reader follows it. Designed-HTML ("isolated") emails
render in a white-canvas iframe (they're authored for light); in **dark mode they're
smart-inverted to match** the pane — driven by the app's REAL dark state (Omniscio's own
theme in-app, NOT the OS `prefers-color-scheme`), with an auto-skip that leaves an
already-dark email alone and a per-email **"Show original (light)"** toggle. Full
behavior + invariants: [supermail-email-render-contract.md](../../.claude/memory/contracts/supermail-email-render-contract.md).

- **Nested thread (Superhuman-style).** A thread opens with only the **latest message
  expanded**; every older message collapses to a single scannable **row** — avatar ·
  sender · one-line snippet · time — so a long thread reads as a compact stack, not a
  wall of full-height cards. Click a row (or `o`) to expand it; click an expanded
  message's header (or `o`) to collapse it back to its row. Every message carries a
  coloured initials **avatar**. Both states live in `message-view.tsx` (the collapsed
  early-return row + the expanded card); only-latest-expanded is seeded in
  `conversation-store.ts`.
- **Centered, pinned subject.** The subject sits **centered above the email frame**, aligned
  to the email's own reading width, and stays **pinned** to the top as you scroll a long thread
  (it never scrolls away with the body). In Full View the header reserves side space so the
  subject clears the floating hamburger and the corner controls. Built in `thread-view.tsx`.
- **Clean by default — toolbars reveal on demand.** The reader's chrome — the top action
  cluster (**Reply** + **Filter / Focus**; Compose + Search were removed) and the bottom
  **triage bar** — is **hidden by default** so the email itself is the focus. Move to the top or
  bottom edge and it **fades/slides in on hover** (a small grip marks the bottom zone); a **pin**
  toggle locks both open. Every keyboard shortcut keeps working the whole time (the hide is purely
  visual), and on a **touch** device the toolbars stay shown (no hover there). Reveal logic:
  `use-chrome-reveal.ts` + the `chromePinned` reading pref; the bottom bar is wrapped in
  `triage-reveal-zone.tsx`.
- **Always-visible "Start a session".** One button stays put in the top-right (inside Omniscio)
  to spin up a Claude session seeded with the whole thread — see "Start a session" above.
- **Attachment chips** show just the file size at rest; the old "✓ Ready" badge is gone
  (a static attachment is not a task). Images still preview as thumbnails when fetchable;
  an in-flight download shows "Downloading…", a server-rejected one "Failed".
- **Focus mode (hide chrome, keyboard-first).** Toggle it with the **Focus** button in
  the reader's top-right controls or the **`Z`** shortcut. It hides the pane's OWN chrome
  — the triage bar, the right-hand contact rail, and the team comment bar — for a
  distraction-free read; every reading-pane shortcut stays live so you can work entirely
  by hotkey. It does NOT touch Omniscio's outer chrome. Device-local
  (`supermail-reading-prefs`, off by default). Store:
  `features/layout/reading-prefs-store.ts`.
- **Reading layout.** **Settings → General → Personalization → Reading layout** picks how the open
  email's reading column is sized: **Standard** (a wider column), **Centered** (narrower), or
  **Editorial** (narrowest). It sizes the message BODY column — the subject lives in
  the pinned header (above), so the heading style no longer varies by layout. The **reply
  composer opens at this SAME width**, so a reply is never wider than the email it answers
  (shared `readingLayoutWidthClass` in `reading-prefs-store.ts`, applied in both
  `thread-view.tsx` and `compose-page.tsx`). Device-local
  (`supermail-reading-prefs`, Standard by default).

### Compose — keep moving through mail with a draft open (desktop)

Opening a **reply / reply-all / forward** on desktop no longer traps the keyboard.
The inbox navigation keys — **J/K + arrows, plus W/S when left-handed nav is on** —
**save the draft (autosave) and jump to the previous/next email** instead of being
swallowed by the composer, so a draft never blocks triage.

- **Navigate mode on desktop.** To free the bare keys, a desktop reply opens WITHOUT
  the body auto-grabbing the caret — press **Enter** (or click) to start typing and
  **Esc** to step back out to navigation. Touch devices keep tap-to-type autofocus (no
  physical keyboard to navigate with), detected via `isCoarsePointer`.
- **Typing still types.** The nav keys are registered in the `compose` context and fire
  only when the caret is NOT in a compose field (the global keyboard listener drops bare
  keys while a text input is focused), so typing j/k/w/s INTO the draft still types.
- **Safe by construction.** No-ops at the first/last email and for a brand-new
  (non-thread) message; the draft is flushed to autosave before the composer closes.
- **When a Gmail draft is minted.** Autosave needs a recipient AND a subject (the backend
  rejects less) — and for a reply / reply-all, which opens with both already filled from the
  source message, it ALSO needs typed body text. Opening a reply and backing out (Esc, Back,
  or clicking away) leaves nothing behind; before this gate an untouched reply minted an
  empty Gmail draft on the thread, which other mail clients showed as a blank message from
  you. Code: `features/compose/use-draft-autosave.ts` (`hasAuthoredContent`).
- Code: `features/compose/use-compose-thread-nav.ts` (+ `is-coarse-pointer.ts`), wired
  in `compose-page.tsx`; the Shortcuts drawer lists it (`shortcut-catalog.ts`). Invariants
  are locked by the sub-app's own vitest tests (`use-compose-thread-nav.test.ts`) — the
  vendored app is outside Omniscio's contract system by design.

## For agents

### CLI control surface (settings + commands)

The CLI control server exposes four routes that let agents read and write Supermail's
device settings and dispatch any Supermail UI command without the panel being open.

### Auth and gating

Unlike the inbox/corpus routes (which require the global full-trust cli-token), these
four routes accept **both** the global token and scoped agent-session tokens. This means
a Claude session can tune Supermail settings and fire commands without the global
cli-token. A subset of operations is approval-gated (see below).

### The four endpoints

| Method  | Path                  | Body                                            | Gating    | Purpose                                                           |
| ------- | --------------------- | ----------------------------------------------- | --------- | ----------------------------------------------------------------- |
| `GET`   | `/supermail/settings` | —                                               | `read`    | Return the cached settings snapshot (works while panel is closed) |
| `PATCH` | `/supermail/settings` | `{ id: string, value: unknown }`                | see below | Write one setting by base storage key                             |
| `GET`   | `/supermail/commands` | —                                               | `read`    | List available commands from the cached command registry          |
| `POST`  | `/supermail/command`  | `{ id: string, args?: Record<string,unknown> }` | see below | Dispatch a command by its action-registry id                      |

### Open ONE thread in its own window

`POST /supermail/threads/:id/open` is a fifth agent-accessible route, and the only one
in the `/supermail/threads/*` family that is **not** cliTokenOnly. It opens (or focuses)
a desktop window showing that single thread so the user can reply in place.

```bash
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  http://127.0.0.1:19519/supermail/threads/<threadId>/open
# → { "ok": true, "data": { "threadId": "...", "window": "newly-created" } }
```

Why a scoped agent token is safe here, when every other route in that file needs the
global one: **this route reads no mail.** It returns no subject, sender or body, and it
never checks that the thread exists. Supermail thread ids are sequential, so an
existence check would hand any agent a mailbox-enumeration oracle — which is exactly
why the corpus routes are cliTokenOnly. Reading nothing is what makes it safe, so do
not "helpfully" add a subject to the response. An unknown id simply lands the window on
Supermail's own "couldn't load this conversation" state.

Returns `409` when 8 windows are already open (close one), and `403` when Supermail is
turned off. Re-opening a thread that already has a window focuses it and returns
`already-open` — that always succeeds, even at the cap, because it opens nothing.
Invariants: [supermail-thread-window-contract.md](../../.claude/memory/contracts/supermail-thread-window-contract.md).

```bash
# Read all settings (panel can be closed)
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:19519/supermail/settings | jq .

# Write a non-sensitive setting immediately
curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"id":"supermail:hover-act","value":false}' \
  http://127.0.0.1:19519/supermail/settings

# List available commands
curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:19519/supermail/commands | jq .

# Dispatch a non-destructive command
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "X-AMC-Source-Session-Id: $AMC_SESSION_ID" \
  -d '{"id":"inbox-focus"}' \
  http://127.0.0.1:19519/supermail/command
```

### Settings registry (29 keys)

The 29 CLI-reachable settings are registered in
`src/plugins/supermail/ui/src/features/settings/control-settings-registry.ts`. Each
entry exposes `read()` (returns the live store value) and `write(value)` (delegates to
the store's existing setter — no new persistence). For the AppSettings bridge that PROMOTES
these settings into Omniscio + the per-identity migration pattern, see
[supermail-ui-settings-cli.md](../../.claude/memory/supermail-ui-settings-cli.md). Key behaviors:

- **Identity-scoped keys** (`supermail:filters:enabled`, `supermail:ai-filter:mode`,
  `supermail:ai-filter:instruction`) resolve the active Supermail account internally;
  callers always use the base key without any per-account suffix.
- **Object-valued settings** (`supermail:toolbar-controls`, `supermail-reading-prefs`,
  `supermail-inbox-view-prefs`) perform a shallow merge on PATCH, so you only need to
  send the fields you want to change.
- **Array-valued settings** (`supermail-triage-actions`, `supermail:pinned-label-ids`)
  replace the full array when a valid array is supplied.

### Command dispatch model

`POST /supermail/command` is **generic**: it resolves the given `id` against the
vendored action registry and fires it. There is no allowlist — every registered
command is CLI-reachable by construction. The `GET /supermail/commands` response lists
all commands currently in the registry (requires the panel to have been open at least
once since the last app start; otherwise returns the persisted cache from the last
session).

A `409` response from either the settings write or the command dispatch means the
Supermail panel is not currently mounted. Settings writes that time out are queued and
applied on the next panel mount.

### Approval gating

Two subsets route through the standard CLI approval gate (a `202` inbox card that
applies only after the user approves):

**Sensitive setting writes** — the three keys in `SUPERMAIL_SENSITIVE_SETTINGS`:

- `supermail:ai-filter:instruction` — a plain-language rule passed to an AI model
  (prompt-injection risk from email content)
- `supermail:ai-filter:mode` — switches AI filtering between preview and live
- `supermail:filters:enabled` — master kill-switch for automatic filter rules

**Destructive command dispatches** — the 13 ids in `SUPERMAIL_DESTRUCTIVE_COMMANDS`
(all send variants, trash, spam, mute, sign-out, discard, `rp-trash`, `rp-spam`,
`rp-remove-all-labels`). These are irreversible or send real email.

### Offline reads and pending writes

The main-side control store (`src/main/services/supermail/supermail-control-store.ts`)
caches the last settings + command list sent by the vendored panel via
`SUPERMAIL_CONTROL_SYNC`, and persists them to `supermail-control.json` under `userData`.
This means `GET /supermail/settings` and `GET /supermail/commands` return data even when
the Supermail panel is closed. A PATCH that cannot reach the panel returns `409`; the
store enqueues the write and drains it automatically when the panel next mounts.

### Parity guard

A build-failing guard pair ensures this surface never regresses silently:

- [tests/unit/lint/supermail-settings-cli-parity.test.ts](../../tests/unit/lint/supermail-settings-cli-parity.test.ts) — every persisted Supermail setting key is CLI-registered XOR explicitly exempted.
- [tests/unit/lint/supermail-command-cli-parity.test.ts](../../tests/unit/lint/supermail-command-cli-parity.test.ts) — every destructive command is in `SUPERMAIL_DESTRUCTIVE_COMMANDS`.

Contract: [.claude/memory/contracts/supermail-cli-parity-contract.md](../../.claude/memory/contracts/supermail-cli-parity-contract.md).

### How it is built (for agents)

> **Developing Supermail (run / test / land / deploy each half)?** See
> [supermail-cross-repo-development.md](../../.claude/memory/supermail-cross-repo-development.md) -
> the in-repo UI + backend map, how they connect, and how each half lands and deploys.

The vendored Supermail source lives in **`src/plugins/supermail/ui/`** and is
deliberately isolated from Omniscio's tooling (its own ESLint/Prettier ignores, its
own `tsconfig.json` via `npm run typecheck:supermail`, and its own scoped
Tailwind build).

Key decisions:

1. **Routing.** Supermail uses URL routes. Inside Omniscio those run in an
   **in-memory router** scoped to the panel; Omniscio's renderer still owns the real
   browser URL. The composition root is
   `src/plugins/supermail/ui/src/amc/AmcSupermailApp.tsx`.
2. **Styling collision.** Supermail and Omniscio both define utility tokens.
   Supermail gets its **own scoped Tailwind build** — every utility is
   namespaced under a `.supermail-scope` wrapper. `supermail.generated.css` is a
   build OUTPUT — **not committed** (gitignored). It is regenerated on demand at the
   **renderer-build boundary**: `scripts/ensure-renderer-build.js`'s `runBuild()` regenerates
   it right before the vite renderer build, so every dev rebuild path gets a fresh sheet —
   `predev` startup AND the `dev-restart-supervisor`'s in-session relaunch — and
   `scripts/electron-build.js` does the same for `npm run build` / `npm run package`; the vitest
   runtime stubs the import so tests need neither the file nor the Tailwind toolchain. (Coupling
   the regen to the boundary — not only to the `predev` chain — is what stops an in-session
   relaunch from bundling a stale sheet: the `.sm-edge-fade` row-overlap fix.)
   Regenerate manually with `npm run build:supermail-css`. Removing it from git ended the
   committed-artifact drift that used to dirty the tree and pause the auto-lander (guarded
   by `tests/unit/lint/supermail-generated-css-not-committed.test.ts`).

   > **GOTCHA — editing supermail CSS is NOT live-reloaded.** The source of truth
   > is `src/plugins/supermail/ui/src/index.css`, but the app loads the _generated_
   > `supermail.generated.css`. The generator runs at the **renderer-build boundary** —
   > `npm run dev` startup AND any in-session dev-restart rebuild — but **nothing re-runs
   > it on a plain `index.css` save**. So a rule you add or
   > change in `index.css` while dev is already up is **invisible** (the app keeps
   > serving stale CSS) until you regenerate: run `npm run build:supermail-css`, keep
   > `npm run dev:supermail-css` (`--watch`) running alongside dev, or restart
   > `npm run dev`. This is the **opposite** of supermail **TSX**, which the Omniscio
   > renderer's own Vite build compiles and hot-reloads live — so TSX edits appear
   > instantly while CSS edits silently don't. (This exact trap hid the split-tab
   > focus-ring fix: the `index.css` rule was correct but never reached the running
   > app until the regen ran. Note the app renders supermail **inline**, not in a
   > webview — `SupermailPanel.tsx` imports the scoped stylesheet directly — so Omniscio's
   > renderer-global `*:focus-visible` outline reaches supermail's own focused
   > elements, which is why that ring appeared on the tabs in the first place.)

3. **Runtime bridge.** Inside Omniscio, the panel installs a synchronous
   `window.__AMC_SUPERMAIL__` bridge that reads/writes Omniscio stores directly.
   The same runtime falls back to browser storage, browser navigation, and no-op
   host hooks for the standalone `mail.jls.dev` build - no webview boundary.
4. **Auth.** The OAuth flow lands in main via the `agentmc://supermail/auth`
   deep link (the backend's primary scheme; `omniscio://supermail/auth` is an accepted
   alias). Main writes the backend-issued JWT to `settings.supermailJwt`
   (encrypted at rest via `ENCRYPTED_APP_SETTINGS_KEYS`) and emits an IPC
   event; the renderer's bridge hydrates the token synchronously.
   **Alternative (opt-in):** with `supermailUseAmcGmail` ON, mail is instead read
   through Omniscio's shared Google grant via the `gmail:api-request` relay (token stays
   in Main) — see "Reading mail through your Omniscio Google connection" above and
   `supermail-shared-gmail-grant-contract.md`.
5. **Resilience.** Supermail's own React root is wrapped in **error boundaries**
   (root + per-message) from `src/plugins/supermail/ui/src/shared/ui/error-boundary.tsx`,
   so a render-time crash shows a contained "Something went wrong" card (or a small
   per-message placeholder) instead of blanking the whole mail app — which it used to,
   since supermail historically had none (Sentry 7572518402). Invariants live in
   `.claude/memory/contracts/supermail-resilience-contract.md`.
6. **Standalone web deploy.** The root workflow
   `.github/workflows/deploy-supermail-frontend.yml` builds
   `src/plugins/supermail/ui/Dockerfile` and deploys Cloud Run service
   `supermail-frontend` for `mail.jls.dev`. The backend deploy is the sibling
   `.github/workflows/deploy-supermail-backend.yml`.

## Related

The overview, the other parts, and everything else worth reading next all sit on [Supermail](supermail.md).
