---
title: Detached session window (Pop Out)
---

# Detached session window (Pop Out)

## What it is

A **single session** can be popped out of the main Omniscio window into its own dedicated BrowserWindow — useful when you want one long-running conversation always visible on a second monitor (or sized just-so on a corner of your main monitor) while the main Omniscio window stays on its primary view.

The detached window is **identical** to the inline session panel: same composer, same scrollback, same toolbar, same keyboard shortcuts — it just floats in its own OS window with its own title bar (`Session — <id-prefix>`). It is **not** a duplicate or a read-only mirror; there is only ever one live conversation, and both the main window and the detached window are views onto it. Send a message from either surface and the other reflects the change immediately.

Pop Out is **always available** — no setting to enable, no feature flag — and it works for **any** session, including ones that have already ended or been archived. It lives in three places: every session's overflow menu (`⋯`) in the session toolbar, the session's **right-click (context) menu** in the sidebar, and the **right-click menu on a session row in the inbox**.

Key behaviors:

- **Pop Out menu entry** lives in the session's `⋯` (overflow) menu, the sidebar right-click (context) menu, AND the inbox row's right-click menu — `Pop Out to Window`, offered for **any** session (running, or already ended/archived). In the sidebar it shows for a single selected session; the inbox item shows for any session-backed row. When the session is already popped out, the same entry swaps to `Focus Detached Window` and brings the existing window to the front instead of opening a duplicate.
- **Sidebar chip** — while a session is detached, its row in the main window's sidebar shows a small `ExternalLink` icon next to the title. Clicking the row in the main window **focuses** the detached BrowserWindow rather than re-mounting the session inline (the inline slot is replaced by a placeholder — see below).
- **Placeholder in the main panel area** — when the detached session is the active one in the main window, the panel area shows "Open in another window" with two buttons: **Bring window to front** (focuses the detached BrowserWindow) and **Reattach here** (closes the detached window, which re-mounts the session inline).
- **Window-position memory (per session)** — Omniscio remembers where you put each session's detached window, how big you made it, AND whether you left it maximized or in full screen. Re-popping the same session restores the last `x/y/width/height` plus that maximized/fullscreen state. First-time detach defaults to 700×600 centered. The saved size is re-applied *after* the window is created on its target monitor, so a window parked on a second monitor whose display-scale differs from your main screen no longer shrinks a little on each reopen (a cross-DPI multi-monitor fix — see the breakout-window-geometry contract).
- **Stays open read-only after the session ends** — if a session reaches a terminal state (ended, archived) while detached, the window does **not** auto-close; you can keep reading the transcript until you close it manually.
- **Ctrl+W / Cmd+W opens a close-confirm modal** — in a popped-out window, Ctrl+W (or Cmd+W) no longer closes the window outright. It opens a small modal offering **Close window** (closes the pop-out and leaves the session running in the background), **Close & stop session** (stops the running session, then closes the window), and **Cancel**. Pressing Ctrl+W / Esc again dismisses it. The title-bar **X** still closes directly (reattach), unchanged. The embedded isolated-view guest never gets this shortcut — it has no window of its own to close, so its Ctrl+W stays forwarded to the main window.
- **A link to ANOTHER session opens in the main window.** Clicking a session link inside a popped-out window — a chat link, a "Spawned by…" provenance link, or a spawned-session chip — does **not** try to navigate the pop-out (it is pinned to its one session and has no session list). Instead it opens that session in the **main** window and brings the main window forward, the same cross-window handoff the floating "Requested by…" prompt uses. (Before 2026-08-24 such a click dead-ended, and a link pointing at the pop-out's own session wrongly said "You're already viewing this session.")
- **Notifications are not de-duped.** OS notifications still fire from the main process regardless of which window is visible. If a session goes to `needs_you` while popped out, you get the usual sound + system notification.
- **The detached window is sandboxed and isolated.** No `<webview>` tags, Chromium OS sandbox enabled, `contextIsolation: true`, `nodeIntegration: false`. The same preload bridge the main window uses is the only renderer→main surface.

## Where to find it

### How to use it

1. **Open the session you want to pop out** in the main Omniscio window. Click into its row in the sidebar so the session panel is the active view.
2. **Open the session's `⋯` overflow menu** in the session toolbar (or **right-click the session's row in the sidebar**) and pick **Pop Out to Window**. A new floating window appears with the session inside it. The main window's panel area swaps to the "Open in another window" placeholder.
3. **Move and resize the detached window** to wherever you want it. Omniscio remembers the position and dimensions per session — next time you pop out **this** session, it reopens in the same spot at the same size.
4. **Send / read messages from either window.** They are the same conversation. The transcript, composer state, and scroll position all stay in sync.
5. **Click the session's row in the main window's sidebar** while it's detached — instead of re-mounting it inline, Omniscio **brings the detached window to the front**. The sidebar row shows a small `ExternalLink` chip next to the title so you can tell which sessions are popped out. (The `⋯` menu's `Focus Detached Window` entry does the same thing.)
6. **Reattach** when you're done with the floating window — either:
   - Click **Close** (the **X**) on the detached window's title bar — the session re-mounts in the main window automatically. (Pressing **Ctrl/Cmd+W** instead opens the close-confirm modal described above; its **Close window** option does the same reattach, while **Close & stop session** also terminates the session.)
   - Or click **Reattach here** on the main window's placeholder. Same effect — the detached window closes, the inline panel comes back.
