---
title: KMS (the Vault) — notes, editor, search and sharing (part 2)
---

# KMS (the Vault) — notes, editor, search and sharing (part 2)

## What it is

This is part 2 of the [KMS](kms.md) page. It covers writing in the vault: the editor's toolbar and its slash commands, the syntax for links, tables, footnotes and lists, how code and long content are rendered, and how the editor behaves around autosave and imported notes.

## Where to find it

The **KMS** panel — open any note and the editor fills the pane. Every feature described here is reached from inside that editor rather than from a separate screen.

## How it behaves

### Editor toolbar (2026-06-06 layout)

The horizontal formatting strip above the editor (between the tabs strip and the document) is a single row, no flex-wrap. It was collapsed on 2026-05-22 from a 22-button five-group grid into inline primaries plus two dropdowns; on **2026-06-06 Undo, Redo, and Numbered list were promoted out of the ⋯ More menu onto the inline strip** so the controls a user reaches for are always visible. The inline order:

- **Undo** / **Redo** — lead the strip (Docs/Word convention). They grey out when there's nothing to undo / redo (the buttons read the editor's `can()` check, refreshed on each editor transaction).
- **Bold** (`Ctrl+B`)
- **Italic** (`Ctrl+I`)
- **Heading ▾** — opens a dropdown with seven rows: **Normal · H1 · H2 · H3 · H4 · H5 · H6** (the full OneNote / Google-Docs heading ladder). The trigger lights up in accent colour whenever the caret sits inside any heading; the specific level (H1–H6) shows active inside the dropdown so you can see at a glance what's currently applied.
- **Bullet list**
- **Numbered list**
- **Link**
- **⋯ More** — opens an overflow menu containing the remaining secondary commands, in this order: Underline, Strikethrough, Highlight, Blockquote, Code block, Task list, Image (opens an OS file picker), Hidden block, Column block, Insert table, Decrease indent, Increase indent. (The two indent commands were added for mobile — where there's no `Tab` key — but the catalog-completeness contract requires every command occupy exactly one desktop list, so on desktop they land here in the overflow; the keyboard `Tab` / `Shift+Tab` stays the primary desktop path.)

Both dropdowns follow the standard WAI-ARIA menu contract: **↓ / ↑** moves the highlight (with wraparound at the ends), **Home / End** jump to the first / last row, **Enter** or **Space** activates the highlighted row, **Esc** or **Tab** closes the menu and restores focus to the trigger. Mouse hover also moves the highlight; click-outside dismisses without picking.

The non-formatting **View** and **Bookmark** icons that live to the right of this strip (Toggle side panel, source-view, find/replace, bookmark current note) are rendered by `KmsView` itself — they're a separate strip and were not touched by the collapse.

When the editor is unmounted or destroyed (closing the last tab, switching away from the KMS panel mid-render), every button greys out and click-handlers short-circuit. This replaces the prior crash path where Undo / Redo's enable check called `editor.can()` against a torn-down editor and surfaced "Something went wrong" in the panel's error boundary — the mobile quick bar (below) adopts the same `isDestroyed`-guarded editor-state subscription, so promoting Undo/Redo onto it is crash-safe too. The layout is data-driven from `TOOLBAR_GROUPS` + the `PRIMARY_COMMAND_IDS` / `HEADING_MENU_COMMAND_IDS` / `OVERFLOW_COMMAND_IDS` lists (desktop) plus `MOBILE_QUICKBAR_COMMAND_IDS` (the mobile quick bar's curated subset) in [/src/renderer/src/features/kms/toolbar/toolbar-commands.ts](/src/renderer/src/features/kms/toolbar/toolbar-commands.ts); a CI lint test pins the contract that every catalog id maps to exactly one of the three **desktop** layout lists (no phantoms, no duplicates) so the layout can't silently drop a button or grow a stray one. The mobile list is a curated subset (not part of that partition — the action sheet still renders the full catalog, so nothing is unreachable).

The dropdown primitive shared by both menus lives at [/src/renderer/src/features/kms/toolbar/ToolbarMenu.tsx](/src/renderer/src/features/kms/toolbar/ToolbarMenu.tsx) and reuses the WAI-ARIA `useMenuKeyboard` hook that the rest of Omniscio's overflow menus use, so the keyboard contract is identical to (for example) the app-toolbar's overflow menu.

### Link popover — open, edit, or remove an existing link

Inserting a link (the toolbar **Link** button or `Ctrl+K`) opens a small edit form — a URL field plus an optional display-text field. Acting on a link that **already exists** surfaces a compact bubble with three actions — **Open** (launches the URL externally), **Edit** (switches to the edit form), **Remove** (unlinks) — alongside the URL and a close button.

How that bubble is revealed is **surface-split**:

- **Desktop** — it appears when you **hover the mouse over a link**, not when you place the text cursor on it. (Merely moving the caret onto a line that contains a link used to pop the bubble and cover the surrounding note content; it no longer does.) A short hover delay avoids flicker, and the bubble stays open while you move the pointer from the link into it, so its buttons stay clickable.
- **Mobile / touch** — there is no hover, so **tapping** into a link opens the bubble (unchanged — long-press `contextmenu` is unreliable on iOS, so tap remains the phone's reliable path to link actions).

On every surface the same three actions also live on the editor's **right-click / long-press context menu** (Open link · Edit link · Remove link), and `Ctrl+K` edits the link under the caret. The bubble is positioned to **never cover the link line** it points at — it sits fully below the link (or above, when there's no room), and clips at the viewport edge rather than landing on the link.

Source: [LinkPopover.tsx](/src/renderer/src/features/kms/popovers/LinkPopover.tsx) (the bubble), [useEditorPopovers.ts](/src/renderer/src/features/kms/hooks/useEditorPopovers.ts) (the hover / tap trigger + hover bridge), [popover-positioning.ts](/src/renderer/src/features/kms/popovers/popover-positioning.ts) (the `avoidAnchor` placement). Invariants: [kms-link-hover-contract.md](/.claude/memory/contracts/kms-link-hover-contract.md).

### Slash commands

Typing `/` anywhere in the editor opens a command menu — a filtered list of block-level formatting commands grouped by category (Headings, Lists, Blocks, Insert). Continue typing to narrow results; the menu matches against title, description, category, and search-term aliases (e.g. `/hr` → Horizontal Rule, `/todo` → Todo List, `/h1` → Heading 1). Arrow Up/Down to navigate (wraps), Enter or Tab to pick, Esc to dismiss.

Available commands: **Heading 1–6**, **Bullet List**, **Numbered List**, **Todo List** (GFM checkboxes), **Blockquote**, **Code Block**, **Horizontal Rule**, **Image** (opens file picker).

Source: [SlashCommandExtension.ts](/src/renderer/src/features/kms/editor/extensions/SlashCommandExtension.ts) (command definitions) + [SlashCommandSuggestion.tsx](/src/renderer/src/features/kms/editor/extensions/SlashCommandSuggestion.tsx) (popup UI).

### Pasting outlines and lists

Pasting a **nested outline keeps its nesting.** When you copy a multi-level list from Google Docs, Word, Notion, a web page, or plain-text notes — where the sub-levels are shown with letters (`a.` `b.`) or extra indentation — the sub-items paste as properly-indented nested list items instead of collapsing to flush-left plain text. The editor detects the outline (numeric / letter markers plus indentation), rebuilds it through the note's markdown pipeline so pasted nesting matches typed nesting, and it round-trips to disk. It stays conservative: ordinary prose, a flat single-level list, and a rich-HTML list that already carries its own nesting all paste unchanged.

Source: [OutlinePasteHandler.ts](/src/renderer/src/features/kms/editor/extensions/OutlinePasteHandler.ts) + [outline-paste.ts](/src/renderer/src/features/kms/editor/outline-paste.ts). Invariants: [kms-outline-paste-contract.md](/.claude/memory/contracts/kms-outline-paste-contract.md).

### Editor keyboard shortcuts

Almost every formatting command has a keyboard shortcut, so you rarely have to reach for the toolbar. The defaults follow **OneNote** conventions, and many commands additionally accept the **Google-Docs / Microsoft-Word** shortcuts so muscle memory from either app works — the list toggles, the full heading ladder (H1–H6 plus "Normal text"), strikethrough, and clear-formatting each accept both apps' keys.

**These keys are rebindable.** Open **Settings → Keyboard Shortcuts → KMS Notes**: the **Panel commands** group rebinds the command-palette keys (Bold, Italic, Underline, headings, Insert link, Find / Replace, Search vault, Toggle source, Command palette), and the **Editor formatting** group rebinds the formatting toggles (bulleted / numbered / task list, strikethrough, clear formatting, code block, blockquote, duplicate block). Press **Rebind**, then your combination; the X disables a key, **Default** restores it. (The structural editor keys — `Tab`/`Shift+Tab` indent, `Ctrl+Delete` delete-block, `Alt+Shift+↑/↓` move-block — stay fixed.)

**Formatting**

| Action                      | Shortcut                                                        |
| --------------------------- | --------------------------------------------------------------- |
| Bold                        | `Ctrl+B`                                                        |
| Italic                      | `Ctrl+I`                                                        |
| Underline                   | `Ctrl+U`                                                        |
| Strikethrough               | `Alt+-`, `Ctrl+-` (OneNote), **or** `Alt+Shift+5` (Google Docs) |
| Bullet list                 | `Ctrl+.` (OneNote) **or** `Ctrl+Shift+8` (Google Docs / Word)   |
| Numbered list               | `Ctrl+/` (OneNote) **or** `Ctrl+Shift+7` (Google Docs / Word)   |
| Task / checkbox list        | `Ctrl+1`                                                        |
| Heading 1 – 6               | `Ctrl+Alt+1` … `Ctrl+Alt+6` (OneNote / Google Docs)             |
| Normal text (clear heading) | `Ctrl+Alt+0` (Google Docs)                                      |
| Blockquote                  | `Ctrl+Shift+.`                                                  |
| Code block                  | `Ctrl+Shift+C`                                                  |
| Clear formatting            | `Ctrl+Shift+N` **or** `Ctrl+\` (Google Docs)                    |

**Block editing**

| Action                                           | Shortcut                                              |
| ------------------------------------------------ | ----------------------------------------------------- |
| Indent / outdent any list line — incl. the first | `Tab` / `Shift+Tab`, or `Alt+Shift+→` / `Alt+Shift+←` |
| Duplicate block                                  | `Ctrl+D`                                              |
| Delete block                                     | `Ctrl+Delete`                                         |
| Move block up / down                             | `Alt+Shift+↑` / `Alt+Shift+↓`                         |

**Insert & navigate**

| Action                            | Shortcut                 |
| --------------------------------- | ------------------------ |
| Insert / edit link                | `Ctrl+K`                 |
| Find in note                      | `Ctrl+F`                 |
| Replace in note                   | `Ctrl+H`                 |
| Search the whole vault            | `Ctrl+P`                 |
| Jump to top / bottom of note      | `Ctrl+Home` / `Ctrl+End` |
| Toggle source (raw-markdown) view | `Ctrl+Shift+M`           |
| Image gallery                     | `Ctrl+Shift+G`           |
| Command palette                   | `Ctrl+Shift+P`           |

The list shortcuts are _toggles_: pressing the bullet shortcut again while the caret is already inside a bullet list lifts the line back out to a plain paragraph. The same goes for numbered lists, headings, blockquote, and code block.

### Tables (GFM pipe tables)

The editor supports GitHub-Flavored Markdown pipe tables. Insert a table via the **⋯ More** toolbar menu → **Insert table** (inserts a 3×3 table with a header row), or type `/table` in the editor to use the slash command.

**Editing:**

- **Tab** / **Shift+Tab** navigate between cells (forward / backward). These keys only intercept while the caret is inside a table — outside a table they still handle list indentation as normal.
- Add / remove rows and columns via the table toolbar that appears on hover (provided by the TipTap table extension).

**Column alignment:** each column can be aligned left, center, or right. The alignment is stored as a per-column attribute on the table node and serialized to GFM alignment markers in the separator row (`:---` left, `:---:` center, `---:` right).

**Markdown roundtrip:** tables serialize to standard GFM pipe syntax and parse back identically — alignment, inline formatting within cells (bold, italic, code, links), and empty cells all survive the roundtrip. The underlying parser (`marked`, used by `@tiptap/markdown`) handles the tokenization natively.

**How it's wired (repo detail).** The implementation lives in [/src/renderer/src/features/kms/editor/extensions/TableExtension.ts](/src/renderer/src/features/kms/editor/extensions/TableExtension.ts), which exports a `TableExtensions` array of 5 extensions (KmsTable, KmsTableRow, KmsTableHeader, KmsTableCell, TableTabNavigation). KmsTable extends the official `@tiptap/extension-table` with a `colAlignment` JSON attribute and custom `parseMarkdown` / `renderMarkdown` hooks. Contract: [kms-table-extension-contract.md](/.claude/memory/contracts/kms-table-extension-contract.md).

**`Tab` indents any list line — even the first one.** Normally `Tab` tucks a line under the line above it (real Markdown nesting). When a line has nothing above it to tuck under — the very first item of a list, or the first child of a sub-list — `Tab` still pushes it one level to the right and `Shift+Tab` brings it back. That extra indent is stored invisibly inside the note (plain Markdown has no nesting for it to live in), so it survives saving and reopening; every other Markdown app still sees a normal list. (Pinned as invariant I8; before 2026-06-04 the first line simply refused to indent.)

**`Ctrl+Delete` (delete block) deletes the current line.** If that line has indented sub-items beneath it, the sub-items are **lifted up** one level to take its place — they are kept, not deleted (so deleting a parent bullet promotes its children rather than wiping them). A plain line with no sub-items is removed entirely, and a single `Ctrl+Z` undoes the whole thing. (Behavior pinned as invariant I7 in the editor contract; before 2026-05-30 it only cleared the line's text, stranding an empty line and its sub-items.)

**`Ctrl+Home` / `Ctrl+End` jump to the top / bottom of the note.** They move the caret to the very start / end of the document and scroll the note there in one keystroke — handy in a long note. (Fixed 2026-06-13 and pinned as invariant I16; before that the editor moved the caret but never scrolled, so the keys looked dead — the scroll container was looked up by a CSS marker the editor wrapper had lost in the port from the old Knowledge Management Tool.)

These are **in-note** editing keys. `Ctrl+Alt+N` is now the global hotkey that opens the **standalone KMS window** (shipped — see "Standalone KMS window" below). It is unrelated to the deferred Phase 7 Quick-Compose capture window.

**How the shortcuts are wired (repo detail).** Two cooperating layers handle these keys. A window-level capture hook ([/src/renderer/src/features/kms/hooks/useKmsShortcuts.ts](/src/renderer/src/features/kms/hooks/useKmsShortcuts.ts)) fires first and owns the bold / italic / underline, heading, link, find / replace, source-toggle, gallery, vault-search, and command-palette keys; any key it does **not** recognise — notably the list toggles — passes straight through to the editor's own ProseMirror keymap ([/src/renderer/src/features/kms/editor/extensions/KeyboardShortcuts.ts](/src/renderer/src/features/kms/editor/extensions/KeyboardShortcuts.ts)), which owns the list and block-editing keys. The Google-Docs aliases `Ctrl+Shift+8` / `Ctrl+Shift+7` are matched on the _physical_ key (`event.code` = `Digit8` / `Digit7`) rather than the typed character, because holding Shift turns `8` into `*` and `7` into `&` on a US layout — matching on the character alone would silently miss. OneNote's strikethrough `Ctrl+-` collides with Omniscio's app-wide "shrink text size" shortcut, so the global dispatcher ([/src/renderer/src/hooks/useKeyboardShortcuts.ts](/src/renderer/src/hooks/useKeyboardShortcuts.ts)) stands down for that one key while focus is inside the notes panel — the same panel-scoped yield `Ctrl+B` / `Ctrl+K` / `Ctrl+\` already use; `Ctrl+=` (zoom in) is left alone, since it has no editor meaning. Both layers, plus the `event.code` rule, are pinned by [/tests/unit/features/kms/editor/KeyboardShortcuts.test.ts](/tests/unit/features/kms/editor/KeyboardShortcuts.test.ts); the full architecture and a safe-change checklist live in the [KMS editor styling & hotkey contract](/.claude/memory/contracts/kms-editor-styling-contract.md).

### List markers, headings, and the focus border

The note editor renders into a plain contenteditable surface that inherits Omniscio's global stylesheet. Omniscio is built with Tailwind, whose "preflight" reset deliberately strips list markers (`ul, ol { list-style: none }`) and flattens the heading scale (`h1`–`h6` all inherit the body size) so stray browser defaults never leak into the app's chrome. That reset also reached the note editor: toggling a bullet or numbered list created real list items, but they showed **no bullet or number**, and headings looked like ordinary body text. Separately, Omniscio paints a 2-pixel accent-coloured focus ring around any focused element — and because that accent is near-white in several themes, the editor box appeared to gain an ugly white border whenever you were typing in it.

Both are fixed by a small **scoped** stylesheet block that re-declares the list markers (disc → circle → square as lists nest; decimal → lower-alpha → lower-roman for numbered lists), resets the **task-list** marker so checkbox items render as a `[checkbox] text` row instead of inheriting a stray bullet, restores the H1–H3 size-and-weight scale, keeps nested bullet and numbered rows evenly spaced at every level (so the gaps no longer grow as lists nest deeper), and removes the focus outline. Every rule is namespaced under a `.kms-prose` wrapper class, so it applies **only** to the KMS note editor and can never leak into the other TipTap editing surfaces in the app (the agent chat composer, automation editors, and so on). Source of truth: the `.kms-prose .ProseMirror …` block in [/src/renderer/src/styles/globals.css](/src/renderer/src/styles/globals.css), applied via the `kms-prose` class on the editor wrapper in [/src/renderer/src/features/kms/editor/KmsEditor.tsx](/src/renderer/src/features/kms/editor/KmsEditor.tsx). The styling invariants are pinned by [/tests/unit/lint/kms-editor-css-contract.test.ts](/tests/unit/lint/kms-editor-css-contract.test.ts); full rationale and the safe-change checklist live in the [KMS editor styling & hotkey contract](/.claude/memory/contracts/kms-editor-styling-contract.md).

**Numbered lists keep counting across an interrupting note.** A paragraph, link, or image dropped _between_ two numbered items splits the underlying markdown into two separate lists, and the editor normalizes every list to start at 1 — so an outline with a reference note in the middle used to render `1, 1, 2` instead of `1, 2, 3`. The `OrderedListContinuation` extension fixes this at render time: it carries the count straight through the interruption so the list reads `1, 2, 3, …`. It works per column and **restarts at a heading** (a new section is a new count), touches only the top level (nested `a`/`i` levels are unchanged), and is **display-only** — it never rewrites the numbers saved in your `.md` file. Pinned by invariant I18 of the styling contract.

### Code syntax highlighting

Fenced code blocks (` ```python … ``` `) render with **language-specific syntax coloring** — keywords, strings, comments, and other tokens are highlighted so code reads naturally inside a note. The editor uses [CodeBlockLowlight](https://tiptap.dev/docs/editor/extensions/nodes/code-block-lowlight) with the same curated 29-language subset the rest of Omniscio uses (js, ts, python, rust, go, java, c#, c++, c, ruby, php, swift, kotlin, bash, shell, powershell, json, yaml, xml, markdown, ini, css, scss, sql, diff, makefile, dockerfile, graphql, plaintext). If you type a language fence the editor doesn't recognize (e.g. ` ```elixir `), the block renders as plain monospace — no crash, no error. The language tag round-trips through markdown save/load, so highlighting reappears on reload. No new CSS was needed — the existing `github-dark.css` token classes and the scoped `.kms-prose .ProseMirror pre` container rules handle both light and dark themes. Pinned by invariant **`code-blocks-syntax-highlighted`** in the [editor styling contract](/.claude/memory/contracts/kms-editor-styling-contract.md).

**How it's wired (repo detail).** `KmsEditor.tsx` disables StarterKit's built-in `CodeBlock` (`codeBlock: false`) and registers `CodeBlockLowlight.configure({ lowlight: kmsLowlight })` after StarterKit, before KeyboardShortcuts. The `kmsLowlight` instance at [/src/renderer/src/features/kms/editor/extensions/kms-lowlight.ts](/src/renderer/src/features/kms/editor/extensions/kms-lowlight.ts) calls `createLowlight(subsetLanguages)` using the same language map from [rehype-highlight-subset.ts](/src/renderer/src/lib/rehype-highlight-subset.ts). Invariants pinned by [kms-editor-css-contract.test.ts](/tests/unit/lint/kms-editor-css-contract.test.ts) (wiring lint) + [kms-code-highlight.test.ts](/tests/unit/features/kms/editor/kms-code-highlight.test.ts) (language registration + CodeBlockLowlight integration).

### Content always fits — no horizontal scroll

However wide a note's content gets — a two-column layout, a very long unbroken word or URL, a wide image — the **page never scrolls sideways**: the editor itself only ever scrolls up and down. (Before this, a wide two-column page could run off the right edge and get cut off — the original bug report showed exactly that.) On a roomy screen the columns shrink to share the pane, long words and links wrap, and images are capped to the pane width. The only things that get their own small internal sideways-scroll are blocks that genuinely can't shrink any further — a fenced **code** block scrolls inside its own frame instead of pushing the whole page over, and **on a phone-width screen (≤767px) a multi-column block does the same**: instead of crushing 2–5 columns into ~90px of character-by-character word-wrap (`Export` → `Exp/ort`), each column keeps a readable width and you swipe the block sideways to reach the rest — the page itself still never moves. This holds at every **Editor width** setting (below); it isn't the same control. The rules live in the same scoped `.kms-prose` block in [/src/renderer/src/styles/globals.css](/src/renderer/src/styles/globals.css) (the editor root scrolls vertical-only; columns get `min-width: 0` so they shrink to fit on desktop, while a `@media (max-width: 767px)` override gives them a readable `min-width` plus a contained `overflow-x: auto` on a phone; the prose wraps via `overflow-wrap: anywhere`), are pinned by [/tests/unit/lint/kms-editor-css-contract.test.ts](/tests/unit/lint/kms-editor-css-contract.test.ts), and are invariant **`content-fits-the-pane`** in the [editor styling contract](/.claude/memory/contracts/kms-editor-styling-contract.md). The same scoped block also gives a multi-column layout **visible borders** — a thin rounded box around the block plus a divider line between columns (the first column has none, so you only ever see dividers _between_ columns), matching the original KMS desktop app it was ported from. The border uses Omniscio's theme-aware divider tone (`--color-surface-200`, the same weight the KMS panels use) so it reads in every theme. (Restored 2026-06-19 — the port had dropped every border rule, so the columns floated with nothing between them.)

### Resilient loading of imported / malformed notes

Notes imported from other tools (Google Docs especially) sometimes carry odd Markdown the editor must open **without crashing**. The load path is hardened two ways:

- **The editor never crashes on a mark it can't place.** The `LinkNormalizer` extension auto-converts a raw `[text](url)` typed or pasted as plain text into a real link — but only where a link mark is schema-legal. Text inside a **code block**, or inside a **malformed list item** an import left holding bare text, can't hold a link mark, so the normalizer skips it (worst case the text stays literal) instead of attempting an invalid replace. Before this, such a replace threw ProseMirror's `Called contentMatchAt on a node with invalid content` and blanked the whole KMS panel with the "Something went wrong" card (ref ecec4d3f, 2026-07-12). Guard lives in [/src/renderer/src/features/kms/editor/extensions/LinkNormalizer.ts](/src/renderer/src/features/kms/editor/extensions/url/LinkNormalizer.ts).
- **Empty list markers with indented content are healed on open.** A Google-Docs import can produce an empty bullet whose image + link sit on the indented lines beneath it (`- ` then `  ![img]` / `  [link]`). `@tiptap/markdown` mis-parses that blank-first-line item, so on load `healEmptyMarkerContinuation` ([markdownIo.ts](/src/renderer/src/features/kms/editor/markdownIo.ts), part of `preflightMarkdown`) pulls the first continuation line up onto the marker — the image + link then render and the bullet is preserved. Load-only; the healing never rewrites a note you didn't edit.

Full incident + the DO-NOT-RETRY list: [kms-linknormalizer-invalid-content-crash postmortem](/.claude/memory/postmortems/kms-linknormalizer-invalid-content-crash-postmortem.md).

### Footnotes (`[^label]` references and `[^label]:` definitions)

The editor supports standard Markdown footnotes. Type `[^label]` inline to insert a footnote reference — it renders as a superscript accent-coloured link. At the bottom of the note (or anywhere a block starts), type `[^label]: text` to create the matching definition. The reference and definition are clickable cross-links: clicking a reference scrolls to its definition, and clicking a definition's label scrolls back to the reference.

**Multi-line definitions** are supported: indent continuation lines with 4 spaces or a tab. Blank lines within the continuation are preserved (multi-paragraph footnotes). The label grammar is `[a-zA-Z0-9_-]+` — alphanumeric, hyphens, and underscores.

**v1 is read-only in the rich editor.** Definitions render as styled blocks (accent label + muted content) but are edited via the source-view toggle (`Shift+Ctrl+M`). The markdown round-trips losslessly — footnote syntax survives save → reload byte-for-byte. Rich inline editing of definitions is a future enhancement.

**How it's wired (repo detail).** Two TipTap nodes in [FootnoteRef.ts](/src/renderer/src/features/kms/editor/extensions/FootnoteRef.ts) and [FootnoteDef.ts](/src/renderer/src/features/kms/editor/extensions/FootnoteDef.ts): `footnoteRef` (inline atom) and `footnoteDef` (block atom), both following the HiddenBlock/TasksBlock opaque-passthrough pattern. Each has a `markdownTokenizer` (inline-level for ref, block-level for def), `parseMarkdown`, and `renderMarkdown` on the extension's `.config` — integrated with `@tiptap/markdown`. NodeViews render the click-to-jump navigation via `data-footnote-ref` / `data-footnote-def` DOM attributes + `scrollIntoView({ behavior: 'smooth', block: 'center' })`. CSS lives in the scoped `.kms-prose .ProseMirror .footnote-*` block in [globals.css](/src/renderer/src/styles/globals.css). Invariants pinned in [Footnote.test.ts](/tests/unit/features/kms/editor/extensions/Footnote.test.ts) (42 tests). Contract: [kms-footnote-contract.md](/.claude/memory/contracts/kms-footnote-contract.md).

## Related

Getting around the vault — the file tree, tabs and adding a note — is the [KMS](kms.md) page, and appearance plus search is [part 3](kms-part-3.md). Images and media are [part 4](kms-part-4.md).
