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

Per-window zoom (each popped-out window remembers its own size)

Every secondary window you pop out — a session, a project, the vault, the scratchpad — gets its own zoom level that scales the whole window and is remembered next time you open it. Session windows remember per session, so two sessions can come back at two different sizes.

What it is

What it is

Per-window zoom gives every Omniscio secondary window — the ones you pop out into their own desktop window — its own zoom level, like the zoom in a web browser. It scales the whole window (text, icons, images) so the whole thing gets bigger or smaller together, and it is saved per window: close a window and reopen it and its zoom comes back.

The key word is independent. Zooming one pop-out does not touch any other window, and — by design — it never affects the main Omniscio window. The main window keeps the app-wide Text size behavior it always had (Text size is one global setting shared across the app; per-window zoom is separate, per-window, and scales everything, not just text). So you can, say, make a popped-out session window large on a second monitor while the main window stays exactly as it was.

Where to find it

Inside any popped-out window — a session you opened in its own window, a project window, the vault or scratchpad window. The controls are the familiar browser zoom keys, so there is no menu to find.

How it behaves

The controls

Inside any of these windows:

  • Ctrl and + (or =) — zoom in one step.
  • Ctrl and - — zoom out one step.
  • Ctrl and 0 — reset to 100%.
  • Ctrl and the mouse wheel — scroll up to zoom in, down to zoom out.

The numpad + / - / 0 keys work too. There is no Settings control and no menu — zoom lives entirely in the keyboard and wheel shortcuts, inside the window you want to change.

The range (75%–150%)