7. **Read an ended session in a popped-out window.** Detached windows do **not** auto-close when their session ends or gets archived. You can keep the transcript open in its own window for as long as you want and close it when you're done.

## How it behaves

### How it works

The `Pop Out to Window` entry — in the session's `⋯` overflow menu ([SessionOverflowMenu.tsx](../../src/renderer/src/features/sessions/SessionOverflowMenu.tsx)), the sidebar right-click menu ([SessionContextMenu.tsx](../../src/renderer/src/features/dashboard/SessionContextMenu.tsx)), and the inbox row's right-click menu ([InboxItemContextMenu.tsx](../../src/renderer/src/features/dashboard/InboxItemContextMenu.tsx), wired in [SidebarOverlays.tsx](../../src/renderer/src/features/dashboard/SidebarOverlays.tsx) for session-backed rows on desktop) — calls the `WINDOW_DETACH_SESSION` IPC ([src/main/ipc/window-handlers.ts](../../src/main/ipc/window-handlers.ts)) through the shared [session-window.ts](../../src/renderer/src/lib/session-window.ts) helper. All three surfaces offer it for any session — the handler is status-agnostic, so there is no terminal gate. When the session is already in `detachedSessionIds`, the same menu slot renders as `Focus Detached Window` and routes to `WINDOW_FOCUS_DETACHED` instead. The handler is idempotent — if a detached window already exists for that session id, it just focuses the existing one and returns `{ state: 'already-open' }`. Otherwise it creates a fresh `BrowserWindow` with `webPreferences: { sandbox: true, contextIsolation: true, nodeIntegration: false, preload: '../preload/index.js' }` and loads the renderer with a `?detached=<sessionId>` query param. The renderer's `App.tsx` branches on that param at boot — if present, it mounts `DetachedSessionApp` ([src/renderer/src/DetachedSessionApp.tsx](../../src/renderer/src/DetachedSessionApp.tsx)) instead of the full `App`, which renders **just** that one session's `SessionPanel` with no sidebars and no inbox.

The main window learns about detached state through two push events on the standard push bus ([src/main/ipc/push-bus.ts](../../src/main/ipc/push-bus.ts)):

- **`WINDOW_DETACHED`** fires when the detached window is created. The main window's renderer adds the session id to its `detachedSessionIds` Set (Zustand slice on `session-store`).
- **`WINDOW_REATTACH_SESSION`** fires on `detached.on('closed', ...)`. The main window's renderer removes the id from the Set, which causes the placeholder to disappear and the inline `SessionPanel` to re-mount.

The renderer's `Dashboard.tsx` keep-alive pool (the bounded mount of recently-active panels) excludes detached sessions via a `liveSessions` filter, and the mount-on-demand path adds an additional `!detachedSessionIds.has(...)` guard so the placeholder doesn't double up with an inline mount. On cold mount the renderer also calls `WINDOW_LIST_DETACHED` to hydrate the Set in case some windows were already open from a previous render.

