# Multi-view: tabs, tear-off, separate windows

Goal: multiple **views** over one backend — like Thunderbird/Outlook — so the
user can keep a message open while working the list, compare two folders, etc.
NOT multiple daemon instances (`--another` already exists for that, and a
second daemon means a second DB connection — contention, not what's wanted).

## Key fact: reminders already spawn separate native windows

`MailxService.showReminderPopup` → injected `popupFn` → msger
`showMessageBoxEx` opens a **real OS WebView window** fed a self-contained HTML
document (`rawHtml: true`: the caller ships a full HTML doc with its own button
row + an inline script that posts results back over the wry bridge). It returns
a handle with `pid` + `close()`.

Consequence: msger spawning extra native windows is **not** the blocker. The
"popups don't inherit the custom protocol" limitation only blocks a window that
must load the *multi-file app* (index.html + app.bundle.js + CSS + importmap
deps, all served from `msger.localhost`). A **single rendered view** is
self-contained content — the reminder shape.

So:
- **Torn-off READ-ONLY message reader** = the reminder mechanism. The daemon
  already has rendered `bodyHtml` from `getMessage`; wrap header + sanitized
  body as a standalone HTML doc, hand to `showMessageBoxEx`. No custom
  protocol, no second IPC channel. Cheap.
- **Torn-off INTERACTIVE view** (reply / navigate / delete / live sync
  updates) needs a real IPC channel back to the daemon → msger multi-channel
  work. Expensive. Deferred.

## Three view modes, one backend

1. **Tabs** inside the main window — one WebView, one IPC channel, a tab strip;
   each tab is an independent three-pane (or a single-message view). Pure
   client work.
2. **Tear-off** — detach a tab into its own OS window.
   - Read-only message tab → `showMessageBoxEx` + `rawHtml` (reminder path).
   - Interactive tab → needs msger multi-channel IPC (later).
3. **Re-dock** — drag a torn-off window's content back in as a tab. For the
   read-only reader this is just: close the popup, re-open the message as an
   in-window tab. For interactive, follows whatever (2) settles on.

## Tabs architecture (the real work)

Today the list view is module-level global state in `client/components/
message-list.ts` — `currentAccountId`, `currentFolderId`, `searchMode`,
`currentSearchQuery`, `positionMemory`, `listCache` — and `message-state.ts` is
a single global "source of truth" for the list+viewer. One window = one view.

Tabs require that view-state to become **per-tab**:

- **Per-tab:** selected account+folder (or search), selected message, list
  scroll position, the `message-state` instance, sort order, flagged-only
  toggle.
- **Shared (stays global):** the IPC connection + sync event stream, folder
  counts, the contacts cache, settings, the parsed-body LRU on the daemon.
  Sync/new-mail events broadcast to every tab; each tab decides if the event
  touches its folder.

Shape: a `TabManager` owning an array of `ViewTab`, each `ViewTab` holding its
own `MessageListState` + the DOM subtree for its three-pane. Only the active
tab's subtree is in layout; inactive tabs keep their DOM detached (cheap) or
their state and rebuild on activate. `message-list.ts` functions take a
`ViewTab` (or read `TabManager.active`) instead of the module globals.

## Staging

1. **Tab strip + per-tab three-pane.** Extract the message-list globals into a
   `ViewTab`/`TabManager`; render a tab strip; New Tab opens another inbox
   view. The bulk of the work and the architectural commit.
2. **Open-in-new-tab** for a message (and a folder). Exercises per-tab state on
   the simplest views.
3. **Tear-off a read-only message tab** → `showMessageBoxEx` standalone window
   (reminder mechanism). Re-dock = reopen as a tab.
4. **Interactive tear-off / true second app window.** Needs msger to let a
   spawned window inherit the custom protocol AND carry its own IPC channel,
   plus the daemon multiplexing IPC and broadcasting events to N channels.
   Separate, cross-component project — do last, only if the read-only
   tear-off proves insufficient.

Start at stage 1. Each stage is independently shippable.
