---
title: Pop out a project or integration window
---

# Pop out a project or integration window

## What it is

A **whole project**, an **integration** (KMS-style work surface), or a **marketplace plugin panel** can be popped out of the main Omniscio window into its own desktop window. This is the project-level sibling of the [detached session window](detached-session-window.md) (which pops out a single conversation).

- **Pop out a project** → you get a self-contained workspace for that one project: its session list on the left, the open conversation on the right. You can switch between that project's sessions and start new ones — all inside the popped-out window, while the main window stays on whatever you left it.
- **Pop out an integration** → you get that integration's panel (Tasks, Recipes, Skills, Tools, Stats, …) in its own window. A **split-surface** integration — one with its own list pane, like **Team Chat** (channel/DM list + conversation), Supermail, or the Marketplace — pops out as the full **two-pane** window, list and panel together, exactly as it looks docked in the main app. The Google work surfaces — **Calendar, Drive, and Sheets** — pop out too, each as its own full panel.

- **Pop out a marketplace plugin** → you get that plugin's panel in its own window, with **no work required from the plugin author**. Any plugin that declares a panel (`ui.entryPoint`) qualifies automatically, including ones published long before this existed. A plugin that ships only an overlay and no panel (virtual-pets) correctly does not offer it — there is nothing to detach.

It is **one mechanism**, not two: in Omniscio's data model an integration _is_ a "virtual project" (a `projects` row whose `folderPath` is a `__sentinel__`), so a single window keyed by a **project id** covers both — a real project renders the workspace, a virtual project renders the integration's panel.

Key behaviors:

- **"Open in new window"** appears in a project's (or integration's) sidebar right-click menu. It is gated by eligibility: a real project always qualifies; a virtual project qualifies only if it renders a real panel and does not already manage its own dedicated window. This includes the Google **Calendar / Drive / Sheets** panels. The two-pane messaging surfaces — **Gmail** and **text messages (SMS)** — stay out for now: a standalone window for them needs its own list-plus-reader layout, which is a separate follow-up.
- **KMS and Scratchpads use their _own_ dedicated windows** — they ship their own standalone windows (with their own toolbar pop-out buttons), so they are deliberately kept off the _generic_ mechanism. Their sidebar right-click **Open in new window** still works — it just opens that dedicated window instead of a second generic one, so you never end up with two windows for the same surface.
- **Live** — the popped-out window stays in sync with the main app: new messages, status changes, and integration updates appear in real time.
- **Signed in, and loading its own data** — a popped-out window is a second window of the same app, and it signs in for itself, so a surface that needs to know who you are works the moment the window opens. The same rule covers the background loading the main window normally does: a panel that reads a live list the main window keeps (Team Chat's channels, for example) loads its own copy inside the popped-out window — a popped-out **Team Chat** shows your channels and reopens the conversation you were last in, exactly as it does docked. What a pop-out deliberately does **not** start is the main window's notification and inbox services, so you never get a second copy of a desktop alert from the window you popped out.
- **Theme-following** — the window adopts your light/dark + visual theme and updates when you change it.
- **Themed title bar (opt-in)** — by default a popped-out window keeps the **native OS title bar**. An integration can instead opt its window into Omniscio's frameless "work-surface" chrome — a slim draggable title strip + themed OS caption buttons (minimize/maximize/close that follow your theme), the same look as the KMS and Scratchpad windows. **Tasks** is the first to use it; real projects and every other integration stay native-framed.
- **Window-position memory (per project)** — Omniscio remembers where you put each project's window and how big you made it; re-opening the same project restores the last `x/y/width/height`, plus whether you left it **maximized or in fullscreen**. First open defaults to 1100×760.
- **Ctrl+W closes it** (Cmd+W on a Mac) — exactly like its close button, and it works the same in **every** pop-out window: the Vault (KMS), Scratchpad, Writer, Support Chat, Job Monitor, project docs and a single SuperMail email. If a dialog is open inside the window, Ctrl+W closes the dialog first; the next Ctrl+W closes the window. It still closes while you are typing — a reply you were composing in SuperMail is kept as a draft, and the Vault, Scratchpad and Writer save your last typing when the window closes. A popped-out **session** window is the one exception: there Ctrl+W asks **Close session window?** first, because closing it can also stop the session.
- **Sandboxed and isolated** — Chromium OS sandbox, `contextIsolation: true`, `nodeIntegration: false`; the same preload bridge the main window uses is the only renderer→main surface. **Exception: a webview-hosting surface** (the Browser integration, and every marketplace plugin panel — a plugin panel _is_ a `<webview>`) gets `webviewTag: true` + host `sandbox: false`, the same posture as the main window; the guest itself is still force-hardened on attach.
- **The main window is untouched — for a project or an integration.** A project shown in its own window is _not_ removed from the main window; they are independent views (no placeholder, no "reattach"). Two mounts of a React panel are two views of one store, which is harmless.
- **A PLUGIN behaves like a session instead, and must.** A plugin panel is a `<webview>`: a separate JS realm with its own timers, network calls and storage handle, so a second copy is not a second view — it is a second running instance on the same origin and partition, and the host delivers every plugin event to BOTH. So while a plugin is popped out the main window drops its copy and shows the same **"Open in another window"** placeholder a detached session shows, with **Bring window to front** and **Reattach here**. Clicking the plugin in the sidebar takes you to that placeholder rather than to the window, which is what makes Reattach reachable at all.
- **A popped-out sidebar _group_ launcher opens its members in their own windows.** The catch-all sidebar groups — **Productivity**, **Automation**, **Insights** — show a roll-up "launcher" of their member apps (Tasks, Scratchpads, KMS, …). Pop that launcher out to a second monitor and click a member tile, and it opens **that app in its own window** (reusing the same "Open in new window" opener), because a popped-out window is pinned to one surface and can't navigate in place. In the main window the same tile switches the view inline, exactly as before.

## Where to find it

Pop out is **always available** on desktop, no setting or feature flag. It lives in the **right-click (context) menu** of the project/integration row in the projects sidebar: **Open in new window**.

## How it behaves

### How to use it

1. **Right-click a project (or integration) row** in the projects sidebar of the main Omniscio window.
2. Pick **Open in new window**. A new window opens: for a project, its workspace (session list + conversation); for an integration, its panel.
3. **Switch sessions / start new ones** inside a project window using its session list — exactly like the main window, scoped to that one project.
4. **Move and resize** the window wherever you want; Omniscio remembers the position and size per project, so next time you open _this_ project it reopens in the same spot.
5. **Open it again** while it's already open — Omniscio just brings the existing window to the front instead of spawning a duplicate.
6. **Close** the window from its title bar — or press **Ctrl+W** — when you're done (the project always remains available in the main window).
7. **For a plugin, bring it back** by clicking the plugin in the sidebar and pressing **Reattach here** on the placeholder. That closes the plugin's window and re-mounts the panel inline. (Closing the window from its title bar does exactly the same thing — the close _is_ the reattach.)

### How it works

The **Open in new window** entry lives in the project row's context menu ([ProjectListItem.tsx](../../src/renderer/src/features/dashboard/ProjectListItem.tsx)); its target is resolved by `resolveProjectPopOut` ([pop-out-eligibility.ts](../../src/renderer/src/integrations/pop-out-eligibility.ts)). For a **generic-eligible** row — a real project, or a virtual project that renders a panel and is not one of the dedicated-window integrations (KMS / Scratchpads), i.e. `canOpenProjectInWindow` returns true — it opens the shared project-window. "Renders a panel" has two sources: the integration's UI manifest has a `panelComponent`, OR its `channel-adapter` sentinel is listed in the main-safe `POPPABLE_CHANNEL_PANELS` table ([poppable-channel-panels.ts](../../src/shared/poppable-channel-panels.ts)) — the second source, which covers **Calendar / Drive / Sheets** (their `channel-adapter` kind forbids them a UI-registry `virtualProjectPath`). It calls the `WINDOW_OPEN` IPC with `{ projectId }`. For the two **dedicated-window** integrations — KMS and Scratchpads — `resolveProjectPopOut` instead returns a `kms` / `scratchpads` target and the item calls that surface's OWN opener (`KMS_WINDOW_OPEN` / `SCRATCHPAD_WINDOW_OPEN`), so the menu offers pop-out for them without routing through — or duplicating — the generic window. The dispatch is locked by [pop-out-eligibility.test.ts](../../tests/unit/integrations/pop-out-eligibility.test.ts).

The handler ([project-window-handlers.ts](../../src/main/ipc/project-window-handlers.ts)) delegates to the manager [project-window.ts](../../src/main/services/project-window.ts). `openProjectWindow(projectId)` is idempotent — if a window already exists for that project it focuses it and returns `{ state: 'already-open' }`; otherwise it resolves the kind (`resolveProjectWindowKind`: real project vs integration, deriving the integration's push allowlist from its manifest `pushChannels`), creates a `BrowserWindow` (`sandbox: true`, `contextIsolation: true`, `nodeIntegration: false`, the shared preload) and loads the renderer with a `?window=<projectId>` query param.

The renderer's `main.tsx` branches on that param at boot (the same single-divergence trick the detached session window uses with `?detached=`) and mounts `DetachedWindowApp` ([DetachedWindowApp.tsx](../../src/renderer/src/DetachedWindowApp.tsx)) instead of the full `App`. `DetachedWindowApp` adopts the theme (`useWindowThemeBootstrap`), resolves the project, and forks:

- **Real project** → `DetachedProjectWorkspace` ([DetachedProjectWorkspace.tsx](../../src/renderer/src/features/dashboard/DetachedProjectWorkspace.tsx)): the shared `SessionHostSidebar` + `useProjectSessionHost` (the contract-blessed reuse path — never hand-rolled session rows) on the left, the selected session's `SessionPanel` on the right.
- **Virtual project (integration)** → `DetachedIntegrationPanel` ([DetachedIntegrationPanel.tsx](../../src/renderer/src/integrations/DetachedIntegrationPanel.tsx)): looks the integration's `panelComponent` up in the UI registry (`getByVirtualPath`) and mounts it. **Channel-adapter panels** (Calendar / Drive / Sheets) are the exception — they have no UI-registry `panelComponent`, so when `getByVirtualPath` misses, it falls back to a small sentinel→panel map (`CHANNEL_ADAPTER_POPOUT_PANELS`, reusing the Dashboard's lazy panel) — the renderer half of the `POPPABLE_CHANNEL_PANELS` table that also drives eligibility and push-routing, the two halves locked by [poppable-channel-panel-parity.test.ts](../../tests/unit/lint/poppable-channel-panel-parity.test.ts). It renders panel-only **only when the integration has no `sidebarComponent`**; whenever one exists it re-hosts that Pane-2 nav beside the panel — **including the split-surface integrations that ALSO set `panelOwnsLayout`** (Team Chat, Supermail, Marketplace, Marketplace-review). That `panelOwnsLayout` flag is a Dashboard-Pane-2 concern (it suppresses the empty session-list fallback) and must never gate the pop-out sidebar — gating on it dropped Team Chat's channel/DM list from the popped-out window until it was fixed. The re-hosted nav sits in a fixed-width column, except a sidebar that owns its width (`sidebarWidthFixed` — Supermail), which is hosted bare the way the main window hosts it: Supermail's folder nav renders nothing while hidden (its default), so a fixed column around it showed an empty strip down the left of the window. Locked by [pop-out-window-contract.md](../../.claude/memory/contracts/pop-out-window-contract.md) `a-split-surface-brings-its-sidebar`.

**Frameless themed chrome (opt-in).** An integration whose manifest sets `standaloneWindowChrome: true` ([tasks-v2.ts](../../src/shared/integrations/tasks-v2.ts) today) gets the frameless "work-surface" chrome instead of the native frame. `openProjectWindow` builds the `BrowserWindow` with the shared `standaloneWindowChromeOptions()` (hidden native title bar, no native caption-button overlay) plus a dark-first `backgroundColor` (no white flash on open), and `DetachedWindowApp` draws the shared [StandaloneWindowTitleBar](../../src/renderer/src/components/ui/StandaloneWindowTitleBar.tsx) strip above the panel with custom HTML caption buttons (`WindowControls`) at the trailing edge (which gets a `flex-1 min-h-0` wrapper so it fills only the remaining height, never overflowing by the strip's height). Both halves read the **same** flag — main via `resolveProjectWindowKind` → `kind.standaloneWindowChrome`, the renderer via `integrationWantsStandaloneChrome` ([integration-registry.ts](../../src/shared/integration-registry.ts)) — so the OS frame and the in-renderer strip can never disagree. Locked by [pop-out-window-contract.md](../../.claude/memory/contracts/pop-out-window-contract.md) `frameless-chrome-is-opted-in-once` and [window-controls-contract.md](../../.claude/memory/contracts/window-controls-contract.md).

**Group-launcher tiles in a pop-out.** A sidebar parent-group roll-up ([ParentGroupRollup.tsx](../../src/renderer/src/features/sidebar-groups/ParentGroupRollup.tsx)) — the Productivity / Automation / Insights launcher — renders in BOTH the main window and a pop-out. Its tiles activate the member inline in the main window (`activateVirtualProject`); but a pop-out's panel is pinned to the window's `?window=` project and can't navigate, so in a pop-out (`isProjectPopoutWindow`, [platform.ts](../../src/renderer/src/lib/platform.ts)) a tile instead opens the member in its own window via the shared `openProjectPopOut` ([pop-out-eligibility.ts](../../src/renderer/src/integrations/pop-out-eligibility.ts)) — the SAME generic / KMS / Scratchpads opener the sidebar "Open in new window" item uses (so the two never drift), with an inline fallback if the row isn't hydrated. Without it the tile was a dead no-op in a pop-out. Locked by [parent-group-rollup-popout.test.tsx](../../tests/unit/features/sidebar-groups/parent-group-rollup-popout.test.tsx).

**Agent Alerts and Approvals in a pop-out.** A popped-out Alerts window re-hosts the list and the reading pane, but in the main window a live alert is drawn by `DesktopMainOverlay` — which a pop-out never mounts — so clicking one used to highlight the row and show nothing. The reading pane ([AlertsVirtualProject.tsx](../../src/renderer/src/features/alerts/AlertsVirtualProject.tsx)) now asks `isMainAppShell()` and, outside the main window, draws the live viewer itself; the Approvals hub ([ApprovalsVirtualProject.tsx](../../src/renderer/src/features/approvals/ApprovalsVirtualProject.tsx)), which had the same dead click, draws the approval pane in place of its list the same way. Two follow-on dead ends are fixed at their root: a session link ([navigate-session.ts](../../src/renderer/src/lib/navigate-session.ts)) from a pop-out pinned to an integration whose panel shows no sessions — every integration except Tasks, the one manifest with `embeddedSessionProjectPath` — now opens in the main window, the same hand-off the single-session pop-out already used (a real-project pop-out still navigates its own workspace); and on desktop the HTML preview ([InboxHtmlPreview.tsx](../../src/renderer/src/features/inbox/InboxHtmlPreview.tsx)) shows its fallback outside the main window, because a pop-out without `hostsWebview` has no `<webview>` support and the frame would spin forever. Both lists also stay live. A pop-out never runs the App shell, which is what starts the app-wide push syncs, so the Alerts list ([AlertsSidebar.tsx](../../src/renderer/src/features/alerts/AlertsSidebar.tsx)) starts the alert sync and the snooze cache itself outside the main window, and the Approvals hub turns on the approvals reload ([useCliPendingPushReload.ts](../../src/renderer/src/hooks/useCliPendingPushReload.ts)) there; the Approvals manifest declares `CLI_PENDING_CHANGED`, because the pop-out filter forwards only the channels a manifest declares. An alert archived elsewhere while it is open on the Agent Alerts hub — in the main window too — is now reconciled instead of spinning on "Loading alert". A jump out now goes to the main window, which comes forward: from a popped-out window, the two navigation funnels (`activateVirtualProject`, `navigateToSettingsProject`) send the main window a link — Settings to the exact page, any other hub to that hub — and change nothing locally; before, they switched the pop-out's own store, which did nothing on screen and wrote the main window's restore anchor. Inside a popped-out alert, **Open channel** lands on the exact Team Chat channel (a reply notice opens its channel rather than the thread), the setup assistant's new session opens in the main window, and Start session plus the actions that need the main app (Review updates, Resume them now, Finish setup, the app tour, Restart) or pick a view inside a hub (View your records, View spend dashboard, Open this job, Review emails) open the alert there, where they work ([main-window-jump.ts](../../src/renderer/src/lib/main-window-jump.ts), [alert-action-popout.ts](../../src/renderer/src/features/alerts/alert-action-popout.ts)). Locked by [AlertsVirtualProject.test.tsx](../../tests/unit/features/alerts/AlertsVirtualProject.test.tsx), [ApprovalsVirtualProject.test.tsx](../../tests/unit/features/approvals/ApprovalsVirtualProject.test.tsx), [AlertsSidebar.test.tsx](../../tests/unit/features/alerts/AlertsSidebar.test.tsx), [alert-reload-reconcile.test.ts](../../tests/unit/stores/alert-reload-reconcile.test.ts), [navigate-session.test.ts](../../tests/unit/lib/navigate-session.test.ts) and [InboxHtmlPreview.fullscreen.test.tsx](../../tests/unit/features/inbox/InboxHtmlPreview.fullscreen.test.tsx).

**Push routing** is one new `'project-window'` role in the central [window-registry.ts](../../src/main/services/window-registry.ts), filtered by [detached-push-filter.ts](../../src/main/services/detached-push-filter.ts) `shouldForward`. The global chrome allowlist (theme / network / focus / update) reaches every window. Beyond that the entry's shape decides routing: an **integration** window forwards exactly its manifest-derived `panelPushChannels`; a **real-project** window forwards session-scoped channels whose session belongs to that project — the originating session's project id is resolved **once per push** by `resolveSessionProjectIdForPush` in [index.ts](../../src/main/index.ts) and handed to the (otherwise pure, DB-free) filter.

**Per-project position memory** reuses the same `detached_window_positions` SQLite table the single-session pop-out uses, under the sentinel key `__window_<projectId>__` (helpers in [queries-detached-window-positions.ts](../../src/main/db/queries-detached-window-positions.ts)). On every open the manager restores the saved bounds — including the maximized / fullscreen state, re-applied via the shared `window-geometry-persist` helpers; `move`/`resize` and entering/leaving fullscreen trigger a 500 ms debounced save, with a final synchronous save on `close`.

**Ctrl+W.** A pop-out never runs `App.tsx`, whose shortcut dispatcher is where the main window's Ctrl+W lives, and Windows installs no application menu — so without help, Ctrl+W reached nothing in a pop-out. Every pop-out root therefore mounts the shared [useCloseWindowShortcut](../../src/renderer/src/hooks/useCloseWindowShortcut.ts) hook, which closes the window through `IPC.WINDOW_CLOSE` (the same path as the title-bar X) and steps aside for an open dialog, for an inner command that already consumed the key, and for a held key. The key match is the one layout-aware `isCloseChord` (the typed letter decides on a Latin layout, so AZERTY's Ctrl+Z is never a close). Locked by [pop-out-window-contract.md](../../.claude/memory/contracts/pop-out-window-contract.md) `ctrl-w-closes-the-pop-out` and the guard [popout-ctrl-w-close-coverage.test.ts](../../tests/unit/lint/popout-ctrl-w-close-coverage.test.ts).

Invariants are locked by [pop-out-window-contract.md](../../.claude/memory/contracts/pop-out-window-contract.md) and its unit tests.

## Related

[detached-session-window.md](detached-session-window.md) is the single-session sibling — it pops out one conversation rather than a whole project. [tray-and-window.md](tray-and-window.md) covers the main window, the tray, and the global hotkeys that sit around these windows, and [start-a-new-session.md](start-a-new-session.md) explains how sessions are spawned, which is what a popped-out project window lists.
