# SoloMD v2.0 Design Document

Status: **Planning** · Owner: solo · Target tag: `v2.0.0` · No partial features ship.

## Why v2.0

We surveyed the Markdown editor landscape (2025–2026): Obsidian, Logseq, iA Writer, Ulysses, Bear, Zettlr, Joplin, Notesnook, Standard Notes, NotePlan, Heynote, Reflect, Apostrophe, AppFlowy, Anytype.

Two **credibility holes** vs the field: we lack **wikilinks/backlinks** and **spell-check**. Two **differentiation wins** still on the table: **inline AI rewrite** for the CJK writing market, and **Pandoc + academic citations** to capture Zettlr's niche cheaply (we already export). Plus **Bases-style YAML property tables** because the foundation (front-matter parsing) is already in place.

The previous release cadence (1.2 → 1.3 → 1.3.1 in 24 hours) was too high. v2.0 is a planned drop: design now, build all six features to completion, then cut once.

## Scope (six features, all required for v2.0)

| ID | Feature | Why it ships |
| --- | --- | --- |
| F1 | Wikilinks + Backlinks panel | Credibility — Obsidian/Logseq/Bear/Reflect all have it; #1 question we'll get |
| F2 | Spell-check (en, zh best-effort) | Credibility — every native writing app ships it |
| F3 | Daily Notes + Tags panel | Adoption — common in Obsidian/Logseq/NotePlan, table stakes for PKM workflows |
| F4 | Inline AI rewrite (⌘J, BYOK) | Differentiation — Reflect is the only competitor; CJK market untapped |
| F5 | Pandoc export + `@citekey` citations | Differentiation — Zettlr's academic niche, low marginal cost given existing export pipeline |
| F6 | Bases-style YAML properties table | Differentiation — Obsidian Bases launched mid-2025; we already parse front-matter |

**Explicitly out of scope:**
- Sync (separate big bet, paid-tier candidate, v2.x or v3)
- Graph view (low ROI, large effort)
- Plugin marketplace
- Real-time collab
- Mobile-only features (Apple Pencil, etc.)

## Architecture overview

```
┌─────────────────────────────────────────────────────────┐
│ Vue 3 / Pinia / CodeMirror 6                            │
│                                                         │
│  Editor pane         Sidebar                            │
│  ┌─────────────┐    ┌──────────────┐                   │
│  │ wikilink    │    │ FileTree     │                   │
│  │ decorations │    │ ── Backlinks │ ◀─ F1             │
│  │ spellcheck  │ ◀─F2│ ── Tags     │ ◀─ F3             │
│  │ ai-overlay  │ ◀─F4│ ── Bases    │ ◀─ F6             │
│  └─────────────┘    └──────────────┘                   │
│                                                         │
│  Settings panel: AI provider, dict path, daily template │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼ tauri::invoke
┌─────────────────────────────────────────────────────────┐
│ Rust (Tauri commands)                                   │
│                                                         │
│  workspace_index.rs   — F1 reverse-link map, F3 tags,   │
│                         F6 properties cache             │
│  spellcheck.rs        — F2 hunspell-rs (en) + jieba/    │
│                         pycorrector-equivalent (zh)     │
│  ai_proxy.rs          — F4 streaming proxy to OpenAI/   │
│                         Anthropic/Ollama (CORS-free)    │
│  pandoc.rs            — F5 detect host pandoc / bundle, │
│                         exec with citation processor    │
│  citations.rs         — F5 .bib / .csl-json parser      │
└─────────────────────────────────────────────────────────┘
```

## Shared infrastructure (build first)

### S1. Workspace index (used by F1, F3, F6)

A Rust-side service that scans the active workspace folder for `.md` files and maintains:

- File path, mtime, size
- Extracted front-matter (YAML)
- Outgoing wikilinks (`[[note]]`, `[[note|alias]]`, `[[note#heading]]`)
- Tags (`#tag` in body + `tags:` in front-matter)
- Headings (for `#heading` link target)
- First-line summary

