---
title: Bookmarks
---

# Bookmarks

## What it is

Bookmarks let you save URLs, file/folder paths, programs, and shell commands so they're one click away from anywhere in Omniscio. Bookmarks are **on by default** — a small bookmark-shaped lucide icon (ARIA label "Bookmarks") appears in the toolbar pill; click it to open a popover that hangs from the toolbar, holds a search box, an Add button, and a hierarchical tree of your saved items. To hide them, turn off the **Enable bookmarks** toggle at **Settings → Widgets** (default ON) — the toolbar icon disappears and the popover is unreachable; your saved rows are not deleted, just hidden.

Bookmarks live in the SQLite `bookmarks` table inside `mission-control.db` (under your OS user-data directory), and the favicon cache is local too. **Bookmarks do sync if you have Cross-Device Sync on** — the engine captures the `bookmarks` table whole-row, so the name and the target (URL · path · exe · cmd) upload to your own zero-knowledge locker and reappear on your other computers; the Company cannot read the contents, but the row does leave this machine. With sync off, the only outbound network call is the best-effort favicon fetch to Google's `s2/favicons` endpoint — bookmarks work just fine offline.

The popover is the single management surface for bookmarks. **Settings → Widgets** holds only the on/off toggle; it does not duplicate the tree or expose inline edit / delete controls. Every create, edit, delete, and reorder you do in the popover emits a `BOOKMARKS_CHANGED` push so any other open surface (e.g. a second Omniscio window) re-reads the table and stays in sync.

## Where to find it

### How to use it

1. **Open the popover** — click the bookmark icon in the toolbar pill. The popover anchors to the toolbar pill, right-aligned to its edge so it hangs from the toolbar rather than overflowing the window. (If the toolbar icon is missing, bookmarks were turned off — flip **Settings → Widgets → Enable bookmarks** back on.)
2. **Search** — start typing in the "Search bookmarks…" input at the top. The filter is a case-insensitive substring match on the bookmark name **or value** (so you can find a URL row by typing part of the URL even if the row has no name); matching rows AND all their ancestor folders stay visible, and every surviving folder auto-expands while a query is active so you don't have to click anything to see results.
3. **Activate a row** — click a non-folder row to fire its action (open the URL, open the path, run the executable, run the command). A single left-click also **closes the popover** automatically, mirroring how a browser's bookmark bar dismisses itself after you pick something. Click a folder row to expand or collapse it; folders never "activate".
4. **Middle-click for background launch** — middle-clicking a non-folder row activates it but **leaves the popover open**, so you can launch several rows in sequence (e.g. open three URLs in a row). This is the same gesture browsers use for "open in background tab".
5. **Right-click a row to edit it** — right-clicking any non-folder row opens the edit modal pre-filled with that bookmark's name, value, and parent folder. From the edit modal you can change the name or value, move the bookmark to another folder via the Parent dropdown, or delete it via the Delete button. Folder rows do **not** open the edit modal on right-click in v1.
6. **Add a bookmark** — click the small Plus (`+`) button in the popover header, or, if you have no bookmarks yet, click the "Add bookmark" call-to-action in the empty state. The Plus button is the general add affordance — the kind picker (URL / folder / path / executable / command / session / project / virtual) lives inside the modal, so you choose what kind of row you're adding after you open it. Next to the Plus is a **Bookmark current session** button (a bookmark-plus icon) that one-click-bookmarks whatever session is active (see "Quick-bookmark a session" below). The form is described below.
7. **Close the popover** — press Esc, click anywhere outside it, or click the toolbar bookmark icon a second time.
8. **Keyboard navigation in the tree** — once a row is focused, ArrowDown/ArrowUp move within visible rows, ArrowRight expands a folder, ArrowLeft either collapses an expanded folder or jumps to the parent of a leaf, and Enter activates a leaf or toggles a folder.

### Adding bookmarks

There is one general add affordance: the small Plus (`+`) button in the popover header (or the **Add bookmark** call-to-action in the empty state). Clicking it opens the unified add modal — what kind of bookmark you're creating is chosen inside the modal, not at the header. The modal opens with the kind picker visible, defaulting to URL. (For **sessions** specifically there are also one-click quick-bookmark paths that skip the modal entirely — see "Quick-bookmark a session" below.)

