---
title: KMS (the Vault) — notes, editor, search and sharing (part 3)
---

# KMS (the Vault) — notes, editor, search and sharing (part 3)

## What it is

This is part 3 of the [KMS](kms.md) page. It covers two things that go together in practice: what the vault looks like, and how you get to the note you want — autocomplete as you type a link, the search modal, and the quick-switcher.

## Where to find it

The **KMS** panel. Search is opened from within it, the quick-switcher is a keyboard shortcut, and the appearance options are in **Settings** under the vault's own section.

## How it behaves

### Softer default colours (Glassmorphism · Dark)

On the default theme — **Glassmorphism · Dark** — the note editor used to render a near-black page under near-white text, with a bright periwinkle accent and code blocks that blended into the background. That maximum contrast read as harsh for a text-heavy editor. The default now uses a gentler palette: a soft **charcoal** reading surface instead of stark black, **eased-off** body text (still crisp, just not glaring) with headings kept a touch brighter so the hierarchy stays clear, a **calmer** accent for links and the active note, and a faint visible container around **code blocks** so fenced code reads as a distinct block.

This refines the _default_ only. It is scoped to the Glassmorphism · Dark theme, so **every other visual theme and Light mode are unchanged**, and you can still switch KMS to any theme you like in the app's Appearance settings. The glassmorphism look itself — the blur, the drifting gradient glow behind the notes, the indigo identity — is untouched; only the surface and text tones are softened, in both the in-app KMS panel and the pop-out KMS window. The values live in the same scoped `.kms-prose` block in [/src/renderer/src/styles/globals.css](/src/renderer/src/styles/globals.css), pinned as invariant **`dark-editor-softened`** in the [editor styling contract](/.claude/memory/contracts/kms-editor-styling-contract.md).

### Custom theme options (appearance)

