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

KMS (the Vault) — notes, editor, search and sharing (part 3)

Part 3 of the KMS page: how the vault looks and how you find a note in it — the colour and theme options, the sidebar you can hide, wiki-link autocomplete as you type, the unified search modal, and the quick-switcher for jumping straight to a note by name.

What it is

This is part 3 of the KMS 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, pinned as invariant dark-editor-softened in the editor styling contract.

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.
  • 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 + 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); the KMS wiring is 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) 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. 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.

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 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 Part E + serialization-is-coalesced-and-source-routed and the typing-lag-pattern postmortem.

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) 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 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 — 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 — 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 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 — 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 — 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 — 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 — three section components, lightbox routing for image rows.
  • Filter parser: /src/renderer/src/features/kms/parse-search-filters.ts.
  • Renderer fetch hook: /src/renderer/src/features/kms/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 (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), 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). The hotkey is kmsQuickFindHotkey / kmsQuickFindHotkeyEnabled, planned in global-hotkeys.ts and fired from fixedHandlers['kms quick find'] in hotkeys.ts as toggleShow() + QUICK_LAUNCH_OPEN_TO_TAB. Full invariants: kms-quick-switcher-contract.md. User-facing tab walkthrough: quick-launch-modal.md.

Related

The vault itself is the KMS page, and writing in the editor is part 2. The AI tools built on top of the vault are part 5.

Last verified 2026-09-28