KMS (the Vault) — notes, editor, search and sharing
Omniscio's built-in Markdown vault: point it at a folder of notes and edit them in place, with a file tree, search, wiki-links, images and sharing. This page covers getting around the vault and the editor; the long tail — the editor's details, media, the AI tools and the internals — continues on the parts below.
What it is
Status: Phases 1-3, 5, 6 shipped — full Markdown vault editor with images, AI summaries, and read-only agent tools. Omniscio hosts a TipTap-based Markdown editor inside the KMS panel, with autosave, persisted open tabs, note CRUD (create / rename / move / soft-delete / hide), wiki-link back-reference rewriting on rename, wiki-link autocomplete, find/replace, source view toggle, a unified search modal across note titles + content + images, full image support (paste / drop / picker import, content-addressed asset dedup, gallery panel, full-screen lightbox, AI-derived per-image title / description / OCR / tags / category, image FTS5 search), per-note AI summaries with drift detection + opt-in auto-regen worker + in-editor status chip, and read-only MCP tools (including hybrid semantic search) that expose the vault to spawned Claude sessions. Writes land on disk under the configured vault root only; soft-delete moves notes into
<vaultRoot>/.trash/<timestamp>__<basename>instead of unlinking. Image assets live in<vaultRoot>/assets/<sha256>.<ext>(content-addressed → automatic dedup across the vault). Phase 4 (Tasks UI), Phases 7 (Quick Compose + hotkeys), and Phase 8 (export, templates, crash recovery, etc.) are deferred or cut from scope (the KMS appearance / custom theme options from Phase 8 DID ship — see "Custom theme options") — see "What's still deferred". For a verified feature-by-feature parity map against the standalone "Knowledge Management Tool" (Nothari) app — what's present / partial / missing and a proposed build-wave roadmap to reach full parity — see kms-parity-audit.md.
KMS is an integration that exposes a folder of Markdown notes (a "vault") to Omniscio as a knowledge surface you can both read and edit in place. You point Omniscio at a vault root on disk, and Omniscio parses every .md file under it once, then watches the folder for changes. The notes show up as a new "KMS" virtual project in the Omniscio sidebar group. From there you can browse the vault list, open a note in the editor, edit it (with autosave to disk), search across every note's body with FTS5, rename / move / soft-delete notes, and see external filesystem changes land in real time when something else on disk updates a file.
The vault format is plain Markdown — one note per .md file, YAML frontmatter optional, #hashtag and [[wiki-link]] syntax recognized but not required. The integration deliberately does not own the file format — it reads and writes whatever round-trips through the editor's serializer, and stays out of the way otherwise. You can continue using the KMS desktop app (or any Markdown editor — VS Code, Obsidian, plain Notepad) alongside Omniscio; the watcher picks external edits up and refreshes the sidebar.
Off by default. The integration is gated by the nothariEnabled setting (default false). With it off, no parser runs, no watcher runs, no DB tables are touched, the editor chunk never loads (TipTap is React.lazy-imported behind the panel), and the sidebar entry is hidden. The intended onboarding is: Settings → Features → flip Enable KMS, then Settings → KMS → set the Vault root folder to the absolute path of your vault (e.g. C:/My Knowledge Vault/). Once both are set, the indexer runs a full pass and the sidebar entry appears.
A one-time reset on 2026-05-26 (src/main/services/force-off-features-migration.ts) flipped nothariEnabled — along with Marketplace, Marketplace Review, and Tasks Outliner — OFF for every install, overriding any deliberate enable, so even users who had KMS running before now start with it off. After that one-time reset your toggle choice is authoritative: enable it once and it stays on across launches. The flag is read with settings.nothariEnabled ?? false, so an install that never persisted the field simply reads as off.
Where to find it
The KMS panel in Omniscio's sidebar — that is the vault, and the editor inside it is where you write. The switches that control it live in Settings, where KMS itself has to be enabled before anything else here appears.
How it behaves
How to use it (Phase 2)
- Turn it on. Settings → Features → flip Enable KMS.
- Point Omniscio at your vault. Settings → Features → KMS vault → Choose folder… (desktop only) picks the vault root. The folder is validated first — it must be a real directory, not a system folder, a shortcut/symlink, or missing — and is saved ONLY if it passes, so a bad pick shows a plain-English reason inline instead of silently failing to index. A good pick applies immediately (no restart): the indexer kicks off a full scan and reports progress in the sidebar. Picking a different folder switches the open vault to it — the KMS panel follows the folder you've configured, not whichever vault you set first. (Changing the folder registers a NEW
nothari_vaultsrow; the registry is orderedcreated_at ASC, so the active vault is resolved by matching the configured root in the main process —getConfiguredVaultId()→KMS_VAULT_LIST.activeVaultId, plus aKMS_ACTIVE_VAULT_CHANGEDpush for the live switch — never blindlyvaults[0]. Pinned by I7 of the contract below.) The currently-set folder is re-checked whenever Settings opens, so a moved/deleted vault surfaces a ⚠ warning. The validate-before-persist seam reuses the same hardenedvalidateVaultRootthe indexer uses, via the read-onlyKMS_VALIDATE_VAULT_ROOTIPC; pinned by the KMS vault-root picker contract. - Wait for the first index. The first scan walks every
.mdfile under the vault root, parses frontmatter / hashtags / wiki-links, and writes a row per note into Omniscio's SQLite DB. Subsequent launches are incremental — only changed files reparse. - Open the KMS sidebar entry. It appears in the Omniscio group. Clicking it mounts the KMS editor shell. The file tree lives in Omniscio's native Pane 2 sub-sidebar — the same slot Tools, Skills, Alarms, AI Coaching use — so you get the standard Omniscio sidebar affordances (resize handle, mobile drill-down). The center pane holds the open-tabs strip on top, the TipTap editor in the middle, and a thin status bar at the bottom. A side-panel drawer (Outline / Backlinks / Smart Connections / Tags / Bookmarks / Hidden) is available behind the toolbar's Toggle side panel button — closed by default after the 2026-05-21 layout rework. Open it on demand to inspect the active note's structure; click the X in its header to dismiss without leaving the editor.
- Open a note. Single-click any row in the file tree. Each file row is labelled by the note's name with the
.mdextension hidden (the tree reuses the tabs'deriveTitlehelper, so a note reads identically in the tree and its tab; folders show verbatim). This is display-only — the inline rename input still seeds with the full.mdfilename, so renames preserve the extension on disk. The note loads into the editor and a tab appears in the tabs strip. Open multiple notes — each gets its own tab, and the strip is preserved across relaunches via thenothari_tabstable. Switching between open notes is instant: the TipTap editor instance is reused across note switches rather than torn down and rebuilt (KMS nav speed Fix 4 dropped the per-tabkeyremount), so there's no rebuild flash. A cross-note switch still resets per-note state — empty undo history (Ctrl+Z can't bleed into the previous note) and scroll-to-top — exactly as the old remount did; a same-note external refresh preserves scroll + cursor. (Behavior pinned in the KMS editor scroll-stability contract.) - Edit. Type into the editor. Five hundred milliseconds after you stop typing, the autosave debounce flushes — the body round-trips through the editor's markdown serializer, the frontmatter is re-attached byte-for-byte, and the file is written to disk. The write is atomic (temp file + rename); the rename is retried through a transient Windows file lock — a real-time AV scan, a file-sync client (OneDrive/Dropbox), or the Search indexer briefly holding the just-written temp file makes a bare
renamethrowEPERM, which used to lose the whole autosave — via the sharedrenameWithRetry(EPERM/EBUSY/EACCES, bounded), so a momentary lock no longer fails a save. Autosave runs silently: there is no on-screen save/dirty indicator (the status bar's amber Unsaved chip and the tab's dirty dot were removed 2026-06-09 — the sub-second type→save→clear cycle made them flicker on every keystroke), and only a persistently failed write (the retry exhausted, or a non-transient error) surfaces, as a toast. The internal dirty bit still exists and remains load-bearing (flush-on-close, flush-on-tab-switch, the privacy dirty-gate) — only its visual presentation was dropped. - Find / replace (3-scope bar).
Ctrl+Fopens the find bar;Ctrl+Hopens it with the replace input visible. The bar carries three scope tabs — This Note · All Documents · Images — over one shared query box;Tab/Shift+Tabcycle them (or click a tab), and each tab shows a live result count. This Note searches the active note in both the rich editor and source view (source view uses textarea selection on the active match), with a<current> / <total>counter,Enter/Shift+Enterto step forward / back, case / whole-word / regex toggles, and replace / replace-all. All Documents + Images run the SAME vault-wide FTS asCtrl+P(theuseUnifiedSearchengine is shared, never duplicated) — arrow keys move the highlight,Enteropens: a document result switches back to the This-Note tab and opens that note with the query still highlighted, an image result opens the lightbox.Esccloses. This brings the Ctrl+F bar to parity with the standalone Nothari app's find bar; behavior pinned in the KMS find/replace contract. - Source view.
Shift+Ctrl+Mtoggles the editor between the rich-text TipTap surface and a raw markdown textarea — useful for hand-editing tricky YAML frontmatter or pasting in pre-formatted content. - Rename and delete. The file tree's context menu (right-click a row) exposes Rename and — on file rows only — Delete (folders carry no Delete: KMS has no folder entity, so deleting a folder path would orphan its notes). Rename rewrites every
[[wiki-link]]back-reference across the whole vault in a single transaction so links never dangle. Delete pops a danger confirmation ("moved to the vault trash … recover from the vault's.trashfolder"), then soft-deletes byrelativePath: the file moves into<vaultRoot>/.trash/<timestamp>__<basename>and the row is removed fromnothari_notes(recoverable by hand only until the retention sweep reaps it — the periodic retention tick permanently deletes trashed note bodies older than your Settings → General → Data Retention (days) window, 30 days by default; see KMS part 6 → Privacy + safety contract, and copy anything you want to keep out of.trashbefore then). Deleting an open note closes its tab and discards any in-flight autosave so no doomed write fires against the gone file. Select several rows (Ctrl/Shift-click) and the menu collapses to Delete N items — one confirmation, deleted deepest-first, partial failures tolerated. The Command-Palette "Delete note" does the same to the active note. (Move is drag-and-drop in the tree, not a menu item.) Behavior pinned in the KMS delete contract. The same menu's Copy link action (file rows only, right under Open) copies a deep link to the page — see Deep linking to a note. - Hide a note (Phase 3 Sub-PR 3f-2). The file tree's context menu also exposes Hide between Rename and Delete — file rows only, never folders. Hiding a note stamps a
hidden_attimestamp on the row but leaves the file on disk untouched. Hidden notes are filtered out of the file tree, the search index, and link suggestions. They remain reachable through the right-pane Hidden panel (see below). - Watch for external changes. The file watcher debounces filesystem events and re-parses changed notes on the fly. The renderer receives a
kms:note-changedpush event with{ vaultId, noteId, relativePath, op: 'added' | 'updated' | 'removed' }and refreshes the tree + the editor if the changed file is the active tab. The watcher can be turned off at Settings → Features → KMS vault → Watch vault for changes (nothariFileWatcherEnabled, default on) — with it off, the vault is indexed once at startup and not re-scanned until the next launch.
The watcher's ignore list is hard-coded and MUST mirror the indexer's IGNORED_DIRS so the live watcher and the full re-scan agree on what is not a note: .git/, .kms/, assets/, node_modules/, and .trash/ (so a soft-deleted note — which the CRUD service moves into .trash/ — is not re-indexed and resurrected in search), plus any non-.md file. Internally the watcher does NOT use chokidar (which opens one OS file-watch handle per file — ~256 for a 243-note vault, tripping the resource-pressure alert): it puts a non-recursive fs.watch on each folder as a change signal, then readdir-diffs the folder to derive the real add/change/remove — so handle count scales with folder count, and it never trusts libuv's Windows directory-watch filename (which is corrupted). The vault root MUST be a real directory (no symlinks, no system paths, no traversal); every write-side handler funnels its user-supplied path through resolveInsideVault() which rejects anything outside the configured root.
Only keep pinned notes as tabs (preview-tab mode)
By default every note you open adds a tab, and the strip caps at 10 unpinned tabs — open an 11th and the least-recently-active unpinned one drops off (pinned tabs never count against the cap). Turning on Settings → Features → KMS → "Only keep pinned notes as tabs" (nothariTabsPreviewMode, default off) switches to a single-preview model: opening a note reuses one ephemeral slot instead of appending, so the strip only ever holds your pinned tabs + the note you're reading. Open another note and it takes over that same slot. Pin a note to keep it. On desktop (with a mouse or trackpad) an unpinned tab shows no pin button — right-click the tab → Pin; a pinned tab then shows a small pin glyph you can click to unpin, so you can still see which tabs are pinned at a glance. On touch (a phone or tablet) the inline pin glyph is always shown on every tab — it is the only pin affordance there, since touch has no right-click. Either way, a pinned note becomes a permanent tab. Reloading the vault (or relaunching) collapses a strip that had accumulated unpinned tabs down to pinned + one preview, so the bar starts clean; the surviving preview is the one you were most recently on. Replacing the current preview flushes its autosave first, so an in-progress edit is never dropped. Off is byte-identical to today's append-and-cap behavior — nothing changes until you opt in. Pinned by the KMS tab preview-mode contract.
Collapsing the file-tree sidebar
The KMS file-tree sub-sidebar (Omniscio's native Pane 2 — the notes/folder list) can be collapsed the same way the regular Sessions and Projects sidebars hide. A collapse button (a chevron with the accessible name "Collapse KMS sidebar") sits at the top of the sub-sidebar; clicking it tucks the file tree away and replaces it with a thin clickable reopen edge-strip (hover reveals a chevron; click brings the tree back). The collapsed/expanded state persists across launches via the integrationSidebarHidden setting (default false — expanded). A rebindable shortcut Ctrl+Alt+\ ("Toggle KMS sidebar", editable at Settings → Keyboard Shortcuts) toggles it too, and is inert anywhere except the KMS screen. (This is separate from the right-pane drawer's Toggle side panel button in step 4 — that hides Outline/Backlinks/Tags/Bookmarks; this hides the whole file-tree column.)
It is desktop-only: on a phone the KMS panel uses its own Files/Sessions drill-down (MobileKmsPanel), so the collapse control never renders there. It is also KMS-specific by opt-in: the collapse is driven by a sidebarCollapsible flag on the KMS integration's UI manifest, so the other routed sub-sidebars (Tools, Skills, Alarms, AI Coaching) are unaffected — setting that flag on another integration is the only change needed to make its sub-sidebar collapsible too.
How it's wired (repo detail). The collapse button lives in KmsSubSidebar.tsx (desktop render path only, after the mobile early-return) and sets integrationSidebarHidden. DashboardDesktopLayout.tsx swaps the routed sub-sidebar for the shared SidebarEdgeGutter reopen strip when shouldShowIntegrationReopenGutter(activeIntegration, integrationSidebarHidden) is true (integration-sidebar-collapse.ts; the gutter side follows the sidebarsRight layout). The setting is a flat appearance setting (appearance-settings.ts); the toggleKmsSidebar shortcut (core.keybindings.ts) resolves the active integration via resolveActiveIntegration and no-ops unless it is collapsible. Invariants are locked by integration-sidebar-collapse.test.ts (scope guard — a non-collapsible integration never collapses), KmsSubSidebar-collapse.test.tsx (desktop-only button + toggle), and kms-ui-registry.test.ts (the sidebarCollapsible flag on KMS).
Sorting the file tree
The sort order is chosen from a sort dropdown in the file-tree header (FileTreeSortMenu.tsx); the ordering itself is a pure comparator in tree-builder.ts (compareNodes). Folders always sort above files at the same level regardless of mode; within each group the chosen comparator applies, with the note name as the final stable tiebreak. A folder is synthetic (KMS has no folder entity — folders are derived from note paths), so it carries no date of its own: for the three time-based sorts each folder takes an aggregate time = the most-recent among the notes it contains (recursively), so the folder group orders by freshest content — the folder you touched most recently rises to the top — instead of collapsing to a name tiebreak. Six modes:
- Name (A→Z) / Name (Z→A) — locale-compared, case-insensitive, numeric-aware.
- Modified — most-recently-edited on top (
updated_at, which mirrors the note file's real on-disk modification time, so the order matches what you'd see in Explorer / the KMS desktop app / Obsidian). Opening a note to read it (no edit) does not move it. - Created — most-recently-created on top (
created_at, the note file's real on-disk creation time). - Recently opened — most-recently-opened on top (
last_opened_atdesc). A note's "opened" time is stamped every time you open it in the editor — including clicking back to an already-open tab — so this answers "what was I just looking at." Notes you've never opened sink to the bottom (alphabetical among themselves), and before you've opened anything in a vault the tree is simply alphabetical (everylast_opened_atis null, so the comparator falls through to name). Restoring your open tabs on app launch does not count as opening — otherwise a relaunch would scramble the order. - Manual — your drag-to-reorder arrangement (persisted in localStorage only; notes you haven't dragged fall back to
updated_atdesc). See the tree-move contract.
How "Recently opened" is wired (repo detail). The open-note action in kms-store.ts optimistically bumps the in-memory note's lastOpenedAt (so the tree reorders instantly) and fires a fire-and-forget KMS_NOTE_TOUCH_OPENED (kms:note-touch-opened) IPC; its handler (kms-handlers.ts) stamps last_opened_at = <now ISO> on the row via touchNoteOpened (queries-kms/notes.ts). The column lives on nothari_notes (migration 20260620230000-kms-notes-last-opened-at-column), so the opened-time persists across restarts and is shared to the mobile/web client exactly like Modified/Created. The indexer's note-upsert never writes the column, so an external edit or a file-watcher reindex can't wipe an opened-time — regression-locked in queries-kms.test.ts. The sort selection is per-device (localStorage), like every other mode; the opened-times are shared via the DB so the ordering is consistent everywhere.
How "Modified" / "Created" get their dates (repo detail). The indexer derives both timestamps from the file itself, not from the moment Omniscio scanned it: noteTimestampsFromStat(stat, now) in indexer.ts maps the file's mtime → updated_at and birthtime → created_at (read off the same fs.stat the indexer already does for the size check), and upsertNote (queries-kms/notes.ts) writes those through — created_at is write-once unless the indexer supplies the file's birthtime to heal a row. So the two date sorts mirror the real files and survive a reindex (cold start / vault re-open re-derives the same dates). Before this fix the reindex stamped updated_at/created_at with now, so a full rescan collapsed every note to within milliseconds of the scan and the Modified/Created sorts degenerated to directory-walk-then-alphabetical order — most visible on an imported vault, where every note claimed the same "modified" minute. Self-write echoes are unaffected: a recognized autosave echo still re-indexes metadata only and preserves updated_at (it never reaches the time-writing path), so typing doesn't churn the sort — see the KMS self-write echo contract.
How folders get a sort time (repo detail). Folder nodes are built on the fly from note paths in buildTree (tree-builder.ts) with no timestamps of their own. A post-order pass computeFolderTimestamps then rolls each folder's updatedAt / createdAt / lastOpenedAt up to the most-recent value among its descendants (a folder holding only never-opened notes keeps a null opened-time, so it sorts last, just like a never-opened file). Before this, folder nodes carried null timestamps and the three time-based sorts silently fell back to alphabetical for folders while the files beside them reordered by date — now locked by tree-tree.test.ts "folders sort by their freshest contained note".
Adding a note — the tree scrolls to it
Adding a blank page — the + button in the file-tree header, or right-click a row / blank area → New note here — creates an Untitled note, opens it in the editor, and scrolls the file tree to it so you always see where it landed. This matters because the sort (above) decides where a fresh Untitled sorts: under Modified / Created it lands at the top and is already in view, but under Name (A→Z) it sorts down near the bottom, which in a large vault is off-screen — so the tree scrolls it into view. The new page is also highlighted as the open note. If you create the note inside a collapsed folder (right-click that folder → New note here), the folder expands first so the new page is visible. The scroll only happens when the row isn't already visible (no jump when it already is), and it works the same in the mobile notes drawer (which reuses the same file tree). You can also start a new page from the + at the end of the open-tabs strip (on both the desktop and mobile tab strips): it creates the same Untitled note (auto-numbered on repeat) and opens it as a new tab — the tab-bar + does create-and-open only, so the new note is still highlighted in the tree once it appears, but it doesn't force the tree to scroll the way the header + does.
How it's wired (repo detail). On a successful kms:note-create, FileTreePanel.tsx handleNewNote records the handler's resolved path (so an auto-numbered Untitled 2.md is the one revealed), calls expandFolders(ancestorFolders(path)) to open any collapsed ancestors, and sets a revealPath state. Because the new row only enters the tree after the async KMS_NOTE_CHANGED → debounced loadTree reload, a reveal effect watches the flat rows and, once the row appears, calls virtualizer.scrollToIndex(findRowIndex(flat, path), { align: 'auto' }) (align:'auto' = scroll only when off-screen; instant, so no reduced-motion concern). The open-note highlight (KMS_TREE_ROW_ACTIVE) keys on the active tab's notePath (not the tab id), so the just-added page stands out. Pinned by N5/N6 of the KMS new-note contract (FileTreePanel.reveal.test.tsx).
Related
The editor's own details — its toolbar, slash commands, tables, footnotes, syntax highlighting and the rest — are on KMS — the editor. Appearance and finding things are on KMS — appearance and search; images and links on KMS — images, media and links; the AI features, extra windows and publishing on KMS — AI, windows and sharing; and mobile, crash recovery and the internals on KMS — mobile, sync and internals.
- The Vault Overview — a searchable/sortable inventory table of every note, plus the
kms_inventoryread-only agent tool — is documented at kms-vault-overview.md. - The IPC channels live in /src/shared/ipc-channels/index.ts — Zod schemas in /src/shared/ipc-schemas.ts, response types in /src/shared/ipc-types.ts, domain types in /src/shared/kms-types.ts (
KmsUnifiedImageHit,KMS_UNIFIED_SEARCH_IMAGES_LIMIT = 8,KmsUnifiedSearchKindFilter). - The settings field is part of
AppSettingsin /src/shared/types.ts; the matching Zod field is inupdateSettingsSchema. Settings search entry:kms-enabled. - The integration is registered in /src/shared/integration-registry.ts with
kind: 'amc-builtin'so the integration-completeness CI lint test will catch any missing wiring claim. - The Phase 2 E2E happy-path lives at /tests/e2e/ui/30-kms.spec.ts, using the helpers in /tests/e2e/helpers/kms-helpers.ts and the tmpdir-isolated vault fixture at /tests/e2e/fixtures/kms-vault-fixture.ts.
- Phase 5 image integration tests: /tests/integration/kms-asset-dedup.test.ts, /tests/integration/kms-asset-refcount.test.ts, /tests/integration/kms-image-analyzer.test.ts, /tests/integration/kms-image-ipc.test.ts, /tests/integration/kms-images-migration-v215.test.ts, /tests/integration/kms-unified-search-images.test.ts.
- The lazy-load contract is pinned by /tests/unit/build-output/kms-view-lazy-load.test.ts.
- Phase 5 implementation plan: /docs/plans/2026-05-16-nothari-phase-5-images-media.md.
Last verified 2026-09-28