- **Pick a kind first** — radio chips at the top of the modal: Folder / URL / Path / Executable / Command / Session / Project / Virtual. Pick Folder to create a folder (no value field appears); pick a launcher kind (URL / Path / Exe / Cmd) to type a value freehand; pick a nav kind (Session / Project / Virtual) and the Value field is replaced by a **target picker** — a filterable list of the live sessions / projects / virtuals in your Omniscio install. Search by name, click to select, and the picker stores the target's UUID (or sentinel) as the bookmark value. Nav-kind bookmarks never accept a freehand value; the picker is the only way in.
- **Add a folder inline while adding a regular bookmark** — for non-folder kinds, the Parent dropdown's first option is `+ New folder…`. Picking it replaces the dropdown with an inline name input and a Parent-of-the-new-folder selector. Type a name, hit Enter (or click the inline Save), and the new folder is created and immediately selected as the parent for the bookmark you're still adding. This is the fastest way to bookmark something into a brand-new folder without leaving the modal.

The shared `BookmarkAddModal` form fields:

- **Kind** — radio chips for Folder / URL / Path / Executable / Command. Hidden in edit mode (changing kinds is a delete + recreate workflow), and hidden when the modal is invoked with `lockedKind='folder'` from the inline "+ New folder…" entry point. Picking a kind reshapes the rest of the form (folders have no value field; URL validates as http(s); other kinds require a non-empty value).
- **Name** — required for **folders**, optional for **URL / Path / Exe / Cmd**, ≤ 80 characters (`MAX_BOOKMARK_NAME_LEN`). When a non-folder row has an empty (or whitespace-only) name, the tree renders its `value` as the visible label AND the row's `title` tooltip, and the search box matches against the value as well — so a URL bookmark with no name is still findable by typing any substring of the URL itself. Folders always require a non-empty trimmed name (the backend's Zod schema rejects folder kinds with empty / whitespace-only names) because folders have no value to fall back to.
- **Value** — hidden for Folder. URL must parse as an `http://` or `https://` URL. Path / Executable / Command must be a non-empty string up to 4096 characters (`MAX_BOOKMARK_VALUE_LEN`).
- **Parent folder** — a dropdown listing every existing folder, indented by depth. The default is "(no parent)" — the bookmark sits at the root. Folders deeper than `MAX_BOOKMARK_NESTING_DEPTH - 1` are disabled in the dropdown so a new bookmark can never be saved past the depth cap.

Validation runs in order: name → value (kind-specific) → URL parse if kind=url. Errors render inline above the form in red. The submit button shows "Saving…" while the IPC call is in flight; on success the modal closes and the tree reloads.

The same modal also handles **edit mode** when invoked with an existing row (the `editing` prop). Right-click any row — folder OR non-folder — to enter edit mode for that row. In edit mode the kind picker is locked — switching kinds is a delete + recreate workflow rather than an in-place change. The parent-folder dropdown also excludes the row being edited and all its descendants, so you cannot accidentally move a folder inside itself or one of its own children. The backend enforces these same cycle and depth rules at the query layer (`bookmarks: cycle (move-to-self)`, `bookmarks: cycle (move-to-descendant)`, `bookmarks: nesting depth exceeded`), so even a malicious IPC call cannot bypass them.

### Quick-bookmark a session