**Per-session position memory** uses a small SQLite table introduced in migration **v205**:

```sql
CREATE TABLE detached_window_positions (
  session_id TEXT PRIMARY KEY,
  x INTEGER NOT NULL,
  y INTEGER NOT NULL,
  width INTEGER NOT NULL,
  height INTEGER NOT NULL,
  updated_at TEXT NOT NULL
)
```

Later migrations added a `maximized INTEGER NOT NULL DEFAULT 0` column and a `fullscreen INTEGER NOT NULL DEFAULT 0` column (Windows reports `isMaximized() === false` while in true OS fullscreen, so the two are tracked separately). Save/restore goes through the shared helpers in [src/main/window-geometry-persist.ts](../../src/main/window-geometry-persist.ts) — `geometryToPersist` (saves the un-maximized rectangle plus both flags), `applyRestoredBounds` (re-applies the saved size), and `applyWindowStateOnRestore` (re-maximizes / re-enters fullscreen).

Helpers live in [src/main/db/queries-detached-window-positions.ts](../../src/main/db/queries-detached-window-positions.ts). On every detach, the handler looks up the row (or null) and constructs the `BrowserWindow` at the saved bounds; first-time detach falls through to a 700×600 default. It then **re-applies the saved size with `setBounds` after construction** and re-applies the maximized/fullscreen state — both before the window is shown. The `setBounds` re-apply is the cross-DPI fix: the constructor interprets width/height in the *primary* monitor's DIP space, so a window created on a secondary monitor with a different scale factor comes back slightly smaller each reopen (cascading to its minimum) unless the size is re-applied once the window is on the target monitor. While the window is open, `move` and `resize` (the high-frequency drag stream) trigger a **500 ms debounced save**, while the discrete state changes — `maximize` / `unmaximize` / `enter-full-screen` / `leave-full-screen` — persist **immediately** (so a maximize can't be lost to a quit inside the debounce window). A final synchronous save also runs on the `close` event (before destruction — `closed` fires after teardown when `getBounds()` would throw). Invariants for all break-out windows: [breakout-window-geometry-contract.md](../../.claude/memory/contracts/breakout-window-geometry-contract.md).

The **sidebar chip** is rendered by `SidebarSessionRow.tsx` via `useSessionStore((s) => s.detachedSessionIds.has(session.id))`. The **placeholder** is `DetachedElsewherePlaceholder.tsx` and shows two buttons that invoke `WINDOW_FOCUS_DETACHED` (focus the popped-out window — falls back gracefully if the window was closed between render and click) and `WINDOW_REATTACH_SESSION` (close the detached window, which fires the reattach push and re-mounts inline).

Single push-bus fan-out via the central `windowRegistry` ([src/main/services/window-registry.ts](../../src/main/services/window-registry.ts)) ensures session-scoped pushes (output, status changes, etc.) reach **all** open BrowserWindows for that session — the main window's keep-alive panel **and** the detached window. No special-casing per push channel.

**Ctrl/Cmd+W close-confirm.** In a pop-out, [DetachedCloseShortcut.tsx](../../src/renderer/src/features/sessions/DetachedCloseShortcut.tsx) registers a capture-phase Ctrl/Cmd+W listener through the gated `useGlobalKeydown` and opens a `DialogShell` confirm instead of letting the window close (the capture-phase `preventDefault` cancels the native close). **Close window** calls `window.close()` (the session keeps running); **Close & stop session** invokes `SESSION_TERMINATE` for the bound session, then closes. It is rendered only in the pop-out branch of `DetachedSessionApp` — never the embedded isolated-view guest, which must not call `window.close()`. Because every `DialogShell` already closes on Ctrl+W (frontend-modal-contract) and `useGlobalKeydown` yields while a modal is open, a second Ctrl+W simply cancels the confirm rather than re-opening it.

## Related

- [tray-and-window.md](tray-and-window.md) — the main window, tray, and global hotkeys
- [start-a-new-session.md](start-a-new-session.md) — how sessions are spawned (the source for what gets popped out)
- [send-a-message.md](send-a-message.md) — composer behavior, same in both windows
- [scroll-position-memory.md](scroll-position-memory.md) — sibling pattern: per-session UI state remembered across launches
