152 lines
5.7 KiB
Markdown
152 lines
5.7 KiB
Markdown
# 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] |