Zoom moves along a seven-step ladder — 75%, 87.5%, 100%, 112.5%, 125%, 137.5%, 150% — so 100% is the middle and you can go three steps smaller or three steps larger. This is deliberately the same scale as the app-wide Text size feature, so it feels like the size control you already know — just per-window, and scaling everything on the window instead of only the text. Stepping past either end simply stays at that end (it won't go below 75% or above 150%).

Saved per window

Each window remembers its own zoom across close and reopen:

  • Session pop-outs remember per session — pop out session A at 125% and session B at 90%, and each comes back at its own level next time.
  • Project / integration windows remember per project.
  • The single-instance windows — KMS, Scratchpad, Quick Launch, Job Monitor, Support Chat, and Clipboard History — each remember their own one zoom.

A brand-new window that you have never zoomed simply opens at its normal 100% — nothing is stored until the first time you actually change the zoom.

Which windows are in scope

Per-window zoom applies to these pop-out / secondary windows:

  • Session pop-out (a session opened in its own window via Pop Out)
  • Project or integration window ("Open in new window")
  • KMS window
  • Scratchpad window
  • Quick Launch window
  • Job Monitor window
  • Support Chat window
  • Clipboard History window

It does not apply to:

  • The main Omniscio window — never gets per-window CSS zoom; it keeps the app-wide Text size instead, and there Ctrl+= / Ctrl+- and Ctrl+mouse-wheel step that Text size (see text-size.md). So Ctrl+scroll works on the main window too — it just steps the shared Text size rather than zooming one window.
  • The embedded isolated-session view — this is a session rendered inside the main window (the experimental "Isolated session view" performance mode), so zooming it would zoom part of the main window; it is excluded and never zooms on its own.
  • Mobile / the phone web app — zoom here is desktop-window-only; the phone keeps its own separate Mobile text size.

The editor nuance (KMS and Scratchpad)

KMS and Scratchpad hold a rich-text editor, and that editor already uses Ctrl+- for its own action (strikethrough). So per-window zoom is polite about it: while your cursor is inside a text field or the rich-text editor, the keyboard zoom shortcuts step aside so the editor keeps its own Ctrl+- (and Ctrl+=). To zoom one of those windows, either use Ctrl + mouse wheel (which always zooms, everywhere), or click somewhere outside the editor first and then press Ctrl + / -.

For agents

For agents — how it works

  • The hook. One shared renderer hook, useWindowZoom(zoomKey), is mounted once in each in-scope window's root component (DetachedSessionApp, DetachedWindowApp, KmsWindowApp, ScratchpadWindowApp, SupportChatWindowApp, quick-launch.tsx, job-monitor.tsx, clipboard-history-window.tsx). It applies the saved factor as the CSS zoom property on document.documentElement (style.setProperty('zoom', …)), owns the Ctrl +/-/0 keys and Ctrl+wheel, and persists the factor (debounced ~300 ms, with a pagehide/unmount flush so a zoom made right before closing still saves). Passing zoomKey = null disables the hook entirely — that is how the embedded isolated-view guest opts out (useWindowZoom(embedded ? null : \session:${sessionId}`)`).
  • Independence is structural. The main window simply never mounts the hook, and CSS zoom is per-document, so one window's zoom physically cannot reach another. There is no HostZoomMap, no per-window session partition, and no custom protocol — zero blast radius on auth / storage / cookies. The global fontSize (Text size) still applies as the base in every window; zoom multiplies on top, exactly like a browser's default font size × page zoom.
  • The ladder + clamp. The 7-step ladder and the step/clamp helpers live in /src/shared/window-zoom.ts (ZOOM_STEPS, clampZoom, stepZoom, DEFAULT_ZOOM = 1.0) — the single source of truth the hook, the Main-side clamp, and the tests all derive from. Factors are clamped both on the client and again server-side, and a non-finite value heals to 1.0.
  • The window-fill. CSS zoom scales the whole document, so a pop-out shell sized 100vh renders at 100vh × zoom and — zoomed out — stops covering a maximized window, exposing the dark window background (the fullscreen zoom-out bug). So the hook writes the factor to a --window-zoom CSS variable (in lock-step with zoom, from one shared value), and each pop-out's top-level shell uses the shared .window-zoom-fill class — height: calc(100vh / var(--window-zoom, 1)) in /src/renderer/src/styles/globals.css — so it spans the window at any zoom (the × zoom render lands back on the window edge). Height-only (shells fill width via auto); the 1 fallback is native 100vh for the main window and any window with no saved zoom, so it is a zero-regression drop-in. It applies to the shells that have a viewport fill (DetachedSessionApp, DetachedWindowApp, KmsWindowApp, ScratchpadWindowApp, SupportChatWindowApp); the fixed/content-sized windows (Quick Launch, Job Monitor, Clipboard History) have no such shell and need nothing. The shared #root needs the same compensation. The ?window= project/integration and ?detached= session pop-outs reuse the main index.html #root, which is position: fixed; overflow: hidden; height: 100dvh — so #root ALSO uses height: calc(100dvh / var(--window-zoom, 1)). A bare 100dvh there renders at 100dvh × zoom, so a zoomed-out #root shrank to the top-left and CLIPPED the .window-zoom-fill shell, cramming the whole pop-out — header included — into a corner with a dark gap on the right + bottom (the reported Tasks pop-out bug). KMS and Scratchpad use their own roots (#kms-window-root / their own) with no explicit height, so they were never affected — which is why only #root needed the fix, and why the KMS/Scratchpad zoom tests never caught it.
  • The frameless title strip. The frameless pop-outs (Tasks / project, KMS, Scratchpad — titleBarStyle: 'hidden', custom HTML caption buttons via WindowControls) draw their own in-renderer title strip with custom min/max/close buttons. CSS zoom scales the whole document, so a bare fixed-height strip rendered at height × zoom (51 px at 125 %, 31 px at 75 %) and the buttons no longer sat on it (the "messed up at the top" bug). Each frameless strip therefore carries the sibling .window-zoom-chrome class — zoom: calc(1 / var(--window-zoom, 1)) in the same globals.css — which counter-zooms the strip (net scale = zoom × 1/zoom = 1) so its height, label, KMS tabs, and caption buttons all render at their natural DIP size. The strips: StandaloneWindowTitleBar (Tasks + Scratchpad) and KmsWindowTitleBar (KMS). .window-zoom-fill (the shell) is unchanged and measured-correct at every zoom.
  • Floating overlays + tooltips. A position: fixed overlay (a tooltip bubble, a portaled menu / popover) has its top/left resolved in the pre-zoom layout space and then re-scaled by the window's zoom at paint — but it is positioned from a getBoundingClientRect() anchor that is ALREADY zoomed, so an uncorrected overlay lands at anchor × zoom: visibly ABOVE the trigger when zoomed out (the reported Tasks chevron-tooltip "way above the chevron" bug), below it when zoomed in, and the error grows with the anchor's distance down the window. The shared clamp primitive containToViewport therefore clamps in visual space and then DIVIDES the final top/left by the root zoom — a private rootZoom(node) that reads getComputedStyle(<html>).zoom; any missing / non-positive / unparseable value heals to 1, so it is a no-op at 100% and in the never-zoomed main window. Every native-title / aria-label / hotkey tooltip is drawn by one bubble that positions through this helper, so they are all zoom-correct in a pop-out for free. Locked by overlay-viewport-clamp-contract.md I2a. (Sibling gaps still open, unreported: capToViewport's size cap and clampToViewport's flip carry the same latent re-scale under zoom — see that contract's CSS-root-zoom known-gap.)
  • The IPC channels. Two Zod-validated channels move the factor: window:zoom-get (IPC.WINDOW_ZOOM_GET) returns { factor: number | null } (null when nothing is saved), and window:zoom-set (IPC.WINDOW_ZOOM_SET) persists { zoomKey, factor } (void). Handlers are in /src/main/ipc/window-zoom-handlers.ts via wrapHandler; zoomKey is validated by a strict schema (length-capped, conservative charset) and factor must be finite.
  • The table. Main persists one row per window identity in a window_zoom SQLite table (zoom_key TEXT PRIMARY KEY, factor REAL, updated_at TEXT), created by the create-window-zoom migration and read/written through /src/main/db/queries-window-zoom.ts (getWindowZoom / saveWindowZoom, an INSERT … ON CONFLICT upsert). The zoomKey is the window identity: session:<id>, project-window:<id>, or a fixed singleton key (kms, scratchpad, quicklaunch, jobmonitor, support-chat, clipboard-history).

The invariants (main-window-never-zoomed, per-document keying, null-key exclusion, server-side clamp, zoom-compensated window-fill, zoom-immune chrome strip) are locked by the contract at /.claude/memory/contracts/per-window-zoom-contract.md and its guard tests.

Related

Related

  • text-size.md — the app-wide Text size feature (Desktop / Mobile), the sibling model this reuses the ladder from; it changes text everywhere, whereas per-window zoom is per-window and scales everything.
  • detached-session-window.md — how a session pops out into its own window (one of the windows this zooms).
  • pop-out-project-window.md — popping a whole project or integration panel into its own window.
  • isolated-session-view.md — the embedded in-window guest that is deliberately excluded from per-window zoom.

Last verified 2026-09-23