The KMS can carry its **own** look, chosen independently of the rest of Omniscio — a visual theme (any of Omniscio's 13 built-in themes OR one of your imported custom `.theme.json` themes), its own light / dark mode, and editor reading options. Everything defaults to "follow the app", so the KMS looks exactly as before until you opt in. Set it under **Settings → Features → KMS → KMS appearance**, or use the **toolbar gear icon** (between Ask AI and Bookmark) for quick-access text size and light/dark toggles without leaving the editor.

- **Visual theme** — a preview-card grid (the same one Settings → Appearance uses) with a "Follow app theme" card first, then the 13 built-ins, then your custom themes. Picking one re-skins the KMS only — the editor panel AND its file-tree sidebar AND the pop-out window — while the rest of Omniscio keeps the global theme. (`nothariVisualTheme`, default `'app'`.)
- **Light / dark** — Follow app / Light / Dark for the KMS specifically, so you can run a light reading vault inside a dark app. (`nothariColorMode`, default `'system'`.)
- **Editor font** — Default (sans) / Serif / Monospace, applied to the note content. (`nothariEditorFont`, default `'sans'`.)
- **Editor font size** — Small / Medium / Large / Extra Large, applied to the note content. (`nothariEditorFontSize`, default `'md'`.)
- **Editor width** — Narrow / Normal / Wide / Full reading column. `'normal'` (≈48rem, ~75ch) is the default so a fresh note gets a readable measure out of the box; `'full'` (edge-to-edge, no max-width) is opt-in for wide / table-heavy notes. (`nothariEditorWidth`, default `'normal'`.)
- **Page background** — Follow theme / Paper / Sepia. Paper and Sepia force a warm, dark-text reading surface independent of the theme (readable even on a dark theme). (`nothariEditorBackground`, default `'theme'`.)
- **File tree side** — Left (default) / Right. Moves the KMS file list — and its Files|Sessions tab strip + collapse handle — to the right of the editor, in both the in-app panel AND the pop-out window. KMS-scoped and independent of the global app sidebar position; default Left is byte-identical to today's layout. (`nothariSidebarPosition`, default `'left'`.) Full contract: [kms-sidebar-position-contract.md](/.claude/memory/contracts/kms-sidebar-position-contract.md).
- **Cross off completed checklist items** — When on, checked task-list items (`- [x]`) show a strikethrough and dimmed text; unchecking restores them. Default ON because that is the universal expectation (Notion, Apple Notes, Google Docs all do this). (`nothariChecklistStrikethrough`, default `true`.)

**How it works (repo detail).** A scoped wrapper element carries a mode class (`dark`/`light`) + a visual-theme class (`theme-<id>` / `theme-custom-<id>`) on the KMS roots ([KmsView.tsx](/src/renderer/src/features/kms/KmsView.tsx) + [KmsSubSidebar.tsx](/src/renderer/src/features/kms/KmsSubSidebar.tsx)) — the same scoped mechanism the Mind Map uses — so the class-based CSS vars cascade onto the KMS subtree only. The resolver is shared with the Mind Map ([scoped-theme.ts](/src/renderer/src/lib/scoped-theme.ts)); the KMS wiring is [useKmsTheme.ts](/src/renderer/src/features/kms/hooks/useKmsTheme.ts). Because the global theme-store only injects a CUSTOM theme's CSS when it is the GLOBAL theme, the KMS injects its own selected custom theme into a separate `#kms-custom-theme-css` element ([kms-theme.ts](/src/renderer/src/features/kms/kms-theme.ts)) so a custom KMS theme is styled even when the app runs a built-in. Editor font / font size / width / background ride `data-kms-*` attributes on the panel root → CSS targeting `.kms-prose .ProseMirror` in [globals.css](/src/renderer/src/styles/globals.css). The standalone window still adopts the base theme via `useWindowThemeBootstrap`. **v1 gaps:** portaled overlays (unified search / command palette / image lightbox) follow the APP theme; the mobile KMS sidebar isn't wrapped. Full contract: [kms-theme-override-contract.md](/.claude/memory/contracts/kms-theme-override-contract.md).

### Cursor stability during autosave round-trip

The autosave loop (Phase 2) emits the edited markdown out through `onChange`, the parent store fans it to subscribers and writes it into `editorContentByPath`, and that same value comes back into the editor as `initialMarkdown` on the next render — a round-trip echo. Before the 2026-05-22 fix the load effect re-ran `editor.commands.setContent` on every echo, replacing the entire document and collapsing the selection to `Selection.atEnd(doc)`. The user-visible symptom was "every keystroke jumps the cursor to the bottom of the buffer". The editor now records the last emitted markdown in `lastEmittedRef` and the load effect early-returns when incoming `initialMarkdown` matches the cached value — selection stays where the user put it. Genuine external changes (tab switch, on-disk update via the watcher) still differ from the cached value and still flow into `setContent`, so the fix only short-circuits the round-trip echo.

**Large-note typing — coalesced serialize (2026-06-22).** `onChange` is now COALESCED: the editor's `onUpdate` marks the tab dirty immediately but DEFERS the whole-doc `getMarkdown()` serialization to a `SERIALIZE_DEBOUNCE_MS` (150 ms) idle window via `flushSerialize`, flushed early on note-switch (reload-effect top) / `blur` / `pagehide` / unmount so no edit is stranded. Re-serializing a 100 KB+ note on every keystroke was the typing lag (the user's vault has notes up to 167 KB). `onChange(full, sourceNotePath)` now carries the editor's loaded note path so a debounced emit always lands on its OWN note even after a warm-kept switch — [KmsView.tsx](/src/renderer/src/features/kms/KmsView.tsx) resolves the tab by that path, with fire-time `resolveActiveTab` as the fallback. `lastEmittedRef` is still set on the (now debounced) emit, so the echo guard above is unchanged. The StatusBar word/char counts were a SECOND whole-doc-per-keystroke cost (`useEditorState` → `doc.textContent`), now on a 300 ms trailing debounce too. Full detail: [kms-editor-scroll-stability-contract.md](/.claude/memory/contracts/kms-editor-scroll-stability-contract.md) Part E + `serialization-is-coalesced-and-source-routed` and the [typing-lag-pattern postmortem](/.claude/memory/postmortems/typing-lag-pattern-postmortem.md).