Storage: in-memory `HashMap<PathBuf, IndexEntry>` + on-disk JSON cache at `~/Library/Application Support/solomd/index/<workspace-hash>.json` for warm starts.

API:
- `workspace_index_init(folder: String) -> Result<()>` — initial scan + watch
- `workspace_index_files() -> Vec<IndexEntry>`
- `workspace_index_backlinks(target: String) -> Vec<BacklinkRef>`
- `workspace_index_tags() -> Vec<TagCount>`
- `workspace_index_query(filter: PropertyFilter) -> Vec<IndexEntry>` (F6)
- File watcher via `notify` crate; emits `solomd://index-updated` on change

This is the foundation. Build before F1.

### S2. CM6 syntax extension (used by F1, F2, F4, F5-citation)

Today the CM6 setup is in `app/src/components/Editor.vue`. We'll extract the extension list to `app/src/lib/cm-extensions.ts` and add a registry pattern so each feature contributes its own decoration plugin without Editor.vue growing into a monster.

```ts
// app/src/lib/cm-extensions.ts
export interface ExtensionContribution {
  name: string;
  extension: Extension;
  enabled: () => boolean;  // reads settings store
}
const registry: ExtensionContribution[] = [
  { name: 'wikilink', extension: wikilinkExt(), enabled: () => true },
  { name: 'spellcheck', extension: spellcheckExt(), enabled: () => settings.spellcheckEnabled },
  { name: 'ai-overlay', extension: aiOverlayExt(), enabled: () => true },
  { name: 'citation', extension: citationExt(), enabled: () => true },
];
```

## F1. Wikilinks + Backlinks

### Editor side (CM6)
- New decoration plugin: matches `\[\[([^\[\]]+?)(\|[^\[\]]+?)?(#[^\[\]]+?)?\]\]`
- Renders as a styled span; click → resolve target via index, open file
- Cmd+click navigation (Mac) / Ctrl+click (Linux/Win)
- Autocomplete: triggered by typing `[[`, dropdown shows fuzzy-matched files from index, picks insert `[[filename]]` with cursor after closing brackets
- Unresolved links: red underline + tooltip "create new note?"

### Renderer side (markdown-it)
- New plugin: turn `[[X]]` into `<a class="md-wikilink" href="solomd-wiki:X">X</a>`
- Preview click handler intercepts `solomd-wiki:` href and opens via `useFiles().openPath`

### Backlinks panel
- New sidebar section `BacklinksPanel.vue`, opens in the same area as outline (mutually exclusive or stacked above)
- Lists every doc that links to the active doc, with line context (3 lines: prev/match/next)
- Click → opens that doc + scrolls to the linking line
- Settings toggle: "Show backlinks panel" (default on for markdown docs)

### Resolution rules
- Match exact filename (case-insensitive on Mac/Win, sensitive on Linux)
- Then match via title field in YAML
- Then match via H1 heading
- Then fuzzy match in same folder
- If still ambiguous, show picker

### Effort: M (5–7 days)

## F2. Spell-check

### Approach
- Use **system spellchecker via webview** as primary. WKWebView (Mac), WebView2 (Win), WebKitGTK (Linux) all expose contenteditable spellcheck — but CodeMirror is not a contenteditable, it's a virtual cursor over a hidden textarea. So we can't free-ride.
- Use **`hunspell-rs`** in Rust for a real, offline, multilingual checker. Bundle `en_US.{aff,dic}` and ship the .aff/.dic for the user's choice.
- For Chinese: **don't** mark CJK as misspelled (single-char "words" aren't spellable); instead only spellcheck Latin runs inside the doc. v2.0 ships Latin-only spellcheck; CJK-aware grammar check is v2.x stretch.

