This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/archive/DESIGN-0.17.3.md

5.7 KiB

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.

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)

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

Tracks directed links between notes, extracted on save.

-- 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]