# DESIGN: Notes Rich Text Editor, Obsidian-Style Linking & Knowledge Graph

**Version:** v0.17.3  
**Status:** Draft  
**Scope:** Upgrade the notes editor from a plain `<textarea>` to a CM6-powered
markdown editor with live preview, `[[wikilink]]` resolution, a backlinks
system, Canvas-rendered force-directed graph visualization, transclusion,
note-from-selection (chat → notes bridge), and daily notes.

**Depends on:** v0.17.2 (CodeMirror 6 bundle provides `CM.codeEditor()` and
the markdown language mode).

---

## Motivation

The notes system (v0.9.3 side panel, CRUD, folders, search) stores markdown
but edits it as raw text in a `<textarea>`. Now that CM6 is bundled, we can
give notes the same live-preview markdown experience that the chat input gets
— plus something the chat input doesn't need: **inter-note linking**.

The Obsidian model is the right reference: markdown files with `[[wikilinks]]`
resolved at render time, bidirectional backlinks, and create-on-reference.
Our advantage over Obsidian: we already have **semantic search via embeddings**
(v0.14.0 KB infrastructure), so link autocomplete can be smarter than
filename matching.

Beyond linking, the notes system sits adjacent to the chat system but has no
bridge between them. Users accumulate valuable AI responses in conversations
that they then manually copy into notes. Note-from-selection closes that gap.
A visual graph makes the link topology navigable and discoverable, turning
notes from a flat list into a connected knowledge base.

---

## Architecture

### New CM6 Factory: `CM.noteEditor()`

A third factory alongside `chatInput()` and `codeEditor()`, tailored for
long-form markdown editing in the notes panel.

```javascript
CM.noteEditor(target, {
    value: '',                    // initial markdown content
    darkMode: true,
    onChange: (text) => {},        // live content callback
    onLink: (title) => {},        // [[link]] activated callback
    linkCompleter: async (query) => [], // fuzzy note search for [[ autocomplete
});
```

**Behavior:**

- Full markdown syntax highlighting (headings, bold, italic, code, lists, links)
- Live inline preview: headings render at size, bold/italic styled, code blocks
  highlighted — the document is still markdown but *looks* formatted
- `[[` triggers autocomplete dropdown of existing notes (fuzzy search)
- `[[Note Title]]` tokens rendered as clickable chips (CM6 `Decoration.widget`)
- `![[Note Title]]` recognized as transclusion markers (decoration differs from
  regular links — see Transclusion section)
- Line numbers off (it's a note, not code)
- Spell check enabled (`EditorView.contentAttributes: { spellcheck: 'true' }`)
- Vim/Emacs keybindings respected (user preference from v0.17.2)

### Wikilink Syntax

Standard double-bracket syntax with optional display text:

```
[[Note Title]]              → links to note titled "Note Title"
[[Note Title|display text]] → shows "display text", links to "Note Title"
![[Note Title]]             → embeds target note content inline (transclusion)
```

Resolution order:
1. Exact title match (case-insensitive)
2. Fuzzy title match (for autocomplete, not for rendering)
3. Unresolved → rendered as red chip with "create" affordance

### Bundle Addition

The note editor factory lives in a new file in `src/editor/`:

```
src/editor/
  index.mjs           # existing — adds noteEditor to window.CM
  chat-input.mjs      # existing
  code-editor.mjs     # existing
  note-editor.mjs     # NEW — noteEditor() factory
  wikilink.mjs        # NEW — CM6 extension: parse, decorate, autocomplete [[links]]
  theme.mjs           # existing
```

The wikilink extension is CM6-native: a `ViewPlugin` that scans for `[[...]]`
patterns and replaces them with `Decoration.widget` nodes (clickable chips).
The autocomplete uses CM6's `autocompletion` with a custom `completeFromList`
source triggered by `[[`.

No bundle size concern — the wikilink plugin is tiny custom code, not an
external dependency.

---

## Data Model

### `note_links` Table

Tracks directed links between notes, extracted on save.

```sql
-- Postgres
CREATE TABLE IF NOT EXISTS note_links (
    source_note_id  UUID NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
    target_note_id  UUID REFERENCES notes(id) ON DELETE CASCADE,
    target_title    TEXT NOT NULL,           -- raw [[title]] text, for unresolved links
    display_text    TEXT,                    -- optional |alias
    is_transclusion BOOLEAN NOT NULL DEFAULT FALSE,
    created_at      TIMESTAMPTZ DEFAULT now(),
    PRIMARY KEY (source_note_id, target_title)
);

CREATE INDEX idx_note_links_target ON note_links(target_note_id)
    WHERE target_note_id IS NOT NULL;

-- SQLite
CREATE TABLE IF NOT EXISTS note_links (
    source_note_id  TEXT NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
    target_note_id  TEXT REFERENCES notes(id) ON DELETE CASCADE,
    target_title    TEXT NOT NULL,
    display_text    TEXT,
    is_transclusion INTEGER NOT NULL DEFAULT 0,
    created_at      TEXT DEFAULT (datetime('now')),
    PRIMARY KEY (source_note_id, target_title)
);

CREATE INDEX IF NOT EXISTS idx_note_links_target ON note_links(target_note_id)
    WHERE target_note_id IS NOT NULL;
```

**Key design decisions:**

- `target_note_id` is **nullable** — an unresolved link (target note doesn't
  exist yet) stores the title but has `NULL` for the FK. When a note with that
  title is created, a background pass resolves dangling links.
- Primary key is `(source_note_id, target_title)` — a note can only link to
  the same title once (last one wins if duplicated).
- `target_title` is always stored for human readability and re-resolution
  after renames.
- `is_transclusion` distinguishes `![[embed]]` from `[[link]]` — useful for

[... truncated for brevity, full content used in tool]