### Rust side
```rust
spellcheck_check(text: String, lang: String) -> Vec<Misspelling>
spellcheck_suggest(word: String, lang: String) -> Vec<String>
spellcheck_add_to_user_dict(word: String) -> Result<()>
spellcheck_set_lang(lang: String) -> Result<()>
```
User dictionary stored at `~/.config/solomd/user-dict-<lang>.txt`.

### CM6 plugin
- Plugin walks visible ranges, splits on word boundaries, queries Rust in batches (debounced)
- Misspelled words get a `cm-misspelled` decoration with red wavy underline
- Right-click on misspelled word → context menu with top 5 suggestions + "Add to dictionary" + "Ignore once"
- Performance: only check visible text, only re-check edited lines, cache results per (line, version)

### Settings
- Toggle on/off (default off for now — opt-in given disk cost ~3MB for en dict)
- Language picker (en / fr / de / es / pt / ru / it — start with en, add others as users ask)

### Effort: M (4–6 days)

## F3. Daily Notes + Tags

### Daily Notes
- **Setting:** "Daily notes folder" (relative to workspace root, default `Daily/`); "Daily note filename format" (`YYYY-MM-DD.md` default); "Daily note template" (text or path to template file)
- **Command:** "Open Today's Note" with shortcut **⌘D** (or **Ctrl+D** on Linux/Win)
  - Resolves `<workspace>/<dailyFolder>/<formatted-date>.md`
  - Creates with template if missing (substitute `{{date}}`, `{{day}}`, `{{week}}`, `{{previous}}`)
  - Opens in current window (new tab)
- **Sidebar widget:** small "Today" / "Yesterday" / "Tomorrow" buttons in the Tags panel header

### Tags
- **Source:** body `#tag` regex + YAML `tags: [a, b]`
- **Sidebar:** `TagsPanel.vue` — alphabetical or by-count, each row shows tag + count
- **Click:** filters file list (or opens a Bases-style view filtered by tag)
- **Autocomplete:** typing `#` in editor → dropdown of existing tags from index
- **Settings toggle:** "Show tags panel" (default on if workspace open)

### Effort: S–M (3–4 days combined)

## F4. Inline AI Rewrite (BYOK)

### Providers (settings UI lets user pick one)
- **OpenAI**: chat-completions, default `gpt-4.1-mini`
- **Anthropic Claude**: messages API, default `claude-haiku-4-5`
- **Ollama**: `http://localhost:11434/api/chat`, default `llama3.2`
- API key stored encrypted in OS keychain via `keyring-rs` (NOT in localStorage)

### Tauri command (`ai_rewrite`)
- Takes selection text, system prompt, user instruction, provider config
- Streams chunks back to frontend via Tauri event
- Frontend renders a floating "AI bubble" near the selection: streaming text, accept (replaces selection), reject (closes bubble)
- Cancel via Esc

### Trigger UX
- **⌘J** on selection: opens action picker (Rewrite / Shorten / Expand / Translate to English / Translate to Chinese / Explain / Custom prompt…)
- Custom prompt: free-form text input
- After picking, request streams; user sees diff inline (deletions struck-through, additions in accent color); ⌘Enter accept, Esc reject

### Privacy & UX
- Selection content goes to provider; show "Sending to <provider>" before first byte
- Add "Disable AI" toggle in settings (separate from provider config so it stays off by default)
- Never auto-trigger; always user-initiated
- Don't log prompts in telemetry

### CodeMirror integration
- New widget: `AIDiffWidget` shows two panes (original / proposed) inline
- ⌘Enter — replaces selection; cursor moves past replacement
- Esc — restores
- ⌘J again while widget open — re-roll with same prompt

### Effort: L (7–10 days, mostly UX polish)

## F5. Pandoc export + `@citekey` citations

### Pandoc detection / bundling
- Detect host pandoc on PATH first (`which pandoc`); fall back to bundled binary in `app/src-tauri/resources/pandoc/`
- Mac: universal pandoc binary (~50 MB) bundled; can download on first export instead to keep installer slim
- Linux/Win: same approach
- **Decision:** Don't bundle in v2.0. Detect host pandoc; if missing, show a one-click "Install pandoc" link in the export dialog. (Avoids 50 MB installer bloat.)