Bookmarking a session you're working in shouldn't mean opening the Add modal and searching for it. Three one-click paths drop a `kind:'session'` bookmark for a session at the **top level** (no folder, no typed name — the row tracks the session's live name):

- **Right-click a session → "Add to Bookmarks"** in the session context menu. Offered for **archived** sessions too (a bookmark must outlive archiving). A multi-select bookmarks each selected session.
- **Shift+B** — the `bookmarkActiveSession` keyboard shortcut bookmarks whichever session is currently active. It's view-agnostic — it won't yank you to the dashboard from Settings / Help / Inbox — and is rebindable at **Settings → Keyboard Shortcuts**.
- **"Bookmark current session" button** in the Bookmarks popover header (a bookmark-plus icon, left of the `+`) bookmarks the active session without leaving the popover.

All three funnel through one shared action (`bookmarkSession` / `bookmarkActiveSession`, [bookmark-session-action.ts](../../src/renderer/src/features/bookmarks/bookmark-session-action.ts)): it **dedupes by session id** — re-bookmarking shows an "already bookmarked" toast instead of creating a duplicate row — creates the bookmark with an empty name so the row tracks the live session name, and registers a **Ctrl+Z undo** that removes the just-created bookmark. With no active session (or a non-session inbox item selected), it shows a gentle "No active session to bookmark" toast and does nothing.

Because the bookmark stores only the session id, archiving the session never breaks it — the row keeps resolving and clicking through (see "Live label and broken-link detection" above).

### Settings

Bookmarks have one Settings surface and one Settings flag.

**Settings → Widgets** holds a single **Enable bookmarks** toggle bound to the `showBookmarksInToolbar` setting key. Default is **ON** — the toolbar bookmark icon shows on a fresh install. When the toggle is off, the toolbar bookmark icon is hidden and the popover is unreachable; flipping it back on reveals the icon immediately. The Settings page does not duplicate the bookmark tree or expose any inline edit / delete controls — managing bookmarks is the popover's job. Disabling bookmarks does not delete the rows in `mission-control.db`; they're hidden until the toggle goes back on.

The toolbar-visibility flag is independent from the **bookmarks CLI kill switch** at **Settings → CLI Control → Enable bookmarks CLI** (`bookmarksCliEnabled`, default ON). The CLI flag gates whether the `127.0.0.1:19519/bookmarks/*` HTTP routes work at all (see "CLI control" below); it has no effect on the in-app popover. The two flags answer different questions:

- `showBookmarksInToolbar` — "should the Omniscio UI show me bookmarks?" (UI visibility; default ON).
- `bookmarksCliEnabled` — "should external scripts and AI agents be able to read or mutate bookmarks via the localhost HTTP server?" (CLI surface; default ON, panic-button OFF).

You can have bookmarks visible in the UI with the CLI off (typical), bookmarks hidden in the UI with the CLI on (e.g. an external launcher driving them headlessly), or any other combination.

## How it behaves

### The eight kinds

Each bookmark has a `kind` that controls its icon and what happens on click. Five are external launchers (open a URL, file, program, or shell command), three are in-app navigation jumps (focus a session, project, or virtual project):

#### External launchers (five)

- **Folder** — a container for other bookmarks. Icon: a folder icon (the only thing on the left of the row — there is no separate chevron / disclosure triangle). Single-clicking the row toggles expand / collapse; folders never "activate" the way a leaf row does. Folders can be nested up to 5 levels deep (`MAX_BOOKMARK_NESTING_DEPTH`). Folder rows show a child-count badge on the right.
- **URL** — an `http://` or `https://` URL. Icon is the cached favicon for the URL's host when one is available, otherwise a small generic globe icon. Single-click opens the URL via Electron's `shell.openExternal` (your default browser) and closes the popover. Middle-click does the same launch but keeps the popover open. Only `http:` and `https:` are accepted by the launcher; other protocols are rejected.
- **Path** — a filesystem path to a file OR folder. Icon: a small file icon. Click hands the path to Electron's `shell.openPath`, which opens a folder in your OS file manager (Explorer on Windows, Finder on macOS) or opens a file in the OS's default app for that file type. Environment variables of the `%NAME%` form are expanded before the path is resolved (e.g. `%USERPROFILE%\Documents` works on Windows).
- **Executable** (`exe`) — an absolute path to an executable. Icon: a small gear/settings icon. Click spawns the executable detached, with stdio ignored, the window hidden during launch, and `cwd` set to your home directory. `.cmd` and `.bat` files are routed through `cmd.exe /c` so the shell parses the line; everything else spawns directly. Environment variables in the `%NAME%` form are expanded.
- **Command** (`cmd`) — an arbitrary shell command. Icon: a small terminal icon. Runs through `cmd.exe /c <value>` detached, `cwd = home`. The **first time** you click a never-confirmed cmd row, Omniscio pops a confirmation modal (see next section). Subsequent clicks fire silently.

#### In-app navigation (three)

These kinds don't launch anything external — they make Omniscio navigate to a target inside the running app. The launcher emits a `DEEP_LINK_ACTION` push, the renderer's deep-link router consumes it, and the main window is foregrounded so a CLI-driven launch surfaces immediately. The bookmark's stored `value` is an opaque ID, not a label — the row displays the **current** name of the target each render, so renaming the session/project updates the bookmark label automatically (see "Live label and broken-link detection" below).

- **Session** — pinned to a specific session row. Icon: a small message-square icon. The stored value is the session's UUID. Click jumps to that session, activates its parent project first if needed, and routes through the normal session-navigation path. **Archived** sessions resolve normally — the launcher's `is_deleted = 0` check passes and the deep-link router hydrates the target — so a bookmark survives archiving (see "Live label and broken-link detection" below). Only soft-deleted sessions are treated as missing — the click surfaces a "session not found" toast with a one-click **Remove bookmark** action.
- **Project** — pinned to a specific project. Icon: a small folder-tree icon. The stored value is the project's UUID. Click activates that project via UUID-direct lookup (not fuzzy name match) so renaming the project doesn't break the bookmark. Soft-deleted projects → "project not found" toast + Remove action.
- **Virtual** — pinned to a virtual project (Inbox, Gmail, Claude project, etc.). Icon: a small sparkles icon. The stored value is the sentinel `folderPath` (`__inbox__`, `__gmail__`, `__claude__`, …) from [src/shared/virtual-project-ids.ts](../../src/shared/virtual-project-ids.ts). Click routes through `activateVirtualProject(folderPath)`; if the virtual is feature-flagged off or not yet wired, the deep-link router's `onError` surfaces a "not available" toast.

A small detail worth knowing: every launch is debounced per-bookmark for 200 ms, so a fast double-click only ever fires one launch.

#### Live label and broken-link detection (nav kinds only)

The three nav kinds — Session, Project, Virtual — render their **current** label every time the row paints. The bookmark row in SQLite stores the target's UUID (for session/project) or sentinel (for virtual) and a fallback `name` captured at creation time; the label resolver in [src/renderer/src/lib/bookmark-label.ts](../../src/renderer/src/lib/bookmark-label.ts) does a lookup against the live Zustand session/project stores on every render and prefers the live name. Renaming a session — in the sidebar, via AI title-generation, anywhere — updates the bookmark label automatically with no refresh. If the row's user-typed `name` is non-empty it still wins (your label > generated label), but with an empty name the live target name is what you see.

A target whose row genuinely no longer exists is "broken." For **session** bookmarks the resolver checks all three session slices — live, archived, and the lazy on-demand cache — and hydrates an unresolved target once on mount via `ensureSessionLoaded` (a single `SESSION_GET` by id, which carries **no** status filter), so a bookmark to an **archived** session — even one older than the 50-most-recent archived window the renderer keeps in memory (`ARCHIVED_INITIAL_LIMIT`) — resolves to its real name and stays clickable. A session row is therefore broken ONLY when the session is truly gone (soft-deleted / never created), never merely because it was archived. (Project / virtual kinds are broken when soft-deleted, never loaded, or a virtual sentinel is disabled.) The row renders with an amber warning icon overlaid on its kind icon and the label gets a strike-through. Clicking a broken row fires a toast with a one-click **Remove bookmark** inline action that deletes the row immediately; double-confirmation modal is skipped because the target is already gone. The check is purely UI — the launcher's backend pre-flight validates the same way (no DB row → `{ ok: false, error: 'session not found' }`) and never emits a push, so a stale CLI launch returns the error cleanly instead of navigating to nothing.

#### Row layout and indent

Every row — folder or leaf — has the same shape: a single 14px icon on the left, the row name, and (for folders with children) a small count badge on the right. There is **no separate chevron / disclosure triangle**; the folder icon itself is the affordance and a click on the row toggles expand / collapse. Removing the chevron also let every row shift slightly left so the tree feels less indented at depth 0.

Indent is computed as `paddingLeft = 6 + depth * 12` pixels — 6px base on the popover's edge, plus 12px per nesting level. A depth-0 row sits 6px in, a depth-1 row sits 18px in, and so on through the 5-level `MAX_BOOKMARK_NESTING_DEPTH` cap.

### First-run cmd confirmation gate

Because a `cmd` bookmark can run arbitrary code, Omniscio gates the very first launch of each cmd row behind an explicit confirmation:

- The launcher returns `{ ok: false, requiresConfirm: true }` for any cmd row whose `confirmed` column is `0`. The popover catches that flag and mounts a confirmation modal **on top** of the popover, so the row that triggered the gate is still visible behind the modal.
- The modal shows the bookmark's name, the literal command in a monospace block, a warning ("Omniscio will execute this command on your machine. Confirm only if you trust the source."), and a checkbox labeled "I understand". The Run button stays disabled until the box is ticked.
- On a successful confirmed run, Omniscio flips `confirmed = 1` for that row in SQLite. From then on, clicks fire silently — no second prompt.

**How to reset the gate**: any time you EDIT the cmd row's **value**, `confirmed` resets back to `0`, so the next click re-prompts. This is enforced in the SQL update — `UPDATE` statements that include the `value` column also run `confirmed = CASE WHEN kind = 'cmd' THEN 0 ELSE confirmed END`. Editing only the row's name or moving it to another folder does **not** reset confirmation; only changing the actual command text does.

### Reordering, drag-into-folder, and renaming

Bookmarks support two reorder gestures, drag-into-folder reparenting, and two rename gestures. Single-click activation is unchanged — clicking a non-folder row still fires its action and closes the popover, mirroring a browser's bookmark-bar behaviour.

#### Drag a row to reorder OR reparent

Press and hold any row, then drag it to a new position. The hit-test depends on what kind of row your pointer is over:

- **Hovering over a folder row — three zones.** A folder row is split horizontally into three bands. The **upper 25%** is "drop ABOVE this folder" (reorder same parent), the **middle 50%** is "drop INTO this folder" (reparent the moved row to be a child of this folder), and the **lower 25%** is "drop BELOW this folder" (reorder same parent). The drop indicator that appears matches the band: a horizontal line above or below for the reorder bands, a highlighted folder row for the middle drop-into band.
- **Hovering over a non-folder row — two zones, unchanged.** Leaf rows split into the upper 50% ("drop ABOVE") and lower 50% ("drop BELOW"). Leaves cannot be drop-into targets — only folders can take children.

Reorder is allowed across any two siblings under the same parent. Drop-into reparents the moved row from wherever it lives now (root or another folder) onto the target folder; cross-parent drag works in both directions. The tree uses a 12-pixel drag threshold (vs the default 8) so trackpad drift doesn't accidentally start a drag.

**Auto-expand on hover.** While dragging, hovering over a **collapsed** folder for **600ms without releasing** auto-expands that folder so you can drill into nested folders mid-drag without dropping and starting over. The 600ms threshold matches Windows Explorer's spring-loaded folder behaviour. Moving the pointer off the folder before the timer fires cancels the expansion. Letting go of the drag (pointerup, pointercancel, alt-tab away, window blur) also cancels the pending timer, so a stray expansion never fires seconds after you've already moved on.

**Successful drop also auto-expands the destination.** When you successfully drop a row INTO a folder (the middle 50% band, or the auto-expand path you used to navigate in), Omniscio expands that destination folder after the move so you immediately see the moved row land in its new home. A **failed** move does NOT auto-expand — for example, dropping a folder into one of its own descendants would create a cycle, the backend rejects it (`bookmarks: cycle (move-to-descendant)`), and the destination stays in whatever expanded state it was already in. The lack of an auto-expand is the user's signal that the move was illegal. Dropping a folder onto itself is also a no-op (the renderer guards it before the IPC call ever fires).

#### Alt+ArrowUp / Alt+ArrowDown to reorder via keyboard

Focus a row, then hold Alt and press ArrowUp or ArrowDown to swap it with the adjacent sibling. Same-parent only — keyboard reorder doesn't reparent across folders. The handler `preventDefault`s and `stopPropagation`s so it doesn't conflict with Omniscio's global keymap. To move a row across folders via keyboard, right-click → edit → change the Parent dropdown.

#### Folder inline rename via double-click

Double-click a folder name in the tree and the name span swaps for an input. Type the new name and press Enter (or blur the input) to save; press Escape to revert. While the input is mounted, the store gates incoming `BOOKMARKS_CHANGED` push events into a deferred reload queue so a concurrent push from another window can't clobber your in-flight draft. Inline rename is folder-only — non-folder rows activate on single-click and a double-click is structurally unreachable.

#### Non-folder rename via right-click → edit modal

Right-click any non-folder row to open the same edit modal. Change the name, click Save. (You can also rename folders this way — right-click a folder row to open its edit modal, where the kind picker is hidden because it's a folder.)

**Cascade-delete confirmation** — when you click Delete on a folder that has children (direct or nested), Omniscio shows a `ConfirmDialog` listing the count of descendants that will be removed. Confirming runs the delete with `ON DELETE CASCADE`, so the folder and every bookmark inside it disappears in one transaction. Deleting a leaf row (no children, or a non-folder) skips the dialog and deletes immediately. Once you confirm, the delete is final — there is no undo for cascade-delete in v1.

### Favicons

URL bookmarks try to fetch a real favicon for the URL's host so the row carries a recognisable icon. The flow:

- Best-effort fetch via Google's `s2/favicons` endpoint (`https://www.google.com/s2/favicons?domain=<host>&sz=64`).
- Cached as a PNG under `<userData>/favicon-cache/<sha256(host)>.png`. The relative path is persisted on the bookmark row's `icon` column.
- On any failure (invalid URL, network error, non-OK response, or a sub-**100-byte** response body — those are almost always Google's own globe placeholder, ~93 bytes), `icon` stays null and the row falls back to **a small generic globe icon** (a lucide `Globe`). It is **not** a "letter circle" — a different doc artifact mentioned that early on, but the actual fallback shipped is a generic globe. The 100-byte threshold replaces an older 300-byte cutoff that was filtering out real low-resolution favicons (e.g. Hacker News' 195-byte icon); a regression-guard test asserts a 150-byte body is accepted.
- When a URL bookmark's host changes, the old cached file is dropped on the next save. An orphan-sweep helper deletes any cache file no bookmark currently references.
- Cached PNGs are served back to the renderer through a custom `favicon://` protocol handler. The renderer's CSP `img-src` allowlist in [src/renderer/index.html](../../src/renderer/index.html) **must include the `favicon:` scheme**, otherwise every `<img src="favicon://...">` is silently blocked at the CSP layer, `onError` fires, and rows fall back to the globe icon even though the cache directory is full of valid PNGs. If you ever see "favicons not appearing" with a populated `<userData>/favicon-cache/` directory, check the CSP first — it's the most common foothold for that bug.

The cache and the original fetch both live entirely on your machine — Google sees only the host you bookmarked, never the rest of the URL or the bookmark name. If you're offline, the fallback globe icon shows immediately and the URL still launches normally.

### Where the data lives

Every bookmark — name, value, kind, parent, display order, confirmed flag, favicon path — lives in the SQLite `bookmarks` table inside `mission-control.db`. The table was introduced in migration v121 and widened in v203 to add `session`, `project`, `virtual` to the `kind` CHECK constraint (SQLite can't ALTER a CHECK in place — v203 rebuilds the table). The favicon cache is `<userData>/favicon-cache/`. Nothing about your bookmark list syncs to any cloud service, no telemetry includes bookmark contents, and the only outbound HTTP request the feature ever makes is the favicon fetch to Google's `s2` endpoint described above. If you sign out, switch accounts, or use Omniscio across multiple machines, your bookmarks travel with the local database, not with your account.

## Related

- [keyboard-shortcuts.md](keyboard-shortcuts.md) — the bookmarks tree's keyboard nav (ArrowUp/Down/Left/Right, Enter) is local to the tree; Omniscio's global shortcuts table lives here.
- [tray-and-window.md](tray-and-window.md) — bookmarks open URLs / paths / executables via the same Electron `shell` APIs and child-process spawn rules described in the window/tray surface.
- [cli-control.md](cli-control.md) — overview of the CLI control server, bearer token delivery, and how mutations are gated. The bookmarks routes follow the immediate-mutation pattern (like keybindings), not the approval-gated pattern (like cron / automation).
- [Bookmarks (part 2)](bookmarks-part-2.md) — the reference half of this page: the localhost CLI routes with their kill switch, curl examples and response shapes, and the implementation map of the queries, launcher, favicon service, IPC channels and renderer state behind the popover.
