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

Quick Launch Modal (part 3)

The third part of the Quick Launch page, for readers working in the codebase: how the floating window is managed, how its global and per-tab hotkeys register, how each tab maps to a component, the voice-suppression seam that keeps main-window voice commands dormant, and the design decisions behind the modal.

What it is

This is part 3 of the Quick Launch Modal page. The main page covers what Quick Launch is, how to open it, how to compose and send, and its settings; part 2 covers every other tab and the mobile sheet. This half is the implementation view: how the window, the hotkeys and the tabs are built, and why they are built that way.

Where to find it

There is no separate UI surface here — this half is for someone working in the codebase. Everything it describes is reached at runtime through the same floating window and the same Settings → Quick Launch page the other two pages cover.

How it behaves

What a reader can observe of the choices below: the modal pops up over whatever is on screen but never covers an exclusive-fullscreen game or video player, it hides rather than closes so an unsent draft survives, and its window is frameless and cannot be moved or resized.

For agents

How it works (for repo-aware readers)

  • Window manager: quick-launch-window.ts owns the BrowserWindow lifecycle as an explicit state machine (Cold, PreWarming, WarmHidden, Opening, Shown, Closing, Hidden) — lazy creation on first open, "kept alive" across opens, primary-display centering recomputed every show, lifecycle listeners (focus / blur / closed) wire up voice-service suppression and click-outside-to-dismiss. It also self-heals a blank pre-warmed window: because the window is loaded once and only show()'d (never reloaded), a silent renderer mount failure used to leave it permanently blank until an app restart. The renderer reports QUICK_LAUNCH_RENDERER_READY on mount, and real BrowserWindows must also emit Electron ready-to-show before reveal; a hidden→shown open that is not reveal-ready reloads once, waits for readiness, or discards before any show() call if readiness times out (fall-through-to-recreate immediate if the reload itself hard-fails — the healthy path stays await-free). render-process-gone / a main-frame did-fail-load also discard the window so the next hotkey lazy-recreates it. Full invariants (I11/I22): quick-launch-entrance-animation-contract.md.
  • Graceful close (v14): every dismiss — Escape / Ctrl+W, click-away, and submit — routes through animateCloseAndHide(), which shrinks the window's BOUNDS toward its centre and glides it away rather than hard-cutting the opaque rectangle (which read as "the dark background stays up / two windows"). An opaque window can't fade — a panel-opacity fade reads as a blank rectangle and win.setOpacity() re-introduces the fragile layered window v11 deleted — so bounds are the only lever. Main runs a setBounds frame-tween (~130 ms); the renderer content scale is a requestAnimationFrame follower that re-reads the LIVE window size every frame (QUICK_LAUNCH_CLOSING → quick-launch-exit, opacity held at 1, over a pixel-frozen wrapper), NOT an independent scale-out keyframe. v14 CENTRES that wrapper in the live viewport (position:fixed; left/top:50%; scaled via translate(-50%, -50%) scale(...)), so the collapse lands on the window centre = screen centre by construction. v13 scaled the wrapper about its TOP-LEFT, so a lagging (too-big) frame was clipped only at the bottom-right and its visible centre drifted up-and-left — the "hides to the left" bug (reproduced 2026-07-12). (The earlier v12 drift was worse: two unsynchronized clocks — main's setInterval bounds-shrink about CENTRE + a renderer wall-clock CSS keyframe about TOP-LEFT — "collapsed in a different direction each time.") A centre-anchored scale/size mismatch is a symmetric dark hairline on all sides (invisible), never a corner pin. Then hide()s + restores full bounds. Re-entrant, setWindowHeight no-ops while closing, and a re-open mid-shrink aborts cleanly. Full invariants (I17/I18): quick-launch-entrance-animation-contract.md.
  • Theme-matched window background (open-flash fix): the opaque BrowserWindow's native backgroundColor is seeded at #18181b (bare-dark surface-100), but the panel paints the ACTIVE theme's bg-surface-100 (glassmorphism #0e0e16, ~1.7× darker). On open, a compositor re-raster momentarily drops the renderer's paint — including the wrapper — so for ~1 frame the fixed, lighter bg showed as a full-modal light/white flash before the dark content painted (worst under glassmorphism, the default theme). Fix: the renderer reports the live --color-surface-100 on every theme apply/change (QUICK_LAUNCH_SET_WINDOW_BG) and main keeps the native backgroundColor matched via win.setBackgroundColor — the window stays fully OPAQUE (never transparent:true / setOpacity), only its solid color follows the theme. Full invariants (I15): quick-launch-entrance-animation-contract.md.
  • Hotkey registration: Electron globalShortcut.register(accelerator, () => quickLaunchWindow.toggleShow()) — re-registered on every settings change so a new value takes effect without an app restart.
  • Per-tab hotkeys: planned by planHotkeyRegistrations() in src/shared/global-hotkeys.ts (a 'quick launch tab' candidate per assigned tab, appended after the fixed four + email recipients; gated on quickLaunchHotkeyEnabled). registerGlobalHotkeys() in src/main/index.ts first runs selectQuickLaunchTabHotkeys(settings.quickLaunchTabHotkeys, isEnabled) to drop disabled/unknown tabs — its isEnabled predicate accepts a repo tab id (parseRepoTabId(id) !== null) as well as an enabled registry action, so repo tabs get a hotkey too (their project existence is validated at fire time, not in the pure planner). Each fired hotkey does await toggleShow() + emitPush(QUICK_LAUNCH_OPEN_TO_TAB, { actionId }). The modal listens for that validated push; both its QUICK_LAUNCH_OPEN_TO_TAB handler and the visibleTabIds fold gate on isTabVisible(actionId, enabledByActionId, existingProjectIds) — the same dual gate that covers repo tabs — so a forced target (registry OR repo) selects, renders, cycles, and reverts on dismiss (a repo target whose project was deleted just falls through to the default tab). The push channel must be in QUICK_LAUNCH_ALLOWLIST (detached-push-filter.ts) and have a modal listener (both pinned by quick-launch-push-listener-coverage.test.ts). Invariants hotkeys-register-last … repo-tabs-hotkey-capable: quick-launch-action-registry-contract.md.
  • Renderer entry: src/renderer/quick-launch.html + src/renderer/src/quick-launch.tsx — third rollup entry alongside index.html and recorder-host.html. Loaded via ELECTRON_RENDERER_URL in dev, from disk in production. On mount it fires QUICK_LAUNCH_RENDERER_READY from a top-level effect (so a render throw never signals ready) — the blank-renderer recovery handshake the window manager uses to tell a healthy window from one that came up blank.
  • Modal component: src/renderer/src/features/quick-launch/QuickLaunchModal.tsx — textarea + chip row + send button + inline error strip. Owns the textarea state; Esc / submit / blur clear it. The tab strip + gear icon + the "+" add-tab button (with its AddTabMenu dropdown) live here; Ctrl+Tab / Ctrl+Shift+Tab cycling and Ctrl+1…Ctrl+9 direct tab-jump share one window-level keydown listener (a useEffect on window, not a panel-scoped onKeyDown) so they fire whenever the modal window is focused regardless of where focus sits inside it. The digit branch guards on !e.altKey (so AltGr / odd-layout combos can't hijack), maps digit n → the n-th visibleTabIds entry (no-op past the 9th or out of range), and only preventDefault()s a key it acts on. Each of the first nine tabs surfaces its number on hover via the shared <Tooltip> and exposes it to assistive tech with aria-keyshortcuts (no raw title=). No collision with the main window's universal Ctrl+1…Ctrl+0 project/inbox switcher: the Quick Launch window is a separate React root (quick-launch.tsx) that never mounts useKeyboardShortcuts, so that dispatcher isn't present here. The "+" lists getAvailableToAddActions(pinnedActionIds, enabledByActionId) and pins a choice by optimistically updating local pinnedActionIds then persisting via IPC.SETTINGS_UPDATE (rollback + inline error on failure) — no new IPC. The strip mounts when >1 tab is visible or there is something to add, so the "+" stays reachable from a single-tab install. Right-clicking a tab opens TabContextMenu (opaque, cursor-positioned via containToViewport) — it now opens for every tab, Session included. At the top is an inline Global hotkey capture field (the shared QuickLaunchTabHotkeyField, autoFocused, pretty-displayed) that persists via handleSetTabHotkey → SETTINGS_UPDATE (optimistic + rollback); below it a Remove from Quick Launch action calls handleRemoveTab — strikes the id from pinnedActionIds, persists via SETTINGS_UPDATE (rollback + inline error), reselects firstVisibleTabId when the removed tab was active — shown for every tab except Session (Remove suppressed for Session, contract remove-unpins-in-place; Session's menu is hotkey-only). Locked by QuickLaunchModal-remove-tab.test.tsx. When the pinned tabs overflow the fixed-width window, the tab row is a horizontally-scrolling region (overflow-x-auto, scrollbar hidden via the modal's inline <style>) with fade-in ◀/▶ chevron buttons (found by aria-label, deliberately NOT data-ui-anchor to stay off the ui-anchor registry lint), vertical-wheel→horizontal scroll, and Ctrl+Tab scroll-into-view; the "+" and gear sit in a flex-shrink-0 cluster OUTSIDE the scroller so they can never be clipped. The arrow-visibility + scroll-into-view geometry is the pure, unit-tested quick-launch-tab-scroll.ts; full invariants in quick-launch-tab-overflow-contract.md. Behavior locked by tests/unit/features/quick-launch/QuickLaunchModal-add-tab.test.tsx + the helper's tests in tests/unit/shared/quick-launch-actions.test.ts. Per-tab bodies are leaf components looked up via the action-component registry.
  • Action registry (shared, React-free): src/shared/quick-launch-actions.ts — declares QUICK_LAUNCH_ACTIONS (a ReadonlyArray<QuickLaunchAction> with id, label, iconName, enabledSelector(settings)) plus QUICK_LAUNCH_ACTION_IDS and DEFAULT_QUICK_LAUNCH_PINNED_ACTION_IDS. Kept pure-data so the main process can validate quickLaunchPinnedActionIds settings against it without pulling in React.
  • Action components (renderer-side): src/renderer/src/features/quick-launch/quick-launch-action-components.ts — QUICK_LAUNCH_ACTION_COMPONENTS: Record<QuickLaunchActionId, ReactComponent> maps each registry ID to the React form that renders when that tab is active. The two registries must stay in lockstep — enforced by tests/unit/lint/quick-launch-action-registry-completeness.test.ts (every action ID has a matching component, every registered component points at a known ID, every action has a non-empty label + icon, IDs are unique).
  • Project picker: src/renderer/src/features/quick-launch/QuickLaunchProjectPicker.tsx — Ctrl+P fuzzy-search overlay; mounted/unmounted via a renderer flag that also tells the main process to swallow Esc inside the picker only.
  • Project icon + initial-letter fallback: src/renderer/src/features/quick-launch/QuickLaunchProjectIcon.tsx wraps the shared <ProjectIcon>; when that would render empty (a real folder with no custom iconPath and no virtual-sentinel __…__ folderPath) it substitutes a first-letter glyph. The tile color falls back to tintForProjectName() — a djb2 hash passed through a MurmurHash3-style mix32 finalizer so the low bits consumed by % palette.length distribute well (plain djb2 % 8 keyed off only the low 3 bits and clustered real projects like Omniscio/Nothari onto one color). The 8-color palette + the pinned five-name tint mapping are locked by tests/unit/features/quick-launch/QuickLaunchSessionTab-project-icon.test.tsx.
  • Session-tab textarea card: src/renderer/src/features/quick-launch/QuickLaunchSessionTab.tsx wraps just the textarea in a rounded-xl inset card (bg-surface-950/60 + border, content edges aligned with the tab strip) and carries the focus indicator on the card via focus-within:ring. The textarea sets focus:shadow-none to suppress the global textarea:focus accent box-shadow (globals.css) that would otherwise paint a square halo inside the rounded card — see gotchas-frontend.md.
  • Voice mic: src/renderer/src/features/quick-launch/QuickLaunchVoiceMic.tsx — own getUserMedia + MediaRecorder + IPC chunking (mirrors <VoiceInput>'s recording side). Rendered only when voiceEnabled is on — the QL window keeps no settings store, so the flag is mirrored in via GET_INITIAL_STATE (state.voiceEnabled, default OFF), the same master Voice-Commands switch the main composer mic gates on; the Session tab renders the mic behind state.voiceEnabled ? … : null. Push-event subscriptions (voice:partial-transcript, voice:final-transcript, voice:status-changed, voice:error) are gated on isRecording so a stray transcript from the main window's own session can't bleed in.
  • Per-tab body components: each tab's body lives in its own file under src/renderer/src/features/quick-launch/ (e.g. QuickLaunchAlarmTab.tsx, QuickLaunchTaskTab.tsx, etc.). Each tab is self-contained — owns its own form state, calls its target IPC directly via ipc.invoke (no Zustand store; QL is its own renderer process and shares no stores with the main window), and calls the modal's animateOutAndClose on successful save. Four "quick-add" tabs (Alarm/Drip/Bookmark/Calendar) target their existing CREATE/UPDATE IPC handlers directly; the Task tab calls a dedicated main-side router (IPC.QUICK_LAUNCH_CREATE_TASK) that files into Tasks (the default / Inbox list) when v2 is enabled, else a v1 task. The three "spawn a session" tabs (QuickLaunchSessionSearchTab.tsx, QuickLaunchAskAmcTab.tsx, and QuickLaunchAutomationHelperTab.tsx) route through the modal's existing submit action with a virtual-project sentinel — the main-process QUICK_LAUNCH_SUBMIT handler detects projectId === SEARCH_PROJECT_ID, ASK_AMC_PROJECT_ID, or AUTOMATION_HELPER_PROJECT_ID (the closed SUBMIT_VIRTUAL_SENTINELS set) and calls prepareSearchSessionsWorkDir() / ensureAskAmcProject() / ensureAutomationHelperProject() to provision the right workdir + lazily seed the project before spawning. Alarm tab is gated on settings.alarmsEnabled; Ask Omniscio on settings.askAmcEnabled; Automation on settings.automationHelperEnabled. Like Search / Ask Omniscio, the Automation tab is now special-cased for the shared attachment slice (Contract standalone-props-only) so a screenshot/PDF of what to automate rides along — QUICK_LAUNCH_SUBMIT already carries images/documents and forwards them to createSessionWithPrompt, so this was front-end-only. The Automation Helper's "builder" persona is applied by project id at spawn time (spawn-cluster-manager's isAutomationHelper → bootstrapAutomationHelperWorkdir()), so spawning it with source: 'quick-launch' behaves identically to the gallery's automation-preset launch.
  • Shared composer field (QuickLaunchComposer): every "type-and-submit" tab (Automation, Ask Omniscio, KMS Note, Alarm, SMS, DM, Task) renders ONE shared QuickLaunchComposer for its input instead of hand-rolling a <textarea> — it composes the isolated no-typing-lag box (QuickLaunchComposerInput) + opt-in attachments/image-paste (only where the tab's backend carries them — Automation + Ask Omniscio, the modal-owned attachment slice) + an opt-in send-later picker (Task) + a consistent Cancel/Submit footer + 'enter' or 'ctrl_enter' submit-key mode, with aboveBox/belowBox slots for each tab's own chrome (the SMS/DM recipient pickers, the Alarm parsed preview, the Task list picker). Capabilities are backend-gated so a tab never shows an affordance the wire drops; a build guard (quick-launch-composer-adoption.test.ts) blocks any new tab from hand-rolling its own box. The Email tab keeps its own composer by design (it's a full email form). Invariants (I1–I6): quick-launch-composer-adoption-contract.md.
  • Special-cased tabs (Contract standalone-props-only): the action registry is pure data, but four tabs need state outside the per-tab leaf (textarea + attachments + project chip for Session; attachments + query for Session Search; attachments + question for Ask Omniscio; attachments + description for Automation). QuickLaunchModal special-cases id === 'session', id === 'session-search', id === 'ask-amc', and id === 'automation-helper' to inject attachments / onRemoveAttachment / onPaste / onOpenFilePicker from the shared modal state. Ask Omniscio + Automation reuse the Session Search attachment wiring verbatim (each is just a session spawned into a different virtual project). The lockstep lint test tests/unit/lint/quick-launch-action-registry-completeness.test.ts still enforces every ID has a component; the contract for these two special-cased IDs is documented in .claude/memory/contracts/quick-launch-action-registry-contract.md.
  • File picker (paperclip): click-to-browse routes through the main-process DIALOG_PICK_ATTACHMENTS handler (not renderer-side <input type="file">, which crashes the Quick Launch renderer via chrome.mojom.UtilWin on some Windows builds). Two QL-window-manager pieces cooperate to keep the modal alive while the native dialog is open:
    • fileDialogOpen flag (src/main/services/quick-launch-window.ts) — the modal calls IPC.QUICK_LAUNCH_SET_FILE_DIALOG_OPEN { active: true } before invoking the picker. The window's blur handler short-circuits when oauthFlowActive || fileDialogOpen (independent gates, both can be set; blur only un-suppresses when both clear). hide() defensively clears the flag so a stuck true can't wedge dismissal.
    • alwaysOnTop drop — while fileDialogOpen is set, the QL window's alwaysOnTop level is removed and restored on release, so the native picker doesn't render behind the QL composer. The renderer's fileDialogInFlightRef guards against double-open from a frantic paperclip click. Both pieces are pinned by tests/unit/services/quick-launch-window.test.ts — six tests cover the blur gate, alwaysOnTop drop/restore, post-release blur-hides-again, defensive hide() clear, pre-window safety, and the OAuth/file-dialog independence.
  • Blur-dismiss survives transient focus-steals: AFTER the oauthFlowActive / fileDialogOpen / sibling-HUD short-circuits above, the blur handler no longer hides synchronously — it routes through the shared two-stage discriminator hud-blur-dismiss.ts. On Windows it defers ~200 ms, then re-checks after another ~120 ms, reading the OS foreground: a deliberate switch (another app or an Omniscio window owns the foreground) dismisses as before, but a transient Win32 console that flashed and vanished during a session spawn (the user runs 100+) leaves the foreground empty at BOTH samples → the launcher stays open and reclaims focus via a topmost-preserving forceForegroundWindow instead of vanishing mid-type. Reuses FocusGuard's win32 primitives (isForeignForegroundWindow / forceForegroundWindow); the two-stage confirm avoids stealing focus back on a real Alt+Tab whose foreground momentarily reads empty. Off-Windows it keeps today's immediate dismiss. Shared with the Clipboard History picker; invariants + tests in hud-blur-dismiss-contract.md.
  • Clickable attachment chips (preview / copy / Save-As / reorder): the chip strip QuickLaunchAttachmentChips.tsx is pure-presentational — it reports gestures up via onPreview / onContextMenu / onReorder / onRemove and reuses useDragReorder (orientation: 'horizontal'). QuickLaunchModal owns pendingAttachments and renders the reused PendingImageLightbox + PendingImageContextMenu verbatim (both self-contained, no sessionId, base64-driven); the lightbox indexes only the image subset so nav never lands on a document. Escape two-step lives in main: quick-launch-window.ts holds a lightboxOpen flag and its before-input-event handler routes Escape / Ctrl+W open popup → snippet picker → picker → lightbox → hide (the popup flag mirrors the page's open-menu/popup registry, so a menu opened over the preview closes first) — while a preview is open it emits QUICK_LAUNCH_LIGHTBOX_DISMISS instead of hide() (the lightbox's own useEscapeClose never fires; main swallows the key first). The renderer drives the flag via QUICK_LAUNCH_SET_LIGHTBOX_OPEN. Save-As wraps IMAGE_SAVE_DATA_AS in the SAME fileDialogInFlightRef + QUICK_LAUNCH_SET_FILE_DIALOG_OPEN guard as the paperclip (the menu never calls the save IPC itself — the caller owns it so it can raise the guard), and the preview overlays INSIDE the fixed-size window (the old open-grow/close-restore height requests are deleted — the window never resizes; entrance-animation contract I21). The dismiss push rides QUICK_LAUNCH_ALLOWLIST (detached-push-filter.ts). Full invariants (documents-are-not-previewable … the-dismiss-push-stays-in-the-quick-launch-renderer) + tests: quick-launch-attachment-affordances-contract.md.
  • Enablement + active-tab flow: enabledByActionId — built by the shared buildEnabledByActionId(settings) (one builder, also used by the Settings card so strip-visibility and the editor hint can't disagree) — is surfaced on QUICK_LAUNCH_GET_INITIAL_STATE (no separate IPC), read on mount + refreshed on focus. Tab visibility runs through the shared isTabVisible(id, enabledByActionId, existingProjectIds) predicate (registry flag OR repo-project-exists). The tab active on open is the first visible tab in saved order via firstVisibleTabId(...) (repo tabs included; falls back to 'session') — applied identically at mount, blur-reset, and submit-reset so they can't drift (contract opens-first-visible). quick-email is gated by quickEmailEnabled alone (recipients no longer required); each gated action carries a disabledHint the Settings card shows when it's off.
  • Repo tabs (repo:<projectId>): a per-repo tab is a pinned id of the form repo:<projectId> (not a registry action) — it opens the Session composer locked to that project. Encode/decode via makeRepoTabId / parseRepoTabId / isRepoTabId in src/shared/quick-launch-actions.ts. QuickLaunchModal gives repo ids a project-exists visibility gate (self-heal on delete), renders them with the project icon + name, routes them to QuickLaunchSessionTab with locked (chip omitted, no picker, Ctrl+P suppressed), and strips the prefix at submit so the bare UUID reaches the unchanged QUICK_LAUNCH_SUBMIT handler. The "+" menu's "New session in a repo…" item opens an add-repo picker (reused QuickLaunchProjectPicker over the spawnable, unpinned repos). Full rules + invariants in .claude/memory/contracts/quick-launch-action-registry-contract.md.
  • Settings card: src/renderer/src/features/settings/sections/quick-launch-tabs/QuickLaunchTabsCard.tsx — two-column pinned/available card mounted in Settings → Quick Launch. In the Available column a row whose feature is off (enabledByActionId[id] === false, via the shared isActionDisabled predicate) renders grayed (opacity-50) with a disabled checkbox so an off tab can't be pinned until its feature is on; the Pinned column deliberately keeps its checkbox interactive so a pinned-but-off tab is still removable (both locked by QuickLaunchTabsCard.test.tsx). Resolves pinned repo:<id> entries to project rows (name + icon) and lazily prunes dead repo ids — from both quickLaunchPinnedActionIds and quickLaunchTabHotkeys — (guarded on the project list being loaded). Its Tab hotkeys sub-section renders one shared QuickLaunchTabHotkeyField per registry action plus one per pinned repo tab (the same field the composer's right-click menu uses, so the two surfaces can't drift). Below the columns it offers an Add a repo session tab <Select> whose options are addableRepoProjects(pinnedActionIds, projects) — spawnable, not-already-pinned, and !isDeleted (the card receives the full settings-store project list, which can include soft-deleted rows the composer's main-side allProjects already excludes; contract repo-picker-addables, locked by QuickLaunchTabsCard.test.tsx). The composer's gear icon round-trips here via the QUICK_LAUNCH_OPEN_SETTINGS IPC handler (src/main/ipc/quick-launch-handlers.ts) — the gear invokes it with { section: 'quick-launch' } — which hides QL, foregrounds the main window, and emits a SETTINGS_DEEP_LINK push the main renderer listens for (in usePluginBridgePushListeners.ts), calling setPendingSettingId('quick-launch-tabs') + navigateToSettingsProject({ section: 'quick-launch' }). The useScrollToSetting hook then polls the DOM for [data-setting-id="quick-launch-tabs"] and scrolls + flashes the card.
  • Voice suppression seam: src/main/services/voice-service.ts — quickLaunchFocused boolean flipped by setQuickLaunchFocused() on the QL window's focus / blur events. While true, handleFinalTranscriptForMode early-returns BEFORE invoking the intent parser, so main-window voice commands stay dormant. The voice:final-transcript push event still fires (the QL mic needs it).
  • IPC: Session-tab submit goes through the same IPC.SESSION_CREATE_WITH_PROMPT path as the main composer's "+ New Session" — Zod-validated, account-routed, attachment-aware. Most non-Session tabs target their own existing IPC (IPC.ALARMS_CREATE, IPC.DRIP_CREATE, IPC.BOOKMARKS_CREATE, IPC.CALENDAR_CREATE_EVENT) — front-ending existing write paths. Two tabs add a dedicated handler: KMS Note (IPC.KMS_QUICK_NOTE_CREATE, wrapping the existing KMS createNote service — primary-vault resolution + Quick Notes/ path derivation live in quick-note-service.ts) and Task (IPC.QUICK_LAUNCH_CREATE_TASK, a router that creates in Tasks / Inbox when v2 is enabled, else a v1 task — so the storeless QL window never decides which task system is active).
  • Settings: Five fields on AppSettings — quickLaunchHotkey (string, default 'CommandOrControl+Space'), quickLaunchHotkeyEnabled (boolean, default true), quickLaunchDefaultProjectId (string | null, default null), quickLaunchPinnedActionIds (string[], default = every action ID in registry order), and quickLaunchTabHotkeys (Record<actionId, accelerator>, default { calendar: 'CommandOrControl+Alt+J', task: 'Alt+Space' } — the per-tab global hotkeys; a newly-shipped default backfills into an existing install via applyDefaultsDeep's per-key merge). The pinned-IDs list uses plain string[] instead of a narrowed action-ID union for forward compatibility — older Omniscio builds reading newer settings will silently drop unknown IDs rather than fail to load; quickLaunchTabHotkeys is likewise loose (Record<string, string>) and unknown ids are dropped at registration time. Zod schema slice in src/shared/ipc-schemas/settings/hotkeys-quicklaunch-settings.ts.

Design decisions worth knowing

  1. Separate BrowserWindow, not a modal inside the main window: The main window can be minimized / on another monitor / completely hidden when you hit the hotkey, and Quick Launch still has to pop up. A standalone window with its own React tree is the cleanest way to satisfy that. Cost: a second renderer process at idle (~50 MB).

  2. Always-on-top, but only "normal" level: Omniscio explicitly does NOT use Electron's 'screen-saver' window level — that would let Quick Launch overlay exclusive-fullscreen games and video players, which is jarring. The trade-off is that exclusive-fullscreen apps eat the hotkey; you have to alt-tab out first.

  3. Hide, not destroy, on dismiss: The React tree mounts once on first open and stays mounted across subsequent opens. The composer is cleared on submit — and on Esc/blur only when Remember unsent Quick Launch text is off; by default an unsent draft (text + pasted-text chips + staged attachments) is kept across opens because the live window state survives. Everything else (project picker state, voice service connection, etc.) persists too. This is the same "keep-alive pool" pattern Omniscio uses for hidden SessionPanels. (Because the draft rides that in-memory tree, it is not written to disk — a full app restart starts blank.)

  4. Primary-display centering, recomputed every show: Caching the centered coordinates would be wrong — the user might dock/undock a monitor between opens, making the cached position land off-screen. Cost is one screen.getPrimaryDisplay() call per open, which is cheap.

  5. No menubar / titlebar / window controls: Frameless + transparent + non-resizable + non-movable. This is a composer, not an app — it should feel like a system overlay, not a window you manage.

Related

part 2 is the tab surfaces these components render, and the main page is the user-facing half. The contracts named above hold the invariants behind each piece, and keyboard-shortcuts.md is the global shortcut catalog Quick Launch registers into.

Last verified 2026-09-26