### Export menu additions
- Existing: HTML, DOCX, PDF (print), PNG
- Add: **EPUB**, **ODT**, **LaTeX**, **RTF**, **Custom (Pandoc template…)**
- Export dialog: format dropdown + (optional) custom template path + (optional) bibliography file

### Citations
- Settings: workspace-level `bibliography:` field (path to `.bib` or `.csl-json`); CSL style file path (default Chicago)
- Front-matter override: `bibliography: refs.bib`, `csl: apa.csl`
- CM6 autocomplete: typing `@` shows fuzzy match over `.bib` keys; insert as `[@key]` (Pandoc syntax)
- Preview render: pre-process source through pandoc citation pre-render OR render `@key` ourselves with a lightweight Rust formatter that emits `(Author Year)` and an end-of-doc bibliography
- Export pipeline: pass `--citeproc --bibliography=<file> --csl=<file>` flags

### Effort: M–L (5–7 days)

## F6. Bases-style YAML properties table

### View
- New top-level page: **Workspace > Properties** (icon next to file tree toggle)
- Renders a table:
  - Rows: every `.md` in workspace
  - Columns: union of YAML keys across all docs
  - First column: filename (clickable → opens doc)
  - Numeric columns: right-aligned, sortable
  - Date columns (ISO 8601): formatted, sortable

### Filters
- Top-of-table filter row: per-column input (text / date range / tag-multiselect)
- Saved views: localStorage list of `{ name, filters, columns, sortBy }`
- Default views: "All notes", "Tagged #project", "Modified this week"

### Implementation
- All data comes from S1 workspace index — no extra Rust work needed
- Pure Vue component over the index `Vec<IndexEntry>`
- Columns lazily computed from any front-matter key seen 2+ times

### Effort: M (4–5 days)

## Build order

1. **Week 1:** S1 workspace index (Rust + watcher + cache), S2 CM6 extension registry refactor
2. **Week 2:** F1 Wikilinks + F1 Backlinks panel
3. **Week 3:** F3 Daily Notes + Tags (rides on S1)
4. **Week 4:** F2 Spellcheck (independent)
5. **Week 5:** F6 Bases (rides on S1)
6. **Week 6–7:** F4 AI Rewrite (most polish-intensive)
7. **Week 7–8:** F5 Pandoc + Citations
8. **Week 8:** Integration testing, perf pass, release blog post (en+zh), tag v2.0.0

Total: ~8 weeks of focused work for a polished v2.0. **No partial ship.**

## Quality bar (all features must meet before tag)

- Bilingual: every UI string in en + zh translations
- Settings UI: every feature surfaces a clear on/off + config in Settings panel
- Help dialog: each feature gets a section in the Shortcuts and/or Markdown syntax tab
- Telemetry: track feature usage (count only, no content); opt-out
- Error handling: every external call has a user-facing fallback message
- Tested in browser (`npm run dev` + chrome-devtools MCP) AND on real Tauri Mac build before tag
- No console errors at idle
- Performance: index init < 500ms for 1000 files; spellcheck doesn't block typing; AI streaming responsive within 200ms of first byte

## Risks & open questions

- **Spellcheck dictionary licensing:** Hunspell dicts are LGPL/MPL; OK for our MIT app. Confirm no GPL dicts.
- **AI provider keychain access:** `keyring-rs` works on Mac/Linux; Windows uses Credential Manager. Confirm Tauri 2 plugin coverage.
- **Pandoc on Windows:** path detection across Chocolatey/Scoop/manual installs needs testing.
- **Index for huge workspaces:** test with 10k files; may need pagination for properties view.
- **Wikilink ambiguity:** how to handle two notes with the same name in different folders. Plan: show picker on first ambiguity, remember user's choice in a `~/.config/solomd/wikilink-resolution.json`.
