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, defaulttrue.)
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-notewith{ vaultId, noteId }. The handler clearshidden_aton 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, fireskms:note-soft-deletewith{ 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 fromnothari_notes. The panel reconciles via the standardkms:note-changedpush 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 ontitleand 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).mdon collision) and the returnednote.relativePathis 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 livevaultId(mutated in place fromKmsEditor.tsxon vault switch so the next[[keystroke picks up the freshest value without re-creating the editor) and the parent-injectedsuggestionRenderfactory. - 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), andbuildWikiLinkSuggestionRender({ createNoteFromQuery })(the TipTapSuggestionOptions.renderfactory consumed byKmsView). - Mount +
createNoteFromQuerywiring: /src/renderer/src/features/kms/KmsView.tsx callsbuildWikiLinkSuggestionRender({ createNoteFromQuery })once (memoized on the callback identity) and threads the result + the activevaultIdinto<KmsEditor>.createNoteFromQuery(title)callskms:note-createwith{ vaultId, relativePath: '<title>.md' }and opens the returned canonical path via the store'sopenNoteaction. - Editor integration: /src/renderer/src/features/kms/editor/KmsEditor.tsx —
buildExtensions({ vaultId, wikiLinkSuggestionRender })registers the extension via.configure({ vaultId, suggestionRender }), and a dedicateduseEffectmutatesext.options.vaultIdin place on vault switch (TipTap'sitemscallback is a live closure overthis.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 limit500.kms:hide-note— input{ vaultId, noteId }→ returns{ noteId, hiddenAt }. Stampshidden_at = ISO8601 nowonnothari_notes.kms:unhide-note— input{ vaultId, noteId }→ returns{ noteId }. Clearshidden_attoNULL.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:
- Notes — Files (top 8). LIKE-banded match against
title + relative_path, ranked by exact-match → prefix → fuzzy → contains. Hidden notes excluded. - 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. - Images (Phase 5 Sub-PR 5h — top 8). FTS5-bm25 against
nothari_images_fts, which indexes AI-derivedtitle + description + ocr_text + tags_json + categoryfor 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 byrelative_pathsubstring.hidden:true/hidden:false— include / exclude hidden notes (default excludes).since:YYYY-MM-DD— only notes whoseupdated_atis 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 (nokind: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, callssearchNotesUnifiedFiles/searchNotesUnifiedContent/searchImagesUnified, joins each image hit'ssha256againstlistAllAssets()to attachrelativePath(rows whose sha has no on-disk file silently drop — the analyzer can outlive a manual asset delete). - Image query:
searchImagesUnifiedin /src/main/db/queries-kms-images.ts — bm25 overnothari_images_fts, JOIN tonothari_image_analysisviacontent_rowid='id', defense-in-depthvault_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,
kindis 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_LISTwhich is alreadyupdated_at DESC); typing fuzzy-matches note titles + paths (server-ranked viaKMS_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-wideCtrl+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