Omniscio documentation
Browse all documentation
  1. Getting Started13
  2. Sessions & Agents115
  3. Inbox & Notifications59
  4. Projects & Tasks95
  5. Automation & Scheduling75
  6. Knowledge & Memory26
  7. AI Features60
  8. Integrations100
  9. Plugins & Marketplace33
  10. Cloud & Teams56
  11. Settings & Customization58
  12. Account & Billing28
  13. Troubleshooting84
  14. CLI & API Reference22
  15. Legal & Policies4
  16. Uncategorised22

Supermail (part 2)

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 "…".

What it is

This is part 2 of the Supermail 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 — 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.

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. "Off" hides the button, and with it the palette entry — but it does NOT disable the keyboard shortcut. The palette entry goes through each action's isAvailable, backed by isActionEnabled(); that helper's own doc says it "gates only those VISIBLE surfaces … it does NOT gate the keyboard shortcut, which always fires". app-shortcuts.tsx binds the handler unconditionally — a hidden button never disables its hotkey (owner directive 2026-08-17), 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.

  • 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.

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.

# 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. 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 — every persisted Supermail setting key is CLI-registered XOR explicitly exempted.
  • 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.

How it is built (for agents)

Developing Supermail (run / test / land / deploy each half)? See 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.

Last verified 2026-10-06