Detached session window (Pop Out)
Pop a session out of the main Omniscio window into one of its own, so you can keep watching or driving it beside other work — how to open one, what it shares with the session it came from, and how to put it back.
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 toFocus Detached Windowand 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
ExternalLinkicon 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/heightplus 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_youwhile 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
- 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.
- 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. - 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.
- Send / read messages from either window. They are the same conversation. The transcript, composer state, and scroll position all stay in sync.
- 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
ExternalLinkchip next to the title so you can tell which sessions are popped out. (The⋯menu'sFocus Detached Windowentry does the same thing.) - 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.
- 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), the sidebar right-click menu (SessionContextMenu.tsx), and the inbox row's right-click menu (InboxItemContextMenu.tsx, wired in SidebarOverlays.tsx for session-backed rows on desktop) — calls the WINDOW_DETACH_SESSION IPC (src/main/ipc/window-handlers.ts) through the shared 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) 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):
WINDOW_DETACHEDfires when the detached window is created. The main window's renderer adds the session id to itsdetachedSessionIdsSet (Zustand slice onsession-store).WINDOW_REATTACH_SESSIONfires ondetached.on('closed', ...). The main window's renderer removes the id from the Set, which causes the placeholder to disappear and the inlineSessionPanelto 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:
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 — 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. 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.
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) 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 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 — the main window, tray, and global hotkeys
- start-a-new-session.md — how sessions are spawned (the source for what gets popped out)
- send-a-message.md — composer behavior, same in both windows
- scroll-position-memory.md — sibling pattern: per-session UI state remembered across launches
Last verified 2026-10-05