### Hidden panel (Phase 3 Sub-PR 3f-2)

The right-pane panel stack carries a **Hidden** entry — collapsed by default — listing every note in the active vault whose `hidden_at` is non-null, sorted with most-recently-hidden first. The panel mirrors Bookmarks: a filter input narrows by note title, a roving-focus listbox renders each row, and clicking a row opens that note in the editor (the same code path as the file tree's open-note action). On row hover the panel reveals two icon buttons:

- **Unhide** — fires `kms:unhide-note` with `{ vaultId, noteId }`. The handler clears `hidden_at` on the row, the file reappears in the tree on the next push, and the panel removes the row optimistically. No confirmation prompt — Unhide is non-destructive (the file never left disk).
- **Permanent Delete** — opens a danger-variant `useConfirmDialog`. On accept, fires `kms:note-soft-delete` with `{ vaultId, relativePath }`. The handler moves the file into `<vaultRoot>/.trash/<timestamp>__<basename>` (the same trash semantics as the file-tree Delete action — recoverable by hand **only until the retention sweep reaps it**, default **30 days**; see [KMS part 6 → Privacy + safety contract](kms-part-6.md)) and removes the row from `nothari_notes`. The panel reconciles via the standard `kms:note-changed` push event.

The panel uses the same push reconciliation contract as Bookmarks — every `kms:note-changed` for a matching `vaultId` triggers a 200ms debounced refetch of `kms:list-hidden-notes`, capped at `KMS_HIDDEN_LIST_LIMIT = 500` rows. Epoch counter drops stale fetches; per-row mutation lock prevents concurrent Unhide / Delete on the same row.

The Hide affordance in the file-tree context menu is **file-only** — folders never gain a Hide row (folders have no `hidden_at` column). The action is tri-state-gated: a file row that hasn't been indexed yet (no `noteId`) also skips the Hide entry, because Hide needs a real row to stamp.

### Smart Connections (Phase 3.2)

The Smart Connections panel shows notes that are semantically related to the
one you're reading but not yet linked. It uses cosine similarity over note
embeddings (from Semantic Search, Phase 3.1) to rank results.

- **Panel location:** Right-pane drawer, between Backlinks and Tags
- **What it shows:** Up to 20 related notes ranked by similarity, with title,
  folder path, and a body preview snippet
- **Filtering:** Already-linked notes (via `[[wiki-link]]`) and hidden notes
  are excluded — you only see connections you haven't made yet
- **One-click link:** Hover a row to reveal a Link button that inserts
  `[[Title]]` at your cursor position
- **Dependency:** Requires notes to be embedded (Semantic Search must be enabled)

### Wiki-link autocomplete (`[[`) (Phase 3 Sub-PR 3g)

Typing `[[` inside the editor opens a small popover below the caret that lists matching note titles in the active vault. As the user keeps typing, each keystroke filters the list against title prefixes (FTS5-ranked) — up to 8 matches show at a time. The popover is keyboard-driven: **Up / Down** moves the highlight, **Enter / Tab** picks the highlighted row, **Esc** dismisses without inserting, and **mouse hover** also moves the highlight so a click on the right row lands a pick. Picking inserts `[[<Title>]]` at the caret, dismissing the popover.

When the typed text doesn't match any existing title exactly (case-insensitive), a **Create new: `<Query>`** row appears at the bottom of the list. Picking it (or pressing Enter when it's the only row) creates a fresh note in the vault root via `kms:note-create`, opens it as a new tab, AND inserts `[[<Title>]]` into the source note — both ends of the new link land in one step. The "Create new" row also serves as the fallback when there are zero hits, so an empty popover never shows. A purely empty query (just `[[` with no characters yet) renders nothing — the popover only mounts once the user has typed at least one character.

The query is debounced at **120 ms** between keystroke and IPC call (deliberately distinct from the 200 ms panel-push debounce so the two don't beat against each other), and the response is sliced to the renderer-side cap of **8 rows** (the backend already caps at 10 — the renderer narrows further so the popover stays terse). A null `vaultId` or empty query short-circuits the IPC entirely, returning `[]` synchronously; off-state envelopes, IPC throws, and missing `data` all collapse to `[]` so the popover stays quiet rather than crashing.

The popover is portaled to `document.body` via `react-dom/client` (lazy-imported so the editor's initial chunk doesn't pull in `react-dom/client`'s create-root surface) and positioned via the shared `positionPopover` helper at [/src/renderer/src/features/kms/popovers/popover-positioning.ts](/src/renderer/src/features/kms/popovers/popover-positioning.ts) so it clamps to the viewport on small displays. The portal root carries `data-ui-anchor="kms-wiki-link-suggestion"` (registered in `STATIC_UI_ANCHORS`) so external AI agents can discover it via `/ui/snapshot`.

### Wiki-link autocomplete IPC surface

- `kms:search-notes-by-title` (Phase 3 Sub-PR 3g-1) — input `{ vaultId, query }` → returns `{ hits: KmsNoteByTitleHit[] }`. Each hit is `{ noteId, title, relativePath }`. The backend ranks by FTS5 prefix-match on `title` and caps at 10 rows; the renderer slices further to 8 for the popover.
- `kms:note-create` (reused from Phase 2 write-side) — fires from the "Create new" row with `{ vaultId, relativePath: '<Title>.md' }`. The handler may canonicalize the path (e.g. `Foo.md` → `Foo (1).md` on collision) and the returned `note.relativePath` is what the renderer opens as a new tab.

### Wiki-link autocomplete file map

- Extension + IPC binding: [/src/renderer/src/features/kms/editor/extensions/WikiLinkSuggestionExtension.ts](/src/renderer/src/features/kms/editor/extensions/WikiLinkSuggestionExtension.ts) — exports `WikiLinkSuggestionExtension`, `fetchWikiLinkItems`, `WIKI_LINK_DEBOUNCE_MS = 120`, `WIKI_LINK_MAX_RESULTS = 8`, `WIKI_LINK_PLUGIN_KEY`. Extension options carry the live `vaultId` (mutated in place from `KmsEditor.tsx` on vault switch so the next `[[` keystroke picks up the freshest value without re-creating the editor) and the parent-injected `suggestionRender` factory.
- Popover UI + factory: [/src/renderer/src/features/kms/editor/extensions/WikiLinkSuggestion.tsx](/src/renderer/src/features/kms/editor/extensions/WikiLinkSuggestion.tsx) — exports `WikiLinkSuggestion` (the React component), `shouldShowCreateNewRow` (the predicate that decides whether to render the Create new row), and `buildWikiLinkSuggestionRender({ createNoteFromQuery })` (the TipTap `SuggestionOptions.render` factory consumed by `KmsView`).
- Mount + `createNoteFromQuery` wiring: [/src/renderer/src/features/kms/KmsView.tsx](/src/renderer/src/features/kms/KmsView.tsx) calls `buildWikiLinkSuggestionRender({ createNoteFromQuery })` once (memoized on the callback identity) and threads the result + the active `vaultId` into `<KmsEditor>`. `createNoteFromQuery(title)` calls `kms:note-create` with `{ vaultId, relativePath: '<title>.md' }` and opens the returned canonical path via the store's `openNote` action.
- Editor integration: [/src/renderer/src/features/kms/editor/KmsEditor.tsx](/src/renderer/src/features/kms/editor/KmsEditor.tsx) — `buildExtensions({ vaultId, wikiLinkSuggestionRender })` registers the extension via `.configure({ vaultId, suggestionRender })`, and a dedicated `useEffect` mutates `ext.options.vaultId` in place on vault switch (TipTap's `items` callback is a live closure over `this.options`, so the next keystroke sees the freshest value without forcing an editor remount).

### Hidden-panel IPC surface

- `kms:list-hidden-notes` — input `{ vaultId, limit? }` → returns `{ notes: KmsHiddenNoteRow[] }`. Each row is `{ id, vaultId, relativePath, title, hiddenAt }`. Default limit `500`.
- `kms:hide-note` — input `{ vaultId, noteId }` → returns `{ noteId, hiddenAt }`. Stamps `hidden_at = ISO8601 now` on `nothari_notes`.
- `kms:unhide-note` — input `{ vaultId, noteId }` → returns `{ noteId }`. Clears `hidden_at` to `NULL`.
- `kms:note-soft-delete` (reused) — handles permanent-delete from the Hidden panel. Same input shape as the file-tree Delete path: `{ vaultId, relativePath }`.

### Unified Search modal (Phase 3 Sub-PR 3h + Phase 5 Sub-PR 5h)

A vault-scoped Command-Palette-style search modal opens from the right-pane Search panel. It returns **three ranked result lists in a single IPC round-trip**:

1. **Notes — Files** (top 8). LIKE-banded match against `title + relative_path`, ranked by exact-match → prefix → fuzzy → contains. Hidden notes excluded.
2. **Notes — Content** (top 12). FTS5-bm25 against the note body; `snippet()` wraps each hit window with `<mark>` markers (32-token window). Skipped when the FTS5-sanitized query is empty.
3. **Images** (Phase 5 Sub-PR 5h — top 8). FTS5-bm25 against `nothari_images_fts`, which indexes AI-derived `title + description + ocr_text + tags_json + category` for every analyzed image in the vault. Skipped when the FTS5-sanitized query is empty. Each hit carries `{ sha256, vaultId, title, relativePath, snippet, rank }`. Clicking an image hit opens the existing **ImageLightbox** (the same surface the Gallery panel opens) — no separate viewer route.

### Filter chips

The query bar accepts space-separated filter tokens that the renderer parses out before sending the residual text to FTS5. Recognized chips:

- `tag:foo` — restrict notes to those tagged `#foo`.
- `file:path/segment` — restrict notes by `relative_path` substring.
- `hidden:true` / `hidden:false` — include / exclude hidden notes (default excludes).
- `since:YYYY-MM-DD` — only notes whose `updated_at` is on or after the date.
- **`kind:note`** (Phase 5 Sub-PR 5h) — only Notes Files + Notes Content run; the Images query is skipped entirely.
- **`kind:image`** (Phase 5 Sub-PR 5h) — only the Images query runs; both Note sub-queries are skipped. The default (no `kind:` chip) runs all three panes.

The chip enum is intentionally closed (no `kind:all`) — absence already means "all three". The kind chip routes which sub-queries execute on the backend; the renderer always renders all three section headings so the user can see which pane is empty vs not-yet-run.

### Empty-query handling

The Files list ALWAYS runs when notes are wanted, even on an empty query — the query helper short-circuits to `[]` so the renderer doesn't need to special-case mount order. The Content and Images lists both gate on `escapeFts5Query(query).length > 0` — `MATCH ''` is an FTS5 syntax error, so an empty FTS5-sanitized query skips the SELECT entirely.

### IPC channel

`kms:unified-search` — input `{ vaultId, query, filters? }` where `filters` is `{ tag?, file?, hidden?, since?, kind? }`. Returns `{ result: { files, content, images } }` regardless of which panes ran — the unused panes are empty arrays.

### Implementation files

- Handler: [/src/main/ipc/kms/search.ts](/src/main/ipc/kms/search.ts) — `kind`-gated branching, calls `searchNotesUnifiedFiles` / `searchNotesUnifiedContent` / `searchImagesUnified`, joins each image hit's `sha256` against `listAllAssets()` to attach `relativePath` (rows whose sha has no on-disk file silently drop — the analyzer can outlive a manual asset delete).
- Image query: `searchImagesUnified` in [/src/main/db/queries-kms-images.ts](/src/main/db/queries-kms-images.ts) — bm25 over `nothari_images_fts`, JOIN to `nothari_image_analysis` via `content_rowid='id'`, defense-in-depth `vault_id = ?` filter.
- Renderer modal: [/src/renderer/src/features/kms/UnifiedSearchModal.tsx](/src/renderer/src/features/kms/UnifiedSearchModal.tsx) — three section components, lightbox routing for image rows.
- Filter parser: [/src/renderer/src/features/kms/parse-search-filters.ts](/src/renderer/src/features/kms/parse-search-filters.ts).
- Renderer fetch hook: [/src/renderer/src/features/kms/useUnifiedSearch.ts](/src/renderer/src/features/kms/hooks/useUnifiedSearch.ts) — debounced per-keystroke fetch, `kind` is part of the cache key.

### Find Note quick-switcher

A fast "jump to any note by title" switcher, surfaced as the **Find Note** tab inside the Quick Launch ("Control Space") modal plus a **dedicated global hotkey, default `Ctrl+Shift+Space`** — Omniscio's in-spirit port of the standalone Nothari app's Ctrl+Shift+Space Find tab (a slice of the deferred "Phase 7 Quick Compose" idea). Distinct from the **Unified Search modal** above: that is the in-panel, full-text search (titles + content + images, `Ctrl+P` while the KMS panel is focused); this is a lightweight, title-only fuzzy **navigator** you can fire from anywhere on your computer — even with Omniscio backgrounded or the KMS screen never opened.

- **Open it.** Press `Ctrl+Shift+Space` (or click the Find Note tab if you've pinned it — it's opt-in / unpinned by default). The Control Space modal opens straight to the tab.
- **Find + jump.** An **empty box lists your most-recently-edited notes** (top 20, from `KMS_NOTE_LIST` which is already `updated_at DESC`); **typing fuzzy-matches note titles + paths** (server-ranked via `KMS_SEARCH_NOTES_BY_TITLE`). Arrow keys move the highlight, **Enter** opens the note — **in the standalone KMS window when you've popped one out, otherwise the main window's KMS panel** (Omniscio foregrounds the target and the note opens — cold-mount safe even if KMS wasn't open yet), **Esc** closes the modal. It only navigates — it never creates or edits.
- **Gated by `Enable KMS`.** Both the tab and the hotkey only appear / register when KMS is on; a KMS-off install never claims the system-wide `Ctrl+Shift+Space`.
- **The hotkey is rebindable.** It's a fixed global hotkey (a clone of the KMS-window hotkey, NOT a per-tab Quick Launch hotkey) — change or disable it at **Settings → Keyboard Shortcuts** ("KMS quick find" OS-level row, next to the KMS-window row).

**How it's wired (repo detail).** The tab is [QuickLaunchFindNoteTab.tsx](/src/renderer/src/features/quick-launch/QuickLaunchFindNoteTab.tsx) (registered `id: 'find-note'` in the QL action registry, `nothariEnabled`-gated). Selecting a note calls the thin `KMS_QUICK_FIND_OPEN` handler ([/src/main/ipc/kms-handlers.ts](/src/main/ipc/kms-handlers.ts)), which **picks the target window**: if a standalone KMS window is open (`getKmsWindow()`) it foregrounds THAT and pushes `KMS_OPEN_NOTE` (the pop-out opens the note via the shared `requestOpenNote` sink); otherwise it foregrounds the main window and re-emits the EXISTING `omniscio://note/…` `open-note` deep-link (see [Deep linking to a note](#deep-linking-to-a-note)). The hotkey is `kmsQuickFindHotkey` / `kmsQuickFindHotkeyEnabled`, planned in [global-hotkeys.ts](/src/shared/global-hotkeys.ts) and fired from `fixedHandlers['kms quick find']` in [hotkeys.ts](/src/main/app/hotkeys.ts) as `toggleShow()` + `QUICK_LAUNCH_OPEN_TO_TAB`. Full invariants: [kms-quick-switcher-contract.md](/.claude/memory/contracts/kms-quick-switcher-contract.md). User-facing tab walkthrough: [quick-launch-modal.md](quick-launch-modal.md).

## Related

The vault itself is the [KMS](kms.md) page, and writing in the editor is [part 2](kms-part-2.md). The AI tools built on top of the vault are [part 5](kms-part-5.md).
